Article
OpenID Connect (OIDC) is an identity layer built on top of OAuth 2.0 that enables clients to verify user identity based on authentication performed by an authorization server. This comprehensive guide covers OIDC integration in Java applications using various libraries and frameworks.
OpenID Connect Core Concepts
Key Components:
- Relying Party (RP): Your application that needs authentication
- OpenID Provider (OP): Authorization server (Auth0, Okta, Keycloak, etc.)
- ID Token: JWT containing user identity information
- Access Token: Used to access protected resources
- UserInfo Endpoint: Returns user claims
OIDC Flow Types:
- Authorization Code Flow: For web applications
- Implicit Flow: For single-page applications (deprecated)
- Hybrid Flow: Combination of above flows
- Client Credentials Flow: For machine-to-machine communication
Project Setup and Dependencies
1. Maven Dependencies
<properties>
<spring-boot.version>2.7.0</spring-boot.version>
<nimbus-oidc.version>9.31</nimbus-oidc.version>
<keycloak.version>19.0.0</keycloak.version>
</properties>
<dependencies>
<!-- Spring Boot Starter Security with OAuth2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Nimbus OIDC SDK -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>oauth2-oidc-sdk</artifactId>
<version>${nimbus-oidc.version}</version>
</dependency>
<!-- Keycloak Adapter (Optional) -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
<version>${keycloak.version}</version>
</dependency>
<!-- JWT Processing -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- HTTP Client -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
</dependencies>
2. Gradle Dependencies
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security:2.7.0'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client:2.7.0'
implementation 'com.nimbusds:oauth2-oidc-sdk:9.31'
implementation 'org.keycloak:keycloak-spring-boot-starter:19.0.0'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
implementation 'org.apache.httpcomponents:httpclient:4.5.14'
}
Manual OIDC Integration with Nimbus OIDC SDK
1. OIDC Configuration Discovery
import com.nimbusds.oauth2.sdk.*;
import com.nimbusds.oauth2.sdk.auth.*;
import com.nimbusds.oauth2.sdk.id.*;
import com.nimbusds.oauth2.sdk.token.*;
import com.nimbusds.openid.connect.sdk.*;
import com.nimbusds.openid.connect.sdk.op.*;
import com.nimbusds.openid.connect.sdk.rp.*;
public class OIDCConfiguration {
private final String issuerUrl;
private OIDCProviderMetadata providerMetadata;
public OIDCConfiguration(String issuerUrl) {
this.issuerUrl = issuerUrl;
discoverProviderMetadata();
}
private void discoverProviderMetadata() {
try {
// Discover OpenID Provider configuration
OIDCProviderConfigurationRequest discoveryRequest =
new OIDCProviderConfigurationRequest(new Issuer(issuerUrl));
providerMetadata = OIDCProviderMetadata.resolve(discoveryRequest);
System.out.println("Discovered OIDC provider: " + providerMetadata.getIssuer());
} catch (Exception e) {
throw new RuntimeException("Failed to discover OIDC provider metadata", e);
}
}
public OIDCProviderMetadata getProviderMetadata() {
return providerMetadata;
}
public String getAuthorizationEndpoint() {
return providerMetadata.getAuthorizationEndpointURI().toString();
}
public String getTokenEndpoint() {
return providerMetadata.getTokenEndpointURI().toString();
}
public String getUserInfoEndpoint() {
return providerMetadata.getUserInfoEndpointURI().toString();
}
public String getJwksUri() {
return providerMetadata.getJWKSetURI().toString();
}
}
2. OIDC Client Implementation
import com.nimbusds.oauth2.sdk.*;
import com.nimbusds.oauth2.sdk.auth.*;
import com.nimbusds.oauth2.sdk.id.*;
import com.nimbusds.oauth2.sdk.token.*;
import com.nimbusds.openid.connect.sdk.*;
import com.nimbusds.openid.connect.sdk.claims.*;
import com.nimbusds.openid.connect.sdk.token.*;
import java.net.URI;
import java.util.*;
public class OIDCClient {
private final OIDCConfiguration oidcConfig;
private final String clientId;
private final String clientSecret;
private final String redirectUri;
public OIDCClient(OIDCConfiguration oidcConfig, String clientId,
String clientSecret, String redirectUri) {
this.oidcConfig = oidcConfig;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.redirectUri = redirectUri;
}
// Generate authorization URL
public String generateAuthorizationUrl(String state, String nonce) {
try {
// Client authentication
ClientID clientID = new ClientID(clientId);
URI callback = new URI(redirectUri);
// Scope for OpenID Connect
Scope scope = new Scope("openid", "profile", "email");
// Generate state and nonce for security
State stateParam = new State(state);
Nonce nonceParam = new Nonce(nonce);
// Create authorization request
AuthenticationRequest authRequest = new AuthenticationRequest.Builder(
new ResponseType(ResponseType.Value.CODE),
scope,
clientID,
callback)
.endpointURI(new URI(oidcConfig.getAuthorizationEndpoint()))
.state(stateParam)
.nonce(nonceParam)
.build();
return authRequest.toURI().toString();
} catch (Exception e) {
throw new RuntimeException("Failed to generate authorization URL", e);
}
}
// Exchange authorization code for tokens
public OIDCTokens exchangeCodeForTokens(String authorizationCode) {
try {
// Create token request
AuthorizationCode code = new AuthorizationCode(authorizationCode);
ClientID clientID = new ClientID(clientId);
Secret clientSecret = new Secret(this.clientSecret);
URI callback = new URI(redirectUri);
// Client authentication
ClientAuthentication clientAuth = new ClientSecretBasic(clientID, clientSecret);
// Token request
TokenRequest tokenRequest = new TokenRequest(
new URI(oidcConfig.getTokenEndpoint()),
clientAuth,
new AuthorizationCodeGrant(code, callback));
// Execute token request
TokenResponse tokenResponse = TokenResponse.parse(tokenRequest.toHTTPRequest().send());
if (!tokenResponse.indicatesSuccess()) {
TokenErrorResponse errorResponse = tokenResponse.toErrorResponse();
throw new RuntimeException("Token request failed: " +
errorResponse.getErrorObject().getDescription());
}
AccessTokenResponse successResponse = tokenResponse.toSuccessResponse();
OIDCTokens tokens = successResponse.getOIDCTokens();
return tokens;
} catch (Exception e) {
throw new RuntimeException("Failed to exchange code for tokens", e);
}
}
// Get user info using access token
public UserInfo getUserInfo(String accessToken) {
try {
// Create user info request
BearerAccessToken bearerToken = new BearerAccessToken(accessToken);
UserInfoRequest userInfoRequest = new UserInfoRequest(
new URI(oidcConfig.getUserInfoEndpoint()),
bearerToken);
// Execute user info request
UserInfoResponse userInfoResponse = UserInfoResponse.parse(
userInfoRequest.toHTTPRequest().send());
if (!userInfoResponse.indicatesSuccess()) {
throw new RuntimeException("UserInfo request failed");
}
UserInfoSuccessResponse successResponse = userInfoResponse.toSuccessResponse();
return successResponse.getUserInfo();
} catch (Exception e) {
throw new RuntimeException("Failed to get user info", e);
}
}
// Validate ID token
public boolean validateIdToken(OIDCTokens tokens, String expectedNonce) {
try {
IDTokenClaimsSet claims = tokens.getIDToken().getJWTClaimsSet();
// Validate issuer
if (!claims.getIssuer().equals(oidcConfig.getProviderMetadata().getIssuer().getValue())) {
return false;
}
// Validate audience
if (!claims.getAudience().contains(clientId)) {
return false;
}
// Validate nonce
if (expectedNonce != null && !expectedNonce.equals(claims.getNonce())) {
return false;
}
// Validate expiration
Date now = new Date();
if (claims.getExpirationTime().before(now)) {
return false;
}
return true;
} catch (Exception e) {
throw new RuntimeException("Failed to validate ID token", e);
}
}
}
3. Complete OIDC Flow Implementation
import javax.servlet.http.*;
import java.util.*;
public class OIDCFlowHandler {
private final OIDCClient oidcClient;
private final Map<String, String> stateStore = new ConcurrentHashMap<>();
public OIDCFlowHandler(OIDCClient oidcClient) {
this.oidcClient = oidcClient;
}
// Initiate login - redirect to OIDC provider
public void initiateLogin(HttpServletRequest request, HttpServletResponse response) {
try {
// Generate secure state and nonce
String state = generateSecureRandom();
String nonce = generateSecureRandom();
// Store state and nonce in session
HttpSession session = request.getSession();
session.setAttribute("oauth_state", state);
session.setAttribute("oauth_nonce", nonce);
// Generate authorization URL
String authUrl = oidcClient.generateAuthorizationUrl(state, nonce);
// Redirect to OIDC provider
response.sendRedirect(authUrl);
} catch (Exception e) {
throw new RuntimeException("Failed to initiate OIDC login", e);
}
}
// Handle OIDC callback
public OIDCUser handleCallback(HttpServletRequest request, HttpServletResponse response) {
try {
HttpSession session = request.getSession();
// Get parameters from callback
String code = request.getParameter("code");
String state = request.getParameter("state");
String error = request.getParameter("error");
// Check for errors
if (error != null) {
String errorDescription = request.getParameter("error_description");
throw new RuntimeException("OIDC error: " + error + " - " + errorDescription);
}
// Validate state
String storedState = (String) session.getAttribute("oauth_state");
if (!state.equals(storedState)) {
throw new RuntimeException("Invalid state parameter");
}
// Get stored nonce
String nonce = (String) session.getAttribute("oauth_nonce");
// Exchange code for tokens
OIDCTokens tokens = oidcClient.exchangeCodeForTokens(code);
// Validate ID token
if (!oidcClient.validateIdToken(tokens, nonce)) {
throw new RuntimeException("Invalid ID token");
}
// Get user info
UserInfo userInfo = oidcClient.getUserInfo(tokens.getAccessToken().getValue());
// Create user object
OIDCUser user = new OIDCUser(userInfo, tokens);
// Clear session attributes
session.removeAttribute("oauth_state");
session.removeAttribute("oauth_nonce");
// Store user in session
session.setAttribute("user", user);
return user;
} catch (Exception e) {
throw new RuntimeException("Failed to handle OIDC callback", e);
}
}
// Refresh tokens
public OIDCTokens refreshTokens(String refreshToken) {
try {
// Implementation of token refresh
// This would make a request to the token endpoint with grant_type=refresh_token
return null; // Simplified for example
} catch (Exception e) {
throw new RuntimeException("Failed to refresh tokens", e);
}
}
private String generateSecureRandom() {
return UUID.randomUUID().toString();
}
// User representation
public static class OIDCUser {
private final UserInfo userInfo;
private final OIDCTokens tokens;
public OIDCUser(UserInfo userInfo, OIDCTokens tokens) {
this.userInfo = userInfo;
this.tokens = tokens;
}
public String getSubject() {
return userInfo.getSubject().getValue();
}
public String getEmail() {
return userInfo.getEmailAddress();
}
public String getName() {
return userInfo.getName();
}
public String getAccessToken() {
return tokens.getAccessToken().getValue();
}
public String getIdToken() {
return tokens.getIDToken().getParsedString();
}
public String getRefreshToken() {
return tokens.getRefreshToken() != null ?
tokens.getRefreshToken().getValue() : null;
}
public Map<String, Object> getClaims() {
try {
return userInfo.toJWTClaimsSet().getClaims();
} catch (Exception e) {
throw new RuntimeException("Failed to get claims", e);
}
}
}
}
Spring Security OAuth2 Integration
1. Spring Security Configuration
import org.springframework.context.annotation.*;
import org.springframework.security.config.annotation.web.builders.*;
import org.springframework.security.config.annotation.web.configuration.*;
import org.springframework.security.oauth2.client.registration.*;
import org.springframework.security.oauth2.client.web.*;
import org.springframework.security.oauth2.core.*;
import org.springframework.security.web.*;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/", "/login", "/error", "/webjars/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/dashboard", true)
.failureUrl("/login?error=true")
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService())
)
)
.logout(logout -> logout
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
)
.exceptionHandling(ex -> ex
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
);
return http.build();
}
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
return new InMemoryClientRegistrationRepository(
googleClientRegistration(),
keycloakClientRegistration()
);
}
private ClientRegistration googleClientRegistration() {
return ClientRegistration.withRegistrationId("google")
.clientId("your-google-client-id")
.clientSecret("your-google-client-secret")
.scope("openid", "profile", "email")
.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth")
.tokenUri("https://www.googleapis.com/oauth2/v4/token")
.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
.userNameAttributeName(IdTokenClaimNames.SUB)
.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
.build();
}
private ClientRegistration keycloakClientRegistration() {
return ClientRegistration.withRegistrationId("keycloak")
.clientId("your-keycloak-client-id")
.clientSecret("your-keycloak-client-secret")
.scope("openid", "profile", "email")
.authorizationUri("http://localhost:8080/auth/realms/your-realm/protocol/openid-connect/auth")
.tokenUri("http://localhost:8080/auth/realms/your-realm/protocol/openid-connect/token")
.userInfoUri("http://localhost:8080/auth/realms/your-realm/protocol/openid-connect/userinfo")
.jwkSetUri("http://localhost:8080/auth/realms/your-realm/protocol/openid-connect/certs")
.userNameAttributeName("preferred_username")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
.build();
}
@Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> customOAuth2UserService() {
return new CustomOAuth2UserService();
}
}
2. Custom OAuth2 User Service
import org.springframework.security.oauth2.client.userinfo.*;
import org.springframework.security.oauth2.core.*;
import org.springframework.security.oauth2.core.user.*;
import java.util.*;
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
OAuth2User oauth2User = delegate.loadUser(userRequest);
// Extract claims and create custom user
Map<String, Object> attributes = oauth2User.getAttributes();
String registrationId = userRequest.getClientRegistration().getRegistrationId();
return createCustomUser(oauth2User, registrationId, attributes);
}
private OAuth2User createCustomUser(OAuth2User oauth2User, String registrationId,
Map<String, Object> attributes) {
// Extract standard claims
String subject = (String) attributes.get("sub");
String email = (String) attributes.get("email");
String name = (String) attributes.get("name");
String picture = (String) attributes.get("picture");
// Create custom user with additional claims
CustomOAuth2User user = new CustomOAuth2User(
oauth2User.getAuthorities(),
attributes,
oauth2User.getNameAttributeKey()
);
user.setSubject(subject);
user.setEmail(email);
user.setFullName(name);
user.setProfilePicture(picture);
user.setRegistrationId(registrationId);
return user;
}
public static class CustomOAuth2User extends DefaultOAuth2User {
private String subject;
private String email;
private String fullName;
private String profilePicture;
private String registrationId;
public CustomOAuth2User(Collection<? extends GrantedAuthority> authorities,
Map<String, Object> attributes, String nameAttributeKey) {
super(authorities, attributes, nameAttributeKey);
}
// Getters and setters
public String getSubject() { return subject; }
public void setSubject(String subject) { this.subject = subject; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getFullName() { return fullName; }
public void setFullName(String fullName) { this.fullName = fullName; }
public String getProfilePicture() { return profilePicture; }
public void setProfilePicture(String profilePicture) { this.profilePicture = profilePicture; }
public String getRegistrationId() { return registrationId; }
public void setRegistrationId(String registrationId) { this.registrationId = registrationId; }
}
}
3. Spring Boot Application Configuration
application.yml:
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID:your-google-client-id}
client-secret: ${GOOGLE_CLIENT_SECRET:your-google-client-secret}
scope: openid,profile,email
keycloak:
client-id: ${KEYCLOAK_CLIENT_ID:your-keycloak-client-id}
client-secret: ${KEYCLOAK_CLIENT_SECRET:your-keycloak-client-secret}
scope: openid,profile,email
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
provider:
keycloak:
authorization-uri: http://localhost:8080/auth/realms/your-realm/protocol/openid-connect/auth
token-uri: http://localhost:8080/auth/realms/your-realm/protocol/openid-connect/token
user-info-uri: http://localhost:8080/auth/realms/your-realm/protocol/openid-connect/userinfo
jwk-set-uri: http://localhost:8080/auth/realms/your-realm/protocol/openid-connect/certs
user-name-attribute: preferred_username
server:
port: 8080
logging:
level:
org.springframework.security: DEBUG
Resource Server with JWT Validation
1. Spring Security Resource Server Configuration
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig {
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuerUri;
@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
private String jwkSetUri;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter =
new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter jwtAuthenticationConverter =
new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(
grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
}
2. JWT Validation Service
import io.jsonwebtoken.*;
import org.springframework.stereotype.Service;
import java.security.PublicKey;
import java.util.*;
@Service
public class JWTValidationService {
private final JwtDecoder jwtDecoder;
private final JwkProvider jwkProvider;
public JWTValidationService(@Value("${oidc.jwks-uri}") String jwksUri) {
this.jwkProvider = new UrlJwkProvider(new URL(jwksUri));
this.jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwksUri).build();
}
public boolean validateToken(String token) {
try {
Jwt jwt = jwtDecoder.decode(token);
return true;
} catch (Exception e) {
return false;
}
}
public Map<String, Object> extractClaims(String token) {
try {
Jwt jwt = jwtDecoder.decode(token);
return jwt.getClaims();
} catch (Exception e) {
throw new RuntimeException("Failed to extract claims from token", e);
}
}
public String extractSubject(String token) {
return extractClaims(token).get("sub").toString();
}
public List<String> extractRoles(String token) {
Map<String, Object> claims = extractClaims(token);
Object roles = claims.get("roles");
if (roles instanceof List) {
return (List<String>) roles;
} else if (roles instanceof String) {
return Arrays.asList(((String) roles).split(","));
}
return Collections.emptyList();
}
public boolean hasRole(String token, String role) {
return extractRoles(token).contains(role);
}
public boolean isTokenExpired(String token) {
try {
Map<String, Object> claims = extractClaims(token);
Long exp = (Long) claims.get("exp");
return new Date(exp * 1000).before(new Date());
} catch (Exception e) {
return true;
}
}
}
Keycloak Integration
1. Keycloak Spring Boot Configuration
@Configuration
@KeycloakConfiguration
public class KeycloakConfig extends KeycloakWebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
KeycloakAuthenticationProvider keycloakAuthenticationProvider =
keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(
new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
@Bean
public KeycloakSpringBootConfigResolver KeycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(
new SessionRegistryImpl());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated();
}
}
application.yml for Keycloak:
keycloak: realm: your-realm auth-server-url: http://localhost:8080/auth ssl-required: external resource: your-client-id credentials: secret: your-client-secret use-resource-role-mappings: true bearer-only: false principal-attribute: preferred_username
2. Keycloak REST API Client
import org.keycloak.admin.client.*;
import org.keycloak.representations.idm.*;
import javax.ws.rs.core.Response;
@Service
public class KeycloakAdminService {
private final Keycloak keycloak;
private final String realm;
public KeycloakAdminService(@Value("${keycloak.auth-server-url}") String serverUrl,
@Value("${keycloak.realm}") String realm,
@Value("${keycloak.resource}") String clientId,
@Value("${keycloak.credentials.secret}") String clientSecret) {
this.realm = realm;
this.keycloak = KeycloakBuilder.builder()
.serverUrl(serverUrl)
.realm(realm)
.clientId(clientId)
.clientSecret(clientSecret)
.grantType("client_credentials")
.build();
}
public UserRepresentation createUser(String username, String email,
String firstName, String lastName) {
UserRepresentation user = new UserRepresentation();
user.setUsername(username);
user.setEmail(email);
user.setFirstName(firstName);
user.setLastName(lastName);
user.setEnabled(true);
CredentialRepresentation credential = new CredentialRepresentation();
credential.setType(CredentialRepresentation.PASSWORD);
credential.setValue("tempPassword");
credential.setTemporary(true);
user.setCredentials(Arrays.asList(credential));
Response response = keycloak.realm(realm).users().create(user);
if (response.getStatus() == 201) {
String userId = response.getLocation().getPath()
.replaceAll(".*/([^/]+)$", "$1");
user.setId(userId);
return user;
} else {
throw new RuntimeException("Failed to create user: " + response.getStatus());
}
}
public void assignRole(String userId, String roleName) {
RoleRepresentation role = keycloak.realm(realm).roles().get(roleName).toRepresentation();
keycloak.realm(realm).users().get(userId).roles().realmLevel().add(Arrays.asList(role));
}
public List<UserRepresentation> getUsers() {
return keycloak.realm(realm).users().list();
}
}
Advanced OIDC Features
1. Token Refresh and Management
import org.springframework.scheduling.annotation.*;
@Service
public class TokenRefreshService {
private final OAuth2AuthorizedClientService clientService;
private final JWTValidationService jwtValidationService;
public TokenRefreshService(OAuth2AuthorizedClientService clientService,
JWTValidationService jwtValidationService) {
this.clientService = clientService;
this.jwtValidationService = jwtValidationService;
}
@Scheduled(fixedRate = 300000) // Check every 5 minutes
public void refreshExpiringTokens() {
// Get all authorized clients and refresh tokens that are about to expire
// Implementation depends on your token storage strategy
}
public OAuth2AccessToken refreshToken(String registrationId, String principalName) {
OAuth2AuthorizedClient authorizedClient =
clientService.loadAuthorizedClient(registrationId, principalName);
if (authorizedClient != null &&
authorizedClient.getRefreshToken() != null &&
jwtValidationService.isTokenExpired(
authorizedClient.getAccessToken().getTokenValue())) {
// Implement token refresh logic
// This would use the refresh token to get a new access token
}
return authorizedClient != null ? authorizedClient.getAccessToken() : null;
}
}
2. Multi-Tenant OIDC Configuration
@Service
public class MultiTenantOIDCService {
private final Map<String, OIDCClient> tenantClients = new ConcurrentHashMap<>();
public void registerTenant(String tenantId, String issuerUrl,
String clientId, String clientSecret) {
OIDCConfiguration config = new OIDCConfiguration(issuerUrl);
OIDCClient client = new OIDCClient(config, clientId, clientSecret,
"https://yourapp.com/" + tenantId + "/callback");
tenantClients.put(tenantId, client);
}
public String getAuthorizationUrl(String tenantId, String state, String nonce) {
OIDCClient client = tenantClients.get(tenantId);
if (client == null) {
throw new IllegalArgumentException("Unknown tenant: " + tenantId);
}
return client.generateAuthorizationUrl(state, nonce);
}
public OIDCFlowHandler.OIDCUser handleTenantCallback(String tenantId,
String code, String state) {
OIDCClient client = tenantClients.get(tenantId);
if (client == null) {
throw new IllegalArgumentException("Unknown tenant: " + tenantId);
}
// Implementation would handle the callback for the specific tenant
return null; // Simplified
}
}
Security Best Practices
- Use HTTPS: Always use HTTPS in production
- Validate State Parameter: Prevent CSRF attacks
- Secure Token Storage: Store tokens securely (HTTP-only cookies)
- Token Expiration: Implement proper token refresh logic
- Scope Limitation: Request only necessary scopes
- PKCE: Use Proof Key for Code Exchange for public clients
- Regular Key Rotation: Handle key rotation properly
- Logging and Monitoring: Monitor authentication events
Conclusion
OpenID Connect provides a robust and standardized way to implement authentication in Java applications. Whether you choose manual integration with Nimbus OIDC SDK, Spring Security's built-in OAuth2 support, or Keycloak adapters, OIDC offers flexibility and security for modern applications. By understanding the flows, properly validating tokens, and implementing best practices, you can build secure, scalable authentication systems that integrate seamlessly with identity providers like Google, Auth0, Okta, and Keycloak.