Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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/skills/rrezartprebreza-oauth2-resource-server/SKILL.md| Model | Eval pass | Runs |
|---|---|---|
| gemini-3.6-flash | 100% | 24 |
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 3% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 142% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 18% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 45% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 21% | 0% |
Spring Boot 4.x ships Spring Security 7 — lambda DSL only; and(), authorizeRequests(), antMatchers(), and AntPathRequestMatcher/MvcRequestMatcher are gone (requestMatchers("/path/**") is backed by PathPatternRequestMatcher).
xml<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security-oauth2-resource-server</artifactId> </dependency>
java@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() { var converter = new JwtGrantedAuthoritiesConverter(); converter.setAuthoritiesClaimName("roles"); // Keycloak uses "roles" converter.setAuthorityPrefix("ROLE_"); var authConverter = new JwtAuthenticationConverter(); authConverter.setJwtGrantedAuthoritiesConverter(converter); return authConverter; } }
yaml# 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 # Auth0 spring: security: oauth2: resourceserver: jwt: issuer-uri: https://your-domain.auth0.com/ audiences: https://your-api.example.com # custom claim validation
java@Component public class JwtClaimExtractor { public UUID getUserId(JwtAuthenticationToken token) { return UUID.fromString(token.getToken().getClaimAsString("sub")); } 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()); } }
java@RestController @RequiredArgsConstructor public class OrderController { @GetMapping("/api/v1/orders/my") public ApiResponse<List<OrderResponse>> myOrders( @AuthenticationPrincipal Jwt jwt // inject JWT directly ) { UUID userId = UUID.fromString(jwt.getSubject()); return ApiResponse.ok(orderService.findByUser(userId)); } // Or with JwtAuthenticationToken for full principal @GetMapping("/api/v1/profile") public ApiResponse<ProfileResponse> profile(JwtAuthenticationToken token) { return ApiResponse.ok(userService.findByEmail( token.getToken().getClaimAsString("email") )); } }
java@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(); UUID userId = UUID.fromString(jwt.getSubject()); return orderRepository.existsByIdAndCustomerId(orderId, userId); } }
hasRole("ADMIN") for scope check — scopes use hasAuthority("SCOPE_admin")issuer-uri validation — always configure to prevent token forgeryrealm_access.rolesgetPrincipal() directly — cast to Jwt or use @AuthenticationPrincipal JwtuserDetailsService bean — not needed for resource servers (stateless JWT)spring-boot-starter-oauth2-resource-server — Boot 4 renamed security starters; use spring-boot-starter-security-oauth2-resource-server.oauth2ResourceServer().jwt(), .and()) — removed in Security 7, won't compile; lambda DSL onlyantMatchers() / AntPathRequestMatcher — removed in Security 7; use requestMatchers("/path/**") (backed by PathPatternRequestMatcher)JwtDecoder with @MockBean in tests — removed in Boot 4; use @MockitoBean (and @AutoConfigureMockMvc — @SpringBootTest no longer provides MockMvc)| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 19,098 | 20,875 | +9% | 1 | 1 | 0% | 3,869 | 3,985 | +3% | 0 | 0 | — |
case-02 | pass→pass | 23,273 | 15,328 | -34% | 1 | 1 | 0% | 3,265 | 4,733 | +45% | 0 | 0 | — |
case-03 | pass→pass | 20,288 | 11,733 | -42% | 1 | 1 | 0% | 3,100 | 3,739 | +21% | 0 | 0 | — |
case-04 | pass→pass | 22,819 | 12,249 | -46% | 1 | 1 | 0% | 3,505 | 3,925 | +12% | 0 | 0 | — |
case-05 | pass→pass | 19,301 | 11,436 | -41% | 1 | 1 | 0% | 2,893 | 3,681 | +27% | 0 | 0 | — |
case-22 | pass→pass | 7,490 | 6,612 | -12% | 1 | 1 | 0% | 1,415 | 2,376 | +68% | 0 | 0 | — |
case-06 | pass→pass | 21,305 | 28,475 | +34% | 1 | 1 | 0% | 3,775 | 5,384 | +43% | 0 | 0 | — |
case-07 | fail→pass | 9,522 | 7,860 | -17% | 1 | 1 | 0% | 739 | 1,791 | +142% | 0 | 0 | — |
case-08 | pass→pass | 13,678 | 6,327 | -54% | 1 | 1 | 0% | 1,729 | 2,545 | +47% | 0 | 0 | — |
case-09 | pass→pass | 13,326 | 11,808 | -11% | 1 | 1 | 0% | 2,749 | 2,678 | -3% | 0 | 0 | — |
case-10 | pass→pass | 9,569 | 10,600 | +11% | 1 | 1 | 0% | 1,572 | 2,094 | +33% | 0 | 0 | — |
case-11 | pass→pass | 12,561 | 12,002 | -4% | 1 | 1 | 0% | 2,550 | 3,685 | +45% | 0 | 0 | — |
case-12 | pass→pass | 9,114 | 4,330 | -52% | 1 | 1 | 0% | 1,680 | 2,140 | +27% | 0 | 0 | — |
case-13 | pass→pass | 19,971 | 8,733 | -56% | 1 | 1 | 0% | 2,230 | 3,084 | +38% | 0 | 0 | — |
case-14 | pass→pass | 10,017 | 11,897 | +19% | 1 | 1 | 0% | 1,949 | 2,714 | +39% | 0 | 0 | — |
case-15 | fail→pass | 9,732 | 5,662 | -42% | 1 | 1 | 0% | 2,046 | 2,412 | +18% | 0 | 0 | — |
case-16 | pass→pass | 14,804 | 8,293 | -44% | 1 | 1 | 0% | 2,405 | 3,033 | +26% | 0 | 0 | — |
case-17 | pass→pass | 6,238 | 5,700 | -9% | 1 | 1 | 0% | 1,162 | 2,134 | +84% | 0 | 0 | — |
case-18 | pass→pass | 9,544 | 9,165 | -4% | 1 | 1 | 0% | 1,798 | 2,745 | +53% | 0 | 0 | — |
case-19 | pass→pass | 11,211 | 9,994 | -11% | 1 | 1 | 0% | 2,357 | 2,803 | +19% | 0 | 0 | — |
case-20 | pass→pass | 13,372 | 9,897 | -26% | 1 | 1 | 0% | 2,464 | 3,204 | +30% | 0 | 0 | — |
case-21 | pass→pass | 10,051 | 8,460 | -16% | 1 | 1 | 0% | 1,860 | 2,598 | +40% | 0 | 0 | — |
case-23 | pass→pass | 11,535 | 9,050 | -22% | 1 | 1 | 0% | 2,329 | 2,808 | +21% | 0 | 0 | — |
case-24 | pass→pass | 4,577 | 3,529 | -23% | 1 | 1 | 0% | 850 | 1,802 | +112% | 0 | 0 | — |
case-25 | pass→pass | 10,539 | 6,579 | -38% | 1 | 1 | 0% | 1,918 | 2,264 | +18% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 25 cases were attempted. The headline lift of +12 percentage points is the difference between those two pass rates over the 25 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.