fix(authserver): canonical Spring Security 7.x filter chain + Jackson modules on custom repo#128
Merged
Merged
Conversation
…tern Closes #124. The authorizationServerSecurityFilterChain bean used a non-canonical DSL combination — .oauth2AuthorizationServer(...) plus .oauth2ResourceServer().jwt() on the same chain with .anyRequest().authenticated() and no securityMatcher. Result: the resource-server bearer-token filter intercepted POST /oauth2/token requests before the token-endpoint filter could authenticate them via basic auth, returning 401 with WWW-Authenticate: Bearer. The canonical pattern in Spring Security 7.x manually installs the OAuth2AuthorizationServerConfigurer via http.with(...) and scopes the chain to the auth-server endpoints via http.securityMatcher(configurer.getEndpointsMatcher()). With the chain scoped to OAuth2 endpoints only, the bearer-token filter never sees them and the token endpoint's own basic-auth filter handles client authentication correctly. Note: Spring Security 7.x removed the OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http) static method that Spring Authorization Server 1.x used; the http.with() pattern plus manual securityMatcher is the replacement. A second SecurityFilterChain @order(2) defaultSecurityFilterChain bean now handles everything NOT claimed by the auth-server endpoints matcher: login form, static resources, /.well-known/*, actuator endpoints. The previously commented-out defaultSecurityFilterChain stub has been replaced with a real implementation. Verified end-to-end: with this filter chain plus the Jackson modules fix landed in the next commit, POST /oauth2/token returns 200 with a 128-char opaque access token, and POST /oauth2/introspect returns the RFC 7662 response shape (active, sub, aud, scope, iss, exp, iat, jti, client_id, token_type). Refs: #122 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… on custom repo The custom JdbcRegisteredClientRepository constructed its ObjectMapper as new ObjectMapper() with no modules registered. Spring Authorization Server stores typed values (OAuth2TokenFormat, ClientAuthenticationMethod, etc.) inside ClientSettings and TokenSettings; Jackson serializes them correctly on write but cannot reconstruct the typed objects on read without the provider's type-info modules. Symptom: with a client configured accessTokenFormat(OAuth2TokenFormat.REFERENCE), the round-tripped setting came back as a LinkedHashMap. Later TokenSettings.getAccessTokenFormat() ClassCast the LinkedHashMap to OAuth2TokenFormat — and worse, because the cast failed silently in the DelegatingOAuth2TokenGenerator, the JwtGenerator ran for clients configured as opaque, throwing HTTP 500 on every POST /oauth2/token request. Fix: register SecurityJacksonModules + OAuth2AuthorizationServerJacksonModule on the existing ObjectMapper at repo construction. Use Jackson 3's JsonMapper.builder() pattern since the codebase is already on tools.jackson.databind.ObjectMapper (Jackson 3 immutable mapper). This is the minimum fix that unblocks #124's acceptance criteria (POST /oauth2/token returns opaque token; introspection returns RFC 7662 response). The custom repo retains other architectural issues — auto-encoding of clientSecret on save, non-interface findAll/deleteById extensions, and the broader build-vs-buy question of why we're reimplementing Spring's stock JdbcRegisteredClientRepository at all. Those are scoped to follow-up issue #127. Verified locally against a fresh MySQL container: $ curl -u 'data_custodian_admin:{bcrypt}secret' \ -d 'grant_type=client_credentials&scope=DataCustodian_Admin_Access' \ http://localhost:9999/oauth2/token {"access_token":"7b_HlXgKfi7V-phbFWODTJW_...","token_type":"Bearer", "expires_in":3599,"scope":"DataCustodian_Admin_Access"} $ curl -u 'data_custodian_admin:{bcrypt}secret' \ -d "token=$T" \ http://localhost:9999/oauth2/introspect {"active":true,"sub":"data_custodian_admin","aud":["data_custodian_admin"], "scope":"DataCustodian_Admin_Access","iss":"http://localhost:9999",...} Refs: #122 #124 #127 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
dfcoffin
added a commit
that referenced
this pull request
May 26, 2026
…pring stock (#130) Closes #127. The hand-written JdbcRegisteredClientRepository (335 lines) reimplemented Spring Authorization Server's stock repository and carried multiple defects: the TokenSettings/ClientSettings serialization bug worked around in #128, auto-encoding of client secrets on save, and a findAll() that returned empty under the autocommit defect fixed in #125. This replaces it with Spring's stock org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository, which is maintained upstream and uses the correct Jackson modules out of the box. Changes: - AuthorizationServerConfig: construct the stock JdbcRegisteredClientRepository (JdbcOperations-only constructor); initializeDefaultClients() now takes the RegisteredClientRepository interface and no longer calls the custom findAll(). - OAuth2ClientManagementConfig: PasswordEncoder bean is now PasswordEncoderFactories.createDelegatingPasswordEncoder() instead of a bare BCryptPasswordEncoder(12). The stock repo stores client secrets verbatim (prefix included) rather than re-encoding on save, so authentication needs a prefix-aware encoder: {bcrypt}... for production, {noop}... for dev seeds. The old bare bcrypt encoder ignored the prefix and is why {noop}secret failed with invalid_client until this change. - Default seed clients use distinct secrets ({noop}dc-secret, {noop}tp-secret, {noop}tpadmin-secret). The stock repo enforces client-secret uniqueness (a security check the custom repo lacked); three identical {noop}secret values were rejected with "duplicate client secret". - New RegisteredClientAdminDao: small JdbcTemplate-backed component exposing the two operations not on the RegisteredClientRepository interface that the admin UI needs - findAllClientIds() and deleteById(). OAuthAdminController now resolves each id through RegisteredClientRepository.findByClientId(), so listing goes through Spring's tested deserialization path. - OAuthAdminController: dropped the `instanceof JdbcRegisteredClientRepository` branches (and their dead fallback paths) in favor of the admin DAO. - Deleted the custom JdbcRegisteredClientRepository and its 559-line test. Updated AuthorizationServerConfigTest (stock class + 1-arg bean signature), OAuthAdminControllerTest (admin DAO mocks; removed 2 tests for the now-gone instanceof fallback), and the MySQL/PostgreSQL TestContainers tests (findAll/deleteById -> RegisteredClientAdminDao). Verified end-to-end against a fresh MySQL container (dev-mysql): POST /oauth2/token (data_custodian_admin:dc-secret, client_credentials) -> 200, 128-char opaque token (0 dots, REFERENCE format) POST /oauth2/introspect -> 200, RFC 7662 response (active, sub, aud, scope, iss, exp, iat, jti, client_id, token_type) POST /oauth2/token with wrong secret -> 401 (delegating encoder enforces auth) Net: +112 / -1045 lines. Pre-existing test debt (NOT introduced here): AuthorizationServerConfigTest's mock-HttpSecurity unit tests and OAuthAdminControllerTest's standaloneSetup security tests fail because of how they're written, and the authserver module is excluded from CI entirely. Both are documented in #129; this change leaves them no worse (OAuthAdminControllerTest 5 -> 4 failures). Refs: #122 #128 #129 Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This was referenced May 26, 2026
dfcoffin
added a commit
that referenced
this pull request
May 27, 2026
Closes #134. ESPI 4.0 uses opaque access tokens only and the GBA Resource Server carries no JWK/JWT. The auth-server carried OIDC + JWK/JWT-signing machinery the opaque flow never exercises. Removed it; the opaque token mint + RFC 7662 introspection path is unchanged. AuthorizationServerConfig: - Removed .oidc(...) from the authorization-server configurer. - Removed the .oauth2ResourceServer().jwt(...) self-protection added in #128. The auth-server no longer validates bearer tokens on its own endpoints: OAuth2 protocol endpoints (token/introspect/revoke) authenticate clients via client_secret_basic; admin/UI endpoints are protected by the @order(2) session-login chain (#122 design §5). - Removed jwkSource()/jwtDecoder() beans, generateRsaKey(), the .jwkSetEndpoint() declaration, and all JWK/JWT imports. application.yml: - Excluded OAuth2AuthorizationServerJwtAutoConfiguration so Spring Boot no longer auto-generates a JWKSource / stands up /oauth2/jwks. Removed jwk-set-uri property. EspiTokenCustomizer RETAINED (not stripped): it is OAuth2TokenCustomizer<JwtEncodingContext> and currently inert (short-circuits unless espi.token.format=jwt), BUT it is the sole home of the ESPI logic adding resource/authorization URIs to the token. That logic must migrate to the opaque token-response path (#122 token-response augmentation), so the class + bean + test are kept deliberately. OIDC removal is a DEFERRAL, not permanent — OIDC returns when multi-utility Third-Party registration is built (#122). Data-custodian: replaced the misleading "Future enhancement: Add JWT support" comment with a note that JWT/JWK is intentionally absent (ESPI opaque-only). Tests: - AuthorizationServerConfigTest: removed the JWK Configuration test class + jwkSource()/ jwtDecoder() assertions (beans gone); espiTokenCustomizer assertions kept. - ClientRegistrationEndpointIntegrationTest: @disabled — every test hits the removed OIDC /connect/register endpoint. Restore when OIDC returns (#122); suite repair tracked in #129. - Blast radius is exactly that one class; OAuth2FlowIntegrationTest and SecurityIntegrationTest exercise only the opaque /oauth2/{authorize,token,introspect} + admin endpoints this change preserves. Verified (fresh MySQL, dev-mysql): - Boots clean (~37s) with NO JWKSource - POST /oauth2/token (client_credentials) -> 200 opaque token (128 chars, 0 dots) - POST /oauth2/introspect -> 200 RFC 7662 - wrong secret -> 401 - /.well-known/openid-configuration -> 404; /oauth2/jwks -> 302 (no key served) Known minor residue: the OAuth2 metadata doc still advertises a jwks_uri that now 302s (removing it needs a metadata customizer; harmless for opaque-only). Left as-is. Refs: #122 #128 #129 Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #124. End-to-end opaque token issuance + RFC 7662 introspection now works on
dev-mysql.Two related commits, one PR — they form a dependency chain (the filter-chain fix exposed the serialization defect; the serialization fix is what makes the filter-chain fix verifiable end-to-end). Splitting them would create a PR whose acceptance criteria provably can't be met.
Commits
1.
fix(authserver): adopt canonical Spring Security 7.x filter chain patternauthorizationServerSecurityFilterChainrewritten from the non-canonical.oauth2AuthorizationServer(...)+.oauth2ResourceServer().jwt()+.anyRequest().authenticated()combination to the canonical Spring Security 7.x pattern:securityMatcher(endpointsMatcher)scopes this chain to OAuth2 endpoints only, so the resource-server bearer-token filter no longer interceptsPOST /oauth2/tokenbefore its basic-auth handler can run.A second
@Order(2)defaultSecurityFilterChainbean now handles login form, static resources,/.well-known/*, and actuator — replacing the previously commented-out stub.Note: Spring Security 7.x removed the
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http)static method that Spring Authorization Server 1.x docs commonly cite. Thehttp.with(...)+ manualsecurityMatcheris the replacement.2.
fix(authserver): register Spring Authorization Server Jackson modules on custom repoThe custom
JdbcRegisteredClientRepositoryconstructednew ObjectMapper()with no Jackson modules. Spring Authorization Server stores typed values (OAuth2TokenFormat,ClientAuthenticationMethod) insideClientSettings/TokenSettings; Jackson serializes correctly on write but reads them back asLinkedHashMapwithout the provider's type-info modules.TokenSettings.getAccessTokenFormat()thenClassCasts, and theDelegatingOAuth2TokenGeneratorfalls through toJwtGeneratoreven for clients configured as opaque — HTTP 500 on everyPOST /oauth2/token.Fix: register
SecurityJacksonModules+OAuth2AuthorizationServerJacksonModuleon the existing ObjectMapper using Jackson 3'sJsonMapper.builder()pattern (codebase is already ontools.jackson.databind.ObjectMapper).This is the minimum fix that unblocks #124's acceptance criteria. The custom repo's other architectural issues — auto-encoding
clientSecreton save, non-interfacefindAll/deleteByIdextensions, the "build-vs-buy" question of reimplementing Spring's stock impl — are scoped to follow-up issue #127.Verification (local, fresh MySQL container)
subscription_idcustom claim once grant→subscription wiring lands.Test plan
mvn -pl openespi-authserver compilesucceedsmvn spring-boot:run -Dspring-boot.run.profiles=dev-mysqlreachesStarted AuthorizationServerApplication(~45s)POST /oauth2/tokenreturns 200 with opaque token;POST /oauth2/introspectreturns RFC 7662 response (results above)Related
🤖 Generated with Claude Code