As a Software Architect, transitioning from the legacy spring-security-oauth2 to the modern Spring Authorization Server (SAS) is a critical shift. This guide provides a deep dive into building a robust identity platform integrated with Spring Cloud Gateway and Social Logins.

1. Core Architecture: How it Works

Spring Authorization Server is the official successor to the legacy OAuth stack. It is built as a standalone library (not a separate product like Keycloak) that you embed in a Spring Boot application.

The OAuth 2.1 / OIDC 1.0 Flow

  1. Client Authentication: The client (e.g., Spring Cloud Gateway) redirects the user to the SAS /oauth2/authorize endpoint.
  2. User Authentication: SAS authenticates the user (via Form Login or Federated Social Login).
  3. Consent: The user grants permission to the client.
  4. Token Issuance: SAS issues a JWT (Access Token) and an optional Refresh Token.

2. Setting Up the Authorization Server (Gradle)

In your build.gradle, include the specialized starter:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-authorization-server'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' // For Social Login
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

The Configuration Bean

SAS requires several beans to define its behavior. The most important is the RegisteredClientRepository.

@Configuration
public class AuthorizationServerConfig {

    @Bean
    @Order(1)
    public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
            .oidc(Customizer.withDefaults());	// Enable OpenID Connect 1.0
        
        http.exceptionHandling((exceptions) -> exceptions
            .defaultAuthenticationEntryPointFor(
                new LoginUrlAuthenticationEntryPoint("/login"),
                new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
            )
        );
        return http.build();
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient gatewayClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("gateway-client")
                .clientSecret("{noop}secret") 
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .redirectUri("[http://127.0.0.1:8080/login/oauth2/code/gateway](http://127.0.0.1:8080/login/oauth2/code/gateway)")
                .scope(OidcScopes.OPENID)
                .scope(OidcScopes.PROFILE)
                .scope("resource.read")
                .build();

        return new InMemoryRegisteredClientRepository(gatewayClient);
    }
}

3. Role Integration & JWT Customization

In an OAuth2 ecosystem, roles are typically passed as “claims” inside the JWT. By default, SAS doesn’t include user roles in the access token. You must customize the OAuth2TokenCustomizer.

Customizing the JWT on the Auth Server

This logic runs on the Authorization Server to fetch roles from your UserDetailsService and inject them into the token.

@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer() {
    return (context) -> {
        if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
            Authentication principal = context.getPrincipal();
            Set<String> authorities = principal.getAuthorities().stream()
                    .map(GrantedAuthority::getAuthority)
                    .collect(Collectors.toSet());
            
            // Add custom "roles" claim to the JWT
            context.getClaims().claim("roles", authorities);
        }
    };
}

Mapping Roles on the Resource Server (Microservices)

Microservices need to know that the roles claim in the JWT should be treated as Spring Security authorities (prefixed with ROLE_).

@Configuration
@EnableMethodSecurity // Enables @PreAuthorize
public class ResourceServerConfig {

    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); // Convert 'ADMIN' to 'ROLE_ADMIN'
        grantedAuthoritiesConverter.setAuthoritiesClaimName("roles"); // Look for 'roles' claim

        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
        return jwtAuthenticationConverter;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
            );
        return http.build();
    }
}

4. Social Login & Federated Identity

To allow users to login via Google or GitHub, SAS acts as an OAuth2 Client to those providers while remaining the Authorization Server for your internal microservices.

Configuration (application.yml)

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: ${GOOGLE_CLIENT_ID}
            client-secret: ${GOOGLE_CLIENT_SECRET}
            scope: openid, profile, email

Enabling Federated Login

In your “Default” Security Filter Chain, swap formLogin() for oauth2Login():

@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests((authorize) -> authorize
            .anyRequest().authenticated()
        )
        .oauth2Login(Customizer.withDefaults()); 
    
    return http.build();
}

5. Integration with Spring Cloud Gateway

The Gateway should act as the OAuth2 Client. It handles the login flow and then relays the JWT to downstream microservices.

Gateway Dependencies (build.gradle)

dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
}

Token Relay Pattern

The TokenRelay filter automatically extracts the Access Token from the session and adds it as a Authorization: Bearer <token> header to downstream requests.

spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/api/users/**
          filters:
            - TokenRelay= 
  
  security:
    oauth2:
      client:
        provider:
          spring-auth-server:
            issuer-uri: http://auth-server:9000
        registration:
          gateway:
            provider: spring-auth-server
            client-id: gateway-client
            client-secret: secret
            authorization-grant-type: authorization_code
            scope: openid, profile, resource.read

6. Securing Spring Boot Admin Server

Securing the Admin Server involves protecting its UI and its communication with client instances.

Admin Server as OAuth2 Client (build.gradle)

dependencies {
    implementation 'de.codecentric:spring-boot-admin-server'
    implementation 'de.codecentric:spring-boot-admin-server-ui'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
}

Security Filter Chain

@Configuration
public class AdminSecurityConfig {
    private final AdminServerProperties adminServer;

    public AdminSecurityConfig(AdminServerProperties adminServer) {
        this.adminServer = adminServer;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        SavedRequestAwareAuthenticationSuccessHandler successHandler = 
            new SavedRequestAwareAuthenticationSuccessHandler();
        successHandler.setTargetUrlParameter("redirectTo");
        successHandler.setDefaultTargetUrl(this.adminServer.getContextPath() + "/");

        http.authorizeHttpRequests(authorize -> authorize
                .requestMatchers(this.adminServer.getContextPath() + "/assets/**").permitAll()
                .requestMatchers(this.adminServer.getContextPath() + "/login").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .loginPage(this.adminServer.getContextPath() + "/login")
                .successHandler(successHandler)
            )
            .csrf(csrf -> csrf.disable());
        
        return http.build();
    }
}

7. Securing Downstream Microservices (Resource Servers)

Microservices behind the gateway only need to trust the Spring Authorization Server.

Security Configuration with Method Security

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @GetMapping("/{id}")
    @PreAuthorize("hasRole('USER')") // Role-based security at the method level
    public Order getOrder(@PathVariable String id) {
        return orderService.findById(id);
    }

    @DeleteMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN')")
    public void deleteOrder(@PathVariable String id) {
        orderService.delete(id);
    }
}

Summary for Architects

ComponentRoleRole Management
Auth ServerIDPFetches roles from DB and embeds them in JWT via OAuth2TokenCustomizer.
GatewayClientAgnostic of internal roles; simply relays the token.
Resource ServerAPIConverts JWT roles claim to GrantedAuthorities and enforces via @PreAuthorize.

By centralizing role mapping in the OAuth2TokenCustomizer, you ensure that all microservices receive a consistent security context regardless of the authentication source (Social vs. Form Login).


Discover more from GhostProgrammer - Jeff Miller

Subscribe to get the latest posts sent to your email.

By Jeffery Miller

I am known for being able to quickly decipher difficult problems to assist development teams in producing a solution. I have been called upon to be the Team Lead for multiple large-scale projects. I have a keen interest in learning new technologies, always ready for a new challenge.