1. Authorization Server

Spring Security OAuth2 Boot simplifies standing up an OAuth 2.0 Authorization Server.

1.1 Do I Need to Stand Up My Own Authorization Server?

You need to stand up your own authorization server if:

  • You want to delegate the operations of sign-in, sign-out, and password recovery to a separate service (also called identity federation) that you want to manage yourself and
  • You want to use the OAuth 2.0 protocol for this separate service to coordinate with other services

1.2 Dependencies

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.

1.3 Minimal OAuth2 Boot Configuration

Creating a minimal Spring Boot authorization server consists of three basic steps:

  1. Including the dependencies.
  2. Including the @EnableAuthorizationServer annotation.
  3. Specifying at least one client ID and secret pair.

1.3.1 Enabling the Authorization Server

Similar to other Spring Boot @Enable annotations, you can add the @EnableAuthorizationServer annotation to the class that contains your main method, as the following example shows:

@EnableAuthorizationServer
@SpringBootApplication
public class SimpleAuthorizationServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(SimpleAuthorizationServerApplication, args);
    }
}

Adding this annotation imports other Spring configuration files that add a number of reasonable defaults, such as how tokens ought to be signed, their duration, and what grants to allow.

1.3.2 Specifying a Client and Secret

By spec, numerous OAuth 2.0 endpoints require client authentication, so you need to specify at least one client in order for anyone to be able to communicate with your authorization server.

The following example shows how to specify a client:

security:
  oauth2:
    client:
      client-id: first-client
      client-secret: noonewilleverguess
[Note]Note

While convenient, this makes a number of assumptions that are unlikely to be viable in production. You likely need to do more than this to ship.

That’s it! But, what do you do with it? We cover that next.

1.3.3 Retrieving a Token

OAuth 2.0 is essentially a framework that specifies strategies for exchanging long-lived tokens for short-lived ones.

By default, @EnableAuthorizationServer grants a client access to client credentials, which means you can do something like the following:

curl first-client:[email protected]:8080/oauth/token -dgrant_type=client_credentials -dscope=any

The application responds with a token similar to the following:

{
    "access_token" : "f05a1ea7-4c80-4583-a123-dc7a99415588",
    "token_type" : "bearer",
    "expires_in" : 43173,
    "scope" : "any"
}

This token can be presented to any resource server that supports opaque OAuth 2.0 tokens and is configured to point at this authorization server for verification.

From here, you can jump to:

1.4 How to Switch Off OAuth2 Boot’s Auto Configuration

Basically, the OAuth2 Boot project creates an instance of AuthorizationServerConfigurer with some reasonable defaults:

  • It registers a NoOpPasswordEncoder (overriding the Spring Security default)
  • It lets the client you provided use any grant type this server supports: authorization_code, password, client_credentials, implicit, or refresh_token.

Otherwise, it also tries to pick up a handful of beans, if they are defined — namely:

  • AuthenticationManager: For looking up end users (not clients)
  • TokenStore: For generating and retrieving tokens
  • AccessTokenConverter: For converting access tokens into different formats, such as JWT.
[Note]Note

While this documentation covers a bit of what each of these beans does, the Spring Security OAuth documentation is a better place to read up on its primitives

If you expose a bean of type AuthorizationServerConfigurer, none of this is done automatically.

So, for example, if you need to configure more than one client, change their allowed grant types, or use something better than the no-op password encoder (highly recommended!), then you want to expose your own AuthorizationServerConfigurer, as the following example shows:

@Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired DataSource dataSource;

    protected void configure(ClientDetailsServiceConfigurer clients) {
        clients
            .jdbc(this.dataSource)
            .passwordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
    }
}

The preceding configuration causes OAuth2 Boot to no longer retrieve the client from environment properties and now falls back to the Spring Security password encoder default.

From here, you may want to learn more about:

1.5 How to Make Authorization Code Grant Flow Work

With the default configuration, while the Authorization Code Flow is technically allowed, it is not completely configured.

This is because, in addition to what comes pre-configured, the Authorization Code Flow requires:

  • End users
  • An end-user login flow, and
  • A redirect URI registered with the client

1.5.1 Adding End Users

In a typical Spring Boot application secured by Spring Security, users are defined by a UserDetailsService. In that regard, an authorization server is no different, as the following example shows:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        return new InMemoryUserDetailsManager(
            User.withDefaultPasswordEncoder()
                .username("enduser")
                .password("password")
                .roles("USER")
                .build());
    }
}

Note that, as is typical of a Spring Security web application, users are defined in a WebSecurityConfigurerAdapter instance.

1.5.2 Adding an End-User Login Flow

Incidentally, adding an instance of WebSecurityConfigurerAdapter is all we need for now to add a form login flow for end users. However, note that this is where any other configuration regarding the web application itself, not the OAuth 2.0 API, goes.

If you want to customize the login page, offer more than just form login for the user, or add additional support like password recovery, the WebSecurityConfigurerAdapter picks it up.

1.5.3 Registering a Redirect URI With the Client

OAuth2 Boot does not support configuring a redirect URI as a property — say, alongside client-id and client-secret.

To add a redirect URI, you need to specify the client by using either InMemoryClientDetailsService or JdbcClientDetailsService.

Doing either means replacing the OAuth2 Boot-provided AuthorizationServerConfigurer with your own, as the following example shows:

@Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Bean
    PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    protected void configure(ClientDetailsServiceConfigurer clients) {
        clients
            .inMemory()
                .withClient("first-client")
                .secret(passwordEncoder().encode("noonewilleverguess"))
                .scopes("resource:read")
                .authorizedGrantTypes("authorization_code")
                .redirectUris("http://localhost:8081/oauth/login/client-app");
    }
}

1.5.4 Testing Authorization Code Flow

Testing OAuth can be tricky since it requires more than one server to see the full flow in action. However, the first steps are straight-forward:

  1. Browse to http://localhost:8080/oauth/authorize?grant_type=authorization_code&response_type=code&client_id=first-client&state=1234
  2. The application, if the user is not logged in, redirects to the login page, at http://localhost:8080/login
  3. Once the user logs in, the application generates a code and redirects to the registered redirect URI — in this case, http://localhost:8081/oauth/login/client-app

The flow could continue at this point by standing up any resource server that is configured for opaque tokens and is pointed at this authorization server instance.

1.6 How to Make Password Grant Flow Work

With the default configuration, while the Password Flow is technically possible, it, like Authorization Code, is missing users.

That said, because the default configuration creates a user with a username of user and a randomly-generated password, you can hypothetically check the logs for the password and do the following:

curl first-client:[email protected]:8080/oauth/token -dgrant_type=password -dscope=any -dusername=user -dpassword=the-password-from-the-logs

When you run that command, you should get a token back.

More likely, though, you want to specify a set of users.

As was stated in Section 1.5, “How to Make Authorization Code Grant Flow Work”, in Spring Security, users are typically specified in a UserDetailsService and this application is no different, as the following example shows:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        return new InMemoryUserDetailsManager(
            User.withDefaultPasswordEncoder()
                .username("enduser")
                .password("password")
                .roles("USER")
                .build());
    }
}

This is all we need to do. We do not need to override AuthorizationServerConfigurer, because the client ID and secret are specified as environment properties.

So, the following should now work:

curl first-client:[email protected]:8080/oauth/token -dgrant_type=password -dscope=any -dusername=enduser -dpassword=password

1.7 How and When to Give Authorization Server an AuthenticationManager

This is a very common question and is not terribly intuitive when AuthorizationServerEndpointsConfigurer needs an AuthenticationManager instance to be specified. The short answer is: Only when using the Resource Owner Password Flow.

It helps to remember a few fundamentals:

  • An AuthenticationManager is an abstraction for authenticating users. It typically needs some kind of UserDetailsService to be specified in order to be complete.
  • End users are specified in a WebSecurityConfigurerAdapter.
  • OAuth2 Boot, by default, automatically picks up any exposed AuthenticationManager.

However, not all flows require an AuthenticationManager because not all flows have end users involved. For example, the Client Credentials flow asks for a token based only on the client’s authority, not the end user’s. And the Refresh Token flow asks for a token based only on the authority of a refresh token.

Also, not all flows specifically require the OAuth 2.0 API itself to have an AuthenticationManager, either. For example, the Authorization Code and Implicit flows verify the user when they login (application flow), not when the token (OAuth 2.0 API) is requested.

Only the Resource Owner Password flow returns a code based off of the end user’s credentials. This means that the Authorization Server only needs an AuthenticationManager when clients are using the Resource Owner Password flow.

The following example shows the Resource Owner Password flow:

.authorizedGrantTypes("password", ...)

In the preceding flow, your Authorization Server needs an instance of AuthenticationManager.

There are a few ways to do this (remember the fundamentals from earlier):

1.7.1 Exposing a UserDetailsService

End users are specified in a WebSecurityConfigurerAdapter through a UserDetailsService. So, if you use the OAuth2 Boot defaults (meaning you haven’t implemented a AuthorizationServerConfigurer), you can expose a UserDetailsService and be done, as the following example shows:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired DataSource dataSource;

    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        return new JdbcUserDetailsManager(this.dataSource);
    }
}

1.7.2 Exposing an AuthenticationManager

In case you need to do more specialized configuration of the AuthenticationManager, you can do so in the WebSecurityConfigurerAdapter and then expose it, as the following example shows:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean(BeansId.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(customAuthenticationProvider());
    }
}

If you use the OAuth2 Boot defaults, then it picks up the bean automatically.

1.7.3 Depending on AuthenticationConfiguration

Any configured AuthenticationManager is available in AuthenticationConfiguration. This means that, if you need to have an AuthorizationServerConfigurer (in which case you need to do your own autowiring), you can have it depend on AuthenticationConfiguration to get the AuthenticationManager bean, as the following class shows:

@Component
public class CustomAuthorizationServerConfigurer extends
    AuthorizationServerConfigurerAdapter {

    AuthenticationManager authenticationManager;

    public CustomAuthorizationServerConfigurer(AuthenticationConfiguration authenticationConfiguration) {
        this.authenticationManager = authenticationConfiguration.getAuthenticationManager();
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) {
        // .. your client configuration that allows the password grant
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.authenticationManager(authenticationManager);
    }
}
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        return new MyCustomUserDetailsService();
    }
}

1.7.4 Manually Wiring An AuthenticationManager

In the most sophisticated case, where the AuthenticationManager needs special configuration and you have your own AuthenticationServerConfigurer, then you need to both create your own AuthorizationServerConfigurerAdapter and your own WebSecurityConfigurerAdapter:

@Component
public class CustomAuthorizationServerConfigurer extends
    AuthorizationServerConfigurerAdapter {

    AuthenticationManager authenticationManager;

    public CustomAuthorizationServerConfigurer(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) {
        // .. your client configuration that allows the password grant
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.authenticationManager(authenticationManager);
    }
}
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean(BeansId.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(customAuthenticationProvider());
    }
}

1.8 Is Authorization Server Compatible with Spring Security 5.1 Resource Server and Client?

No, not out of the box. Spring Security 5.1 supports only JWT-encoded JWK-signed authorization, and Authorization Server does not ship with a JWK Set URI.

Basic support is possible, though.

In order to configure Authorization Server to be compatible with Spring Security 5.1 Resource Server, for example, you need to do the following:

  • Configure it to use JWKs
  • Add a JWK Set URI endpoint

1.8.1 Configuring Authorization Server to Use JWKs

To change the format used for access and refresh tokens, you can change out the AccessTokenConverter and the TokenStore, as the following example shows:

@EnableAuthorizationServer
@Configuration
public class JwkSetConfiguration extends AuthorizationServerConfigurerAdapter {

	AuthenticationManager authenticationManager;
	KeyPair keyPair;

	public JwkSetConfiguration(AuthenticationConfiguration authenticationConfiguration,
			KeyPair keyPair) throws Exception {

		this.authenticationManager = authenticationConfiguration.getAuthenticationManager();
		this.keyPair = keyPair;
	}

    // ... client configuration, etc.

	@Override
	public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
		// @formatter:off
		endpoints
			.authenticationManager(this.authenticationManager)
			.accessTokenConverter(accessTokenConverter())
			.tokenStore(tokenStore());
		// @formatter:on
	}

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

	@Bean
	public JwtAccessTokenConverter accessTokenConverter() {
		JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
		converter.setKeyPair(this.keyPair);
		return converter;
	}
}

1.8.2 Add a JWK Set URI Endpoint

Spring Security OAuth does not support JWKs, nor does @EnableAuthorizationServer support adding more OAuth 2.0 API endpoints to its initial set. However, we can add this with only a few lines.

First, you need to add another dependency: com.nimbusds:nimbus-jose-jwt. This gives you the appropriate JWK primitives.

Second, instead of using @EnableAuthorizationServer, you need to directlyu include its two @Configuration classes:

  • AuthorizationServerEndpointsConfiguration: The @Configuration class for configuring the OAuth 2.0 API endpoints, such as what format to use for the tokens.
  • AuthorizationServerSecurityConfiguration: The @Configuration class for the access rules around those endpoints. This is the one that you need to extend, as shown in the following example:
@FrameworkEndpoint
class JwkSetEndpoint {
	KeyPair keyPair;

	public JwkSetEndpoint(KeyPair keyPair) {
		this.keyPair = keyPair;
	}

	@GetMapping("/.well-known/jwks.json")
	@ResponseBody
	public Map<String, Object> getKey(Principal principal) {
		RSAPublicKey publicKey = (RSAPublicKey) this.keyPair.getPublic();
		RSAKey key = new RSAKey.Builder(publicKey).build();
		return new JWKSet(key).toJSONObject();
	}
}
@Configuration
class JwkSetEndpointConfiguration extends AuthorizationServerSecurityConfiguration {
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		super.configure(http);
		http
			.requestMatchers()
				.mvcMatchers("/.well-known/jwks.json")
				.and()
			.authorizeRequests()
				.mvcMatchers("/.well-known/jwks.json").permitAll();
	}
}

Then, since you do not need to change AuthorizationServerEndpointsConfiguration, you can @Import it instead of using @EnableAuthorizationServer, as the following example shows:

@Import(AuthorizationServerEndpointsConfiguration.class)
@Configuration
public class JwkSetConfiguration extends AuthorizationServerConfigurerAdapter {

    // ... the rest of the configuration from the previous section
}

1.8.3 Testing Against Spring Security 5.1 Resource Server

Now you can POST to the /oauth/token endpoint (as before) to obtain a token and then present that to a Spring Security 5.1 Resource Server.