# 09 — Manual filter vs built-in resource server [← testing](08-testing.md) · [next: production checklist →](10-production-checklist.md) Both are in this repository, behind profiles, secured identically. Run them side by side: ```bash ./scripts/run.sh hs256 # hand-written OncePerRequestFilter ./scripts/run.sh hs256,resourceserver # oauth2ResourceServer().jwt() ``` ## The configuration, side by side **Manual** — [`SecurityConfig`](../src/main/java/com/ankurm/jwtauth/config/SecurityConfig.java): ```java .addFilterBefore(new JwtAuthenticationFilter(jwtDecoder, revokedTokens, entryPoint), UsernamePasswordAuthenticationFilter.class); ``` plus ~120 lines of filter, plus the entry point and access-denied handler wired by hand. **Built-in** — [`ResourceServerSecurityConfig`](../src/main/java/com/ankurm/jwtauth/config/ResourceServerSecurityConfig.java): ```java .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))); ``` One line. `BearerTokenAuthenticationFilter` is inserted in the right slot, `BearerTokenAuthenticationEntryPoint` and `BearerTokenAccessDeniedHandler` are wired, the `JwtDecoder` bean is picked up automatically, and the RFC 6750 headers are correct on both 401 and 403. ## The chains it produces Manual ([transcript](output/curl-transcript-hs256.txt) step 19): ``` … HeaderWriterFilter, JwtAuthenticationFilter, RequestCacheAwareFilter, … ``` Built-in ([transcript](output/resource-server-loose.txt) step 1): ``` … HeaderWriterFilter, LogoutFilter, OAuth2ProtectedResourceMetadataFilter, BearerTokenAuthenticationFilter, RequestCacheAwareFilter, … ``` Same slot. The extra `OAuth2ProtectedResourceMetadataFilter` is Spring Security 7's RFC 9728 support — see [doc 11](11-spring-security-7-changes.md). ## What moves when you switch | concern | manual filter | resource server | |---|---|---| | resolve the header | `DefaultBearerTokenResolver` (you call it) | built in | | decode + verify | `jwtDecoder.decode(token)` (you call it) | built in | | `exp`/`nbf`/`iss` | `JwtValidators` on the decoder | same decoder, same validators | | `aud` | your validator | your validator | | `token_type` | an `if` in the filter | an `OAuth2TokenValidator` | | denylist / `jti` | an `if` in the filter | an `OAuth2TokenValidator` | | authority mapping | your converter | `JwtAuthenticationConverter` | | 401 shape | you wire the entry point | built in | | 403 shape | you wire the handler | built in | The two custom checks do not disappear — they move onto the decoder. That is the trap in the next section. ## The trap: the built-in path has no opinion about your claims Run the same request against both resource-server profiles: ``` # 4. REFRESH token presented as an access token. HTTP 200 hs256,resourceserver (loose) HTTP 401 hs256,resourceserver,strict (validator wired) ``` Files: [loose](output/resource-server-loose.txt) · [strict](output/resource-server-strict.txt). Switching from a hand-written filter to `oauth2ResourceServer()` **silently drops** every custom check that lived in the filter, because the framework has never heard of your `token_type` claim or your denylist. The build still passes. The tests still pass, if they only test the happy path. Move them explicitly: ```java decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>( JwtValidators.createDefaultWithIssuer(issuer), AudienceValidator.forAudience(audience), new AccessTokenTypeValidator())); ``` `JwtValidatorFactory` in this repository composes exactly that, and the extras are supplied by an `ObjectProvider` so both profiles get them. Note also that a denylist check does not belong in an `OAuth2TokenValidator` on purity grounds — validators are supposed to be pure functions of the token — but it is where it has to go if you want it on the built-in path. The alternative is an `AuthenticationSuccessHandler` or a small filter *after* `BearerTokenAuthenticationFilter`, which puts you halfway back to the manual approach. ## Which to pick **Use the built-in resource server** if your tokens are ordinary OAuth 2.0 / OIDC access tokens, especially from a real authorization server. It is less code, it is maintained, and it gets RFC compliance right in places you would not think to (the `charset` in `WWW-Authenticate`, RFC 9728 metadata, `insufficient_scope` on 403). **Write the filter** if you need behaviour the framework has no hook for — custom header schemes, a token bound to a device fingerprint, per-request key selection across tenants — or if you are teaching, because the filter is where the flow becomes legible. **What this repository actually recommends:** start with the built-in one. If you find yourself adding `OAuth2TokenValidator`s for things that are not claims, that is the signal to switch. A useful third option for a real system: run [Spring Authorization Server](https://spring.io/projects/spring-authorization-server) as the issuer and consume its tokens with `oauth2ResourceServer()`. Then neither half of this repository is your code.