UniAuth.ID

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: sub

3. 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

Next