lastminute.com logo

Technology

Distributed authorization in Microservices with Spring

valerio_vaudi
valerio vaudi

How can you centralize your user in a distributed world?


Microservices have been around for quite a few years. This architectural style is very popular and has been adopted by many organizations. However, if you search for tutorial and blogs material about this topic, it’s hard to find quality resources that go beyond “getting started” stuff. In lastminute.com group we care about security, a feature so little discussed in the microservices world.

Security is a feature.

In a monolithic application, it’s easy to add security features. There exist an implementations in a lot of languages of the most common security techniques and protocols like basic auth, form auth, LDAP authentication and so on. However in a distributed system, all of the previous techniques does not work. First of all, due to the nature of Microservices architecture, it is a bad idea to give each microservice access to the user datasource. In many cases (e.g. a rest service ), perform the login each time does not work. We need a better solution. What can we use? The answer to this question is a set of advanced protocols: Kerberos, SAML 2, OAuth2 or OpenId Connect.
These protocols can be used for different purpose:

  • OAuth2 is very suitable for authorization
  • OpenId Connect and SAML 2 are suitable for SSO federated authentication

One of the most important advantage of OAuth2 is the simplicity of integration and the grade of adoption. Even if OAuth2 for its nature is not particularly suitable for SSO, there are many tricks that can be used in order to simulate what OpenId Connect add to OAuth2. Remember that OpenId Connect is a standard layer of authentication on the top of OAuth2.

OAuth2 adoption

How can you do that? What protocol should you choose? In the enterprise it’s very common to see Kerberos or SAML 2, but unfortunately adopting these protocols inside your application is quite complex. In many cases a flexible and secure protocol, will also add complexity to the system. SAML 2 is an example. It is very popular especially in the bank domain. It is a powerful XML-based protocol, but quite complex to adopt. The strength of protocols like OAuth2 and OpenId Connect are:

  • more web friendly
  • simple adoption
  • a big support community (also from big companies like Google, Facebook)

We can use OAuth2 for many use cases, mainly for authorization delegation. But using some tricks, it is possible to use OAuth2 for simulate an SSO. In this Post I will show how you can implement an authorization server that can be used with web applications written in Spring boot 1.5.x and 2.1.x. I will show you how you can configure these application for a SSO login.

OAuth2 terminology

There exist plenty of informations on the web about OAuth2 terminology, but I think that a small introduction could be helpful. In OAuth2 we have many actors like:

  • Resource Owner: who can grant access to a protected resource
  • Client Application: an application of third party that want to use one or more resources protected from OAuth2
  • Resource Server: who have the protected resource like an order-manager service
  • Authorization Server: who issue access token that in many implementation can be an opaque token like an UUID or a JWT token

In order to grant a token in OAuth2 we can follow several Protocol Flow like: Implicit, Resource Owner Password Credentials, Client Credentials and Authorization Code. These flows are used by the protocol to grant an access token to a client application in secure way.
In order to implement a web SSO with OAuth2, we can follow the Authorization Code Flow and let the OAuth2 Authorization server provide an endpoint in order to expose Principal information. This technique is very similar to the user info endpoint that an OpenId Connect server should expose: the userinfo endpoint should expose claims about the authenticated End-User.

Put all together

Now that we have explained why OAuth2 is a good choice and how theoretically we can achieve our goal, it is time to start to write code! As first thing, we need to know that starting from Spring 5.x OAuth2 and OpenId Connect have been merged directly into the spring security framework. Previously it was part of Spring Security OAuth that was a separated module. Now it is possible to use OAuth2 for SSO and resource server directly using Spring security while for the authorization server it is still necessary available on the old spring security OAuth module. The implementation include an Authorization Server written in Java and two client web application for SSO:

  • one written in Spring boot 1.5.x in Kotlin
  • another in Spring boot 2.1.x in Kotlin

Authorization server

Implementing an OAuth2 Authorization Server in Spring Security is very simple. This is true if you are using Spring Cloud Security that via auto configuration will save you from tedious configuration tasks, while providing multiple configuration endpoints among other stuffs. Spring is very rich in terms of authorization server capabilities. It permits to use opaque token or JWT token, configure code store service, token store and many other components of OAuth2.

For this use case I chose JWT as token, signed with RSA. This is a strategic choice. RSA is very suitable for security reason considering that the native support for SSO in spring book 2.1.x involves the usage of JWK, Json Web Key. JWK is a standard protocol to exchange key on the web. Exchanging a secret symmetric key on the web is not so secure with respect to exchange a public RSA asymmetric key, and then it works very well even with the old Spring boot 1.5.x implementation that on the client application side and resource server side has already a very good support for JWK, again for legacy spring boot 1.5.x use Spring Cloud Security. Since that on the authorization server does not exist a preconfigured endpoint to exchange JWK keys we have to implement it by ourselves.

JWK keys Framework endpoint

First of all we need to have a keystore.jks. Then we can use Nimbus, the same library used by Spring on Spring Boot 2.1.x in order to implement the JWK endpoint that will be used by our web application in SSO in order to retrieve the public key to verify the token signature. As a consequence of the fact that we have to implement userinfo endpoint on the Authorization server, we need to configure it in order to be filtered by Spring Security and give access to the current logged principal information. This is important for two reason:

  • first, the most similar way to OpenId Connect in spring security 5.x require to retrieve the user name from the current logged user during the login handshaking
  • In Spring Cloud Security we have two main strategy to validate a token: use token info or not, if we prefer token info, the JWT token validation will be done validating the token signature, otherwise Spring will contact the authorization server to retrieve information and rebuild the principal.

Using a userinfo enpoint give us more flexibility in all the scenarios. But for simplicity in this post we will focus on JWT validation with public key both for legacy and non spring boot application.
The dependency to enable our auth server to became a resource server is very simple.

 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
 </dependency>

For JWK endpoint a possible implementation of our framework endpoint may be like the one below. This endpoint should be public in OAuth2 because it is used to retrieve the key to validate the token signature.

@FrameworkEndpoint
public class JWTKeyEndPoints {

    @GetMapping("/sign-key/public")
    public ResponseEntity key() {
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("keystore.jks"), "secret".toCharArray());
        KeyPair keypair = keyStoreKeyFactory.getKeyPair("keypair");

        JWK jwk = new RSAKey.Builder((RSAPublicKey) keypair.getPublic())
                .privateKey((RSAPrivateKey) keypair.getPrivate())
                .keyUse(KeyUse.SIGNATURE)
                .keyID("resource")
                .build();


        return ResponseEntity.ok(new Jwts(asList(jwk.toJSONObject())));
    }
}

class Jwts {
    public List<JSONObject> keys;

    public Jwts(List<JSONObject> keys){
        this.keys = keys;
    }

}

User Info Endpoint

The only information that is needed from an OAuth2Login in Spring Boot 2.1.x is the username. A possible implementation maybe like the one you can find below.

User Endpoint

@FrameworkEndpoint
public class UserInfoEndPoint {

    @GetMapping("/user-info")
    public ResponseEntity key(JwtAuthenticationToken principal) {
        String userName = String.valueOf(principal.getToken().getClaims().get("user_name"));
        return ResponseEntity.ok(new UserInfo(userName));
    }
}

class UserInfo {
    private final String userName;

    UserInfo(String userName) {
        this.userName = userName;
    }

    public String getUserName() {
        return userName;
    }
}

Security Configuration

@EnableWebSecurity
@Order(SecurityProperties.DEFAULT_FILTER_ORDER)
public class LoginConfig extends WebSecurityConfigurerAdapter {

    private static final String[] WHITE_LIST = new String[]{"/login", "/user-info", "/sign-key/public", "/oauth/authorize", "/oauth/confirm_access"};

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().httpBasic().disable()
                .formLogin().loginPage("/login").permitAll()
                .and()
                .requestMatchers().antMatchers(WHITE_LIST)
                .and()
                .authorizeRequests().anyRequest().permitAll()
        .and().oauth2ResourceServer().jwt();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

  // ......

}

The most interesting things here are oauth2ResourceServer().jwt() and the AuthenticationManager authenticationManagerBean() bean. The first method call configures the user info endpoint and all WHITE_LIST endpoints like a protected resource, even though the watchful eye can say that those endpoints are public and that everyone can reach it, the specific implementation is safe. Even though the userinfo endpoint has a permitAll() then the rest call should be authenticated because otherwise the principals will be null and the endpoint will raise an 500 error due to a NullPoinerException. Apart from the accessibility modifier (public or not), it is necessary that the route is passed from the spring security filter otherwise would not have injected the principal. But the experience show that when we protect the endpoint then something on spring boot 2.1.x SSO goes wrong due to authorization. The authenticationManagerBean() bean definition is necessary for spring security and for internal purpose, especially for the password flow.

Authorization server token converter

The authorization server is very similar to a spring cloud security application. We extend a base AuthorizationServerConfigurerAdapter class and annotate our configuration class with @EnableAuthorizationServer:

@Configuration
@EnableAuthorizationServer
public class SecurityOAuth2AutorizationServerConfig extends AuthorizationServerConfigurerAdapter {

   // omit those configuration because are very common and in many post on the web it is possible find information about

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("keystore.jks"), "secret".toCharArray());
        KeyPair keyPair = keyStoreKeyFactory.getKeyPair("keypair");
        return new RsaJwtAccessTokenConverter("resource", keyPair);
    }

}

The interesting thing here is the accessTokenConverter that is the responsible for writing the JWT token. Unfortunately the standard implementation does not provide the possibility to add the header in the JWT. This could be a problem for the authentication process on a Spring Boot 1.5.x when use a JWK to retrieve the public key for validate the JWT. The exception that we can see is “Invalid JWT/JWS: KEY_ID is a required JOSE Header”. To solve this problem we can create an our RsaJwtAccessTokenConverter that extends the built in JwtAccessTokenConverter. We need to extend it because the JwtTokenStore can be configured with a class similar to this:


public class RsaJwtAccessTokenConverter extends JwtAccessTokenConverter {

  /**
   * Field name for token id.
   */
  public static final String JKW_ID = "kid";

  /**
   * Field name for access token id.
   */

  private final String jwk;
  private final KeyPair keyPair;

  private AccessTokenConverter tokenConverter = new DefaultAccessTokenConverter();

  private JsonParser objectMapper = JsonParserFactory.create();

  private String verifierKey = new RandomValueStringGenerator().generate();

  private Signer signer;
  private SignatureVerifier verifier;

  public RsaJwtAccessTokenConverter(String jwk, KeyPair keyPair) {
      this.jwk = jwk;
      this.keyPair = keyPair;
      applyKeyPair();
  }

  protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
      String content;
      try {
          content = objectMapper.formatMap(tokenConverter.convertAccessToken(accessToken, authentication));
      } catch (Exception e) {
          throw new IllegalStateException("Cannot convert access token to JSON", e);
      }
      /*here is the importatn thing pass the header while in the standard Spring implementation the header are the basic standard header
       * that do not include the kd header */
      return JwtHelper.encode(content, signer, jwtHeaders()).getEncoded();
  }

  protected Map jwtHeaders() {
      Map headers = new HashMap();
      headers.put(JKW_ID, jwk);
      return headers;
  }

  protected void applyKeyPair() {
      PrivateKey privateKey = keyPair.getPrivate();
      Assert.state(privateKey instanceof RSAPrivateKey, "KeyPair must be an RSA ");
      this.signer = new RsaSigner((RSAPrivateKey) privateKey);
      RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
      this.verifier = new RsaVerifier(publicKey);
      this.verifierKey = "-----BEGIN PUBLIC KEY-----\n" + new String(Base64.encode(publicKey.getEncoded()))
              + "\n-----END PUBLIC KEY-----";
  }


  public void setKeyPair(KeyPair keyPair) {
      throw new UnsupportedOperationException();
  }

  /**
   * I force an exception for force the setting of all the object in construction
   */
  public void setSigningKey(String key) {
      throw new UnsupportedOperationException();
  }

}

SSO on the legacy spring boot 1.5.x

In a Spring Boot 1.5.x app the configuration is very straightforward. The only important thing to remember is to properly configure the JWK endpoint.

Application Classes

@SpringBootApplication
class WebBootiful15xAppApplication

fun main(args: Array<String>) {
    SpringApplication.run(WebBootiful15xAppApplication::class.java, *args)
}

@Configuration
@EnableOAuth2Sso
class OAuth2SecurityConfig : WebSecurityConfigurerAdapter() {

    override fun configure(http: HttpSecurity) {
        http.authorizeRequests().anyRequest().authenticated()
    }
}

@Controller
class IndexController {

    @GetMapping("/index")
    fun index(model: Model): String {
        val authentication = SecurityContextHolder.getContext().authentication
        model.addAttribute("user", authentication)
        return "index"
    }
}

application.yml

server:
  use-forward-headers: true
  port: 8081
  context-path: /bootifull-15-site

security:
  oauth2:
    client:
      clientId: client
      clientSecret: client-secret
      accessTokenUri: http://localhost:9090/auth/oauth/token
      userAuthorizationUri: http://localhost:9090/auth/oauth/authorize
      auto-approve-scopes: ".*"
      registered-redirect-uri: http://localhost:9090/auth/singin
      clientAuthenticationScheme: form
    resource:
      jwk:
        key-set-uri: http://localhost:9090/auth/sign-key/public

Look at the key-set-uri in the yaml. The JWK keyId header will now be used by Spring to retrieve the correct key in the JWK endpoint.

SSO on the spring boot 2.1.x

The sample application is a very simple web application written in Kotlin. The basic configuration of the web application to benefit of the oauth2 login is very simple. The most important thing is to add oauth2Login():

@SpringBootApplication
class WebBootiful21xAppApplication

fun main(args: Array<String>) {
    runApplication<WebBootiful21xAppApplication>(*args)
}

@Controller
class IndexController {

    @GetMapping("/index")
    fun index(model: Model): String {
        val authentication = SecurityContextHolder.getContext().authentication
        model.addAttribute("user", authentication)
        return "index"
    }
}

@EnableWebSecurity
class SecurityConfig : WebSecurityConfigurerAdapter() {

    override fun configure(http: HttpSecurity) {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .oauth2Login()
    }
}

The most interesting thing here is the configuration of the client application and the provider. Spring Security let you use predefined providers like Google, Okta, but of course it is possible to register your own oauth2 provider like this:

server.use-forward-headers: true
server.servlet.context-path: /bootifull-21-site

spring.security.oauth2.client.registration.client.client-id: client
spring.security.oauth2.client.registration.client.client-secret: client-secret
spring.security.oauth2.client.registration.client.client-name: Federated SSO
spring.security.oauth2.client.registration.client.provider: my-oauth-provider
spring.security.oauth2.client.registration.client.scope[0]: read
spring.security.oauth2.client.registration.client.scope[1]: write
spring.security.oauth2.client.registration.client.scope[2]: trust
spring.security.oauth2.client.registration.client.redirect-uri: http://localhost:8080/bootifull-21-site/login/oauth2/code/client
spring.security.oauth2.client.registration.client.client-authentication-method: basic
spring.security.oauth2.client.registration.client.authorization-grant-type: authorization_code

spring.security.oauth2.client.provider.my-oauth-provider.authorization-uri: http://localhost:9090/auth/oauth/authorize
spring.security.oauth2.client.provider.my-oauth-provider.token-uri: http://localhost:9090/auth/oauth/token
spring.security.oauth2.client.provider.my-oauth-provider.user-info-authentication-method: header
spring.security.oauth2.client.provider.my-oauth-provider.jwk-set-uri: http://localhost:9090/auth/sign-key/public
spring.security.oauth2.client.provider.my-oauth-provider.user-name-attribute: userName
spring.security.oauth2.client.provider.my-oauth-provider.userInfoUri: http://localhost:9090/auth/user-info

Here we have to configure two things: client applications and providers. Basically this makes it simple to register more client applications and authentication/authorization providers. In previous Spring security versions, it was mandatory to customize many components. In the modern Spring versions (5.x) we are able to configure authentication providers and client applications in a way that is simple and managed by the framework.

In the configuration, note that we are using as authorization-grant-type the authorization_code that is the OAuth2 flow used during the authentication. In the provider configuration it is mandatory to configure some endpoints. Most of those are the same of the legacy Spring Boot 1.5.x application. The main difference is that in spring cloud security we can choose between opaque or JWT token and many other customization. Starting from spring security 5.x, only JWT tokens are supported. It is not possible to set the provider or client application in order to use only the token info and don’t configure the userInfoUri. In a legacy Spring Boot application in case of JWT we can choose to pass the public key, symmetric key or configure a JWK uri for the token validation. In our sample we choose to configure a JWK set uri. Even if it can appear at first look less flexible respect to the past, actually this is a choice in order to be more complaint with the standard. In the OAuth2 provider landscape use JWK as means to exchange key is a very common way to do that, in this way the configuration supports OAuth2 and OpenId Connect protocols very well.

For the complete documentation please refers to official Spring Security documentation

Conclusion

The newest Spring Boot 2.1.x has a very elegant way to build OAuth2 login and resource server. This configuration style is on track with the classical Spring Security configuration. The DSL is very rich and allows to configure many aspects. The future of Spring Security goes toward enabling OAuth2 and OpenId Connect directly into the core security and in this post I’ve discussed all the basic steps needed to build an authorization server capable to work with our legacy and more recent applications. This gives us the time to update our application to a Spring Boot 2.1.x base line, enabling us to migrate to jdk 11.

For the interested users the code is available here


Read next

SwiftUI and the Text concatenations super powers

SwiftUI and the Text concatenations super powers

fabrizio_duroni
fabrizio duroni
marco_de_lucchi
marco de lucchi

Do you need a way to compose beautiful text with images and custom font like you are used with Attributed String. The Text component has everything we need to create some sort of 'attributed text' directly in SwiftUI. Let's go!!! [...]

A Monorepo Experiment: reuniting a JVM-based codebase

A Monorepo Experiment: reuniting a JVM-based codebase

luigi_noto
luigi noto

Continuing the Monorepo exploration series, we’ll see in action a real-life example of a monorepo for JVM-based languages, implemented with Maven, that runs in continuous integration. The experiment of reuniting a codebase of ~700K lines of code from many projects and shared libraries, into a single repository. [...]