Sign In With Spring Security And The OpenID Provider
A Spring Boot app signs users in through the OpenID Provider with
oauth2Login. Spring reads
discovery from issuer-uri.
1. Register The Client
Section titled “1. Register The Client”POST /admin/applications:
{ "name": "Spring app", "clientType": "confidential", "firstParty": true, "tokenEndpointAuthMethod": "client_secret_basic", "redirectUris": ["http://127.0.0.1:13002/login/oauth2/code/seamless"], "postLogoutRedirectUris": ["http://127.0.0.1:13002/"], "grantTypes": ["authorization_code", "refresh_token"]}Spring’s default redirect is {baseUrl}/login/oauth2/code/{registrationId}. A shared secret needs
allow_client_secrets. To avoid one, see
Use private_key_jwt.
2. Add The Dependencies
Section titled “2. Add The Dependencies”<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.2</version></parent>
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security-oauth2-client</artifactId> </dependency></dependencies>3. Configure It
Section titled “3. Configure It”application.yaml:
server: port: 13002spring: security: oauth2: client: registration: seamless: client-id: ${SEAMLESS_CLIENT_ID} client-secret: ${SEAMLESS_CLIENT_SECRET} client-authentication-method: client_secret_basic authorization-grant-type: authorization_code scope: openid,email provider: seamless: issuer-uri: ${SEAMLESS_ISSUER}PKCE needs no setting in this version. With the configuration above, Spring Security 7.1.2 sent
PKCE from a confidential client, which the provider requires. The Spring documentation covers this under
requireProofKey.
Do not set it to false. Older versions were not tested. If yours does not send PKCE, the provider
answers Authorization Server policy requires PKCE.
Sign-out, so that it also ends the Seamless session, uses Spring’s RP-initiated logout handler:
@Configurationpublic class SecurityConfig { @Bean SecurityFilterChain security(HttpSecurity http, ClientRegistrationRepository registrations) throws Exception { var logout = new OidcClientInitiatedLogoutSuccessHandler(registrations); logout.setPostLogoutRedirectUri("{baseUrl}/"); return http .authorizeHttpRequests((a) -> a.anyRequest().authenticated()) .oauth2Login(Customizer.withDefaults()) .logout((l) -> l.logoutSuccessHandler(logout)) .build(); }}Read the user from OidcUser:
@GetMapping("/")String home(@AuthenticationPrincipal OidcUser user) { return "Signed in as " + user.getEmail() + ", subject " + user.getSubject() + ", acr " + user.getClaimAsString("acr");}The provider issues no name or preferred_username claim, so identify users by getSubject() or
getEmail(). Add the seamless scope for the roles
claim. Mapping roles to Spring authorities was not tested.
Use private_key_jwt
Section titled “Use private_key_jwt”Register the client with "tokenEndpointAuthMethod": "private_key_jwt" and your public jwks, as in
the generic guide. Then set the method and
leave the secret empty:
spring: security: oauth2: client: registration: seamless: client-secret: '' client-authentication-method: private_key_jwtAdd a token client that signs the assertion, following
JWT Bearer client authentication.
oauth2Login picks this bean up:
@BeanOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> tokenClient() throws Exception { JWK key = JWK.parse(Files.readString(Path.of(System.getenv("SEAMLESS_PRIVATE_JWK_FILE")))); var client = new RestClientAuthorizationCodeTokenResponseClient(); client.addParametersConverter( new NimbusJwtClientAuthenticationParametersConverter<>((registration) -> key)); return client;}The file holds the private JWK with its kid, which must match the registered key.
4. Trust A Private CA
Section titled “4. Trust A Private CA”A publicly trusted certificate needs none of this. For a private CA, copy the JVM’s trust store, import your CA, and point the JVM at the copy, so the public CAs stay trusted:
cp "$JAVA_HOME/lib/security/cacerts" /tmp/truststore.jkskeytool -importcert -noprompt -alias seamless-local -file /ca.crt \ -keystore /tmp/truststore.jks -storepass changeitexport JAVA_TOOL_OPTIONS="-Djavax.net.ssl.trustStore=/tmp/truststore.jks -Djavax.net.ssl.trustStorePassword=changeit"java -jar target/demo-0.0.1.jarThis covered discovery, the token call and the JWKS. changeit is the JDK’s default store password.
Use your own for anything longer lived than a test.
The test ran this in a maven:3.9-eclipse-temurin-21 container on the recipe’s compose network, with
SEAMLESS_ISSUER, SEAMLESS_CLIENT_ID and SEAMLESS_CLIENT_SECRET set.
5. Check It
Section titled “5. Check It”- Open
http://127.0.0.1:13002/. Every route needs a user, so the browser goes to the hosted page without an intermediate login page. - Sign in. A first-party client shows no consent page.
- The page shows the user’s email and
sub.acrisnullafter a first email-code sign-up and wasphrhafter a passkey sign-in on the virtual authenticator. - Open
/logoutand confirm. The hosted page asks whether to sign out of everything or only this app. After Sign out the browser returns to/, which needs a user again, so it lands on the hosted sign-in page.
Troubleshooting
Section titled “Troubleshooting”Each of these was hit while testing.
| What you see | Cause and fix |
|---|---|
The app exits at startup: Unable to resolve Configuration with the provided Issuer of "https://..." |
Spring fetches discovery at startup. In the test the cause was the JVM not trusting the CA. See step 4. |
Registration and shared-secret errors are in the generic guide.
What Was And Was Not Tested
Section titled “What Was And Was Not Tested”Tested. The configuration above, with both client_secret_basic and private_key_jwt, with
Playwright’s Chromium and a virtual WebAuthn authenticator: a new user registering and a passkey sign-in
from a clean browser, ending on a page showing the user. Sign-out was run with client_secret_basic.
The stack was the local recipe from
seamless-auth-oidc-provider, adjusted as described in the
generic guide.
Not tested. A browser that trusts the CA normally, a real authenticator, a publicly trusted
certificate, a production deployment, https or running behind a proxy (server.forward-headers-strategy),
WebFlux, mapping roles to authorities, refresh tokens, older Spring versions, or Spring as a resource
server.