Spring Boot Tutorial
Spring Authorization Server
As soon as you have several applications — a web front end, a mobile app, a few microservices — you do not want each one to handle passwords and issue its own tokens. You want one central authorization server that authenticates users and issues OAuth2 access tokens and OpenID Connect ID tokens, while every API simply validates those tokens.
Spring Authorization Server is the Spring team's implementation of that server. From Spring Security 7 it ships as part of Spring Security itself, and Spring Boot 4 configures it with spring-boot-starter-security-oauth2-authorization-server. This lesson builds a working authorization server, registers clients for user login and machine-to-machine access, and connects a resource server to it.
Roles in an OAuth2 System
Four parties take part in OAuth2, and it helps to name them precisely.
- Resource owner — the user who owns the data.
- Client — the application asking for access (a web app, SPA backend, mobile app or service).
- Authorization server — authenticates the user, asks for consent and issues tokens. This is what you build in this lesson.
- Resource server — the API that accepts access tokens (see the JWT resource server lesson).
Which Grant Types to Use
OAuth 2.1 and current best practice reduce the choice to a few flows. The authorization_code grant with PKCE is for anything involving a user (web, SPA via a backend-for-frontend, mobile). client_credentials is for service-to-service calls with no user. refresh_token renews access tokens. The old implicit and password grants are removed from Spring Security 7 and should not be used.
Endpoints the Server Exposes
Once running, the server publishes standard endpoints that clients and resource servers discover automatically through /.well-known/openid-configuration:
/oauth2/authorize— starts the user login/consent flow./oauth2/token— exchanges codes, client credentials or refresh tokens for tokens./oauth2/jwks— public keys resource servers use to verify token signatures./oauth2/introspectand/oauth2/revoke— token introspection and revocation./userinfo— OpenID Connect user information.
Registered Clients and Persistence
Each client application is a RegisteredClient with a client id, a hashed secret, allowed grant types, redirect URIs, scopes and token settings. For demos you can declare them in application.yml and Spring Boot builds an InMemoryRegisteredClientRepository. In production, use JdbcRegisteredClientRepository, JdbcOAuth2AuthorizationService and JdbcOAuth2AuthorizationConsentService so clients, issued tokens and consents survive restarts and scale across instances. Keep the signing keys stable across restarts too — if keys change, every issued token becomes invalid.
Customising Token Claims
An OAuth2TokenCustomizer<JwtEncodingContext> bean lets you add claims to access or ID tokens — for example the user's roles, tenant id or subscription plan — so resource servers can make authorization decisions without calling back to the user database.
Examples
Authorization server with clients declared in configuration
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security-oauth2-authorization-server</artifactId>
</dependency>
# application.yml of auth-server (port 9000)
server:
port: 9000
spring:
security:
user:
name: asha
password: "{noop}user123" # demo only — use a real UserDetailsService
oauth2:
authorizationserver:
issuer: http://localhost:9000
client:
web-app:
registration:
client-id: web-app
client-secret: "{bcrypt}$2a$10$6hN5E3sQ8kK6f6YwQ2iY6eJ0b5n0m2J0yJ3dO6oQ3bHk8lE9uF0bK"
client-authentication-methods: client_secret_basic
authorization-grant-types: authorization_code, refresh_token
redirect-uris: http://localhost:8080/login/oauth2/code/webnest
scopes: openid, profile, orders.read
require-authorization-consent: true
require-proof-key: true
reporting-service:
registration:
client-id: reporting-service
client-secret: "{bcrypt}$2a$10$Vd1Wm9aYxJgYv6k1v0k5mO2nK7n8V1a0cS6oR2wQ1bT9uZ4pA3hXe"
client-authentication-methods: client_secret_basic
authorization-grant-types: client_credentials
scopes: orders.read
GET http://localhost:9000/.well-known/openid-configuration
{"issuer":"http://localhost:9000","authorization_endpoint":"http://localhost:9000/oauth2/authorize","token_endpoint":"http://localhost:9000/oauth2/token","jwks_uri":"http://localhost:9000/oauth2/jwks", ...}
Machine-to-machine token with client_credentials
curl -s -u reporting-service:reporting-secret \
-d "grant_type=client_credentials&scope=orders.read" \
http://localhost:9000/oauth2/token
{"access_token":"eyJraWQiOiI4YjY...","scope":"orders.read","token_type":"Bearer","expires_in":299}
Adding roles to access tokens and persisting clients in the database
@Configuration
public class AuthServerConfig {
// Put the user's roles into every access token
@Bean
OAuth2TokenCustomizer<JwtEncodingContext> rolesClaim() {
return context -> {
if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())
&& context.getPrincipal() != null) {
Set<String> roles = context.getPrincipal().getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.filter(a -> a.startsWith("ROLE_"))
.map(a -> a.substring(5))
.collect(Collectors.toSet());
context.getClaims().claim("roles", roles);
}
};
}
// Production: store clients, authorizations and consents in the database
@Bean
RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbc) {
return new JdbcRegisteredClientRepository(jdbc);
}
@Bean
OAuth2AuthorizationService authorizationService(JdbcTemplate jdbc,
RegisteredClientRepository clients) {
return new JdbcOAuth2AuthorizationService(jdbc, clients);
}
@Bean
OAuth2AuthorizationConsentService consentService(JdbcTemplate jdbc,
RegisteredClientRepository clients) {
return new JdbcOAuth2AuthorizationConsentService(jdbc, clients);
}
}
Decoded access token payload:
{"sub":"asha","aud":"web-app","scope":["openid","orders.read"],"roles":["USER"],"iss":"http://localhost:9000","exp":1790503200}
A web app logging in through the authorization server, and an API trusting it
# web-app (port 8080) — OAuth2 client
spring:
security:
oauth2:
client:
registration:
webnest:
provider: webnest
client-id: web-app
client-secret: ${WEB_APP_SECRET}
authorization-grant-type: authorization_code
scope: openid, profile, orders.read
provider:
webnest:
issuer-uri: http://localhost:9000
# orders-api (port 8081) — resource server
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:9000
Browser -> web-app /dashboard -> redirect to auth-server login -> consent screen -> back to web-app
web-app calls orders-api with the access token -> 200 OK (signature verified with keys from /oauth2/jwks)
Common Mistakes
- Generating a new RSA key pair on every startup, which silently invalidates all issued tokens after each deploy.
- Keeping registered clients and authorizations in memory in production; tokens and consents vanish on restart and do not work with more than one instance.
- Still using the password grant, which is removed in Spring Security 7 — use authorization_code with PKCE for users.
- Putting large or sensitive data into token claims; tokens are readable by anyone who holds them.
- Using http:// issuer URLs outside local development — tokens and codes must only travel over HTTPS.
Key Points to Remember
- Spring Authorization Server issues OAuth2 access tokens and OIDC ID tokens for all of your applications from one place.
- In Boot 4 add spring-boot-starter-security-oauth2-authorization-server; clients can be declared under spring.security.oauth2.authorizationserver.client.
- Use authorization_code + PKCE for users, client_credentials for services and refresh_token for renewal.
- Persist clients, authorizations and consents with the JDBC implementations and keep signing keys stable.
- OAuth2TokenCustomizer adds claims such as roles so resource servers can authorize locally.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.