Spring Cloud Gateway Combined with the Security Practice of OAuth2.0 Protocol
Tags: java, security, oauth2, springsecurity
Published: 2023-03-26 14:41:41

## Overview
Spring Cloud Gateway is an API Gateway built on top of the Spring ecosystem. It is built on top of [Spring Boot](https://spring.io/projects/spring-boot#learn), [Spring WebFlux](https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html), and [Project Reactor](https://projectreactor.io/docs).

In this section, you will use Spring Cloud Gateway to route requests to a Servlet API service.

**What you will learn in this article:**
- OpenID Connect authentication - used for user authentication.
- Token relay - Spring Cloud Gateway API gateway acts as a client and forwards tokens to resource requests.

**Prerequisites:**
- Java 8+
- MySQL
- Redis

## OpenID Connect authentication
OpenID Connect defines a user authentication mechanism based on the OAuth2 authorization code flow. The following diagram shows the complete process of authentication between Spring Cloud Gateway and the authorization service. For clarity, some parameters have been omitted.


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


## Build authorization service

In this section, we will use [Spring Authorization Server](https://spring.io/projects/spring-authorization-server#overview) to build an authorization service that supports the OAuth2 and OpenID Connect protocols. At the same time, we will use the RBAC0 basic permission model to control access rights. This authorization service also supports Github third-party login as an OAuth2 client.

### Relevant database table structure
We have created a basic RBAC0 permission model for this article and provided the table structures required for persistence storage of OAuth2 authorization services and OAuth2 clients. The *oauth2_client_role* table defines the external system role and the mapping relationship with the local platform role. The SQL statements related to the creation of related tables and initialization data can be obtained [here](https://github.com/ReLive27/spring-security-oauth2-sample/tree/main/gateway-oauth2-login/auth-server/src/main/resources/db/migration).


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

### Role description
By default, the authorization service in this section provides two roles, with the following role attributes and access permissions:

|    | read | write |
| :------------- | :---- | :---- |
| ROLE_ADMIN     |  ✅    |  ✅     |
| ROLE_OPERATION |  ✅    |  ❎     |

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

<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-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>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>8.0.21</version>
</dependency>
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>druid-spring-boot-starter</artifactId>
  <version>1.2.3</version>
</dependency>
```

### Configuration
First, let's start with the `application.yml` configuration, where we specify the port number and MySQL connection configuration:

```yaml
server:
  port: 8080

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


Next, we will create `AuthorizationServerConfig` to configure the required beans for OAuth2 and OIDC. First, we will add OAuth2 client information and persist it to the database:

```java
    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        RegisteredClient registeredClient = RegisteredClient.withId("relive-messaging-oidc")
                .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-gateway-oidc")
                .scope(OidcScopes.OPENID)
                .scope(OidcScopes.PROFILE)
                .scope(OidcScopes.EMAIL)
                .scope("read")
                .clientSettings(ClientSettings.builder()
                        .requireAuthorizationConsent(false)
                        .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();

        JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
        registeredClientRepository.save(registeredClient);
        return registeredClientRepository;
    }

```

Second, we will create the persistence container class required during the authorization process:

```java
    @Bean
    public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
    }

 
    @Bean
    public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
    }
```

The authorization server needs a signing key for its tokens, so 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;
      }
    }
```


Next, we will create the `SecurityFilterChain` used for OAuth2 authorization. `SecurityFilterChain` is a filter chain provided by Spring Security.

```java
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
                new OAuth2AuthorizationServerConfigurer<>();
        //配置OIDC
        authorizationServerConfigurer.oidc(Customizer.withDefaults());

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

Above, we configured the default configuration for OAuth2 and OpenID Connect, and redirected authentication requests to the login page. At the same time, we also enable the OAuth2 resource service configuration provided by Spring Security. This configuration is used to protect the OpenID Connect */userinfo* user information endpoint.




When enabling the Spring Security OAuth2 resource service configuration, we specify JWT verification, so we need to specify the `jwk-set-uri` in `application.yml` or add `JwtDecoder` declaratively. Here, we use declarative configuration:

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

Next, we will customize the access token. In this example, we use the RBAC0 permission model, so we add the `authorities` for the permission code of the current user's role to the `access_token`:

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

    @Autowired
    RoleRepository roleRepository;

    @Bean
    public OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer() {
        return (context) -> {
            if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
                context.getClaims().claims(claim -> {
                    claim.put("authorities", roleRepository.findByRoleCode(context.getPrincipal().getAuthorities().stream()
                            .map(GrantedAuthority::getAuthority).findFirst().orElse("ROLE_OPERATION"))
                            .getPermissions().stream().map(Permission::getPermissionCode).collect(Collectors.toSet()));
                });
            }
        };
    }
}

```

> RoleRepository is the persistence layer object of the role table. In this example, we use the JPA framework and the relevant code will not be shown in this article. If you are not familiar with JPA, you can use Mybatis instead.


Below we will configure the authorization service Form authentication method.

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

Next, we will create a `JdbcUserDetailsService` that implements `UserDetailsService`, which is used to look up the password and permission information of the logged-in user during the authentication process. If you are interested in why `UserDetailsService` needs to be implemented, you can check the source code of `UsernamePasswordAuthenticationFilter` -> `ProviderManager` -> `DaoAuthenticationProvider`. In `DaoAuthenticationProvider`, the user information is obtained by calling UserDetailsService#loadUserByUsername(String username).


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

```

And we will inject it into Spring:

```java
    @Bean
    UserDetailsService userDetailsService(UserRepository userRepository) {
        return new JdbcUserDetailsService(userRepository);
    }
```

When attempting to access an unauthenticated interface, the user will be directed to the login page and prompted to enter their username and password, as shown below:


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

Users usually need to use multiple platforms provided and hosted by different organizations. These users may need to use specific (and different) credentials for each platform. When users have many different credentials, they often forget their login credentials.


**Federated authentication** is used to authenticate users using external systems. This can be used with Google, Github or any other identity provider. Here, I will use Github for user authentication and data synchronization management.

### Github Identity Authentication

First, we will configure the Github client information, and you only need to change the **clientId** and **clientSecret**. Secondly, we will use the `JdbcClientRegistrationRepository` persistence layer container class introduced in [Spring Security Persistence OAuth2 Client](https://dev.to/relive27/spring-security-persistent-oauth2-client-1gcc) to store the GitHub client information in the database.

```java
    @Bean
    ClientRegistrationRepository clientRegistrationRepository(JdbcTemplate jdbcTemplate) {
        JdbcClientRegistrationRepository jdbcClientRegistrationRepository = new JdbcClientRegistrationRepository(jdbcTemplate);
        ClientRegistration clientRegistration = ClientRegistration.withRegistrationId("github")
                .clientId("123456")
                .clientSecret("123456")
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .redirectUri("{baseUrl}/{action}/oauth2/code/{registrationId}")
                .scope(new String[]{"read:user"})
                .authorizationUri("https://github.com/login/oauth/authorize")
                .tokenUri("https://github.com/login/oauth/access_token")
                .userInfoUri("https://api.github.com/user")
                .userNameAttributeName("login")
                .clientName("GitHub").build();

        jdbcClientRegistrationRepository.save(clientRegistration);
        return jdbcClientRegistrationRepository;
    }
```

Next, we will instantiate `OAuth2AuthorizedClientService` and `OAuth2AuthorizedClientRepository`:

- **OAuth2AuthorizedClientService**: responsible for persisting OAuth2AuthorizedClient between web requests.
- **OAuth2AuthorizedClientRepository**: used to save and persist authorization clients between requests.

```java

    @Bean
    OAuth2AuthorizedClientService authorizedClientService(
            JdbcTemplate jdbcTemplate,
            ClientRegistrationRepository clientRegistrationRepository) {
        return new JdbcOAuth2AuthorizedClientService(jdbcTemplate, clientRegistrationRepository);
    }

    @Bean
    OAuth2AuthorizedClientRepository authorizedClientRepository(
            OAuth2AuthorizedClientService authorizedClientService) {
        return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);
    }
```

For each user logging in with Github, we need to assign platform roles to control which resources they can access. Here, we will create the AuthorityMappingOAuth2UserService class to grant user roles:

```java
@RequiredArgsConstructor
public class AuthorityMappingOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
    private DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
    private final OAuth2ClientRoleRepository oAuth2ClientRoleRepository;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        DefaultOAuth2User oAuth2User = (DefaultOAuth2User) delegate.loadUser(userRequest);

        Map<String, Object> additionalParameters = userRequest.getAdditionalParameters();
        Set<String> role = new HashSet<>();
        if (additionalParameters.containsKey("authority")) {
            role.addAll((Collection<? extends String>) additionalParameters.get("authority"));
        }
        if (additionalParameters.containsKey("role")) {
            role.addAll((Collection<? extends String>) additionalParameters.get("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());
        //When no client role is specified, the least privilege ROLE_OPERATION is given by default
        if (CollectionUtils.isEmpty(mappedAuthorities)) {
            mappedAuthorities = new HashSet<>(
                    Collections.singletonList(new SimpleGrantedAuthority("ROLE_OPERATION")));
        }
        String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
        return new DefaultOAuth2User(mappedAuthorities, oAuth2User.getAttributes(), userNameAttributeName);
    }
}
```

We can see that the permission information is obtained from the authority and role attributes, and the role attributes mapped to this platform are searched through OAuth2ClientRoleRepository.

> Note: `authority` and `role` are custom attributes of the platform, unrelated to the OAuth2 protocol and Open ID Connect protocol. In a production environment, you can negotiate with an external system to agree on an attribute to transmit permission information.
> 
> 
> `OAuth2ClientRoleRepository` is the persistence layer container class for the `oauth2_client_role` table, implemented by JPA.


For the mapped role information that has not been obtained in advance, we will assign the default `ROLE_OPERATION` minimum permission role. In this example, users who log in with Github will also be assigned the `ROLE_OPERATION` role.



For users who successfully authenticate with Github and log in for the first time, we will obtain their user information and persist it to the `user` table. Here, we will implement `AuthenticationSuccessHandler` and add user persistence logic.

```java
public final class SavedUserAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private final AuthenticationSuccessHandler delegate = new SavedRequestAwareAuthenticationSuccessHandler();


    private Consumer<OAuth2User> oauth2UserHandler = (user) -> {
    };

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        if (authentication instanceof OAuth2AuthenticationToken) {
            if (authentication.getPrincipal() instanceof OAuth2User) {
                this.oauth2UserHandler.accept((OAuth2User) authentication.getPrincipal());
            }
        }

        this.delegate.onAuthenticationSuccess(request, response, authentication);
    }

    public void setOauth2UserHandler(Consumer<OAuth2User> oauth2UserHandler) {
        this.oauth2UserHandler = oauth2UserHandler;
    }
}
```


We will inject `UserRepositoryOAuth2UserHandler` into `SavedUserAuthenticationSuccessHandler` through the `setOauth2UserHandler(Consumer<OAuth2User> oauth2UserHandler)` method. `UserRepositoryOAuth2UserHandler` defines specific persistence layer operations:

```java
@Component
@RequiredArgsConstructor
public final class UserRepositoryOAuth2UserHandler implements Consumer<OAuth2User> {

    private final UserRepository userRepository;

    private final RoleRepository roleRepository;

    @Override
    public void accept(OAuth2User oAuth2User) {
        DefaultOAuth2User defaultOAuth2User = (DefaultOAuth2User) oAuth2User;
        if (this.userRepository.findUserByUsername(oAuth2User.getName()) == null) {
            User user = new User();
            user.setUsername(defaultOAuth2User.getName());
            Role role = roleRepository.findByRoleCode(defaultOAuth2User.getAuthorities()
                    .stream().map(GrantedAuthority::getAuthority).findFirst().orElse("ROLE_OPERATION"));
            user.setRoleList(Arrays.asList(role));
            userRepository.save(user);
        }
    }
}
```

We obtain the role information mapped by `defaultOAuth2User.getAuthorities()` and store it in the database with the user information.

> UserRepository and RoleRepository are persistence container classes.

Finally, we add OAuth2 Login configuration to SecurityFilterChain:

```java
    @Autowired
    UserRepositoryOAuth2UserHandler userHandler;

    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeRequests(authorizeRequests ->
                        authorizeRequests.anyRequest().authenticated()
                )
                .oauth2Login(oauth2login -> {
                    SavedUserAuthenticationSuccessHandler successHandler = new SavedUserAuthenticationSuccessHandler();
                    successHandler.setOauth2UserHandler(userHandler);
                    oauth2login.successHandler(successHandler);
                });
      
      	...
        
        return http.build();
    }
```

## Create a Spring Cloud Gateway application

In this section, we will use [Spring Security OAuth2 Login](https://docs.spring.io/spring-security/reference/reactive/oauth2/login/index.html) in Spring Cloud Gateway to enable OpenID Connect authentication and relay Access Token to downstream services.


### Maven dependencies

```xml
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-gateway</artifactId>
  <version>3.1.2</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-redis</artifactId>
  <version>2.6.7</version>
</dependency>

<dependency>
  <groupId>org.springframework.session</groupId>
  <artifactId>spring-session-data-redis</artifactId>
  <version>2.6.3</version>
</dependency>

<dependency>
  <groupId>io.netty</groupId>
  <artifactId>netty-all</artifactId>
  <version>4.1.76.Final</version>
</dependency>

```

### Configuration

First, we add the following properties to `application.yml`:

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

Here, the cookie name is specified as `GATEWAY-CLIENT` to avoid conflicts with the authorization service `JSESSIONID`.


Route to the resource server through Spring Cloud Gateway:

```yaml
spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
      routes:
        - id: resource-server
          uri: http://127.0.0.1:8090
          predicates:
            Path=/resource/**
          filters:
            - TokenRelay

```
The TokenRelay filter extracts the access token stored in the user session and adds it as an Authorization header to the outgoing request. This allows downstream services to authenticate requests.

We will add OAuth2 client information in `application.yml`:

```yaml
spring:
  security:
    oauth2:
      client:
        registration:
          messaging-gateway-oidc:
            provider: gateway-client-provider
            client-id: relive-client
            client-secret: relive-client
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            scope:
              - openid
              - profile
            client-name: messaging-gateway-oidc
        provider:
          gateway-client-provider:
            authorization-uri: http://127.0.0.1:8080/oauth2/authorize
            token-uri: http://127.0.0.1:8080/oauth2/token
            jwk-set-uri: http://127.0.0.1:8080/oauth2/jwks
            user-info-uri: http://127.0.0.1:8080/userinfo
            user-name-attribute: sub
```

OpenID Connect uses a special scope value `openid` to control access to the UserInfo endpoint, and other information is consistent with the client registration information parameters of the authorization service in the previous section.


Spring Security intercepts unauthenticated requests and performs authentication on the authorization server. For simplicity, [CSRF](https://docs.spring.io/spring-security/site/docs/5.0.x/reference/html/csrf.html) is disabled.

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

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        http
                .authorizeExchange(authorize -> authorize
                        .anyExchange().authenticated()
                )
                .oauth2Login(withDefaults())
                .cors().disable();
        return http.build();
    }
}
```

After completing OpenID Connect authentication in Spring Cloud Gateway, the user information and token are stored in the session, so we add `spring-session-data-redis` to provide Redis-supported distributed session functionality and add the following configuration in `application.yml`:

```yaml
spring:
  session:
    store-type: redis 
    redis:
      flush-mode: on_save 
      namespace: gateway:session
  redis:
    host: localhost
    port: 6379
    password: 123456
```

Based on the above example, we use Spring Cloud Gateway to drive authentication and know how to authenticate users, obtain tokens (after user consent), but do not authenticate/authorize requests through Gateway (Spring Gateway Cloud is not the audience target of Access Token). The reason behind this approach is that some services are protected while others are public. Even in a single service, sometimes only a few endpoints need to be protected, not every endpoint. That's why I leave the request authentication/authorization to specific services.

Of course, from the implementation perspective, it does not prevent us from performing authentication/authorization in Spring Cloud Gateway, it is just a matter of choice.



## Resource server

In this section, we use Spring Boot to set up a simple resource server. The example provides two API interfaces for the resource server and protects them through Spring Security OAuth2 resource server configuration.

### 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-resource-server</artifactId>
  <version>2.6.7</version>
</dependency>
```

### Configuration

Add the `jwk-set-uri` property in `application.yml`:

```yaml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://127.0.0.1:8080
          jwk-set-uri: http://127.0.0.1:8080/oauth2/jwks 

server:
  port: 8090
```

Create the `ResourceServerConfig` class to configure the Spring Security security module, and use the `@EnableMethodSecurity` annotation to enable annotation-based security.

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

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests((authorize) -> authorize
                .anyRequest().authenticated()
                 )
                .oauth2ResourceServer()
                .jwt();
        return http.build();
    }
}
```

Spring Security resource server uses the `scope` and `scp` attributes in the claim by default to verify the token and extract permission information.

> Spring Security `JwtAuthenticationProvider` extracts permission information and other information through the `JwtAuthenticationConverter` auxiliary converter.

However, in this example, the internal permissions use the authorities attribute, so we use `JwtAuthenticationConverter` to manually extract permissions.

```java
    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities");
        grantedAuthoritiesConverter.setAuthorityPrefix("");

        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
        return jwtAuthenticationConverter;
    }
```
Here, we specify the permission attribute as `authorities` and completely remove the permission prefix.

Finally, we will create API interfaces for testing in the example and use `@PreAuthorize` to protect the interface, which can only be accessed by corresponding permissions:

```java
@RestController
public class ArticleController {

    List<String> article = new ArrayList<String>() {{
        add("article1");
        add("article2");
    }};

    @PreAuthorize("hasAuthority('read')")
    @GetMapping("/resource/article/read")
    public Map<String, Object> read(@AuthenticationPrincipal Jwt jwt) {
        Map<String, Object> result = new HashMap<>(2);
        result.put("principal", jwt.getClaims());
        result.put("article", article);
        return result;
    }

    @PreAuthorize("hasAuthority('write')")
    @GetMapping("/resource/article/write")
    public String write(@RequestParam String name) {
        article.add(name);
        return "success";
    }
}
```

## Testing our application

After we have started the service, we access [http://127.0.0.1:8070/resource/article/read](http://127.0.0.1:8070/resource/article/read) in the browser and we will be redirected to the authorization service login page as shown below:


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


After we enter the username and password (admin/password), we will get the request response information:


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

The role of the admin user is `ROLE_ADMIN`, so we try to request [http://127.0.0.1:8070/resource/article/write?name=article3](http://127.0.0.1:8070/resource/article/write?name=article3)


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

After logging out, we also access [http://127.0.0.1:8070/resource/article/read](http://127.0.0.1:8070/resource/article/read), but this time we use Github login, and the response information is as shown below:


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

We can see that the response information shows that the user has switched to your Github username.


Users who log in with Github are assigned the `ROLE_OPERATION` role by default, and `ROLE_OPERATION` does not have access to [http://127.0.0.1:8070/resource/article/write?name=article3](http://127.0.0.1:8070/resource/article/write?name=article3). Let's try to test it:


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

The result shows that our request is rejected, with a 403 status code indicating that we do not have access permissions.

## Conclusion

In this article, you learned how to use Spring Cloud Gateway to protect microservices with OAuth2. In the example, the browser cookie only stores the session ID, and the JWT access token is not exposed to the browser but flows internally in the service. This way, we experience the advantages of JWT and also use the cookie-session to compensate for the shortcomings of JWT, such as when we need to implement a forced user logout function.

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

Thanks for reading! 😊