spring-oauth2-resource-server
Wire a Spring Boot application to accept and validate OAuth2/OIDC JWT access
tokens (resource-server role). This covers protecting an API with bearer tokens.
It does not configure the login/authorization-code flow (that is the client
role) or opaque-token introspection.
Provider-agnostic: it is driven entirely by an issuer-uri, so it works with
Keycloak, Auth0, Okta, Cognito, Entra ID, or any OIDC-compliant authorization
server.
Before you start
Discover the target project's shape rather than assuming it (use Glob/Grep):
- Build tool:
build.gradle / build.gradle.kts vs pom.xml, and whether
Gradle uses a version catalog (gradle/libs.versions.toml).
- Spring Boot version: read it from the build file. It changes two test
imports (see
references/dependencies.md).
- Base package: the package under
src/main/java/... holding the main
@SpringBootApplication class. New classes go in a config subpackage of it.
- Config format:
application.yml vs application.properties.
Then gather the values to plug in:
- issuer-uri: the OIDC issuer base URL.
- audience: the value expected in the token's
aud claim (usually the API's
client id / identifier).
- allowed CORS origins: the browser origins that call this API.
Steps
Reference files live in this skill's references/ directory. Copy them into the
project, then substitute com.example.app with the real base package and adjust
placeholders.
Add dependencies. Add spring-boot-starter-oauth2-resource-server (main)
and spring-security-test (test) using the snippet in
references/dependencies.md that matches the project's build tool.
Add SecurityConfig. Copy references/SecurityConfig.java into
<base-package>/config/. It defines a stateless filter chain
(anyRequest().authenticated(), oauth2ResourceServer().jwt()), CORS scoped
to configured origins, and a JwtDecoder that adds audience validation on
top of the default signature/issuer/expiry checks.
Add AudienceValidator. Copy references/AudienceValidator.java into the
same config/ package. Recommended: without it, a token minted for any other
client of the same issuer is accepted. If you deliberately do not want
audience checking, omit both this class and the jwtDecoder bean, and Spring
auto-configures a decoder from issuer-uri alone.
Add config properties. Merge references/application.yml into the
project's config (or translate to .properties), filling in issuer-uri,
audience, and allowed-origins. Keep them env-overridable.
Add a test. Copy references/SecurityConfigTest.java, point
@WebMvcTest at a protected controller, and set the request path. It proves
anonymous requests get 401 and a valid JWT is accepted, without a live issuer
(the JwtDecoder is mocked).
Verify. Build and run the test suite (./gradlew test or mvn test).
Then confirm at runtime: a request with no token returns 401, and a request
with a valid bearer token from the issuer returns 200.
Notes
- CSRF is disabled in the template because a token-authenticated API holds no
server-side session or auth cookie. If this same app also serves
cookie/session-authenticated endpoints, do not blanket-disable CSRF.
- Roles/scopes: this sets up authentication (valid token required). To
authorize by scope or role, add a
JwtAuthenticationConverter and
.hasAuthority(...) rules; out of scope here.
- Follow-up: run the
security-reviewer agent afterward to audit the result
(audience, CORS, issuer, token handling).
1---2name: spring-oauth2-resource-server3description: Configure a Spring Boot app as an OAuth2 resource server that validates JWT access tokens against any OIDC provider (Keycloak, Auth0, Okta, Cognito, Entra ID). Sets up the security filter chain, JWT decoder with audience validation, CORS, config properties, and a slice test. Use when adding token-based API authentication to a Spring Boot service.4---56# spring-oauth2-resource-server78Wire a Spring Boot application to accept and validate OAuth2/OIDC JWT access9tokens (resource-server role). This covers protecting an API with bearer tokens.10It does not configure the login/authorization-code flow (that is the client11role) or opaque-token introspection.1213Provider-agnostic: it is driven entirely by an `issuer-uri`, so it works with14Keycloak, Auth0, Okta, Cognito, Entra ID, or any OIDC-compliant authorization15server.1617## Before you start1819Discover the target project's shape rather than assuming it (use Glob/Grep):2021- **Build tool**: `build.gradle` / `build.gradle.kts` vs `pom.xml`, and whether22 Gradle uses a version catalog (`gradle/libs.versions.toml`).23- **Spring Boot version**: read it from the build file. It changes two test24 imports (see `references/dependencies.md`).25- **Base package**: the package under `src/main/java/...` holding the main26 `@SpringBootApplication` class. New classes go in a `config` subpackage of it.27- **Config format**: `application.yml` vs `application.properties`.2829Then gather the values to plug in:3031- **issuer-uri**: the OIDC issuer base URL.32- **audience**: the value expected in the token's `aud` claim (usually the API's33 client id / identifier).34- **allowed CORS origins**: the browser origins that call this API.3536## Steps3738Reference files live in this skill's `references/` directory. Copy them into the39project, then substitute `com.example.app` with the real base package and adjust40placeholders.41421. **Add dependencies.** Add `spring-boot-starter-oauth2-resource-server` (main)43 and `spring-security-test` (test) using the snippet in44 `references/dependencies.md` that matches the project's build tool.45462. **Add `SecurityConfig`.** Copy `references/SecurityConfig.java` into47 `<base-package>/config/`. It defines a stateless filter chain48 (`anyRequest().authenticated()`, `oauth2ResourceServer().jwt()`), CORS scoped49 to configured origins, and a `JwtDecoder` that adds audience validation on50 top of the default signature/issuer/expiry checks.51523. **Add `AudienceValidator`.** Copy `references/AudienceValidator.java` into the53 same `config/` package. Recommended: without it, a token minted for any other54 client of the same issuer is accepted. If you deliberately do not want55 audience checking, omit both this class and the `jwtDecoder` bean, and Spring56 auto-configures a decoder from `issuer-uri` alone.57584. **Add config properties.** Merge `references/application.yml` into the59 project's config (or translate to `.properties`), filling in `issuer-uri`,60 `audience`, and `allowed-origins`. Keep them env-overridable.61625. **Add a test.** Copy `references/SecurityConfigTest.java`, point63 `@WebMvcTest` at a protected controller, and set the request path. It proves64 anonymous requests get 401 and a valid JWT is accepted, without a live issuer65 (the `JwtDecoder` is mocked).66676. **Verify.** Build and run the test suite (`./gradlew test` or `mvn test`).68 Then confirm at runtime: a request with no token returns 401, and a request69 with a valid bearer token from the issuer returns 200.7071## Notes7273- **CSRF** is disabled in the template because a token-authenticated API holds no74 server-side session or auth cookie. If this same app also serves75 cookie/session-authenticated endpoints, do not blanket-disable CSRF.76- **Roles/scopes**: this sets up authentication (valid token required). To77 authorize by scope or role, add a `JwtAuthenticationConverter` and78 `.hasAuthority(...)` rules; out of scope here.79- **Follow-up**: run the `security-reviewer` agent afterward to audit the result80 (audience, CORS, issuer, token handling).