Wiring your own Authentication with SpringBootOAuth2

Stephen Schatzl
Just Eat Takeaway-tech
5 min readMar 1, 2019

So I am new to the Java “Spring” world and coming from the Node.js galaxy. I have noticed differences between the communities. To Java lovers, I am sorry, but your documentation is a bit tedious and let's just say I feel old simply reading it. Despite the “wordy” documentation, I have come to understand how powerful this framework can be.

Our Problem

We wanted to use the “state of the art” OAuth2 Spring Boot Starter. Allowing us to maintain as little as possible as a team and focus on development. When approaching this, we ran into the problem of mostly terminology. So let me be very clear, the terminology here is very particular. We wanted all the default jazz but of course, we wanted some custom stuff, who doesn't right? Well, yes this became the problem we wanted to override Spring Security Authentication in the SpringBootOAuth2 Starter.

https://spring.io/guides/topicals/spring-security-architecture/

For us, the most important detail when using spring security was the AuthenticationManager, which provides only one method for authenticating the user. By default, Spring Boot autoconfigures a specific AuthenticationManager for each Application Context.

Our other services depend on a custom token that we need to use in order to authenticate our users. Therefore, we decided to implement the AuthenticationProvider interface allowing us to wire our authentication to our LegacyTokenService. LegacyTokenService allows us to provide our own proprietary authentication logic.

In order to obtain an OAuth token, you must hit the Spring Boot default /oauth/token endpoint. For example with the following payload.

curl -X POST \
http://localhost:8089/oauth/token \
-H ‘Content-Type: application/x-www-form-urlencoded’ \
-d grant_type=password&client_id=client&username=EncryptedUserID&password=EncryptedLegacyToken&client_secret=yourClientSecret

http://files.meetup.com/6015342/Spring%20Toronto%20-%20Joe%20Grandja.pdf

Spring Boot’s first step will be to deserialize the request to /oauth/token and put the username and password into an Authentication Principal Object. Please note the above diagram. Afterwards, the ProviderManager goes through all authentication providers and tries to authenticate using one of them. Below is our implementation.

@Service
public class TokenAuthenticationProvider implements AuthenticationProvider {

private LegacyTokenService legacyTokenService;

public TokenAuthenticationProvider(LegacyTokenService legacyTokenService) {
this.legacyTokenService = legacyTokenService;
}

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String userId = authentication.getName();
String legacyToken = authentication.getCredentials().toString();

if (legacyToken != null && userId != null && legacyTokenService.validateTokenAndUserId(legacyToken, userId)) {
return new UsernamePasswordAuthenticationToken(userId, legacyToken, Arrays.asList(new SimpleGrantedAuthority("ROLE_USER")));
} else {
throw new BadCredentialsException("");
}
}

@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}

}

The ProviderManager first tests whether the AuthenticationProvider is able to support() the Authentication Request, if yes it will use the authenticate method to authenticate payload in the request.

We had lots of problems when overriding the WebSecurityConfigurerAdapter and the AuthorizationServerConfigurerAdapter. Again, because of similar terminology.

Below is our WebSecurityConfigurerAdapter configuration.

  • The client secret comes from the request body client_secret
  • AuthenticationManager Provides the default Authentication Manager
  • HttpSecurity — we disabled some default settings
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder(4);
}

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

@Override
protected void configure(HttpSecurity http) throws Exception {
http.anonymous().disable();
http.csrf().disable();
}
}

Below is our AuthorizationServerConfigurerAdapter configuration.

ClientDetailsServiceConfigurer configure clients e.g. mobile, frontend, integration layer, etc…

  • .withClient(“client”) consumes client_id from request body
  • .authorizedGrantTypes(grant_types) Sets specific grant types, by password(yourLegacyToken)
  • .authorities(“USER”) TokenAuthenticationProvider sets this with ROLE_USER
  • scopes(“read”)
  • .secret(yourClientSecret) from your application.properties file
  • .accessTokenValiditySeconds(60) set how long JWT tokens are valid
  • .autoApprove(true) To kill default radiobox approval page

AuthorizationServerEndpointsConfigurer configures the security of the default oauth endpoints e.g. /oauth/token

AuthenticationConfiguration contains our TokenAuthenticationProvider which again is used to wire our LegacyTokenService logic to authentic users with a legacy encrypted token and a legacy encrypted user-id.

AuthorizationServerSecurityConfigurer extends default settings of WebSecurity to allow form login and disable default Spring Web Security.

We set the root Authentication Manager, which is automatically configured.

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {

private AuthenticationConfiguration authenticationConfiguration;

@Value("${your_client_secret")
private String yourClientSecret;

@Value("${your_security.jwt.signingkey}")
private String yourSigningKey;


@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client")
.authorizedGrantTypes("password")
.authorities("USER")
.scopes("read")
.secret(yourClientSecret)
.accessTokenValiditySeconds(120)
.autoApprove(true);
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(this.authenticationConfiguration.getAuthenticationManager())
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST).tokenStore(tokenStore())
.accessTokenConverter(accessTokenConverter());
}

@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
}

@Autowired
public void setAuthenticationConfiguration(AuthenticationConfiguration authenticationConfiguration) {
this.authenticationConfiguration = authenticationConfiguration;
}

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

@Bean
public JwtAccessTokenConverter accessTokenConverter() {

JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(yourSigningKey);
return converter;

}

@Bean
@Primary
public DefaultTokenServices tokenService() {

DefaultTokenServices defaultTokenServices = new DefaultTokenServices();

defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}

}

The authorization server part is complete now. In our case, we used the authorization server as an independent component.

The second part of this process is to configure your resource component. In your component, you must add a configuration which extends ResourceServerConfigurerAdapter and @EnableResourceServer is important to add at the top. Here, the most important part is setting up your tokenStore() using the out of the box OAuth2 server. Not only is the token token store important but configuring the JWT converter to be signed with your signing key.

@Configuration
@EnableResourceServer
public class ResourceConfig extends ResourceServerConfigurerAdapter {

/**
* Resource Configuration for Oauth Server
*/

@Value("${security.jwt.signingkey}")
private String signingkey;

@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenServices(tokenService());
}

@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anonymous()
.antMatchers("/whatever/here")
.and()
.authorizeRequests().antMatchers("/**").authenticated()
.and();
}

@Bean
@Primary
public DefaultTokenServices tokenService() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
return defaultTokenServices;
}

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

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(signingkey);
return converter;
}

}

To sum it up, Spring Boot is very robust you just need to know how to tackle the default auto configurations it sets on setup. This is the first step to authorizing a client and authenticating user credentials. As they say, don’t write your own encryption and you will have fewer problems. Remember to read the OAuth2 spec and see what method works for your specific use case. We found tutorials and documentation confusing when trying to provide our own authentication of tokens using the standard, out-of-the-box SpringBootOAuth2 Starter defaults. Hopefully, this article will save you some time if you’re trying to do the same.

Helpful Info:

Spring OAuth2 Blog:

Let us know what you think, drop a comment below! Also, make sure to check out our careers page to discover Tech jobs at Takeaway.com!

--

--

Stephen Schatzl
Just Eat Takeaway-tech

“This was another of our fears: that Life wouldn’t turn out to be like Literature.” ― Julian Barnes, The Sense of an Ending