You are running Spring Authorization Server as a multi-instance production IdP. What are the key architectural concerns and how do you address them?
answer
- Jdbc* repos + shared DB (code redeemed on any node)
- stable keys from secret store + rotation overlap
- explicit issuer behind proxy, forward headers
- PKCE + encoded secrets + exact redirect URIs + consent
- short TTL + refresh rotation; jwks/discovery stay public
basics
~20 sPersist clients and authorizations in a shared database (Jdbc*Repository, not in-memory), load stable signing keys from a secret store with a rotation plan, set an explicit issuer that matches the public URL behind your proxy, and enforce PKCE/consent and short token TTLs.
solid answer
~40 sAcross instances nothing can live only in memory. Use `JdbcRegisteredClientRepository`, `JdbcOAuth2AuthorizationService`, and `JdbcOAuth2AuthorizationConsentService` over a shared DB so clients, live authorizations, codes and consents are visible to every node — `InMemory*` variants break in a cluster. Signing keys must come from a shared secret store and be stable across restarts; publish multiple JWKs to enable zero-downtime rotation. Set an explicit `issuer` in `AuthorizationServerSettings` matching the external URL, because behind a load balancer/proxy request-derived issuers are wrong and break the `iss` claim and discovery. Harden clients: require PKCE for public clients (`requireProofKey`), require consent where appropriate, encode secrets with a strong `PasswordEncoder`, restrict redirect URIs exactly, and keep access-token TTLs short with refresh-token rotation. Ensure discovery/jwks stay publicly reachable, and secure the login/consent UI with the normal Spring Security filter chain.
code
java · 21 lines@Bean
RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbc) {
return new JdbcRegisteredClientRepository(jdbc); // shared across instances
}
@Bean
OAuth2AuthorizationService authorizationService(JdbcTemplate jdbc, RegisteredClientRepository repo) {
return new JdbcOAuth2AuthorizationService(jdbc, repo); // codes/tokens visible cluster-wide
}
@Bean
OAuth2AuthorizationConsentService consentService(JdbcTemplate jdbc, RegisteredClientRepository repo) {
return new JdbcOAuth2AuthorizationConsentService(jdbc, repo);
}
@Bean
AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder()
.issuer("https://auth.example.com") // public URL, must match resource-server issuer-uri
.build();
}go deeper
Know that production needs a database and real secrets, not in-memory demo config.
Name the Jdbc* services and the need for stable keys and an explicit issuer.
Explain why code redemption needs shared state, PKCE/consent hardening, and TTL/rotation choices.
Own the full architecture: cluster state, key lifecycle, proxy/issuer correctness, revocation strategy, and the build-vs-buy IdP decision.
Running Spring Authorization Server as a real identity provider raises concerns beyond the happy-path demo. **1. Shared, persistent state (the cluster killer).** An authorization_code flow is *stateful*: the server stores the issued code, the authorization, and the user's consent, then the client redeems the code at the token endpoint — possibly on a *different* instance. If state lives in memory, the redeem hits a node that never saw the code and fails. So use the JDBC implementations over a shared database: - `JdbcRegisteredClientRepository` — clients. - `JdbcOAuth2AuthorizationService` — live authorizations, authorization codes, access/refresh tokens. - `JdbcOAuth2AuthorizationConsentService` — recorded user consents. The project ships the SQL schema for these tables. Alternatively use sticky sessions + a distributed store, but shared JDBC is the standard answer. In-memory variants are for tests only. **2. Stable, rotatable signing keys.** As covered in the signing question: keys must be loaded from a secret manager (Vault, KMS, mounted keystore), identical on every instance, and stable across restarts. Publish a `JWKSet` with the current key plus recently retired public keys so verification survives rotation. Rotate: add new key → sign with it → keep old public key until max token TTL elapses → remove. Never generate at boot. **3. Issuer / hostname correctness.** Behind an ALB/nginx the server sees an internal host. If you don't set `AuthorizationServerSettings.builder().issuer(publicUrl)`, the `iss` claim and discovery URLs reflect the wrong host, and resource servers configured with the public `issuer-uri` reject tokens. Set it explicitly per environment and ensure proxy headers (`X-Forwarded-*`) are honored (`ForwardedHeaderFilter`/`server.forward-headers-strategy`). **4. Client & flow hardening.** - **PKCE**: require it for public clients (SPAs, mobile) via `ClientSettings.builder().requireProofKey(true)`; combined with `ClientAuthenticationMethod.NONE`. - **Secrets**: encode with BCrypt/Argon2 via `PasswordEncoder`; never `{noop}` in prod. - **Redirect URIs**: register exact absolute URIs; the server matches exactly, blocking open-redirect abuse. - **Consent**: `requireAuthorizationConsent(true)` for third-party clients so users approve scopes. - **Scopes**: minimize; map to resource-server authorities. **5. Token lifetime & revocation.** Short access-token TTL (minutes) limits blast radius; use refresh tokens with rotation (`TokenSettings` — reuse detection). For instant revocation needs, consider opaque tokens + introspection, accepting the per-call cost. Provide `/oauth2/revoke`. **6. Endpoint exposure & the login UI.** Discovery and `/oauth2/jwks` must be public (resource servers fetch them unauthenticated) — don't accidentally secure them. The authorization endpoint needs an authenticated user, so wire a normal Spring Security `SecurityFilterChain` (form login/social login) as the second filter chain; the authorization-server chain (from `OAuth2AuthorizationServerConfiguration.applyDefaultSecurity`) handles the protocol endpoints. Enforce HTTPS everywhere (tokens are bearer credentials). **7. Observability & operations.** Audit token issuance/failures, monitor jwks fetch errors, alarm on issuer mismatches, and DB-back everything so you can scale horizontally and deploy blue/green. **Trade-off framing.** Self-hosting gives full control and no per-MAU vendor cost but you now own key management, rotation, uptime of a security-critical service, and protocol correctness — weigh against Keycloak/Okta/Auth0. Choose Spring Authorization Server when you want a lightweight, embeddable, Spring-native IdP and have the operational maturity to run it.
- Why does an authorization_code flow break in a clustered deployment using InMemoryOAuth2AuthorizationService?The code and authorization are stored on the node that issued the code, but the token-endpoint redemption may land on a different node that has no record of it, so the exchange fails. Use JdbcOAuth2AuthorizationService over a shared DB.
- How do you keep the issuer correct behind a load balancer?Set an explicit issuer in AuthorizationServerSettings to the public URL and honor X-Forwarded-* headers (ForwardedHeaderFilter / server.forward-headers-strategy) so generated URLs and the iss claim match what clients/resource servers expect.
- When would you pick Keycloak or Okta over Spring Authorization Server?When you don't want to own key management, HA, admin UI, user federation, and protocol upkeep for a security-critical service; Spring Authorization Server suits a lightweight, Spring-native, embeddable IdP where you have the operational maturity.
saying these in an interview costs you the question
- Using InMemory* services in a multi-instance deployment
- Leaving issuer unset behind a proxy
- Generating signing keys at startup
- {noop} secrets or wildcard/loose redirect URIs in production
- Securing /oauth2/jwks or discovery so resource servers can't reach them