Techniques to Understand Spring Authorization Server Authentication—from Browser Redirects to Token Signing
Techniques to Understand Spring Authorization Server Authentication—from Browser Redirects to Token Signing Below is a clear, end-to-end explanation of how Spring Authorization Server (SAS) authenticates a user and a client, issues tokens, and pr...

I am Tuanh.net. As of 2024, I have accumulated 8 years of experience in backend programming. I am delighted to connect and share my knowledge with everyone.
1. What actually happens when “Login with X” bounces you around?
Authentication objects and back again.
/oauth2/authorize, /oauth2/token, /oauth2/jwks, /oauth2/introspection, etc. Within that chain, it installs a suite of AuthenticationProviders that know how to:
- parse and verify client authentication (client secret, private key JWT, mTLS),
- enforce PKCE and consent on the authorization request,
- mint tokens in the token endpoint (authorization code, refresh token, client credentials, device code),
- publish JWKs so resource servers can verify JWT signatures offline.
1.1 The minimal working server you can actually run
// build.gradle (or use Maven equivalents)
// dependencies {
// implementation("org.springframework.boot:spring-boot-starter-web")
// implementation("org.springframework.boot:spring-boot-starter-oauth2-authorization-server")
// implementation("org.springframework.boot:spring-boot-starter-security")
// implementation("org.springframework.boot:spring-boot-starter-thymeleaf") // for default login
// implementation("com.nimbusds:nimbus-jose-jwt")
// }
// src/main/java/com/example/authserver/SecurityConfig.java
package com.example.authserver;
import com.nimbusds.jose.jwk.;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.web.SecurityFilterChain;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;
@Configuration
public class SecurityConfig {
// 1) The dedicated chain that serves /oauth2/ endpoints
@Bean
SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
// Optional: enable OpenID Connect endpoints
http.getConfigurer(OAuth2AuthorizationServerConfiguration.class)
.oidc(Customizer.withDefaults());
// JWT resource server support for UserInfo endpoint if needed
http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
// 2) The app's own login form / session for resource owner authentication
@Bean
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/assets/**").permitAll()
.anyRequest().authenticated())
.formLogin(Customizer.withDefaults());
return http.build();
}
// 3) Two example OAuth clients (public PKCE + confidential secret-based)
@Bean
RegisteredClientRepository registeredClientRepository(PasswordEncoder encoder) {
RegisteredClient publicPkceClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("toolbox-pkce")
.clientAuthenticationMethod(auth -> {}) // public; no client secret
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://127.0.0.1:8081/login/oauth2/code/toolbox")
.scope(OidcScopes.OPENID)
.scope("profile.read")
.clientSettings(settings -> settings.requireProofKey(true).requireAuthorizationConsent(true))
.build();
RegisteredClient confidentialClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("service-client")
.clientSecret(encoder.encode("service-secret-123"))
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.scope("reports.read")
.build();
return new InMemoryRegisteredClientRepository(publicPkceClient, confidentialClient);
}
// 4) Local in-memory users for the login screen
@Bean
UserDetailsService userDetailsService(PasswordEncoder encoder) {
return username -> User.withUsername("anh")
.password(encoder.encode("password"))
.roles("USER")
.build();
}
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
// 5) JWK source for signing JWT access tokens and ID tokens
@Bean
JWKSource<securitycontext> jwkSource() {
RSAKey rsaKey = generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
// 6) Decoder for resource server validations (UserInfo, etc.)
@Bean
JwtDecoder jwtDecoder(JWKSource<securitycontext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
private static RSAKey generateRsa() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
return new RSAKey.Builder(publicKey)
.privateKey(keyPair.getPrivate())
.keyID(UUID.randomUUID().toString())
.build();
}
private static KeyPair generateRsaKey() {
try {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);
return kpg.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
}
:8080. A PKCE client running at :8081 redirects to http://localhost:8080/oauth2/authorize?... where you’ll authenticate as anh/password. After consent, the client exchanges the code for tokens at /oauth2/token. The server publishes keys at /oauth2/jwks so resource servers can verify signatures.
1.2 The request morphs: from URL params to strongly-typed Authentication
/oauth2/authorize, the AuthorizationEndpointFilter parses the query string into an OAuth2AuthorizationCodeRequestAuthenticationToken. That flows into OAuth2AuthorizationCodeRequestAuthenticationProvider. If the resource owner hasn’t logged in, Spring Security kicks in with the application filter chain to show the login page. Once you authenticate, SAS resumes the pending authorization with the now-authenticated Principal. If requireAuthorizationConsent is true, the ConsentPage is rendered; otherwise, it proceeds to issue an authorization code and redirects the browser back to the client’s redirect_uri.
OAuth2AuthorizationCodeAuthenticationToken. The provider verifies:
- the client’s identity (secret, private key JWT, or nothing for public clients),
- the authorization code is valid and bound to the same client and
redirect_uri, - PKCE’s
code_verifiermatches the originalcode_challenge.
OAuth2AuthorizationService.
1.3 Where do JWTs actually come from?
JwtEncoder fed by your JWKSource. That encoder signs the token using the private RSA key you generated, and the server publishes the public key via /oauth2/jwks. The claims are shaped by OAuth2TokenCustomizer
if you provide one. For example, you can add tenant_id, roles, or a nonce for OIDC ID tokens, as shown below:
// src/main/java/com/example/authserver/JwtCustomizerConfig.java
package com.example.authserver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
@Configuration
public class JwtCustomizerConfig {
@Bean
OAuth2TokenCustomizer<jwtencodingcontext> tokenCustomizer() {
return context -> {
if (context.getTokenType().getValue().equals("access_token")) {
context.getClaims().claim("tenant_id", "klara-tech")
.claim("roles", new String[]{"USER", "REPORT_VIEWER"});
}
if (context.getTokenType().getValue().equals("id_token")) {
context.getClaims().claim("ui_theme", "dark");
}
};
}
}
2. How Spring’s filter chains and providers choreograph the dance
- Client →
/oauth2/authorizewithclient_id,redirect_uri,scope,state, andcode_challenge(PKCE). - Server → Login form because resource owner is anonymous.
- User authenticates (session or remember-me cookie from your app’s chain).
- Server → Consent page if required; otherwise it skips.
- Server → Redirect back to client with
?code=...&state=.... - Client →
/oauth2/token(back channel) withgrant_type=authorization_code,code,redirect_uri, andcode_verifier(PKCE). - Server mints tokens (access token, refresh token, optional ID token).
- Server publishes JWKs at
/oauth2/jwks; resource servers cache this to verify signatures.
2.1 Client authentication: secret, private key JWT, and mTLS
RegisteredClient, you choose:
- Secret:
clientAuthenticationMethod=client_secret_basicorpost. - Public PKCE: no secret,
requireProofKey(true)—safer for SPAs and native apps. - Private key JWT: the client signs a
client_assertionJWT with its private key. - mTLS: mutual TLS binds the client identity to its certificate.
invalid_client. The boring answer is also the correct answer: configure your client exactly as it will authenticate, then SAS will do the rest.
2.2 Consent, scopes, and claims—why you sometimes see that scary screen
requireAuthorizationConsent(true), SAS persists the authorized scopes per user and client. Requesting a subset next time usually skips consent; requesting new scopes brings the consent UI back. Downstream, you should map scopes to claims in your JWT customizer or in your resource servers’ authorization rules—for example, hasAuthority("SCOPE_profile.read") on a controller.
3. Deep dive: Token lifetimes, refresh, introspection, revocation
TokenSettings). Short access tokens are good hygiene; refresh tokens renew sessions without re-prompting the user, provided the client is confidential or a trusted public app with rotation.
- Introspection (
/oauth2/introspection) lets internal services verify opaque tokens. With JWTs you rarely need this, but it’s vital when you reject self-contained tokens in favor of references. - Revocation (
/oauth2/revoke) is how clients voluntarily discard refresh or access tokens. For security events, consider invalidating the authorization in theOAuth2AuthorizationServicestore.
4. Resource Server verification—trust but verify
/oauth2/jwks matters. A typical resource server config:
// In a downstream API
@EnableWebSecurity
class ApiSecurity {
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().hasAuthority("SCOPE_profile.read"))
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
}
spring.security.oauth2.resourceserver.jwt.jwk-set-uri to the auth server’s JWK set. The API never sees a client secret; it sees a signed claim set and decides access from scopes/claims.
5. OpenID Connect layer—ID tokens and the /userinfo endpoint
.oidc(Customizer.withDefaults())), SAS exposes an ID token and endpoints like /.well-known/openid-configuration and /userinfo. The ID token is about authentication (who the user is, when they authenticated, and for which client), while the access token is about authorization. If you’re building login for a web app, you probably want OIDC on.
OAuth2TokenCustomizer
, or by plugging an OidcUserInfoMapper to determine what /userinfo returns. Just don’t leak private data; least privilege applies to identity too.
6. Advanced hooks you’ll actually need in real life
- Persistent stores: swap the in-memory
RegisteredClientRepositoryandOAuth2AuthorizationServicefor JDBC or JPA. This is non-negotiable in multi-instance deployments. - Event listeners: audit who granted what; wire a listener for authorization success/failure.
- Account linking / multi-tenant claims: populate
tenant_id,org_id,plan, androlesreliably from your directory or user DB. - Strong client auth: prefer private key JWT or mTLS for service-to-service. Secrets are the plastic forks of security—use them only when you must.
@Bean
RegisteredClientRepository jdbcClients(JdbcTemplate jdbc) {
JdbcRegisteredClientRepository repo = new JdbcRegisteredClientRepository(jdbc);
// bootstrap or migrate clients here...
return repo;
}
@Bean
OAuth2AuthorizationService jdbcAuthorizationService(JdbcTemplate jdbc, RegisteredClientRepository clients) {
return new JdbcOAuth2AuthorizationService(jdbc, clients);
}
7. Troubleshooting like a pro (a.k.a. reading the right logs)
# application.properties
logging.level.org.springframework.security=DEBUG
logging.level.org.springframework.security.oauth2=DEBUG
logging.level.org.springframework.security.oauth2.server.authorization=DEBUG
- Redirect URI mismatch → The exact string (scheme, host, port, path) must match the registered value.
- Invalid PKCE →
code_verifiermissing or doesn’t matchcode_challenge. - Wrong client auth method → Public client trying to use a secret, or vice versa.
- Clock skew → Misaligned times between services can break JWT validation (
nbf,exp).
8. Security posture—turning a demo into a fortress
9. The “why” behind the “what”
10. Where to go next (and what to harden)
/oauth2/jwks and key rotation alarms. If you expose OIDC, curate the ID token and /userinfo payloads so they remain minimal and stable—clients love stability almost as much as they love access tokens.
Read more at : Techniques to Understand Spring Authorization Server Authentication—from Browser Redirects to Token Signing





