Building SSO based on Spring Authorization Server (Part 1 of 3)

D Snezhinskiy
17 min readOct 17, 2023

--

Hello everyone!

In this article, I would like to share my experience exploring the details of Spring Authorization Server, which I gained while working on the development of our internal SSO server.

Single sign-on (SSO) is an authentication scheme that allows a user to log in with a single ID to any of several related, yet independent, software systems.

The content got quite long, so I’ve decided to divide it into three parts of about the same length. Make sure to follow along so you don’t miss the next parts 😊

Table of Contents

Part 1

  • Introduction
  • Chapter 1: Code Grant Authorization
  • Chapter 2: Grant Password Authorization

Part 2 link

  • Chapter 3: Transition to Opaque Tokens
  • Chapter 4: PostgreSQL + Role Model

Part 3 link

  • Chapter 5: Authorization using Social Login (Google as an example)
  • Chapter 6: Resource Server Configuration

Introduction

This article wouldn’t exist if I hadn’t faced some challenging issues during the process and invested a significant amount of time. I don’t think I’d be too far from the truth to assume that the Authorization Server component of Spring has the potential to pose challenges for others as well. So, the main goal of the article is to highlight potential issues and offer solutions, bringing everything together in one detailed guide.

Organizing the Content

The article will be divided into several parts, and in each of them, we will solve one major task. To help you follow along, each chapter corresponds to a branch in my GitHub repository. A specific branch link will be provided at the end of each chapter.

Additionally, the article will include a significant amount of code… unfortunately, there’s no way around it. And, as well, we will be actively debugging, so there will be plenty of screenshots. They are necessary for a more complete understanding.

So, what will we do:

Technical Requirements

  1. Java 17
  2. Using the latest versions of Spring Boot (3.1.3 at the time of writing) and Authorization Server 1.1.2.
  3. PostgreSQL for storing registered Users
  4. Opaque Tokens Instead of JWT

Functional Requirements

  1. User authorization via username/password using the standard form on the SSO side
  2. User authorization via a form on the SPA (Single Page Application) side by sending a request with username and password to the SSO Endpoint
  3. Authorization using a third-party provider, also known as Social Login, demonstrated with Google as an example.
  4. We need to have the ability to gain authorized access to the Resource Server using the introspection endpoint to verify authorities based on the provided Opaque token.

Project organization

  1. A root, empty Gradle project used as a container for the Authorization Service and Resource Service
  2. Authorization Service Based on Spring Authorization Server
  3. Resource Server based on OAuth 2.0 (demo-backend)

So, let’s get started. First, we’ll create the project and launch it with basic authentication settings.

Chapter 1. Code Grant Authorization

Just for convenience, I’ve created a parent Gradle project named demosso. And inside of it, I’ve created another project called authorizationServer as a separate module. You can organize your projects as you prefer, it doesn’t matter.

We will need three dependencies:

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-oauth2-authorization-server'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
}

In the new project, we’ll create several configuration files:

  1. SecurityConfiguration — for basic security settings
  2. AuthorizationServerConfiguration — for everything related to the OAuth2 authorization server
  3. TokenConfiguration — for token generation settings.

Let’s start with SecurityConfiguration and create a defaultSecurityFilterChain bean, where we specify that all requests must be secured with authentication. So, the only endpoints that will grant unauthorized access are /login and /logout.

As for user storage, for now, we’ll use an InMemory repository. Let’s create a userDetailsService bean for it and specify all the necessary parameters.

SecurityConfiguration.java

@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class SecurityConfiguration {

@Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(
authorize -> authorize
.anyRequest().authenticated()
)
.formLogin(withDefaults())
.logout((logout) -> logout.permitAll())
.build();
}

@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.builder()
.username("admin")
// {noop} means "no operation," i.e., a raw password without any encoding applied.
.password("{noop}secret")
.roles("ADMIN")
.authorities("ARTICLE_READ", "ARTICLE_WRITE")
.build();

return new InMemoryUserDetailsManager(user);
}
}

Now, we’ll continue with the authorization server configuration.

AuthorizationServerConfiguration.java

@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfiguration {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

http
.exceptionHandling(
exceptions ->
exceptions.authenticationEntryPoint(
new LoginUrlAuthenticationEntryPoint("/login")
)
);

return http.build();
}

@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient demoClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientName("Demo client")
.clientId("demo-client")
.clientSecret("{noop}demo-secret")
.redirectUri("http://localhost:8080/auth")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.build();

return new InMemoryRegisteredClientRepository(demoClient);
}
}

Here, we create a authorizationSecurityFilterChain Bean and register LoginUrlAuthenticationEntryPoint as the exception handler. It’s used to redirect users to the login form page in case there’s no authenticated session.

Next, we will make the client repository — registeredClientRepository. In this context, clients are external applications (such as Mobile Apps or SPAs) that will redirect users to our SSO for the authentication process. You can find more information about this in the documentation under the RegisteredClient section.

As you may have noticed, I’ve currently specified two types of authorization:

AUTHORIZATION_CODE: Used for authorizing end-users through the authorization process and providing an access code. We’ll explore this in more detail later.

REFRESH_TOKEN: Used for refreshing the access token without re-authenticating the client

Here, we’ve completed this section and are now moving on to configuring the token generator. It’s only required for us to define one more bean — the JSON Web Key source, which is needed for signing the JWT token. I prefer to separate configurations by purpose, so I’ve extracted the token generation settings into a separate file.

TokenConfiguration.java

@Configuration(proxyBeanMethods = false)
public class TokenConfiguration {

@Bean
public JWKSource<SecurityContext> jwkSource() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}

private static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
}

The key generation rule is defined right here in the generateRsaKey() method.

It’s important to mention that this key generation method is temporary and should not be used on a production authorization server. This is because each instance of the authorization service will always have randomly generated keys, making it impossible to verify tokens.

Now, the only thing left is to define the port and logging settings in the properties.yml file.

properties.yml

server:
port: 8081

logging:
level:
root: INFO
com.demosso.authorizationserver: DEBUG
org.springframework.jdbc: DEBUG
org.springframework.security: TRACE

I would also create a controller right away, which will be needed for checking access and allow us to inspect the Authentication object. To do this, I’ll set a breakpoint on the line return “foo”.

FooController.java

@RestController
public class FooController {

@GetMapping(path = "/foo")
public String foo() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return "foo";
}
}

With these settings, our configuration is complete, and we are ready to start with the minimal setup. Let’s launch the application and immediately navigate to http://localhost:8081 to access the login form.

After entering the username and password, we are redirected to a page with a 404 error message. However, this is not a problem since we didn’t initially attempt to access any specific URL. If we had initially tried to access our controller at /foo, after authentication, we would have been redirected there.

Anyway, what’s important is that we’ve successfully authorized, and proof of that is the log entry.

If you now visit the page http://localhost:8081/foo, you will be able to inspect the contents of the Authentication object.

It’s important to note that the login we used during the authorization process is now contained in the ‘username’ field of the ‘principal’ object. We can potentially include it in the claims section of the JWT token.

JSON web tokens (JWTs) claims are pieces of information asserted about a subject.

Additionally, there is other data here that is also of interest to us, such as ‘authorities,’ which determine the user’s permissions in our system.

And now comes the moment when you might be thinking, ‘This is all very interesting, but we didn’t set up an SSO server just to authenticate ourselves on it.’ Our goal is to interact with the SSO from an external system or from another service and verify that our user is who he says he is, and obtain a list of authorities.

All of this can be achieved using the ‘Authorization Code Grant’ authentication method. Below is a diagram illustrating how it works within the context of our project.

Let’s look at how it works, step by step.

  1. Let’s suppose we have a client app (SPA) located at http://localhost:8080. So, the user initially accesses the client (SPA or Mobile App), and the client redirects the user to the SSO login page for authorization. Additionally, after authorization, we want the user to be redirected back to the page http://localhost:8080/auth.
    To achieve this, we construct the URL for redirection to the authorization server as follows:
    http://localhost:8081/oauth2/authorize?response_type=code&client_id=demo-client&redirect_uri=http://localhost:8080/auth
  2. Then, if we want to simulate user actions, we navigate to the generated URL as if the client had redirected us.
  3. The authorization server stores our request in cache and redirects us to the login form.
  4. Authorize using a login and password (as a reminder, in our case, it’s admin and secret)
  5. In case of successful authorization, the server redirects the user to the URL provided in the ‘redirect_uri’ parameter in the initial request.

The browser fairly informs us that such a page does not exist, but it doesn’t matter because we’ve obtained the authorization code.

It’s only left to request an access token from the authorization server. To do this, we refer to the documentation (Section 3.2.2 Token Request). It recommends sending a POST request with the ‘Authorization Basic’ header and the following parameters:

code: obtained in the previous step
grant_type: authorization_code
redirect_uri: http://localhost:8080/auth

Below is a demonstration of how to do this using POSTMAN.

Congratulations, we have obtained a JWT access token. Now you can copy it and visit the website https://jwt.io to verify that it contains valid data.

If you’ve carefully examined the PAYLOAD section, you might have noticed that the attribute ‘authorities’ is missing. This is because the PAYLOAD is constructed based on the claims section of the token, and only some of them (registered claims) are automatically populated. However, we can define additional attributes by customizing the access token generation process.

According to the recommendations from the documentation OAuth2TokenCustomizer, we should define two beans: tokenGenerator and jwtTokenCustomizer. Let’s add them to TokenConfiguration.

TokenConfiguration.java

@Bean
public OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator(
JWKSource<SecurityContext> jwkSource,
OAuth2TokenCustomizer<JwtEncodingContext> jwtTokenCustomizer
) {
NimbusJwtEncoder jwtEncoder = new NimbusJwtEncoder(jwkSource);
JwtGenerator jwtGenerator = new JwtGenerator(jwtEncoder);
jwtGenerator.setJwtCustomizer(jwtTokenCustomizer);
OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();

return new DelegatingOAuth2TokenGenerator(
jwtGenerator, accessTokenGenerator, refreshTokenGenerator
);
}

@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> jwtTokenCustomizer() {
return context -> {
UserDetails principal = (UserDetails) context.getPrincipal().getPrincipal();

context.getClaims()
.claim(
"authorities",
principal.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet())
);
};
}

Now, if you go through the authorization and token retrieval process again, you should see that the authorities section is present in the token.

{
"sub": "admin",
"aud": "demo-client",
"nbf": 1694264018,
"iss": "http://localhost:8081",
"exp": 1694264318,
"iat": 1694264018,
"authorities": [
"ARTICLE_WRITE",
"ARTICLE_READ"
]
}

This is the end of the first stage. We did it!
You can take a look at the result by following the link to GitHub.

Chapter 2. Grant Password Authorization

According to the second item on the list of functional requirements, we would like to implement user authentication by sending a login and password by the SPA or Mobile App side.

The authentication method I mean is called ‘Grant Password,’ also known as ‘Resource Owner Password Credentials’ (ROPC). Here’s what the scheme will look like:

There’s nothing special here.

  1. The user initially accesses the client (SPA or Mobile App).
  2. Next, the client provides a form for the user to enter their username and password.
  3. The form with the username and password is submitted.
  4. Client forwards the username and password as a request to the authorization server (localhost:8081/oauth2/token).
  5. If authentication is successful, the server responds with an access_token.

I’m sure that everyone has used applications that provide a similar interface.

However, if you look at the latest OAuth2.1 specification, you’ll find that this authentication method is missing from it, even though OAuth2 didn’t have any issues with it.

Nowadays, the Grant Password type is considered less secure because it requires sending the username and password directly to the client application without using additional security mechanisms. In accordance with the official specification, Spring has removed support for Grant Password, so we need to implement it ourselves.

When a client sends a request to the application, the server (servlet container) creates a FilterChain, which contains filters and servlets designed to process this request.

Filters are small components that can perform various functions, such as authentication checks, logging, data transformation, and more. Here are a few examples:

  • Authentication Filters
  • Logging and Auditing Filters
  • Image conversion Filters
  • Data compression Filters
  • Encryption Filters
  • Mime-type chain Filter

If you look into the Filter interface, it’s clear that its primary purpose is to modify the ServletRequest and ServletResponse.

public interface Filter {
//...
void doFilter(
ServletRequest request, ServletResponse response, FilterChain chain
)throws IOException, ServletException;
//...
}

Thus, by executing filters sequentially one after another, the FilterChain provides a flexible and easily customizable way to process requests in the application before they reach the main servlet and after its processing.

Now, what interests us is one of the filters called DelegatingFilterProxy. It delegates request processing to a filter registered in the application context, and in the case of Spring Security, this filter will be the FilterChainProxy. The FilterChainProxy is responsible for processing requests through the security filter chain known as SecurityFilterChain.

The SecurityFilterChain is another important piece of the puzzle. It uses the matches(HttpServletRequest request) method to determine which filters should process a specific HttpServletRequest.

I think for a better understanding, it would be useful to dive into FilterChainProxy and see what filters are contained there. Here they are:

Right now, our primary focus is on OAuth2TokenEndpointFilter because it handles requests to the oauth2/token endpoint, which we want to use. OAuth2TokenEndpointFilter doesn’t do anything special. It simply invokes the AuthenticationManager and gets an Authentication object from it, which contains the answer to whether the authorization was successful or not.

Implementation of the AuthenticationManager is the ProviderManager, and if we take a look at the ProviderManager, we will see that it contains all the currently used implementations of the AuthenticationProvider interface.

You may notice that the list also includes OAuth2AuthorizationCodeAuthenticationProvider (highlighted with a red arrow). And, also, please pay attention to the “parent” field, which contains DaoAuthenticationProvider. It is called if none of the providers can handle the request. This is important, and we will revisit DaoAuthenticationProvider later.

And now, let’s see how the appropriate provider is selected. Here’s what is stated in the comment for the authenticate method in the ProviderManager class:

Attempts to authenticate the passed Authentication object.
The list of AuthenticationProviders will be successively tried until an AuthenticationProvider indicates it is capable of authenticating the type of Authentication object passed. Authentication will then be attempted with that AuthenticationProvider.

The existing information is enough to move forward and implement your custom authentication method. Additionally, I recommend referring to the documentation section for reading more. (How-to: Implement an Extension Authorization Grant Type).

So, to implement a custom authentication method, we need to satisfy two primary requirements:

  • Create a converter that can perform an initial check to verify whether the HttpServletRequest meets the necessary conditions. If it does, the converter should then transform the request into an Authentication object.
  • Create a provider responsible for handling the created Authentication and generating an Access Token.

For better understanding, I’ve illustrated on the scheme everything that occurs after the HttpServletRequest enters the OAuth2TokenEndpointFilter.

Let’s start with the converter, which we’ll call OAuth2GrantPasswordAuthenticationConverter. It’s necessary to check inside it that the request contains all the required data (username/password) and has the correct type (grant_type=grant_password).
By the way, it would be a good idea to store the constant “grant_password” somewhere. I didn’t hesitate to create a separate class for this ;-)

AuthorizationGrantTypePassword.java

public class AuthorizationGrantTypePassword {
public static final AuthorizationGrantType GRANT_PASSWORD =
new AuthorizationGrantType("grant_password");
}

So, returning to the converter… If all conditions are satisfied, we should return an Authentication object. However, Authentication is an interface, so what kind of object should be returned?

To answer this question, it’s helpful to revisit the previously mentioned OAuth2AuthorizationCodeAuthenticationProvider and see what its OAuth2AuthorizationCodeAuthenticationConverter returns. This is a simplified version of the code, yet it still shows the main idea:

public class OAuth2AuthorizationCodeAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
private final String code;
private final String redirectUri;

//...
}

Let’s create our own token using a similar approach with our own parameters.

GrantPasswordAuthenticationToken.java

public class GrantPasswordAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {

private static final long serialVersionUID = 1L;
private final String username;
private final String password;
private final Set<String> scopes;

public GrantPasswordAuthenticationToken(
Authentication clientPrincipal,
String username,
String password,
@Nullable Set<String> scopes,
@Nullable Map<String, Object> additionalParameters
) {
super(GRANT_PASSWORD, clientPrincipal, additionalParameters);
Assert.hasText(username, "username cannot be empty");
Assert.hasText(password, "password cannot be empty");
this.username = username;
this.password = password;
this.scopes = Collections.unmodifiableSet(
scopes != null ? new HashSet<>(scopes) : Collections.emptySet());
}

public String getUsername() {
return this.username;
}

public String getPassword() {
return this.password;
}

public Set<String> getScopes() {
return this.scopes;
}
}

And similarly, following the example of OAuth2AuthorizationCodeAuthenticationConverter, we’ll create our own converter.

OAuth2GrantPasswordAuthenticationConverter.java

public class OAuth2GrantPasswordAuthenticationConverter implements AuthenticationConverter {

@Nullable
@Override
public Authentication convert(HttpServletRequest request) {

String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);

if (!GRANT_PASSWORD.getValue().equals(grantType)) {
return null;
}

MultiValueMap<String, String> parameters = getParameters(request);

// scope (OPTIONAL)
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
if (StringUtils.hasText(scope)
&& parameters.get(OAuth2ParameterNames.SCOPE).size() != 1
) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}

// username (REQUIRED)
String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);
if (!StringUtils.hasText(username)
|| parameters.get(OAuth2ParameterNames.USERNAME).size() != 1
) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}

// password (REQUIRED)
String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);
if (!StringUtils.hasText(password)
|| parameters.get(OAuth2ParameterNames.PASSWORD).size() != 1
) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}

Set<String> requestedScopes = null;
if (StringUtils.hasText(scope)) {
requestedScopes = new HashSet<>(
Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
}

Map<String, Object> additionalParameters = parameters.entrySet().stream()
.filter(entry ->
!OAuth2ParameterNames.GRANT_TYPE.equals(entry.getKey())
&& !OAuth2ParameterNames.SCOPE.equals(entry.getKey())
&& !OAuth2ParameterNames.PASSWORD.equals(entry.getKey())
&& !OAuth2ParameterNames.USERNAME.equals(entry.getKey())
)
.collect(Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue().get(0)));

Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();

return new GrantPasswordAuthenticationToken(
clientPrincipal, username, password, requestedScopes, additionalParameters
);
}

private static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
Map<String, String[]> parameterMap = request.getParameterMap();
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
parameterMap.forEach((key, values) -> {
if (values.length > 0) {
for (String value : values) {
parameters.add(key, value);
}
}
});
return parameters;
}
}

As you can see, we couldn’t use the OAuth2EndpointUtils.getParameters(request) method because it’s not accessible from the outside. So we had to simply copy it.

And, following the same approach, we will implement our GrantPasswordAuthenticationProvider, taking OAuth2AuthorizationCodeAuthenticationProvider as an example. I won’t include its code here as it has become quite extensive, so I’ll simply provide a link to it on github — GrantPasswordAuthenticationProvider.

Also, at this stage, I would like to introduce another enhancement that will come in handy soon. If you remember, in the UserDetailsService, we return a User object from the Spring Security package, which implements UserDetails, making it convenient. But this will change because sooner or later, we will introduce our custom user. I would like to keep the convenience that comes with UserDetails, so I will add our own implementation — CustomUserDetails, which, by the way, is used in GrantPasswordAuthenticationProvider.

CustomUserDetails.java

public class CustomUserDetails implements UserDetails {
private String password;
private final String username;
private final Collection<? extends GrantedAuthority> authorities;

public CustomUserDetails(String username, Collection<? extends GrantedAuthority> authorities) {
this.username = username;
this.authorities = authorities;
}

public CustomUserDetails(String username, String password, Collection<String> authorities) {
this.username = username;
this.password = password;
this.authorities = authorities.stream()
.map(authority -> new SimpleGrantedAuthority(authority))
.collect(Collectors.toList());
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}

@Override
public String getPassword() {
return password;
}

@Override
public String getUsername() {
return username;
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}
}

We’re almost done and just need to fine-tune the configuration a bit. Our client has introduced a new authentication method, so let’s update the RegisteredClientRepository bean accordingly.

AuthorizationServerConfiguration.java

@Bean
public RegisteredClientRepository registeredClientRepository() {
//...

.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantTypePassword.GRANT_PASSWORD)

//...
}

Now, let’s extend the authorizationSecurityFilterChain configuration by adding a tokenEndpoint section where we’ll reference our provider.

AuthorizationServerConfiguration.java

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationSecurityFilterChain(
HttpSecurity http,
GrantPasswordAuthenticationProvider grantPasswordAuthenticationProvider
) throws Exception {
//...

http
.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.tokenEndpoint(tokenEndpoint ->
tokenEndpoint
.accessTokenRequestConverter(new OAuth2GrantPasswordAuthenticationConverter())
.authenticationProvider(grantPasswordAuthenticationProvider)
);

//...
}

For convenience, let’s define the GrantPasswordAuthenticationProvider right here in the server configuration as a separate bean. To create it, we’ll also need to define a PasswordEncoder and OAuth2AuthorizationService. And during testing, we’ll use simple InMemory storage for the OAuth2AuthorizationService.

AuthorizationServerConfiguration.java

//...

@Bean
public GrantPasswordAuthenticationProvider grantPasswordAuthenticationProvider(
UserDetailsService userDetailsService, OAuth2TokenGenerator<?> jwtTokenCustomizer,
OAuth2AuthorizationService authorizationService, PasswordEncoder passwordEncoder
) {
return new GrantPasswordAuthenticationProvider(
authorizationService, jwtTokenCustomizer, userDetailsService, passwordEncoder
);
}

@Bean
public OAuth2AuthorizationService authorizationService() {
return new InMemoryOAuth2AuthorizationService();
}

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

//...

And finally, we need to make some modifications in jwtTokenCustomizer. This is because the Context we create in GrantPasswordAuthenticationProvider for generating the access token is slightly different from the one we have in the case of OAuth2AuthorizationCodeAuthenticationProvider, which was used in the Authorization Code Grant flow method.

The PRINCIPAL field from which we extract the properties needed for generating claims is different. You can see it by setting a breakpoint in jwtTokenCustomizer

Here’s what the PRINCIPAL obtained from GrantPasswordAuthenticationProvider looks like:

And here’s what is contained within PRINCIPAL in the case of the Authorization Code Grant flow:

It’s necessary to consider this difference in jwtTokenCustomizer.

@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> jwtTokenCustomizer() {
return context -> {
UserDetails userDetails = null;

if (context.getPrincipal() instanceof OAuth2ClientAuthenticationToken) {
userDetails = (UserDetails) context.getPrincipal().getDetails();
} else if (context.getPrincipal() instanceof AbstractAuthenticationToken) {
userDetails = (UserDetails) context.getPrincipal().getPrincipal();
} else {
throw new IllegalStateException("Unexpected token type");
}

if (!StringUtils.hasText(userDetails.getUsername())) {
throw new IllegalStateException("Bad UserDetails, username is empty");
}

context.getClaims()
.claim(
"authorities",
userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet())
)
.claim(
"username", userDetails.getUsername()
);
};
}

That’s it! You can go ahead and run it to test the new authentication method. Don’t forget to specify the clientId and clientSecret.

So, we’ve successfully obtained the JWT token! Congratulations! We’ve configured our custom authentication method… but we’ve broken the authorization code flow. Yes, direct login and password authentication via the login form is no longer working.

It’s not an obvious behavior, isn’t it? There’s a similar issue on github #10005. In short, the default AuthenticationProvider is loaded using InitializeAuthenticationProviderBeanManagerConfigurer. If we go into it, we’ll see the following:

Lazily initializes the global authentication with an AuthenticationProvider if it is not yet configured and there is only a single Bean of that type.

And we have no reason to doubt this. The following code snippet confirms it.

To resolve it, we need to manually create a DaoAuthenticationProvider bean and register it in the authorizationSecurityFilterChain as follows:

AuthorizationServerConfiguration.java

//...

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationSecurityFilterChain(
HttpSecurity http,
GrantPasswordAuthenticationProvider grantPasswordAuthenticationProvider,
DaoAuthenticationProvider daoAuthenticationProvider
) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

http
.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.tokenEndpoint(tokenEndpoint ->
tokenEndpoint
.accessTokenRequestConverter(new OAuth2GrantPasswordAuthenticationConverter())
.authenticationProvider(grantPasswordAuthenticationProvider)
.authenticationProvider(daoAuthenticationProvider)
);

http
.exceptionHandling(
exceptions ->
exceptions.authenticationEntryPoint(
new LoginUrlAuthenticationEntryPoint("/login")
)
);

return http.build();
}

@Bean
public DaoAuthenticationProvider daoAuthenticationProvider(
PasswordEncoder passwordEncoder, UserDetailsService userDetailsService
) {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
return daoAuthenticationProvider;
}

//...

If now we run the project, all authentication methods should work. This marks the completion of the second stage!

The next part will be released in a week, and I’m sure you’ve subscribed and won’t miss it, right? 😉

References

Spring Authorization Server Reference — RegisteredClient

Spring Authorization Server Reference — OAuth2TokenCustomizer

How-to: Implement an Extension Authorization Grant Type

JSON Web Token Claims

The OAuth 2.1 (draft-ietf-oauth-v2–1–09 will be removed after RFC publishing)

https://jwt.io — JWT debugger

Contacts

LinkedIn: Snezhinskiy Dmitriy
Email: d.snezhinskii@gmail.com

--

--