Spring Security OAuth2 Login
Tags: java, security, oauth2, springsecurity
Published: 2023-03-09 14:29:39

## Overview
**OAuth 2.0 is not an authentication protocol**.
What is identity authentication? **Authentication** is the solution to the "Who are you?". Authentication tells the app who the current user is and whether they are using the app.In practice, it may also tell you the user's name, email address, mobile phone number, etc.

If an extension to OAuth 2.0. Enables messages from authorization servers and protected resources to convey information about users and their authentication context. We can provide the client with all the information for the user to log in securely.
The main advantages of this identity authentication method based on the OAuth 2.0 authorization protocol:
- The user performs authentication on the authorization server, the end user's original credentials are not passed to the client application through the OAuth 2.0 protocol.
- Allows users to enforce consent decisions at runtime.
- The user can also authorize access to other protected APIs along with his identity information. Through a call, the application can know whether the user is logged in, how to call the user, the user's mobile phone number, email address, etc.


In this article, we will use the OAuth 2.0 authorization code mode to securely pass the authorization service user information and log in to the client application.

In this article you will learn:
- Build basic authorization service and client service.
- Customize the authorization server access token and add role information.
- Custom authorization server user info endpoint.
- The client service uses `GrantedAuthoritiesMapper` for authorization mapping.
- Client service custom `OAuth2UserService` implements parsing multi-layer Json data.


## OAuth2 Authorization Server &#x1F680;

In this section we will use [Spring Authorization Server](https://spring.io/projects/spring-authorization-server) to build an authorization server.In addition, we will also customize the *access_token* and custom user information endpoints.


### Maven dependencies
```xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
  <version>2.6.7</version>
</dependency>

<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-oauth2-authorization-server</artifactId>
  <version>0.3.1</version>
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
  <version>2.6.7</version>
</dependency>
```


### Configuration

First configure the service port 8080 through `application.yml`:

```yaml
server:
  port: 8080
```


Next we will create the `OAuth2ServerConfig` configuration class to define the specific beans needed for the OAuth2 authorization service. First we register an OAuth2 client:

```java
@Bean
public RegisteredClientRepository registeredClientRepository() {
  RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
    .clientId("relive-client")
    .clientSecret("{noop}relive-client")
    .clientAuthenticationMethods(s -> {
      s.add(ClientAuthenticationMethod.CLIENT_SECRET_POST);
      s.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
    })
    .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
    .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
    .redirectUri("http://127.0.0.1:8070/login/oauth2/code/messaging-client-authorization-code")
    .scope(OidcScopes.PROFILE)
    .clientSettings(ClientSettings.builder()
                    .requireAuthorizationConsent(true)
                    .requireProofKey(false)
                    .build())
    .tokenSettings(TokenSettings.builder()
                   .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
                   .idTokenSignatureAlgorithm(SignatureAlgorithm.RS256)/
                   .accessTokenTimeToLive(Duration.ofSeconds(30 * 60))
                   .refreshTokenTimeToLive(Duration.ofSeconds(60 * 60))
                   .reuseRefreshTokens(true)
                   .build())
    .build();
  return new InMemoryRegisteredClientRepository(registeredClient);
}
```

The above stores the OAuth2 client in memory. If you need to use database persistence, please refer to the article [Using JWT with Spring Security OAuth2](https://dev.to/relive27/using-jwt-with-spring-security-oauth2-41jp). Specify the OAuth2 client information as follows:

- **clientId**: relive-client
- **clientSecret**: relive-client
- **redirectUri**: http://127.0.0.1:8070/login/oauth2/code/messaging-client-authorization-code
- **scope**: profile

Next let's configure other default configurations of the OAuth2 authorization service, and redirect unauthenticated authorization requests to the login page:

```java
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
  OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

  return http
    .exceptionHandling(exceptions -> exceptions.
                       authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")))
    .build();
}
```

The authorization server token token format uses [JWT RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519), so we need a signing key for the token, let's generate an RSA key:

```java
@Bean
public JWKSource<SecurityContext> jwkSource() {
  RSAKey rsaKey = Jwks.generateRsa();
  JWKSet jwkSet = new JWKSet(rsaKey);
  return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}

static class Jwks {

  private Jwks() {
  }

  public static RSAKey generateRsa() {
    KeyPair keyPair = KeyGeneratorUtils.generateRsaKey();
    RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
    RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
    return new RSAKey.Builder(publicKey)
      .privateKey(privateKey)
      .keyID(UUID.randomUUID().toString())
      .build();
  }
}

static class KeyGeneratorUtils {

  private KeyGeneratorUtils() {
  }

  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;
  }
}
```


Next we will customize the *access_token* access token and add role information to the token:

```java
@Configuration(proxyBeanMethods = false)
public class AccessTokenCustomizerConfig {

    @Bean
    public OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer() {
        return (context) -> {
            if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
                context.getClaims().claims(claim -> {
                    claim.put("role", context.getPrincipal().getAuthorities().stream()
                            .map(GrantedAuthority::getAuthority).collect(Collectors.toSet()));
                });
            }
        };
    }
}
```

You can see that Spring Security provides us with `OAuth2TokenCustomizer` for extending token information. We get the current user information from `OAuth2TokenContext`, and extract the Authorities information from it and add it to the JWT claim.


<br />

Below we will create a Spring Security configuration class to configure the basic authentication capabilities of the authorization service.
```java
@Configuration(proxyBeanMethods = false)
public class DefaultSecurityConfig {

    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/userInfo")
                .access("hasAnyAuthority('SCOPE_profile')")
                .mvcMatchers("/userInfo")
                .access("hasAuthority('SCOPE_profile')")
                .anyRequest().authenticated()
                .and()
                .formLogin(Customizer.withDefaults())
                .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
        return http.build();
    }

    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    @Bean
    public UserDetailsService users() {
        UserDetails user = User.withDefaultPasswordEncoder()
                .username("admin")
                .password("password")
                .roles("ADMIN")
                .build();
        return new InMemoryUserDetailsManager(user);
    }
}
```
In the above configuration class, we did the following things:

1. Enable Form authentication method;
2. Configure login username and password;
3. Use `oauth2ResourceServer()` to configure JWT authentication, and declare `JwtDecoder`;
4. Protecting the /userInfo endpoint requires *profile* permissions for access;


At this point, we also need to create a Controller class to provide the OAuth2 client service to obtain user information:
```java
@RestController
public class UserInfoController {

    @PostMapping("/userInfo")
    public Map<String, Object> getUserInfo(@AuthenticationPrincipal Jwt jwt) {
        return Collections.singletonMap("data", jwt.getClaims());
    }
}
```


We return user information in JSON format:

```json
{
  "data":{
    "sub":"admin"
    ...
  }
}
```

## OAuth2 Client Service &#x1F680;

This section will use Spring Security to configure OAuth2 client login. And we will use `GrantedAuthoritiesMapper` to map authority information. And we will create `DefaultJsonOAuth2UserService` for parsing multi-layer JSON user information data.

### Maven dependencies
```xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
  <version>2.6.7</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
  <version>2.6.7</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-oauth2-client</artifactId>
  <version>2.6.7</version>
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
  <version>2.6.7</version>
</dependency>

```

### Configuration

First, we specify the client service port number 8070, and configure the OAuth2 client related information. The client information needs to be consistent with the authorization server registration information.

```yaml
server:
  port: 8070
  servlet:
    session:
      cookie:
        name: CLIENT-SESSION

spring:
  security:
    oauth2:
      client:
        registration:
          messaging-client-authorization-code:
            provider: client-provider
            client-id: relive-client
            client-secret: relive-client
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            scope: profile
            client-name: messaging-client-authorization-code
        provider:
          client-provider:
            authorization-uri: http://127.0.0.1:8080/oauth2/authorize
            token-uri: http://127.0.0.1:8080/oauth2/token
            user-info-uri: http://127.0.0.1:8080/userInfo
            user-name-attribute: data.sub
            user-info-authentication-method: form

```



Next, configure Spring Security related beans. We first enable Form form authentication and OAuth2 login capabilities. Here we specify to redirect to the /home path after successful authentication.
```java
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
  http.authorizeHttpRequests()
    .anyRequest()
    .authenticated()
    .and()
    .formLogin(from -> {
      from.defaultSuccessUrl("/home");
    })
    .oauth2Login(Customizer.withDefaults())
    .csrf().disable();
  return http.build();
}
```

Below we use `GrantedAuthoritiesMapper` to map user permissions:


```java
@Bean
GrantedAuthoritiesMapper userAuthoritiesMapper() {
  //Role mapping relationship, authorization server ADMIN role corresponds to client OPERATION role
  Map<String, String> roleMapping = new HashMap<>();
  roleMapping.put("ROLE_ADMIN", "ROLE_OPERATION");
  return (authorities) -> {
    Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
    authorities.forEach(authority -> {
      if (OAuth2UserAuthority.class.isInstance(authority)) {
        OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority) authority;
        Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
        List<String> role = (List) userAttributes.get("role");
        role.stream().map(roleMapping::get)
          .filter(StringUtils::hasText)
          .map(SimpleGrantedAuthority::new)
          .forEach(mappedAuthorities::add);
      }
    });
    return mappedAuthorities;
  };
}
```
The above maps the OAuth2 authorization service **ADMIN** role to the client service **OPERATION** role. Of course, you can also expand to database operations, so you need to maintain the authorization service role and client service role mapping table, which will not be expanded here.

`GrantedAuthoritiesMapper` is used as an authority mapper in OAuth2 login, CAS login, SAML and LDAP.

The source code of `GrantedAuthoritiesMapper` in `OAuth2LoginAuthenticationProvider` is as follows:

```java
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    OAuth2LoginAuthenticationToken loginAuthenticationToken = (OAuth2LoginAuthenticationToken) authentication;
    //...

    /* map authorities */
    Collection<? extends GrantedAuthority> mappedAuthorities = this.authoritiesMapper
      .mapAuthorities(oauth2User.getAuthorities());
    /* map authorities */

    OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
      loginAuthenticationToken.getClientRegistration(), loginAuthenticationToken.getAuthorizationExchange(),
      oauth2User, mappedAuthorities, accessToken, authorizationCodeAuthenticationToken.getRefreshToken());
    authenticationResult.setDetails(loginAuthenticationToken.getDetails());
    return authenticationResult;
}
```
So when we custom implement `GrantedAuthoritiesMapper`. After successful OAuth2 login, store the mapped permission information in the authentication information `OAuth2LoginAuthenticationToken`.



Next will implement the `OAuth2UserService` custom `DefaultJsonOAuth2UserService` class. Of course Spring Security provides `DefaultOAuth2UserService`, so why not use it? The reason is simple. First, let us review the format of the authorization server returning user information:

```json
{
  "data":{
    "sub":"admin"
    ...
  }
}
```
The user information is nested in the `data` field, and the `DefaultOAuth2UserService` does not process this format when processing the user information response. The following is a snippet of the `DefaultOAuth2UserService` source code:
```java
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        Assert.notNull(userRequest, "userRequest cannot be null");
        if (!StringUtils.hasText(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri())) {
            OAuth2Error oauth2Error = new OAuth2Error("missing_user_info_uri", "Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: " + userRequest.getClientRegistration().getRegistrationId(), (String)null);
            throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
        } else {
            String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
            if (!StringUtils.hasText(userNameAttributeName)) {
                OAuth2Error oauth2Error = new OAuth2Error("missing_user_name_attribute", "Missing required \"user name\" attribute name in UserInfoEndpoint for Client Registration: " + userRequest.getClientRegistration().getRegistrationId(), (String)null);
                throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
            } else {
                RequestEntity<?> request = (RequestEntity)this.requestEntityConverter.convert(userRequest);
               /* Get user information */
              ResponseEntity<Map<String, Object>> response = this.getResponse(userRequest, request);
                //Get the response body information directly here. By default, this userAttributes contains relevant user information, and does not parse multi-layer JSON
                Map<String, Object> userAttributes = (Map)response.getBody();
               /* Get user information */
                Set<GrantedAuthority> authorities = new LinkedHashSet();
                authorities.add(new OAuth2UserAuthority(userAttributes));
                OAuth2AccessToken token = userRequest.getAccessToken();
                Iterator var8 = token.getScopes().iterator();

                while(var8.hasNext()) {
                    String authority = (String)var8.next();
                    authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
                }

                return new DefaultOAuth2User(authorities, userAttributes, userNameAttributeName);
            }
        }
    }
```
When the `DefaultOAuth2User` is finally created, you will get the following error message:

```
Missing attribute 'sub' in attributes
```


Below we use userNameAttributeName with "." as the separator. Extract user information to achieve. The following is the key code:

```java
public class DefaultJsonOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

    //...

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        //...
        RequestEntity<?> request = this.requestEntityConverter.convert(userRequest);
        ResponseEntity<JsonNode> response = getResponse(userRequest, request);
        JsonNode responseBody = response.getBody();

        //Multi-layer JSON extracts user information attributes
        Map<String, Object> userAttributes = new HashMap<>();
        if (userNameAttributeName.contains(".")) {
          String firstNodePath = userNameAttributeName.substring(0, userNameAttributeName.lastIndexOf("."));
          userAttributes = this.extractUserAttribute(responseBody, firstNodePath);
          userNameAttributeName = userNameAttributeName.substring(firstNodePath.length() + 1);
        } else {
          userAttributes = JsonHelper.parseMap(responseBody.toString());
        }

        //...
    }
}
```

Finally, we create the Controller class and use the [thymeleaf](https://www.thymeleaf.org/) template engine to build the home page information. Different permission information sees different results in the home page list.

```java
@Controller
public class HomeController {

    private static Map<String, List<String>> articles = new HashMap<>();

    static {
        articles.put("ROLE_OPERATION", Arrays.asList("Java"));
        articles.put("ROLE_SYSTEM", Arrays.asList("Java", "Python", "C++"));
    }

    @GetMapping("/home")
    public String home(Authentication authentication, Model model) {
        String authority = authentication.getAuthorities().iterator().next().getAuthority();
        model.addAttribute("articles", articles.get(authority));
        return "home";
    }
}
```


## Test
After we start the service, visit http://127.0.0.1:8070/login. After successful login with username, you will see:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i5jn0vvzssu7uvx2mivf.png)

We log out, and log in with OAuth2, you will see different information:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qla0qo6ba55v6rkd5bt8.png)

## Conclusion
It is feasible for us to use the OAuth2.0 authorization protocol to build identity authentication proofs. But we cannot ignore the pitfalls in between.

1. The token itself does not convey information about the authentication event. Tokens may be issued directly to clients, using the OAuth 2.0 Client Credentials model without user interaction.
2. No client can get information about the user and his login status from the *access_token*. OAuth 2.0 *access_token* are intended for resource servers.(In this article, we use the JWT *access_token* to enable the client service to obtain information such as user permissions by customizing the *access_token* information. However, the OAuth2.0 protocol does not define the *access_token* format. We only use the characteristics of JWT to make it happen.)
3. The client can present the *access_token* to the resource service to obtain user information. So it's easy to think that just having a valid *access_token* proves that the user is logged in, and this line of thinking is only true in some cases. For example, when a user completes identity authentication on an authorization server and immediately generates an *access_token*.(Because the *access_token* validity period may be much longer than the authentication session validity period.)
4. The biggest problem with the user information API in the OAuth2.0 protocol is that different identity providers may have different user information APIs. A user's unique identifier may be "user_id" or "sub".


So we need a unified OAuth2.0 standard identity authentication protocol. OpenID Connect is an open standard that defines an interoperable way to perform user authentication using OAuth 2.0. This will be covered in a follow-up article.

As always, the source code used in this article is available on [GitHub](https://github.com/ReLive27/spring-security-oauth2-sample/tree/main/oauth2-login).

Thanks for reading! 😘