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:clientSecret
encoded 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
….