MTI TEK
Spring Framework | Spring OAuth2

Spring OAuth2: Authorization Server, Resource Server & Client

This tutorial walks through a three-module OAuth2 implementation using Spring Boot: an Authorization Server that issues JWT tokens, a Resource Server that validates them, and a Client that drives the Authorization Code flow. All three run as independent Spring Boot applications on distinct ports (8082, 8081, 8080) using hostname aliases to simulate separate domains.

OAuth2 Concepts

Hostname Setup

Each service uses a distinct hostname alias to avoid cookie/session collisions and to exercise real cross-origin redirect behaviour. Add the following to C:\Windows\System32\drivers\etc\hosts (or /etc/hosts on Linux/macOS):

127.0.0.1 mtitekauthclient
127.0.0.1 mtitekauthserver
127.0.0.1 mtitekauthresource

Project 1 — Authorization Server

Dependencies

Two-Chain Security Architecture

Two SecurityFilterChain beans are required: one dedicated to authorization server endpoints, the other for everything else (login form, OIDC discovery URLs). Ordering is important.

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

    http.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
            .with(authorizationServerConfigurer, configurer -> configurer.oidc(Customizer.withDefaults()))
            .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
            .exceptionHandling(exceptions -> exceptions.defaultAuthenticationEntryPointFor(new LoginUrlAuthenticationEntryPoint("/login"), new MediaTypeRequestMatcher(MediaType.TEXT_HTML)));

    return http.formLogin(Customizer.withDefaults()).build();
}

@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(authorize -> authorize
                    .requestMatchers("/.well-known/openid-configuration", "/.well-known/oauth-authorization-server", "/oauth2/jwks").permitAll()
                    .anyRequest().authenticated())
            .formLogin(Customizer.withDefaults());
    return http.build();
}

Registered Client

RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
        .clientId("mtitek-client-id")
        .clientSecret(passwordEncoder.encode("mtitek-client-secret"))
        .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
        .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
        .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
        .redirectUri("http://mtitekauthclient:8080/login/oauth2/code/mtitek-registration-id")
        .scope("addAppProfile").scope("updateAppProfile").scope("deleteAppProfile")
        .scope(OidcScopes.OPENID)
        .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).requireProofKey(false).build())
        .build();
return new InMemoryRegisteredClientRepository(registeredClient);

RSA Key — Persistent JKS

Generate the keystore once:

keytool -genkeypair \
  -alias mtitek-auth-server \
  -keyalg RSA \
  -keysize 2048 \
  -storetype JKS \
  -keystore mtitek-auth-server.jks \
  -validity 3650 \
  -storepass mtitek-auth-server-keystore-pwd \
  -keypass mtitek-auth-server-key-pwd

Place mtitek-auth-server.jks in src/main/resources/ and configure via application.yml:

app:
  security:
    keystore:
      path: mtitek-auth-server.jks
      password: mtitek-auth-server-keystore-pwd
      alias: mtitek-auth-server
      keyPassword: mtitek-auth-server-key-pwd

Issuer URI — Must Match Exactly

@Bean
public AuthorizationServerSettings authorizationServerSettings() {
    return AuthorizationServerSettings.builder().issuer("http://mtitekauthserver:8082").build();
}

UserDetailsService via JPA

@Bean
UserDetailsService userDetailsService(AppUserRepository userRepo) {
    return username -> userRepo.findByUsername(username);
}

Project 2 — Resource Server

Dependency and JWT Configuration

spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://mtitekauthserver:8082/oauth2/jwks

Dual Security Filter Chain

@Bean
@Order(1)
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
    return http.securityMatcher("/api/**")
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers(HttpMethod.POST, "/api/appProfiles/**").hasAuthority("SCOPE_addAppProfile")
                    .requestMatchers(HttpMethod.PUT, "/api/appProfiles/**").hasAuthority("SCOPE_updateAppProfile")
                    .requestMatchers(HttpMethod.PATCH, "/api/appProfiles/**").hasAuthority("SCOPE_updateAppProfile")
                    .requestMatchers(HttpMethod.DELETE, "/api/appProfiles/**").hasAuthority("SCOPE_deleteAppProfile")
                    .anyRequest().permitAll())
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
            .httpBasic(Customizer.withDefaults())
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .csrf(csrf -> csrf.disable())
            .build();
}

AppProfileController — In-Memory Store

@RestController
@RequestMapping(path = "/api/appProfiles", produces = "application/json")
@CrossOrigin(origins = "http://mtitekauthclient:8080")
public class AppProfileController {
    Map<String, AppProfile> appProfiles = new ConcurrentHashMap<>();
    ...
}

Project 3 — OAuth2 Client

OAuth2 Client Registration and Provider

spring:
  security:
    oauth2:
      client:
        registration:
          mtitek-registration-id:
            provider: mtitek
            client-id: mtitek-client-id
            client-secret: mtitek-client-secret
            authorization-grant-type: authorization_code
            redirect-uri: "http://mtitekauthclient:8080/login/oauth2/code/{registrationId}"
            scope: openid,addAppProfile,updateAppProfile,deleteAppProfile
        provider:
          mtitek:
            issuer-uri: http://mtitekauthserver:8082
            authorization-uri: http://mtitekauthserver:8082/oauth2/authorize
            token-uri: http://mtitekauthserver:8082/oauth2/token
            jwk-set-uri: http://mtitekauthserver:8082/oauth2/jwks
            user-info-uri: http://mtitekauthserver:8082/userinfo
            user-name-attribute: sub

Client Security Filter Chain

@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
            .oauth2Login(oauth2Login -> oauth2Login
                    .loginPage("/oauth2/authorization/mtitek-registration-id")
                    .authorizationEndpoint(authorization -> authorization.authorizationRequestRepository(new HttpSessionOAuth2AuthorizationRequestRepository())))
            .exceptionHandling(ex -> ex.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/oauth2/authorization/mtitek-registration-id")))
            .oauth2Client(Customizer.withDefaults());
    return http.build();
}

Resolving and Injecting the Access Token

@Component
@Slf4j
public class AppProfileRestService implements AppProfileService {
    private final OAuth2AuthorizedClientService oauth2AuthorizedClientService;
    private final RestTemplate restTemplate;

    public AppProfileRestService(OAuth2AuthorizedClientService oauth2AuthorizedClientService) {
        this.oauth2AuthorizedClientService = oauth2AuthorizedClientService;
        this.restTemplate = new RestTemplate();
        this.restTemplate.getInterceptors().add(getBearerTokenInterceptor());
    }

    private ClientHttpRequestInterceptor getBearerTokenInterceptor() {
        return (request, body, execution) -> {
            String accessToken = resolveAccessToken();
            if (accessToken != null) {
                request.getHeaders().add("Authorization", "Bearer " + accessToken);
            }
            return execution.execute(request, body);
        };
    }

    public String resolveAccessToken() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication instanceof OAuth2AuthenticationToken oauth2AuthenticationToken) {
            String registrationId = oauth2AuthenticationToken.getAuthorizedClientRegistrationId();
            if (registrationId.equals("mtitek-registration-id")) {
                OAuth2AuthorizedClient client = oauth2AuthorizedClientService.loadAuthorizedClient(registrationId, oauth2AuthenticationToken.getName());
                if (client != null && client.getAccessToken() != null) {
                    return client.getAccessToken().getTokenValue();
                }
            }
        }
        return null;
    }
}

Web Controller and @ModelAttribute

@Controller
@RequestMapping("/appProfiles")
@RequiredArgsConstructor
public class AppProfilesController {
    private final AppProfileService appProfileService;

    @ModelAttribute
    public void addAppProfilesToModel(Model model) {
        List<AppProfile> appProfiles = appProfileService.getAppProfiles();
        model.addAttribute("appProfiles", appProfiles);
    }

    @ExceptionHandler(HttpClientErrorException.class)
    public String handleHttpClientErrorException(HttpClientErrorException ex, Model model) {
        model.addAttribute("errorMessage", ex.getMessage());
        model.addAttribute("errorStatusCode", ex.getStatusCode());
        return "appProfileErrorForm";
    }
}

Authorization Code Flow — End-to-End

Step 1 — Initiate Authorization

GET http://mtitekauthserver:8082/oauth2/authorize
  ?response_type=code
  &client_id=mtitek-client-id
  &scope=openid+addAppProfile+updateAppProfile+deleteAppProfile
  &redirect_uri=http://mtitekauthclient:8080/login/oauth2/code/mtitek-registration-id

Step 2 — Exchange Code for Tokens

curl -iv mtitekauthserver:8082/oauth2/token \
    -H "Content-type: application/x-www-form-urlencoded" \
    -d "grant_type=authorization_code" \
    -d "redirect_uri=http://mtitekauthclient:8080/login/oauth2/code/mtitek-registration-id" \
    -d "code=<authorization_code>" \
    -u mtitek-client-id:mtitek-client-secret

Step 3 — Call the Resource Server

curl http://mtitekauthresource:8081/api/appProfiles \
    -H "Authorization: Bearer <access_token>"

Step 4 — Refresh the Access Token

curl mtitekauthserver:8082/oauth2/token \
    -H "Content-type: application/x-www-form-urlencoded" \
    -d "grant_type=refresh_token&refresh_token=<refresh_token>" \
    -u mtitek-client-id:mtitek-client-secret

Authorization Server Discovery Endpoints