Spring Boot Quickstart
Integrate UniAuth with Spring Boot using spring-boot-starter-oauth2-client — Spring Security's first-party OIDC client. Full discovery, PKCE, JWKS verification, automatic token refresh.
Discovery-first: set issuer-uri and Spring auto-fetches every endpoint from https://uniauth.id/.well-known/openid-configuration. No manual URL config.
1. Dependencies
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>Gradle equivalent:
// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'2. application.yml
# application.yml
spring:
security:
oauth2:
client:
registration:
uniauth:
provider: uniauth
client-id: ${UNIAUTH_CLIENT_ID}
client-secret: ${UNIAUTH_CLIENT_SECRET}
scope:
- openid
- profile
- email
- groups
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
client-authentication-method: client_secret_basic
provider:
uniauth:
# Discovery URL — Spring auto-fetches authorize/token/userinfo/JWKS
issuer-uri: https://uniauth.id
user-name-attribute: sub3. Security Configuration
// src/main/java/com/yourapp/config/SecurityConfig.java
package com.yourapp.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
@Configuration
public class SecurityConfig {
private final ClientRegistrationRepository clientRegistrationRepository;
public SecurityConfig(ClientRegistrationRepository clientRegistrationRepository) {
this.clientRegistrationRepository = clientRegistrationRepository;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/public/**", "/logout/backchannel").permitAll()
.requestMatchers("/admin/**").hasAuthority("ROLE_admin")
.anyRequest().authenticated()
)
.oauth2Login(oauth -> oauth
.userInfoEndpoint(u -> u
.userAuthoritiesMapper(new GroupsAuthoritiesMapper())
)
)
.logout(logout -> logout
.logoutSuccessHandler(oidcLogoutSuccessHandler())
)
// Disable CSRF for back-channel logout webhook
.csrf(csrf -> csrf.ignoringRequestMatchers("/logout/backchannel"));
return http.build();
}
// RP-initiated logout: Spring posts id_token_hint to UniAuth end_session_endpoint
private LogoutSuccessHandler oidcLogoutSuccessHandler() {
OidcClientInitiatedLogoutSuccessHandler handler =
new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository);
handler.setPostLogoutRedirectUri("{baseUrl}/");
return handler;
}
}4. Map groups Claim → Authorities
// src/main/java/com/yourapp/config/GroupsAuthoritiesMapper.java
package com.yourapp.config;
import java.util.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
public class GroupsAuthoritiesMapper implements GrantedAuthoritiesMapper {
@Override
public Collection<? extends GrantedAuthority> mapAuthorities(
Collection<? extends GrantedAuthority> authorities) {
Set<GrantedAuthority> mapped = new HashSet<>();
for (GrantedAuthority authority : authorities) {
mapped.add(authority); // keep OIDC_USER / SCOPE_* authorities
if (authority instanceof OidcUserAuthority oidc) {
Object groups = oidc.getIdToken().getClaims().get("groups");
if (groups instanceof Collection<?> coll) {
for (Object g : coll) {
mapped.add(new SimpleGrantedAuthority("ROLE_" + g));
}
}
}
}
return mapped;
}
}5. Example Controller
// src/main/java/com/yourapp/web/HomeController.java
package com.yourapp.web;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/")
public String home(@AuthenticationPrincipal OidcUser user, Model model) {
if (user != null) {
model.addAttribute("sub", user.getSubject());
model.addAttribute("email", user.getEmail());
model.addAttribute("name", user.getFullName());
model.addAttribute("groups", user.getClaim("groups"));
}
return "home";
}
@GetMapping("/admin")
public String admin() {
// Only accessible to ROLE_admin via SecurityConfig
return "admin";
}
}6. Back-channel Logout Webhook
// src/main/java/com/yourapp/web/BackchannelLogoutController.java
package com.yourapp.web;
import com.nimbusds.jose.jwk.source.RemoteJWKSet;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import java.net.URL;
import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.web.bind.annotation.*;
@RestController
public class BackchannelLogoutController {
@Value("${spring.security.oauth2.client.registration.uniauth.client-id}")
private String clientId;
private final FindByIndexNameSessionRepository<? extends Session> sessions;
private final DefaultJWTProcessor<SecurityContext> jwtProcessor;
public BackchannelLogoutController(FindByIndexNameSessionRepository<? extends Session> sessions)
throws Exception {
this.sessions = sessions;
this.jwtProcessor = new DefaultJWTProcessor<>();
jwtProcessor.setJWSKeySelector(new JWSVerificationKeySelector<>(
JWSAlgorithm.RS256,
new RemoteJWKSet<>(new URL("https://uniauth.id/.well-known/jwks.json"))
));
}
@PostMapping("/logout/backchannel")
public ResponseEntity<Void> backchannelLogout(@RequestParam("logout_token") String token) {
try {
SignedJWT jwt = SignedJWT.parse(token);
JWTClaimsSet claims = jwtProcessor.process(jwt, null);
// Validate iss, aud
if (!"https://uniauth.id".equals(claims.getIssuer())) return ResponseEntity.badRequest().build();
if (!claims.getAudience().contains(clientId)) return ResponseEntity.badRequest().build();
// OIDC Back-Channel Logout 1.0 checks
if (claims.getClaim("nonce") != null) return ResponseEntity.badRequest().build();
Object events = claims.getClaim("events");
if (!(events instanceof Map) ||
!((Map<?, ?>) events).containsKey("http://schemas.openid.net/event/backchannel-logout")) {
return ResponseEntity.badRequest().build();
}
// Invalidate user sessions by principal name (sub)
String sub = claims.getSubject();
sessions.findByPrincipalName(sub).keySet()
.forEach(id -> sessions.deleteById(id));
return ResponseEntity.ok().build();
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
}
}Register https://yourapp.com/logout/backchannel as backchannel_logout_uri in the Developer Console. For session invalidation to work, configure Spring Session (spring-session-data-redis orspring-session-jdbc).
7. Environment
# .env or docker run -e …
UNIAUTH_CLIENT_ID=uni_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
UNIAUTH_CLIENT_SECRET=unis_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx