Spring Boot OAuth2 workflow behind Zuul for internal client authorization

Lukasz Frankowski
8 min readApr 14, 2018

--

Photo by Danielle Rice on Unsplash

This article describes how to create Spring Boot application with oauth2 authorization using password grant type. This grant type is appropriate for internal clients which we trust to get username and password from the user, like for example internal web UI or native mobile app.

Spring Security OAuth2: glitchy, obscure (dozen of different points to touch to configure everything together), badly documented (no single comment on configuration classes, very poor docs) and unalterable ( private, final, they even don't use spring dependency injection concepts and prefer instantiation of key classes using their constructors in private methods). This is the second worst project from Spring I've seen after Spring Webflow.

Furthermore there’s just lack of good examples. There’s a huge development in Spring Boot recently (2.0.0 released) and the most of examples I’ve found about Spring Boot + OAuth2 are already outdated and don’t work. Moreover people focus on things apparently different from those I need. I’ve found tons of examples how to authenticate to your app with google or facebook. Another huge package of examples are authorization server implementations going through the access code flow with approval screen. Apparently almost nobody wants to create application with standard web UI authenticated to oauth2 server with standard username and password with only optional access code flow for external clients.

Honorable mentions

Before begin I’d like to mention the only two resources worthwhile reading I’ve found. One is OAuth 2 Developers Guide which explains not much but you can treat this as a short list of hook points. The second one is very good Spring Security and Angular article. However if you print this article to PDF you’ll see it has over 100 pages, while OAuth2 is a really simple flow. The capacity of this article in my opinion shows how difficult is to explain Spring OAuth2 implementation, while oauth2 itself is meant to be simple as possible.

The goal

In our journey we will try to go step by step through configuring Spring OAuth2 for the following case: we want to have an application with (possibly) multiple internal clients for which it’s safe for user to enter his password, what should be accomplished by password oauth2 grant type. These clients are our applications, like standard web UI or mobile clients. For these client, especialy the web UI, we want the application to behave like standard session-based application which users are used to. This means in particular automatic logout after 30 min.

Building authorization server

Let’s start with the book of building authorization server from scratch:

@SpringBootApplication
@EnableAuthorizationServer
public class OAuthASApplication implements AuthorizationServerConfigurer {

@Bean
public UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(

User.withDefaultPasswordEncoder()
.username("user")
.password("user")
.authorities("ROLE_USER")
.build()

);
}

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

clients.inMemory()

.withClient("internal")
.secret("internal_secret")
.scopes("account", "contacts", "internal")
.resourceIds()
.authorizedGrantTypes("password", "refresh_token")
.autoApprove(true)
.accessTokenValiditySeconds(10*60)
.refreshTokenValiditySeconds(30*60);

}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.userDetailsService(userDetailsService());
}

public static void main(String[] args) {
SpringApplication.run(OAuthASApplication.class, args);
}

}

What we do here is basically:

  1. In userDetailsService() we create example user details service providing users and their passwords. This bean usually uses some database, but in this example we use in-memory implementation. We create here a single user with ROLE_USER authority.
  2. in configure(ClientDetailsServiceConfigurer) we create oauth2 internal clients:
  • With the name: internal.
  • With the internal_secret password required to access /oauth/token endpoint (with username: internal taken from client name).
  • This client allows to access some hypothetical oauth2 scope indicated by scopes() method.
  • This client supports two grant types in authorizedGrantTypes(): password - returns the access token after explicit username and password is given; refresh_token - allows to get new access token using refresh token.
  • This client won’t display approval screen to get access to selected oauth2 scopes, but autoApprove-s all his scopes.
  • The access token will expire after 10 mins (accessTokenValiditySeconds) while the refresh token will expire after 30 mins (refreshTokenValiditySeconds). The refreshTokenValiditySeconds reflects standard session cookie expiration time for session-based web apps.
  1. In configure(AuthorizationServerEndpointsConfigurer) we just configure previously created UserDetailsService to be used with authorization endpoint.

Let’s now try how does it work:

curl -X POST \
http://localhost:8080/as/oauth/token \
-H 'Authorization: Basic aW50ZXJuYWw6aW50ZXJuYWxfc2VjcmV0' \
-H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' \
-F grant_type=password \
-F username=user \
-F password=user

Where:

  1. Authorization is a Basic Auth header with internal:internal_secret credentials.
  2. We want to use grant_type=password, i.e. get the access token after direct passing of username and password to the token endpoint.

What we get here is:

{
"error": "unsupported_grant_type",
"error_description": "Unsupported grant type: password"
}

After reading some docs it turns out configure(AuthorizationServerEndpointsConfigurer endpoints) metod needs authentication manager setup to support password grant type. However, AuthenticationManager is no longer accessible as a bean in Spring Security. This is really obscure as I discussed here. Anyway, to get it working we need to create yet another security configuration providing this bean:

@Configuration
public static class AuthenticationMananagerProvider extends WebSecurityConfigurerAdapter {

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

}

Now, having this bean we can add it to AuthorizationServerEndpointsConfigurer:

@Autowired protected AuthenticationManager authenticationManager;

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService());
}

And check if it works again with tha same CURL, and we get … the login page.

This is the point at which I got stuck for a longer time, but finally decided to do one trick, which is using @EnableResourceServer on authorization server configuration and creating simple HttpSecurity config. So, it looks the authorization server cannot work properly without resource server...

@SpringBootApplication
@EnableAuthorizationServer
@EnableResourceServer
public class OAuthASApplication implements AuthorizationServerConfigurer, ResourceServerConfigurer {

@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest()
.authenticated(); }

@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
}

}

There are two things making it really difficult to pass through. First one is you already have a place with HttpSecurity configuration in previously created:

@Configuration
public static class AuthenticationMananagerProvider extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
// [...]
}

}

However you can spend hundreds of hours on providing various configuration here, beat your head against the wall etc, and nothing works. The Spring team decided to provide a trap here, only for persevere people.

The second thing making it really difficult to jump over is that it still doesn’t work after this fix, because our previous CURL shows the following:

{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}

After another iteration of digging in the code, this time with trace debug level it turned out that Spring OAuth2 expects getting a password in encoded form of DelegatingPasswordEncoder, which consists of the encoder string and then the password encoded (for example: {bcrypt}$2a$10$Y[...]). This is weird because I'd need to encode this password in javascript on client side. Fortunately we can change it to default plain password (which is safe to be sent using SSL) with yet another security config (this is another AuthorizationServerConfigurer interface method):

@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.passwordEncoder(NoOpPasswordEncoder.getInstance());
}

With this fix this is amazing because you can get access token from the Spring OAuth2 at the very first time:

{
"access_token": "46a67de0-bf45-440e-a3d5-58aa0ee8f96e",
"token_type": "bearer",
"refresh_token": "04e1f577-8ab2-4504-887b-90c14b1ab5b7",
"expires_in": 597,
"scope": "account contacts internal"
}

At this exact moment you’re so happy you’ve got it, so “why haven’t I implemented whole this stuff myself in 2 hours or so” thought doesn’t even come to you mind.

Refreshing the token

When you implement the client for oauth2 authorization server you use the access token in Authorization: Bearer [token] header to authorize your requests. But you also always need to consider the access token expiration time. If it expires (in this examples after 10 mins) you need to get the new access token using refresh token:

curl -X POST \
http://localhost:8080/as/oauth/token \
-H 'Authorization: Basic aW50ZXJuYWw6aW50ZXJuYWxfc2VjcmV0' \
-H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' \
-F grant_type=refresh_token \
-F refresh_token=04e1f577-8ab2-4504-887b-90c14b1ab5b7

What you get is another access token you can use:

{
"access_token": "0203934c-4a91-422e-ac36-45123d9f347a",
"token_type": "bearer",
"refresh_token": "04e1f577-8ab2-4504-887b-90c14b1ab5b7",
"expires_in": 599,
"scope": "account contacts internal"
}

The problem here is that refresh token ( 04e1f577-8ab2-4504-887b-90c14b1ab5b7 in the example) is still the same. This is default behavior for external application access scenarios, where refresh token has a very long lifetime, for example 14 days, and once every 14 days you need to authorize the external application again. However, if you want to authorize native web client and simulate standard session cookie behavior, which is getting logged out 30 mins after last click, the constant refresh token will be a problem. Consider the user who logs in and then is using the app more than 30 mins: he will be unconditionally logged out after some time (30 min in the example) even if he is actively clicking in the app, because this is the lifetime of refresh token.

Better approach here is to use non-reusable refresh token, what can be configured in AuthorizationServerEndpointsConfigurer using reuseRefreshTokens() method:

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService())
.reuseRefreshTokens(false); // HERE
}

Now, after obtaining a new access token with the refresh token, the returning refresh token will be different and its timeout starts again. This way we can simulate standard web application behavior, and user will be logged out some time after his last click.

Of course there’s no simple solutions in Spring OAuth2 because in configure(ClientDetailsServiceConfigurer) we can for example configure another client (or even multiple clients dynamically loaded from db) which is appropriate to handle external client application accomplishing access_token grant type, and for which we may want to apply long-lived and non-reusable refresh token. Spring OAuth2 won't allow that because it encapsulated refresh token policy in one configure(AuthorizationServerEndpointsConfigurer) method applied for all oauth2 clients.

Have a lot of fun time hacking this :)

Getting the data from the local resource server

Having such a great thing like forced resource server in my authorization server I wouldn’t be myself to not to check how does it work to get the secured resource (this is the local invocation of the service which also implements the authorization server). I’m checking this with this simple controller:

@RestController
@RequestMapping("/")
public class HelloController {

@GetMapping("/hello")
public String hello() {
return "hello";
}

}

And Authorization: Bearer header using previously obtained access token:

curl -X GET \
http://localhost:8080/as/hello \
-H 'Authorization: Bearer 0203934c-4a91-422e-ac36-45123d9f347a'

Getting the expected reply:

hello

Getting the data from the remote resource server

In my GitHub repository there’s a simple microservice config comprising of Zuul Proxy at http://localhost:8080 which proxies two independent microservices:

  1. OAuth2 Authorization Server (and resource server) at http://localhost:8080/as.
  2. OAuth2 Resource Server at http://localhost:8080/rs.

The second resource server doesn’t know anything about authorization server, so hitting it with the same /hello endpoint as above:

curl -X GET \
http://localhost:8080/rs/hello \
-H 'Authorization: Bearer 0203934c-4a91-422e-ac36-45123d9f347a'

Gives the following results:

{
"error": "invalid_token",
"error_description": "Invalid access token: 0203934c-4a91-422e-ac36-45123d9f347a"
}

The remote solution is still to be done here, this is why I mark this subject with [todo]. For the purpose of this article I wanted to figure out something different than my solution in the current project (see below). The first step is the standard implementation doing calls from a remote service to the authorization server confirming access tokens. The second one is to use JWT-based implementation.

NOTE: In my current system implementation I synchronize all service tokens with shared token store using Redis.

Full sources from this post can be found on GitHub.

--

--

Lukasz Frankowski

Writing about software development, especially Java, architecture and microservices.