Skip to content

Mixing old style Spring Security @EnableAuthorizationServer and @EnableResourceServer) with new style Spring Security 5 clients

Notifications You must be signed in to change notification settings

kjq/springboot-oauth2-with-spring5-client

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 

Repository files navigation

Mixing old style Spring Security @EnableAuthorizationServer and @EnableResourceServer) with new style Spring Security 5 clients

We wanted to provide an Authorization Server using OAuth2 and JWT.

We decided to use Spring because of the integration with the rest of the security mechanisms already available. Other options we strongly considered for ease were Apache CXF (it just works) OAuth2 implementation and Shiro with manually configured filters (has not been updated in ages but we will end up using for permissions).

For clients, we wanted to take advantage of the new Spring Security 5 APIs. They communicate with the Authorization server either using OAuth2 proper or JWT.

The approach below allows for:

  • Authenticating through an OAuth2 login
  • Authenticating through POSTman against the API directly using a Bearer token

This is not complete and very rough but it works and covers all of the scenarios...

Authorization Server and Resource Server using Spring Boot Security (old Authorization/Resource style)

Spring Authorization (@EnableAuthorizationServer) and Resource (@EnableResource) server using the old style API (shim or whatever you want to call it) because it has not been updated for the new Spring Security 5 API fully.

In order for newer clients to take use this server easily we had to additionally expose the "well known" JSON (http://localhost:8080/.well-known/jwks.json) with the public key so that new style Spring Security5/OAuth2 clients could access it.

One thing to note was I wrestled with the order of the filters so I explicitly defined them using the @Order annotation.

import static com.google.common.collect.Lists.newArrayList;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
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.AuthorizationServerEndpointsConfiguration;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerSecurityConfiguration;
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.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.AccessTokenConverter;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
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 org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import javax.inject.Inject;
import java.security.KeyPair;

@Slf4j
@Configuration
public class OAuthServerConfiguration
{
    @Inject
    void globalUserDetails(final AuthenticationManagerBuilder auth, PasswordEncoder passwordEncoder) throws Exception
    {
        auth.inMemoryAuthentication()
            .withUser("user")
            .password(passwordEncoder.encode("pass"))
            .roles("USER").and()
            .withUser("admin")
            .password(passwordEncoder.encode("admin"))
            .roles("ADMIN");
    }

    @Bean
    JwtAccessTokenConverter jwtAccessTokenConverter(final KeyPair keyPair)
    {
        final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setKeyPair(keyPair);
        return converter;
    }

    @Bean
    AuthenticationProvider jwtAuthenticationProvider()
    {
        return new JwtAuthenticationProvider();
    }

    @Bean
    KeyPair jwtKeyPair()
    {
        final KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(
            new ClassPathResource(KEY_STORE),
            PRIVATE_KEY_SECRET.toCharArray());
        return keyStoreKeyFactory.getKeyPair(PRIVATE_KEY_ALIAS);
    }

    @Bean
    TokenStore jwtTokenStore(JwtAccessTokenConverter accessTokenConverter)
    {
        return new JwtTokenStore(accessTokenConverter);
    }

    @Bean
    @Primary
    DefaultTokenServices oauthTokenServices(final TokenStore tokenStore)
    {
        final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore);
        return defaultTokenServices;
    }

    @Order(2)
    @Configuration
    @EnableResourceServer
    @Import(AuthorizationServerEndpointsConfiguration.class)
    protected static class AuthorizationServerConfiguration extends AuthorizationServerSecurityConfiguration
    {
    }

    @Configuration
    protected static class OAuthAuthorizationConfiguration extends AuthorizationServerConfigurerAdapter
    {
        @Inject
        private AuthenticationManager authenticationManager;

        @Inject
        private AccessTokenConverter accessTokenConverter;

        @Inject
        private TokenStore tokenStore;

        @Inject
        private PasswordEncoder passwordEncoder;

        @Override
        public void configure(final ClientDetailsServiceConfigurer clients) throws Exception
        {
            clients.inMemory()
                   .withClient("foobar")
                   .authorities("ROLE_TEST")
                   .resourceIds("audience")
                   .secret(passwordEncoder.encode("secret"))
                   .autoApprove(false)
                   .accessTokenValiditySeconds(3600)
                   .refreshTokenValiditySeconds(2592000)
                   .authorizedGrantTypes(
                       "implicit",
                       "password",
                       "authorization_code",
                       "client_credentials",
                       "refresh_token")
                   .scopes(
                       "foo",
                       "read",
                       "write")
                   .redirectUris(
                       "http://localhost:8080/api/test",
                       "http://localhost:8090/api/test",
                       "http://localhost:8080/authorize/oauth2/code/my-client",
                       "http://localhost:8090/login/oauth2/code/my-client",
                       "http://localhost:8080/login/oauth2/code/my-client");
        }

        @Override
        public void configure(final AuthorizationServerSecurityConfigurer oauthServer)
        {
            oauthServer
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()")
                .allowFormAuthenticationForClients();
        }

        @Override
        public void configure(final AuthorizationServerEndpointsConfigurer endpoints)
        {
            //Create our token enhancer chaing
            final TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
            tokenEnhancerChain.setTokenEnhancers(newArrayList(
                new OAuthTokenEnhancer(),
                (TokenEnhancer)accessTokenConverter));

            //Configure the endpoints
            endpoints
                .tokenStore(tokenStore)
                .tokenEnhancer(tokenEnhancerChain)
                .authenticationManager(authenticationManager);
        }
    }

    @Configuration
    protected static class OAuthResourceConfiguration extends ResourceServerConfigurerAdapter
    {
        @Inject
        private AuthenticationManager authenticationManager;

        @Inject
        private TokenStore tokenStore;

        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception
        {
            resources.tokenStore(tokenStore)
                     .authenticationManager(this.authenticationManager);
        }
    }

    @Order(1)
    @Configuration
    @EnableWebSecurity
    protected static class OAuthWebSecurityConfig extends WebSecurityConfigurerAdapter
    {
        @Inject
        private AuthenticationProvider jwtAuthenticationProvider;

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

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception
        {
            auth.authenticationProvider(jwtAuthenticationProvider);
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception
        {
            http.requestMatchers()
                .antMatchers("/login",
                             "/logout",
                             "/oauth/authorize",
                             "/oauth/confirm_access",
                             "/favicon.ico",
                             WELL_KNOWN_URI)
                .and()
                .authorizeRequests()
                .antMatchers("/login",
                             "/logout",
                             "/oauth/authorize",
                             "/oauth/confirm_access",
                             "/favicon.ico",
                             WELL_KNOWN_URI).permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().permitAll()
                .and()
                .logout().permitAll();
        }
    }
}

JWK Set "WellKnown" Endpoint

New Spring OAuth clients need the jwk-set-uri property set in order to access this. It exposes the public key to the clients.

@FrameworkEndpoint
class JksSetController
{
    @Inject
    private KeyPair keyPair;

    @ResponseBody
    @GetMapping(WELL_KNOWN_URI)
    public Map<String, Object> getKey(Principal principal)
    {
        final RSAPublicKey rsaPublicKey = (RSAPublicKey)this.keyPair.getPublic();
        final RSAKey key = new RSAKey.Builder(rsaPublicKey)
            .keyUse(new KeyUse("sig"))
            .algorithm(new Algorithm("RS256"))
            .build();
        return new JWKSet(key).toJSONObject();
    }
}

JWT Authentication Provider to receive the token on the Authorization side

I needed to create and inject a custom authentication provider to understand the JWT token coming in from the client. At this point, the intent is to get any pertinent details and return them back through the endpoint (if needed). When using the JWT by itself we don't even need (or make) any call as everything is self-contained.

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;

public class JwtAuthenticationProvider implements AuthenticationProvider
{

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException
    {
        final UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
            authentication.getPrincipal(),
            authentication.getCredentials(),
            null);
        return token;
    }

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

OAuth2 Client, Login, and Resource Server (using new Spring Security 5 style)

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Slf4j
@Configuration
public class OAuthClientConfiguration extends WebSecurityConfigurerAdapter
{
    @Override
    public void configure(final HttpSecurity http) throws Exception
    {
            http.csrf().disable()
                .cors()
                .and()
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .oauth2Login()
                .and()
                .oauth2Client()
                .and()
                .oauth2ResourceServer()
                .jwt();

            log.info("Enabled authorized requests...");
    }
}

OAuth2 Client, Login, and Resource Configuration (using new Spring Security 5 style)

  security:

    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: http://localhost:8080/.well-known/jwks.json

      client:
        registration:
          my-client:
            provider: my-provider
            client-id: foobar
            client-secret: secret
            client-name: My Client
            client-authentication-method: basic
            redirect-uri-template: '{baseUrl}/{action}/oauth2/code/{registrationId}'
            authorization-grant-type: authorization_code

        provider:
          my-provider:
            authorization-uri: http://localhost:8080/oauth/authorize
            token-uri: http://localhost:8080/oauth/token
            jwk-set-uri: http://localhost:8080
            user-info-uri: http://localhost:8080/userinfo
            user-info-authentication-method: header
            user-name-attribute: name

cURL commands from POSTMan

Client Credentials Grant

curl -X POST \
  'http://foobar:secret@localhost:8080/oauth/token?grant_type=client_credentials' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -H 'cache-control: no-cache'

Password Grant

curl -X POST \
  'http://foobar:secret@localhost:8080/oauth/token?grant_type=password&username=user&password=pass' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -H 'cache-control: no-cache' \

Implicit Grant

curl -X GET \
  'http://localhost:8080/oauth/authorize?client_id=foobar&grant_type=implicit&response_type=token&redirect_uri=http://localhost:8080/' \
  -H 'Content-Type: application/json' \
  -H 'cache-control: no-cache'

Test Call using Bearer

curl -X GET \
  http://localhost:8090/api/test \
  -H 'Authorization: Bearer {TOKEN}' \
  -H 'cache-control: no-cache'

POM Dependencies

Using the latest Spring Boot (at the time): 2.1.3.RELEASE

Authorization Server (also includes Resource dependencies below as-well)

 <dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
</dependency>

Resource Server (client)

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>

About

Mixing old style Spring Security @EnableAuthorizationServer and @EnableResourceServer) with new style Spring Security 5 clients

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published