A server that starts
Spring Authorization Server on port 9000 with OIDC, a development client and user, token policy, and discovery and JWKS endpoints.
You will build
a Spring Boot authorization server with two filter chains, an in-memory client and user, OIDC enabled, and published signing keys
You will understand
why the protocol endpoints get their own filter chain, what a registered client declares, and what the issuer and JWKS are for
- one configuration class, in memory
Now we turn the protocol model into a running application.
Our first checkpoint is intentionally simple:
Spring Boot starts on :9000
+
OAuth authorization endpoints exist
+
OIDC is enabled
+
A demo client and user can authenticate
+
JWTs can be signed
Persistence comes later. Starting in memory makes it easier to understand which Spring components are actually required.
Project baseline
This series uses the repository’s baseline:
Java 25
Spring Boot 4.1.x
Spring Security 7.1.x
Maven
JAR packaging
Generate the project with Spring Initializr and include at least:
- OAuth2 Authorization Server;
- Validation;
- DevTools (optional for local development).
The key dependency is the Spring Boot authorization-server starter. The current Spring Security documentation shows this as the normal Boot entry point for an authorization-server application.
Port and application name
src/main/resources/application.yaml:
spring:
application:
name: spring-boot-oauth2-authorization-server
server:
port: 9000
Why 9000? No protocol requirement. It simply keeps the auth server visually separate from example clients and resource servers.
The first security filter chain
Create AuthorizationServerConfig.
The authorization-server endpoints need their own security matcher and authentication entry point:
@Bean
@Order(1)
SecurityFilterChain authorizationServerSecurityFilterChain(
HttpSecurity http
) throws Exception {
http.oauth2AuthorizationServer(authorizationServer -> {
http.securityMatcher(
authorizationServer.getEndpointsMatcher()
);
authorizationServer.oidc(Customizer.withDefaults());
});
http.authorizeHttpRequests(authorize ->
authorize.anyRequest().authenticated()
);
http.exceptionHandling(exceptions ->
exceptions.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
);
return http.build();
}
Then add the ordinary application/login chain:
@Bean
@Order(2)
SecurityFilterChain defaultSecurityFilterChain(
HttpSecurity http
) throws Exception {
http.authorizeHttpRequests(authorize ->
authorize.anyRequest().authenticated()
);
http.formLogin(Customizer.withDefaults());
return http.build();
}
Why two chains?
flowchart TD
Request[Incoming request]
M{Authorization Server endpoint?}
A[Order 1: OAuth/OIDC chain]
D[Order 2: default application chain]
Request --> M
M -->|Yes| A
M -->|No| D
OAuth protocol endpoints and ordinary application endpoints have different behavior. Separating chains makes later admin/resource-server configuration much easier to reason about.
Development user
For the first checkpoint, an in-memory user is enough:
@Bean
UserDetailsService userDetailsService(
PasswordEncoder passwordEncoder
) {
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder.encode("password"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
And:
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
Do not interpret these credentials as production defaults. They disappear once we implement database-backed identity and profile-isolated development seed data.
Register a development OAuth client
Create an in-memory registered client:
@Bean
RegisteredClientRepository registeredClientRepository(
PasswordEncoder passwordEncoder,
TokenSettings tokenSettings
) {
RegisteredClient client = RegisteredClient
.withId(UUID.randomUUID().toString())
.clientId("demo-client")
.clientSecret(passwordEncoder.encode("demo-secret"))
.clientAuthenticationMethod(
ClientAuthenticationMethod.CLIENT_SECRET_BASIC
)
.authorizationGrantType(
AuthorizationGrantType.AUTHORIZATION_CODE
)
.authorizationGrantType(
AuthorizationGrantType.REFRESH_TOKEN
)
.redirectUri("http://127.0.0.1:8081/callback")
.postLogoutRedirectUri("http://127.0.0.1:8081/")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.scope("read")
.scope("write")
.clientSettings(
ClientSettings.builder()
.requireProofKey(true)
.requireAuthorizationConsent(true)
.build()
)
.tokenSettings(tokenSettings)
.build();
return new InMemoryRegisteredClientRepository(client);
}
Important choices:
Authorization Code yes
Refresh Token yes
PKCE required yes
Consent required yes
Client authentication client_secret_basic
Token lifetime policy
Keep token policy in a dedicated bean:
@Bean
TokenSettings tokenSettings() {
return TokenSettings.builder()
.authorizationCodeTimeToLive(Duration.ofMinutes(5))
.accessTokenTimeToLive(Duration.ofMinutes(15))
.refreshTokenTimeToLive(Duration.ofDays(30))
.reuseRefreshTokens(false)
.build();
}
reuseRefreshTokens(false) means a refresh operation rotates the refresh token instead of returning the same long-lived value forever.
Issuer
For local development:
@Bean
AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder()
.issuer("http://localhost:9000")
.build();
}
Later we will externalize this value. The issuer becomes part of token validation and discovery, so production deployments need a stable public HTTPS issuer.
Generate an RSA JWK
At this stage we can generate an ephemeral RSA key at startup. Later we will replace it with persistent PEM files.
Conceptually:
flowchart LR
Boot[Application startup] --> RSA[Generate RSA keypair]
RSA --> JWK[JWKSource]
JWK --> Sign[JWT encoder signs tokens]
JWK --> JWKS["/oauth2/jwks"]
A typical configuration is:
@Bean
JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = generateRsaKey();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
and:
@Bean
JwtDecoder jwtDecoder(
JWKSource<SecurityContext> jwkSource
) {
return OAuth2AuthorizationServerConfiguration
.jwtDecoder(jwkSource);
}
The exact RSA helper is ordinary Java key-generation code. Keep it private to the configuration class for now.
Start the server
./mvnw spring-boot:run
Check discovery:
curl -s \
http://localhost:9000/.well-known/openid-configuration \
| jq
You should see endpoint metadata including authorization, token, JWKS, and UserInfo URLs once OIDC is enabled.
Check JWKS:
curl -s http://localhost:9000/oauth2/jwks | jq
Figure: what exists now
flowchart TD
Browser[Browser] --> Login["/login"]
Browser --> Authorize["/oauth2/authorize"]
Authorize --> Client[In-memory RegisteredClient]
Authorize --> User[In-memory UserDetails]
Authorize --> Token["/oauth2/token"]
Token --> JWK[Ephemeral RSA key]
JWK --> JWT[JWT access token]
Common mistakes
OIDC endpoints missing
Confirm:
authorizationServer.oidc(Customizer.withDefaults());
Without OIDC, openid does not activate the identity layer you expect.
Redirect URI mismatch
OAuth redirect URIs are security-sensitive. Use the exact registered value:
http://127.0.0.1:8081/callback
Do not casually swap it with localhost unless both are registered intentionally.
New signing key after every restart
Expected for now. We deliberately use an ephemeral key at this stage. Persistent keys arrive later.
Verification checklist
[ ] Application starts on 9000
[ ] /login works
[ ] discovery endpoint returns JSON
[ ] /oauth2/jwks returns an RSA public JWK
[ ] demo-client exists in memory
[ ] PKCE is required
[ ] refresh-token reuse is disabled
The code
This part corresponds to these commits in the repository:
7b7fb33add filter chain for the protocol endpoints47f77b1add default security fileer chainafbfcf3add temp client and user800f63ffeat: configure OAuth2 authorization server
Check out the last one to see the project exactly as this part leaves it:
git checkout 800f63f
References
- Spring Security Authorization Server — Getting Started: https://docs.spring.io/spring-security/reference/servlet/oauth2/authorization-server/getting-started.html
- Spring Authorization Server configuration model: https://docs.spring.io/spring-authorization-server/reference/configuration-model.html
- OpenID Connect Core: https://openid.net/specs/openid-connect-core-1_0.html
- JWT — RFC 7519: https://www.rfc-editor.org/rfc/rfc7519
- JWK — RFC 7517: https://www.rfc-editor.org/rfc/rfc7517
Next: execute the complete Authorization Code + PKCE flow manually and understand every value involved.