Spring Security and OpenID Connect
Tags: java, security, oauth2, springsecurity
Published: 2023-03-19 13:55:05

## Overview

OpenID Connect is an open standard published by the OpenID Foundation in February 2014. It defines an interworking way to perform user authentication using OAuth 2.0. OpenID Connect builds directly on OAuth 2.0 and remains compatible with it.

When an authorization server supports OIDC, it is sometimes called an Identity Provider (Idp), because it provides information to **client** about the owner of a resource. And the **client** is also referred to as the Relying Party (RP) in the OpenID Connect process.

The OpenID Connect flow looks the same as OAuth2. The main difference is that in the authorization request, a specific scope `openid` is used, while in the obtain token, **the login relying party (RP) receives both an access token and an ID token (signed JWT)**. ID token differ from access token in that ID token are sent to and parsed by the login relying party (RP).

**In this article you will learn:**
- Configure the authorization service to support OpenID Connect
- Custom id token
- The login relying party implements permission mapping through `OAuth2UserService`


**Prerequisites:**
- java 8+
- MySQL

## Use Spring Authorization Server to build identity provider server (IdP) &#x1F680;


In this section, we will use [Spring Authorization Server](https://spring.io/projects/spring-authorization-server) to build an identity provider service, and implement a custom ID token through `OAuth2TokenCustomizer`.

### 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 we configure the identity provider service port 8080:

```yml
server:
  port: 8080
```

Next we create the `AuthorizationServerConfig` configuration class, in which we configure OAuth2 and OICD related beans. We first register a 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-oidc")
                .scope(OidcScopes.OPENID)
                .scope(OidcScopes.PROFILE)
                .scope(OidcScopes.EMAIL)
                .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 properties we are configuring are:
- clientId -- The identity provider will use this to identify which client is trying to access the resource
- clientSecret -- A secret known to both client and identity provider service, which provides trust between the two
- clientAuthenticationMethod -- Client authentication method, in our example, we will support basic and post authentication methods
- authorizationGrantType -- Grant type, support authorization code and refresh token
- redirectUri -- The redirection URI, which the client will use in the redirection-based flow
- scope -- This parameter defines the permissions the client may have. In our case, we will have the required **openid** and **profile**, **emali**, to fetch additional identity information

OpenID Connect uses a special permission scope value openid to control access to the UserInfo endpoint. OpenID Connect defines a set of standardized OAuth2 permission scopes, corresponding to the subset of user attributes profile, email, phone, address, see the table:

|scope of authority   | statement                                                       |
| ---------- | :----------------------------------------------------------- |
| **openid** | **sub**                                                      |
| profile    | Name, family_name, given_name, middle_name, nickname, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, locale, updated_at |
| email      | email, email_verified                                        |
| address    | address is a JSON object, contains formatted, street_address, locality, region, postal_code, country |
| phone      | phone_number, phone_number_verified                          |


Let's define `OidcUserInfoService` according to the above specification, which is used to extend /userinfo user information endpoint response:

```java
public class OidcUserInfoService {

    public OidcUserInfo loadUser(String name, Set<String> scopes) {
        OidcUserInfo.Builder builder = OidcUserInfo.builder().subject(name);
        if (!CollectionUtils.isEmpty(scopes)) {
            if (scopes.contains(OidcScopes.PROFILE)) {
                builder.name("First Last")
                        .givenName("First")
                        .familyName("Last")
                        .middleName("Middle")
                        .nickname("User")
                        .preferredUsername(name)
                        .profile("http://127.0.0.1:8080/" + name)
                        .picture("http://127.0.0.1:8080/" + name + ".jpg")
                        .website("http://127.0.0.1:8080/")
                        .gender("female")
                        .birthdate("2022-05-24")
                        .zoneinfo("China/Beijing")
                        .locale("zh-cn")
                        .updatedAt(LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
            }
            if (scopes.contains(OidcScopes.EMAIL)) {
                builder.email(name + "@outlook.com").emailVerified(true);
            }
            if (scopes.contains(OidcScopes.ADDRESS)) {
                JSONObject address = new JSONObject();
                address.put("address", Collections.singletonMap("formatted", "Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance"));
                builder.address(address.toJSONString());
            }
            if (scopes.contains(OidcScopes.PHONE)) {
                builder.phoneNumber("13028903134").phoneNumberVerified("false");
            }
        }
        return builder.build();
    }
}
```

Next, we'll configure a bean to apply default OAuth2 security. Use the above `OidcUserInfoService` to configure UserInfoMapper in OIDC. oauth2ResourceServer() configures the resource server to use JWT authentication to secure the /userinfo endpoint provided by Spring Security. For unauthenticated requests we redirect it to the /login login page.

> Note: Sometimes the "authorization server" and "resource server" are the same server.

```java
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
                new OAuth2AuthorizationServerConfigurer<>();

        //custom User Mapper
        Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper = (context) -> {
            OidcUserInfoAuthenticationToken authentication = context.getAuthentication();
            JwtAuthenticationToken principal = (JwtAuthenticationToken) authentication.getPrincipal();
            return userInfoService.loadUser(principal.getName(), context.getAccessToken().getScopes());
        };
        authorizationServerConfigurer.oidc((oidc) -> {
            oidc.userInfoEndpoint((userInfo) -> userInfo.userInfoMapper(userInfoMapper));
        });

        RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();

        return http.requestMatcher(endpointsMatcher).authorizeRequests((authorizeRequests) -> {
            ((ExpressionUrlAuthorizationConfigurer.AuthorizedUrl) authorizeRequests.anyRequest()).authenticated();
        }).csrf((csrf) -> {
            csrf.ignoringRequestMatchers(new RequestMatcher[]{endpointsMatcher});
        }).apply(authorizationServerConfigurer)
                .and()
                .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
                .exceptionHandling(exceptions -> exceptions.
                        authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")))
                .apply(authorizationServerConfigurer)
                .and()
                .build();
    }

```

Each authorization server needs its signing key for tokens, let's generate a 2048 byte 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;
  }
}

```

We will then enable the Spring Web Security module using a configuration class annotated with `@EnableWebSecurity`.

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

    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeRequests(authorizeRequests ->
                        authorizeRequests.anyRequest().authenticated()
                )
                .formLogin(withDefaults());
        return http.build();
    }

    //...
}
```

Here we use Form authentication, so we also need to provide username and password for login authentication.

```java
    @Bean
    UserDetailsService users() {
        UserDetails user = User.withDefaultPasswordEncoder()
                .username("admin")
                .password("password")
                .roles("ADMIN")
                .build();
        return new InMemoryUserDetailsManager(user);
    }
```

We will change the **ID Token** claim and add the user role attribute to pass the role information to the client.

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

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

```

## Relying Party Service (RP) Implementation &#x1F680;


In this section, we will use Spring Security to build the relying party service, and design the relevant database table structure to express the permission relationship between the associated identity provider service and the relying party service, and implement permission mapping through `OAuth2UserService`.

Part of the code in this section involves JPA-related knowledge. If you don’t understand it, it doesn’t matter. You can replace it with Mybatis.

### 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-oauth2-client</artifactId>
  <version>2.6.7</version>
</dependency>

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

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</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>

<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>8.0.21</version>
</dependency>

```

### Relevant database table structure

This is the relevant database table used by the RP service in this article, and the SQL statements related to creating tables and initializing data can be obtained from [here](https://github.com/ReLive27/spring-security-oauth2-sample/tree/main/oidc-login/rp/src/main/resources/db/migration).


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

### Configuration

First, we configure the service port and database connection information in the `application.yml` file.

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

spring:
  datasource:
    druid:
      db-type: mysql
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/oidc_login?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
      username: <<root>> 
      password: <<password>>

```

Next we will enable the Spring Security configuration. Use Form authentication, and use *oauth2Login()* to define the default configuration of OAuth2 login.

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

```

Next, we will configure the storage method of the OAuth2 client based on the MySql database. You can also learn more about it from the [Spring Security Persistent OAuth2 client](https://dev.to/relive27/spring-security-persistent-oauth2-client-1gcc).

```java
    /**
     * Define the JDBC client registration repository
     *
     * @param jdbcTemplate
     * @return
     */
    @Bean
    public ClientRegistrationRepository clientRegistrationRepository(JdbcTemplate jdbcTemplate) {
        return new JdbcClientRegistrationRepository(jdbcTemplate);
    }

    /**
     * Responsible for {@link OAuth2AuthorizedClient} persistence between web requests
     *
     * @param jdbcTemplate
     * @param clientRegistrationRepository
     * @return
     */
    @Bean
    public OAuth2AuthorizedClientService authorizedClientService(
            JdbcTemplate jdbcTemplate,
            ClientRegistrationRepository clientRegistrationRepository) {
        return new JdbcOAuth2AuthorizedClientService(jdbcTemplate, clientRegistrationRepository);
    }

    /**
     * OAuth2AuthorizedClientRepository is a container class for saving and persisting authorized clients between requests
     *
     * @param authorizedClientService
     * @return
     */
    @Bean
    public OAuth2AuthorizedClientRepository authorizedClientRepository(
            OAuth2AuthorizedClientService authorizedClientService) {
        return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);
    }

```

We are no longer using memory-based username and password. We have added the username and password to the user table when initializing the database, so we need to implement the `UserDetailsService` interface to obtain user information during Form authentication.

```java
@RequiredArgsConstructor
public class JdbcUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        com.relive.entity.User user = userRepository.findUserByUsername(username);
        if (ObjectUtils.isEmpty(user)) {
            throw new UsernameNotFoundException("user is not found");
        }
        if (CollectionUtils.isEmpty(user.getRoleList())) {
            throw new UsernameNotFoundException("role is not found");
        }
        Set<SimpleGrantedAuthority> authorities = user.getRoleList().stream().map(Role::getRoleCode)
                .map(SimpleGrantedAuthority::new).collect(Collectors.toSet());
        return new User(user.getUsername(), user.getPassword(), authorities);
    }
}


```
Here `UserRepository` extends `JpaRepository` and provides database operations for user tables. The detailed code can be obtained from the link at the end of the article.



Now we will solve how to map the IdP service user role to the existing role of the RP service. In the previous article, `GrantedAuthoritiesMapper` was used to map the role. In this article we will use `OAuth2UserService` to add role mapping strategy, which is more flexible than `GrantedAuthoritiesMapper`.

```java
public class OidcRoleMappingUserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
    private OidcUserService oidcUserService;
    private final OAuth2ClientRoleRepository oAuth2ClientRoleRepository;

    //...

    @Override
    public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
        OidcUser oidcUser = oidcUserService.loadUser(userRequest);

        OidcIdToken idToken = userRequest.getIdToken();
        List<String> role = idToken.getClaimAsStringList("role");
        Set<SimpleGrantedAuthority> mappedAuthorities = role.stream()
                .map(r -> oAuth2ClientRoleRepository.findByClientRegistrationIdAndRoleCode(userRequest.getClientRegistration().getRegistrationId(), r))
                .map(OAuth2ClientRole::getRole).map(Role::getRoleCode).map(SimpleGrantedAuthority::new)
                .collect(Collectors.toSet());
        oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());

        return oidcUser;
    }
}


```

Finally, we will create a `HomeController` to make the test effect more visually significant by controlling the content displayed on the page. We will display different information according to the role and use the [thymeleaf](https://www.thymeleaf.org/) template engine to render.

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

After completing the configuration, we can visit [http://127.0.0.1:8070/home](http://127.0.0.1:8070/home) for testing.


## Conclusion
In this article, Spring Security's support for OpenID Connect is shared. As always, the source code used in this article is available [on GitHub](https://github.com/ReLive27/spring-security-oauth2-sample/tree/main/oidc-login).


Thanks for reading! &#x1F60A;