OAuth 2 and Spring Boot 2

The grant types/ flows

First, we will describe some grant types and in the next point we will see how to implement them with Spring Boot 2

The implicit flow / The implicit grant

Steps :

1) the client requests /oauth/authorize on the authorization server such as :

http://foo-oauth-server/oauth/authorize?response_type=token&client_id=fooClientId&state=fooState&redirect_uri=fooRedirectUri&scope=read%20write

If the user is not logged, the authorization server will first invite one to log in.
Then the authorization request is processed by asking to the user whether ones’ approval about the use of one’s resources by the client application.

2) If the user approves the request, the authorization servers generates a token and returns it to the URI specified in the redirect_uri parameter.
The token and some additional information are added in the location part of the URL such as :

http://redirectUriFoo/#access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJqb2huIiwic2NvcGUiOlsiYmFyIiwiZm9vIiwicmVhZCIsIndyaXRlIl0sIm9yZ2FuaXphdGlvbiI6ImpvaG5ObVpSIiwiZXhwIjoxNTcyNjAyOTQ0LCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiNGEyYTgyZDItM2NiMS00ZjU4LTg1N2YtOTdkZTY5NDFkMTVlIiwiY2xpZW50X2lkIjoic2FtcGxlQ2xpZW50SWQifQ.52gX1iEeUipDEghaqKI4bcHxeAmHMuPhGTaZZDwC0Jw&token_type=bearer&state=6lEvtqnlUJh9cqBABnDxNbj7o5tztAEfvrdvL1ot&expires_in=3599&jti=4a2a82d2-3cb1-4f58-857f-97de6941d15e

3) The client may so invoke the api/resource server by providing to that the token.
The header of the request has to contain the Authorization header with the token as value such as : Authorization: Bearer fooToken

The authorization code grant

Steps :

1) Similarly to the implicit grant, the client requests /oauth/authorize on the authorization server such as :

http://foo-oauth-server/oauth/authorize?response_type=code&client_id=fooClientId&redirect_uri=fooRedirectUri

These 3 parameters are required. We notice a single difference : the response_type is code
. Note that here the state and the scopes are not valued but we could have done the same thing in cthe previous flow (implicit)
Indeed, with implicit and authorization code grants, these two parameters are not required but note that is a good practice (cross site protection for the one and specify clearly the client requirements for the other one) to value them.
Then as previously, the authorization server will first invite one to log in and then the authorization request is processed.

2) If the user approves the request, the authorization servers generates a code and not a token and returns it to the URI specified in the redirect_uri parameter.
Here just a code is returned (no additional information are added) and that is added as a query parameter of the redirect_uri returned by the server such as :

http://redirect_uri/?code=oFGcwL

3) To get the token, the client has to exchange it with the authorization server.
It has to be done via a request on the /oauth/token endpoint on the authorization server.
That request has to use the POST method and specify in the header two information :
Content-type: application/x-www-form-urlencoded and Authorization with the Basic mode. For the last one, it means that we expect as value : clientId:clientSecretencoded in base 64.
As alternative to basic authentication, we can value clientId and password in the request body.
Additionally, we need to pass 3 params/values in the body of the request : grant_type, redirect_uri and code such as :

grant_type=authorization_code&redirect_uri=fooRedirectUri&code=fooCode


The server return as response something like that :

{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJqb2huIiwic2NvcGUiOlsiZm9vIiwicmVhZCIsIndyaXRlIl0sIm9yZ2FuaXphdGlvbiI6ImpvaG5pbGd0IiwiZXhwIjoxNTcyNjEwNjU1LCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiMTc0OGFmY2QtMWI3OS00MjQ4LTg3ZmUtN2JhNGVkZmU0N2M3IiwiY2xpZW50X2lkIjoiZm9vQ2xpZW50SWRQYXNzd29yZCJ9.BaU8wtS9RHFXn3cZuamdwtXg_aExqlIoLJ_fQa0K_E8","token_type":"bearer","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJqb2huIiwic2NvcGUiOlsiZm9vIiwicmVhZCIsIndyaXRlIl0sIm9yZ2FuaXphdGlvbiI6ImpvaG5pbGd0IiwiYXRpIjoiMTc0OGFmY2QtMWI3OS00MjQ4LTg3ZmUtN2JhNGVkZmU0N2M3IiwiZXhwIjoxNTc1MTk5MDU1LCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiMGRkYzk3YzEtN2FiMy00ZWQwLWI5MDctMWJmM2Q4MTVkZTkzIiwiY2xpZW50X2lkIjoiZm9vQ2xpZW50SWRQYXNzd29yZCJ9.4C4Zz1VEltRe_4qWbIlkrMn-bZjIEnALrQka1UfSFBw","expires_in":3599,"scope":"api1.read api2.write","jti":"1748afcd-1b79-4248-87fe-7ba4edfe47c7"}

4) The client can so use the token.
It is exactly as in the implicit part : we fill requests to the api with the Authorization header valued with the token.

The client credentials grant

Steps :

1) the client authenticates and requests a token in a single step via /oauth/token on the authorization server.
That step is very similar to the step of the exchange token in the authorization code grant.
All is identical but one thing :
– here we don’t need to pass 3 params/values in the body of the request : grant_type, redirect_uri and code but only 1 : grant_type such as :

grant_type=client_credentials

That is rather expected : we don’t have any intermediary code in that grant, as well as we don’t want to redirect anywhere since the client credentials grant is for machines accesses, not user accesses.
2) The machine client can so use the token.
It is exactly as in the implicit (described in detail above) or the authorization code part : we fill requests to the api with the Authorization header valued with the token.

EndPoints

– check the token validity : /oauth/check_token/?token=fooToken
– authorize a client : /oauth/authorize
– exchange the code for a token (auth.code way) or get a token (client creds way) : /oauth/token

How to implement OAuth 2 with Spring Boot ?

The token store

Creating a token from the authorization server makes sense only if it can also assert that a received token from a client is valid and with the required scope.
In OAuth2,  some types of tokens  can hold themselves the authentication data (user,client, grants…) such as JWT token and others are just reference to a record in the database that we need to lookup. 
We can see that in spring-oauth2, we also have multiple flavors as token store.
The TokenStore interface represents the Persistence interface for OAuth2 tokens 
and the library provides multiple implementations :

– InMemoryTokenStore : stores the token in memory of the server. Only to use in dev because that is very restrictive in terms of recovery, scalability and data distribution.

– JwkTokenStore : provides support for verifying the JSON Web Signature (JWS) for a JSON Web Token (JWT) using a JSON Web Key (JWK).
This TokenStore implementation is exclusively meant to be used by a Resource Server as it’s sole responsibility is to decode a JWT and verify it’s signature (JWS) using the corresponding JWK.
That implementation requires no token storage (advantage) but has also some limitations : usable only by resource servers and doesn’t support many operations (storeAccessToken(), removeAccessToken(), findTokensByClientId()…).
When you use a centralized authentication/authorization server as Okta, using that option makes sense because that is light and enough from the client side.

– JwtTokenStore : that just reads data from the tokens themselves. Not really a store since it never persists anything, and methods like getAccessToken(OAuth2Authentication) always return null. But nevertheless a useful tool since it translates access tokens to and from authentications. 
Beware : if you use this implementation you is very encouraged to make the authorization server and the clients to use a signing key for the JWT (close enough to the JwkTokenStore). The authorization server should generate the  private/public keys and sharing the public one with the resource servers.

– JdbcTokenStore : stores the token in a database. Supports all operations.

Two things to setup : Authentication and Authorization

Spring or any other way, we should not forget that oauth 2 is not an authentication protocol, that is a authorization protocol.
It means that we should first setup the authentication layer and that is powered by the simple/traditional Spring Security library.
Once the users (or resource owners in the OAuth library terminology) authenticated, comes the authorization part. And to achieve that we setup the suitable Spring OAuth configuration.
Materially, in Spring Boot 2, the spring-security-oauth2 library pulls the basic spring-security libraries :

] +- org.springframework.security.oauth:spring-security-oauth2:jar:2.3.5.RELEASE:compile
[INFO] |  +- org.springframework:spring-context:jar:5.1.5.RELEASE:compile
[INFO] |  +- org.springframework.security:spring-security-core:jar:5.1.4.RELEASE:compile
[INFO] |  +- org.springframework.security:spring-security-config:jar:5.1.4.RELEASE:compile
[INFO] |  +- org.springframework.security:spring-security-web:jar:5.1.4.RELEASE:compile

Libraries required

Here the dependencies required both for the authorization and the authentication part:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>${spring-security-oauth2.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
            <version>${spring-security-jwt.version}</version>
        </dependency>

Two of these dependencies are not managed by Spring Boot, so we need to set version for them as stated in the official documentation :
To use the auto-configuration features in this library, you need spring-security-oauth2, which has the OAuth 2.0 primitives and spring-security-oauth2-autoconfigure. Note that you need to specify the version for spring-security-oauth2-autoconfigure, since it is not managed by Spring Boot any longer, though it should match Boot’s version anyway.
For JWT support, you also need spring-security-jwt.

The authorization server implementation

That part setup the OAuth authorization part but we could also setup the authentication part along that, that is inside the same physical project and deployed application.
Here the authentication configuration :

package com.davidxxx.resource.config;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
  @Autowired
  private BCryptPasswordEncoder passwordEncoder;
 
  @Override
  protected void configure(final HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .anyRequest().authenticated()
        .and().formLogin().permitAll()
        .and().csrf().disable();
  }
 
  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
        .withUser("david").password(passwordEncoder.encode("123")).roles("USER").and()
        .withUser("scoubi").password(passwordEncoder.encode("123")).roles("ADMIN");
  }
 
  @Override
  @Bean
  public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
  }
 
}

And here the authorization part :

package com.davidxxx.resource.config;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
 
import java.util.Arrays;
 
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
 
  private static final int ACCESS_TOKEN_VALIDITY_SECONDS = 3600;
  private AuthenticationManager authenticationManager;
  private BCryptPasswordEncoder passwordEncoder;
 
  public OAuth2AuthorizationServerConfig(AuthenticationManager authenticationManager,
                                         BCryptPasswordEncoder passwordEncoder) {
    this.authenticationManager = authenticationManager;
    this.passwordEncoder = passwordEncoder;
  }
 
  @Override
  public void configure(final AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
    oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess("permitAll()");
  }
 
  @Override
  public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {  
    clients.inMemory()
           .withClient("appClientOne")
           .scopes("resourceFoo.read", "resourceFoo.write")
           .authorizedGrantTypes("implicit")
           .autoApprove(false)
           .accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS)
           .redirectUris("http://localhost:8086/")
           .and()
 
           .withClient("appClientTwo")
           .secret(passwordEncoder.encode("secret"))
           .authorizedGrantTypes("authorization_code")
           .scopes("resourceFoo.read", "resourceFoo.write")
           .accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS)
           .redirectUris("http://localhost:8089/")
           .and()
 
           .withClient("appClientMachine")
           .secret(passwordEncoder.encode("machine"))
           .authorizedGrantTypes("client_credentials")
           .scopes("resourceBar.read")
           .accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS);
  }
 
  @Override
  public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    final TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
    tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), accessTokenConverter()));
    endpoints.tokenStore(tokenStore())
             .tokenEnhancer(tokenEnhancerChain)
             .authenticationManager(authenticationManager);
  }
 
  @Bean
  public TokenStore tokenStore() {
    return new JwtTokenStore(accessTokenConverter());
  }
 
  @Bean
  public JwtAccessTokenConverter accessTokenConverter() {
    final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    converter.setSigningKey("19991");
    return converter;
  }
 
  @Bean
  public TokenEnhancer tokenEnhancer() {
    return new CustomTokenEnhancer();
  }
 
}

We defined 3 clients inconfigure(final ClientDetailsServiceConfigurer clients) to accept 3 clients with distinct allowed oauth 2 grant types : implicit, authorization code and client credentials.
And that’s all, our authorization and authentication application is ready to be deployed.

The foo resource server

It is a Rest API used by a SPA client (appClientTwo client) and that API uses itself resources from another resource server : the bar resource server via the appClientMachine client.
Here is the main configuration :

package com.davidxxx.resource.config;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
 
@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
 
  @Override
  public void configure(final HttpSecurity http) throws Exception {
    http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
        .and()
        .authorizeRequests().anyRequest().permitAll();
  }
 
  @Bean
  public TokenStore tokenStore() {
    return new JwtTokenStore(accessTokenConverter());
  }
 
  @Bean
  public JwtAccessTokenConverter accessTokenConverter() {
    final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    converter.setSigningKey("19991");
    return converter;
  }
 
}

To be able to annotate the protected methods of the api/resource by #oauth2.clientHasRole('ROLE_XXX'), we add this configuration :

package com.davidxxx.resource.config;
 
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
import org.springframework.security.oauth2.provider.expression.OAuth2MethodSecurityExpressionHandler;
 
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
 
    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        return new OAuth2MethodSecurityExpressionHandler();
    }
 
}

And globally, that is done.

The bar resource server

It is a Rest API used only by the foo resource server
Its configuration is the same as the foo resource server.foo

CURL Commands

….

Ce contenu a été publié dans Non classé. Vous pouvez le mettre en favoris avec ce permalien.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *