ai-observability
Use when adding Spring AI-specific model observations, token usage, latency, externally configured cost attribution, advisor…
Use when configuring Spring Boot as an OAuth2 resource server, validating JWTs from an external auth provider (Keycloak, Auth0, Okta, Cognito), extracting claims, or implementing scope-based authorization.
Claude Code
$ npx -y skills add rrezartprebreza/spring-boot-skills --skill oauth2-resource-server --agent claude-code
Codex
$ npx skills add rrezartprebreza/spring-boot-skills --skill oauth2-resource-server -a codex -y
Cursor
$ npx skills add rrezartprebreza/spring-boot-skills --skill oauth2-resource-server -a cursor -y
Gemini CLI
$ npx skills add rrezartprebreza/spring-boot-skills --skill oauth2-resource-server -a gemini-cli -y
GitHub Copilot
$ npx skills add rrezartprebreza/spring-boot-skills --skill oauth2-resource-server -a github-copilot -y
OpenCode
$ npx skills add rrezartprebreza/spring-boot-skills --skill oauth2-resource-server -a opencode -y
Windsurf
$ npx skills add rrezartprebreza/spring-boot-skills --skill oauth2-resource-server -a windsurf -y
name: oauth2-resource-server description: > Use when configuring Spring Boot as an OAuth2 resource server, validating JWTs from an external auth provider (Keycloak, Auth0, Okta, Cognito), extracting claims, or implementing scope-based authorization.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class ResourceServerConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.requestMatchers("/api/v1/admin/**").hasAuthority("SCOPE_admin")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter()))
)
.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthConverter() {
// Keep the defaults: the "scope" OR "scp" claim (Okta/Entra use scp) -> SCOPE_*.
// Calling setAuthoritiesClaimName(...) replaces them — then hasAuthority("SCOPE_admin")
// can never pass. Extend the defaults with Keycloak realm roles instead.
var scopes = new JwtGrantedAuthoritiesConverter();
var converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
Collection<GrantedAuthority> authorities = new ArrayList<>(scopes.convert(jwt));
// Keycloak nests realm roles under realm_access.roles -> ROLE_* (for hasRole)
Map<String, Object> realmAccess = jwt.getClaimAsMap("realm_access");
if (realmAccess != null && realmAccess.get("roles") instanceof Collection<?> roles) {
roles.forEach(role -> authorities.add(new SimpleGrantedAuthority("ROLE_" + role)));
}
return authorities;
});
return converter;
}
}Choose the provider configuration matching your issuer; these are alternatives, not duplicate `spring:` mappings to paste into one YAML document.
# Keycloak
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://keycloak.example.com/realms/my-realm
jwk-set-uri: https://keycloak.example.com/realms/my-realm/protocol/openid-connect/certs
audiences: my-api # without it, tokens minted for ANY client in the realm are accepted# Auth0
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://your-domain.auth0.com/
audiences: https://your-api.example.com # custom claim validation@Component
public class JwtClaimExtractor {
// Provider subjects are opaque strings ("auth0|64f1c2", Okta "00u1abcd..."), not UUIDs.
public String getSubject(JwtAuthenticationToken token) {
return token.getToken().getSubject();
}
public String getEmail(JwtAuthenticationToken token) {
return token.getToken().getClaimAsString("email");
}
public List<String> getRoles(JwtAuthenticationToken token) {
// Keycloak nests roles under realm_access.roles
Map<String, Object> realmAccess = token.getToken().getClaimAsMap("realm_access");
if (realmAccess == null) return List.of();
return (List<String>) realmAccess.getOrDefault("roles", List.of());
}
}Use the exact `(iss, sub)` pair as the external identity key, with a database unique constraint on that pair. Email and username are optional, mutable profile attributes, not account lookup or automatic account-linking keys, even when email is verified. The [ExternalIdentity template](templates/ExternalIdentity.java) accepts an already verified access token; signature, issuer, audience and expiry validation must happen first. Allow only configured issuers; never discover an arbitrary issuer URL from an untrusted token. For existing subject-only mappings, backfill the known issuer before introducing a second issuer. Account linking requires an explicit authenticated flow proving control of both accounts.
See [OpenID claim stability](https://openid.net/specs/openid-connect-core-1_0.html#ClaimStability).
@RestController
@RequiredArgsConstructor
public class OrderController {
@GetMapping("/api/v1/orders/my")
public ApiResponse<List<OrderResponse>> myOrders(
@AuthenticationPrincipal Jwt jwt // inject JWT directly
) {
ExternalIdentity identity = ExternalIdentity.from(jwt);
return ApiResponse.ok(orderService.findByOwner(identity.issuer(), identity.subject()));
}
// Or with JwtAuthenticationToken for full principal
@GetMapping("/api/v1/profile")
public ApiResponse<ProfileResponse> profile(JwtAuthenticationToken token) {
ExternalIdentity identity = ExternalIdentity.from(token.getToken());
return ApiResponse.ok(userService.findByExternalIdentity(identity.issuer(), identity.subject()));
}
}@PreAuthorize("hasAuthority('SCOPE_orders:read')")
public List<Order> findAll() { ... }
@PreAuthorize("hasRole('ADMIN') or @orderSecurity.isOwner(#orderId, authentication)")
public Order findById(UUID orderId) { ... }
// Custom security bean
@Component("orderSecurity")
public class OrderSecurityService {
public boolean isOwner(UUID orderId, Authentication auth) {
Jwt jwt = (Jwt) auth.getPrincipal();
ExternalIdentity identity = ExternalIdentity.Production-grade Claude Code and Codex skills for Spring Boot developers
Use when adding Spring AI-specific model observations, token usage, latency, externally configured cost attribution, advisor…
Use when versioning Spring MVC or WebFlux APIs in Spring Boot 3 / Spring Framework 6. Covers explicit URL, header, and media-type…
Use when introducing or correcting grouped Spring Boot configuration, typed property binding, validation, profiles or secret…
Use when packaging a Spring Boot 3 application as an OCI image or GraalVM native executable. Covers buildpacks, layered images,…
Use when evolving aggregate invariants, value objects or domain events in an existing DDD-style Spring Boot 3 application, or…
Use when implementing Kafka, RabbitMQ, Pulsar, or JMS producers and consumers in Spring Boot 3. Covers event contracts,…
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…
Ask which skill or flow fits your situation. A router over the skills in this repo.