1. Overview of OpenID Connect in Keycloak
OpenID Connect (OIDC) is an authentication protocol built on the OAuth 2.0 platform. Keycloak fully supports the OIDC specification and expands many features for enterprises. In this article, we will dive into creating, configuring, and integrating OIDC clients.
OIDC Endpoints trong Keycloak
Keycloak provides OIDC standard endpoints. You can get all endpoint information via Well-Known Configuration:
GET https://<keycloak-host>/realms/<realm-name>/.well-known/openid-configuration
Các endpoints quan trọng:
| Endpoint | URL Pattern | Mục đích |
|---|---|---|
| Authorization | /realms/{realm}/protocol/openid-connect/auth | Khởi tạo authentication flow |
| Token | /realms/{realm}/protocol/openid-connect/token | Lấy/refresh tokens |
| UserInfo | /realms/{realm}/protocol/openid-connect/userinfo | Lấy thông tin user |
| Logout | /realms/{realm}/protocol/openid-connect/logout | Đăng xuất (RP-Initiated Logout) |
| Token Introspection | /realms/{realm}/protocol/openid-connect/token/introspect | Kiểm tra token validity |
| Token Revocation | /realms/{realm}/protocol/openid-connect/revoke | Thu hồi token |
| JWKS | /realms/{realm}/protocol/openid-connect/certs | Public keys cho JWT verification |
| Device Authorization | /realms/{realm}/protocol/openid-connect/auth/device | Device Authorization Grant |
2. OIDC Client Types
Keycloak hỗ trợ ba loại client chính, mỗi loại phù hợp với kiến trúc ứng dụng khác nhau:
2.1 Public Client
Client không thể bảo mật client secret — thường là ứng dụng chạy hoàn toàn trên browser hoặc mobile device.
Đặc điểm: Không có client secret, xác thực qua redirect URI
Use cases: Single Page Applications (React, Angular, Vue), Mobile apps, Desktop apps
Auth flow: Authorization Code + PKCE (bắt buộc)
Cấu hình:
Client authentication= OFF
// Ví dụ: SPA không có backend — PHẢI dùng Public Client + PKCE
// Client KHÔNG lưu trữ secret, chỉ dùng code_verifier/code_challenge
Client ID: my-spa-app
Client authentication: OFF
Valid redirect URIs: http://localhost:3000/*
Web origins: http://localhost:3000
2.2 Confidential Client
Client has the ability to secure client secrets — usually server-side applications.
Features: Has client secret or private key, authentication when calling token endpoint
Use cases: Server-side web apps (Spring Boot, Django, .NET), Backend APIs, Service-to-service communication
Auth flow: Authorization Code, Client Credentials, or both
Configuration:
Client authentication= ON
// Ví dụ: Spring Boot backend app — dùng Confidential Client
Client ID: my-backend-api
Client authentication: ON
Client secret: auto-generated hoặc custom
Valid redirect URIs: http://localhost:8081/login/oauth2/code/keycloak
2.3 Bearer-only Client (Legacy)
Client only receives and validates bearer tokens — does not initiate login flow.
Features: No redirect URI, only validate incoming tokens
Use cases: Pure API services, Microservices only accept authenticated requests
Note: In Keycloak 25+, bearer-only has been deprecated. Instead, create a confidential client and only enable
Service accounts roles
| Characteristic | Public | Confidential | Bearer-only (deprecated) |
|---|---|---|---|
| Client authentication | OFF | ON | N/A |
| Client secret | No | Yes | No |
| Can initialize login | Yes | Yes | No |
| Redirect URI | Required | Required | None |
| PKCE | Required | Optional | N/A |
| Use main case | SPA, Mobile | Server app | Pure API |
3. Create OIDC Client via Admin Console
3.1 Steps to create Client
Access Admin Console → select realm → Clients → Create client
General Settings:
- Client type: OpenID Connect
- Client ID:
my-app(unique identifier) - Name: My Application (display name)
- Description: Client description
- Always display in UI: OFF
Capability config:
- Client authentication: ON (confidential) or OFF (public)
- Authorization: ON if fine-grained authorization is needed
- Authentication flow: select appropriate flows
Login settings:
- Root URL, Home URL, Valid redirect URIs, Valid post logout redirect URIs, Web origins
3.2 Create Client using Admin CLI
# Đăng nhập Admin CLI bin/kcadm.sh config credentials \ --server http://localhost:8080 \ --realm master \ --user admin \ --password adminTạo confidential client
bin/kcadm.sh create clients -r my-company
-s clientId=my-backend-app
-s name="My Backend Application"
-s enabled=true
-s protocol=openid-connect
-s publicClient=false
-s 'redirectUris=["http://localhost:8081/*"]'
-s 'webOrigins=["http://localhost:8081"]'
-s serviceAccountsEnabled=true
-s directAccessGrantsEnabled=falseTạo public client
bin/kcadm.sh create clients -r my-company
-s clientId=my-spa-app
-s name="My SPA Application"
-s enabled=true
-s protocol=openid-connect
-s publicClient=true
-s 'redirectUris=["http://localhost:3000/*"]'
-s 'webOrigins=["http://localhost:3000"]'
-s directAccessGrantsEnabled=false
3.3 Create Client using Admin REST API
# Lấy access token ACCESS_TOKEN=$(curl -s -X POST \ "http://localhost:8080/realms/master/protocol/openid-connect/token" \ -d "client_id=admin-cli" \ -d "username=admin" \ -d "password=admin" \ -d "grant_type=password" | jq -r '.access_token')Tạo client
curl -s -X POST
"http://localhost:8080/admin/realms/my-company/clients"
-H "Authorization: Bearer $ACCESS_TOKEN"
-H "Content-Type: application/json"
-d '{ "clientId": "my-backend-app", "name": "My Backend Application", "enabled": true, "protocol": "openid-connect", "publicClient": false, "redirectUris": ["http://localhost:8081/*"], "webOrigins": ["http://localhost:8081"], "serviceAccountsEnabled": true, "directAccessGrantsEnabled": false, "attributes": { "pkce.code.challenge.method": "S256" } }'
4. Client Settings details
4.1 General Settings
| Setting | Description | Note |
|---|---|---|
| Client ID | Unique identifier for client | Cannot be changed after creation |
| Name | Display name | Support localization key: ${my-client-name} |
| Description | Client description | |
| Always displayed in UI | Always displayed on Account Console | Used for internal tools |
4.2 Access Settings
| Setting | Description | Example |
|---|---|---|
| Root URL | Root URL, prepend to relative URLs | http://localhost:3000 |
| Home URL | Default URL when redirecting to client | /dashboard |
| Valid redirect URIs | List of valid redirect URIs (wildcard *) | http://localhost:3000/* |
| Valid post logout redirect URIs | Valid post logout URIs | + (inherit redirect URIs) |
| Web origins | CORS allowed origins | + (inherits redirect URIs) |
| Admin URL | URL cho backchannel operations | URL backend (logout, policy enforcement) |
Security note for redirect URIs:
NEVER uses the wildcard
*as a redirect URI in production — this is a vulnerability Open RedirectDeclare correctly necessary redirect URIs
Use HTTPS in production
Avoid using localhost in production redirect URIs
# ❌ KHÔNG NÊN — quá rộng, dễ bị Open Redirect attack
Valid redirect URIs: *
# ❌ KHÔNG NÊN — wildcard domain
Valid redirect URIs: https://*.example.com/*
# ✅ NÊN — khai báo chính xác
Valid redirect URIs:
https://myapp.example.com/callback
https://myapp.example.com/silent-renew
4.3 Capability Config
| Setting | Description | When to turn on |
|---|---|---|
| Client authentication | ON = confidential, OFF = public | ON cho server apps |
| Authorization | Fine-grained authorization (UMA) | When resource-based permissions are needed |
| Standard flow | Authorization Code Flow | Most use cases |
| Direct access grants | Resource Owner Password Credentials | Legacy apps (not recommended) |
| Implicit flow | Implicit Grant (deprecated) | Not recommended |
| Service accounts roles | Client Credentials Grant | Machine-to-machine auth |
| OAuth 2.0 Device Authorization Grant | Device Code Flow | Smart TV, CLI tools |
| OIDC CIBA Grant | Client-Initiated Backchannel Auth | Banking, telecom |
4.4 Login Settings
| Setting | Description |
|---|---|
| Login theme | Theme for this client's login page |
| Consent required | Show consent screen to user |
| Display client on screen | Display client name on consent screen |
| Client consent screen text | Custom text for consent |
4.5 Logout Settings
| Setting | Description |
|---|---|
| Front channel logout | Logout qua browser redirect (OpenID Connect Front-Channel Logout) |
| Backchannel logout URL | URL receives backchannel logout requests from Keycloak |
| Backchannel logout session required | Include session ID in logout token |
| Backchannel logout revoke offline sessions | Revoke offline sessions when logout |
# Ví dụ Backchannel Logout URL cho Spring Boot
Backchannel logout URL: http://localhost:8081/logout/connect/back-channel/keycloak
# Ví dụ Front Channel Logout URL
Front channel logout URL: http://localhost:3000/logout-callback
5. OIDC Authentication Flows details
5.1 Authorization Code Flow
This is the flow most recommended by for most use cases. The user is redirected to the Keycloak login page. After successful authentication, Keycloak returns an authorization code, the client exchanges the code for tokens.
┌──────────┐ ┌──────────┐ ┌──────────┐
│ User │ │ Client │ │ Keycloak │
│ (Browser)│ │ (App) │ │ (IdP) │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ 1. Click Login│ │
│───────────────>│ │
│ │ 2. Redirect │
│<───────────────│ /auth? │
│ │ response_type │
│ │ =code& │
│ │ client_id=... │
│ 3. Login page │ │
│───────────────────────────────>│
│ 4. Enter credentials │
│───────────────────────────────>│
│ 5. Redirect with code │
│<──────────────────────────────│
│───────────────>│ │
│ │ 6. Exchange │
│ │ code for │
│ │ tokens │
│ │───────────────>│
│ │ 7. Tokens │
│ │<──────────────│
│ 8. Authenticated│ │
│<───────────────│ │
Request Authorization Code:
GET /realms/my-company/protocol/openid-connect/auth?
response_type=code&
client_id=my-app&
redirect_uri=http://localhost:3000/callback&
scope=openid profile email&
state=random-state-value&
nonce=random-nonce-value
Exchange Code for Tokens:
POST /realms/my-company/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code& code=AUTH_CODE_FROM_CALLBACK& client_id=my-app& client_secret=CLIENT_SECRET& redirect_uri=http://localhost:3000/callback
Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "eyJhbGciOiJIUzUxMiIs...",
"token_type": "Bearer",
"id_token": "eyJhbGciOiJSUzI1NiIs...",
"not-before-policy": 0,
"session_state": "a-session-id",
"scope": "openid profile email"
}
5.2 Authorization Code Flow + PKCE
PKCE (Proof Key for Code Exchange, RFC 7636) protects Authorization Code Flow from authorization code interception attack. Required for Public Clients and recommended for all clients.
How it works:
Client creates
code_verifier(random string 43-128 characters)Client calculates
code_challenge= Base64URL(SHA256(code_verifier))Send
code_challengein authorization requestSend
code_verifierin token request — Keycloak verify by hashing and comparing
PKCE configuration in Keycloak:
Go to client → tab Advanced → Advanced Settings:
| Setting | Value | Description |
|---|---|---|
| Proof Key for Code Exchange Code Challenge Method | S256 | Mandatory PKCE with SHA-256 (recommended) |
| plain | PKCE with plain text (not secure) | |
| (empty) | Optional PKCE |
PKCE flow:
# 1. Tạo code_verifier (client-side) code_verifier="dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"2. Tạo code_challenge = Base64URL(SHA256(code_verifier))
code_challenge="E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
3. Authorization request với code_challenge
GET /realms/my-company/protocol/openid-connect/auth? response_type=code& client_id=my-spa-app& redirect_uri=http://localhost:3000/callback& scope=openid profile email& state=random-state& code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM& code_challenge_method=S256
4. Token request với code_verifier
POST /realms/my-company/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code& code=AUTH_CODE& client_id=my-spa-app& redirect_uri=http://localhost:3000/callback& code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
5.3 Implicit Flow (Deprecated)
SHOULD NOT be used. OAuth 2.0 Security Best Current Practice (RFC 9700) recommends not using Implicit Flow because tokens are returned via URL fragments, easily stolen via browser history or referrer header.
Replace: Use Authorization Code Flow + PKCE for all clients, including SPAs.
If forced to support legacy systems:
# Bật Implicit Flow trong client settings Capability Config → Implicit flow: ONRequest (trả về token trực tiếp)
GET /realms/my-company/protocol/openid-connect/auth? response_type=id_token token& client_id=legacy-app& redirect_uri=http://localhost:3000/callback& scope=openid profile& state=random-state& nonce=random-nonce
5.4 Client Credentials Flow
For machine-to-machine authentication — no user interaction. The client authenticates itself with its own credentials.
Use cases:
Microservice calls microservice
Backend batch jobs
Scheduled tasks needs API access
CI/CD pipelines
Configuration:
Create Confidential Client (
Client authentication= ON)Enable Service accounts roles in Capability Config
Assign roles to service account: Client → Service account roles tab
# Request token
POST /realms/my-company/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&
client_id=my-service&
client_secret=MY_CLIENT_SECRET&
scope=openid
# Response
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"expires_in": 300,
"token_type": "Bearer",
"not-before-policy": 0,
"scope": "openid profile email"
}
# Lưu ý: KHÔNG có refresh_token và id_token trong Client Credentials flow
5.5 Device Authorization Grant (RFC 8628)
For devices with limited input — Smart TV, IoT devices, CLI tools. User authenticates on another device (phone, laptop) with code.
Configuration:
Client → Capability Config → enable OAuth 2.0 Device Authorization Grant
Realm Settings → configure OAuth Device Code lifespan (default 600 seconds)
# Bước 1: Device request — lấy device code và user code
POST /realms/my-company/protocol/openid-connect/auth/device
Content-Type: application/x-www-form-urlencoded
client_id=my-tv-app
# Response
{
"device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
"user_code": "WDJB-MJHT",
"verification_uri": "http://localhost:8080/realms/my-company/device",
"verification_uri_complete": "http://localhost:8080/realms/my-company/device?user_code=WDJB-MJHT",
"expires_in": 600,
"interval": 5
}
# Bước 2: Hiển thị user_code và verification_uri trên TV/device
# User truy cập verification_uri trên phone/laptop, nhập user_code, đăng nhập
# Bước 3: Device polling — kiểm tra xem user đã xác thực chưa
POST /realms/my-company/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:device_code&
client_id=my-tv-app&
device_code=GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS
# Response khi user chưa xác thực
{
"error": "authorization_pending",
"error_description": "The authorization request is still pending"
}
# Response khi user đã xác thực — nhận tokens
{
"access_token": "eyJhbGciOi...",
"refresh_token": "eyJhbGciOi...",
"id_token": "eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 300
}
5.6 CIBA — Client-Initiated Backchannel Authentication (OIDC CIBA)
CIBA allows the client to initiate authentication without redirecting the user via browser. Instead, Keycloak sends authentication requests to users via other channels (push notification, SMS, email).
Use cases:
Banking: Point-of-sale authenticates payment via mobile app
Telecom: SIM-based authentication
Call centers: Agent to authenticate customer via phone
CIBA configuration:
Client → Capability Config → enable OIDC CIBA Grant
Realm Settings → Authentication → tab CIBA Policy:
| Setting | Description | Default value |
|---|---|---|
| Backchannel Token Delivery Mode | poll, ping, or push | poll |
| Expires In | Authentication request expiration time | 120 seconds |
| Interval | Interval between polling requests | 5 seconds |
| Authentication Requested User Hint | User hint type: login_hint, login_hint_token, id_token_hint | login_hint |
# CIBA authentication request
POST /realms/my-company/protocol/openid-connect/ext/ciba/auth
Content-Type: application/x-www-form-urlencoded
client_id=my-pos-app&
client_secret=CLIENT_SECRET&
scope=openid&
[email protected]&
binding_message=Xac+nhan+thanh+toan+500k
# Response
{
"auth_req_id": "eyJhbGciOiJSUzI1NiIs...",
"expires_in": 120,
"interval": 5
}
# Polling for token (giống Device Auth)
POST /realms/my-company/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:openid:params:grant-type:ciba&
client_id=my-pos-app&
client_secret=CLIENT_SECRET&
auth_req_id=eyJhbGciOiJSUzI1NiIs...
Custom CIBA Authentication Channel Provider:
Default Keycloak uses CIBALoginUserResolver internally. To send actual push notifications, you need to implement SPI custom:
// Implement interface CIBAAuthenticationChannelProvider public class MyCIBAChannelProvider implements CIBAAuthenticationChannelProvider {@Override public void requestAuthentication( CIBALoginUserResolver.CIBALoginUser user, AuthenticationChannelRequest request) { // Gửi push notification đến user's device // binding_message: "Xác nhận thanh toán 500k" pushNotificationService.send( user.getDeviceToken(), request.getBindingMessage(), request.getAuthResultUrl() ); } @Override public boolean verifyAuthentication(String authResultId) { // Verify kết quả từ user's device return authResultStore.isApproved(authResultId); }
}
6. Integrating OIDC Client with React (SPA)
6.1 Using keycloak-js adapter
Keycloak provides an official JavaScript adapter for SPAs:
# Cài đặt
npm install keycloak-js
Configure Keycloak client for React:
Client ID: my-react-app
Client authentication: OFF (public client)
Valid redirect URIs: http://localhost:3000/*
Valid post logout redirect URIs: http://localhost:3000/*
Web origins: http://localhost:3000
PKCE Code Challenge Method: S256
Initialize Keycloak in React:
// src/keycloak.ts import Keycloak from "keycloak-js";const keycloak = new Keycloak({ url: "http://localhost:8080", realm: "my-company", clientId: "my-react-app", });
export default keycloak;
// src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import keycloak from "./keycloak";
keycloak
.init({
onLoad: "login-required", // hoặc 'check-sso'
pkceMethod: "S256",
checkLoginIframe: false, // tắt cho production tránh cookie issues
silentCheckSsoRedirectUri:
window.location.origin + "/silent-check-sso.html",
})
.then((authenticated) => {
if (authenticated) {
console.log("User is authenticated");
console.log("Token:", keycloak.token);
console.log("User info:", keycloak.tokenParsed);
// Auto-refresh token trước khi hết hạn
setInterval(() => {
keycloak
.updateToken(70) // refresh nếu token hết hạn trong 70 giây
.then((refreshed) => {
if (refreshed) {
console.log("Token was refreshed");
}
})
.catch(() => {
console.error("Failed to refresh token");
keycloak.login(); // redirect về login nếu refresh thất bại
});
}, 60000);
ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
).render(
<React.StrictMode>
<App keycloak={keycloak} />
</React.StrictMode>
);
} else {
console.warn("Not authenticated");
keycloak.login();
}
})
.catch((error) => {
console.error("Keycloak init failed:", error);
});
// src/App.tsx
import Keycloak from "keycloak-js";
interface AppProps {
keycloak: Keycloak;
}
function App({ keycloak }: AppProps) {
const handleLogout = () => {
keycloak.logout({
redirectUri: window.location.origin,
});
};
const callApi = async () => {
// Tự động gắn Bearer token vào API calls
const response = await fetch("http://localhost:8081/api/data", {
headers: {
Authorization: `Bearer ${keycloak.token}`,
},
});
const data = await response.json();
console.log(data);
};
return (
<div>
<h1>Welcome, {keycloak.tokenParsed?.preferred_username}</h1>
<p>Email: {keycloak.tokenParsed?.email}</p>
<p>Roles: {keycloak.tokenParsed?.realm_access?.roles?.join(", ")}</p>
<button onClick={callApi}>Call API</button>
<button onClick={handleLogout}>Logout</button>
</div>
);
}
export default App;
6.2 Use react-oidc-context (replaces keycloak-js)
Another option is to use the react-oidc-context library based on oidc-client-ts — which does not depend on the Keycloak-specific adapter:
npm install react-oidc-context oidc-client-ts
// src/main.tsx
import { AuthProvider } from "react-oidc-context";
const oidcConfig = {
authority: "http://localhost:8080/realms/my-company",
client_id: "my-react-app",
redirect_uri: "http://localhost:3000/callback",
post_logout_redirect_uri: "http://localhost:3000",
scope: "openid profile email",
automaticSilentRenew: true,
};
ReactDOM.createRoot(document.getElementById("root")!).render(
<AuthProvider {...oidcConfig}>
<App />
</AuthProvider>
);
// src/App.tsx
import { useAuth } from "react-oidc-context";
function App() {
const auth = useAuth();
if (auth.isLoading) return <div>Loading...</div>;
if (auth.error) return <div>Error: {auth.error.message}</div>;
if (!auth.isAuthenticated) {
return <button onClick={() => auth.signinRedirect()}>Login</button>;
}
return (
<div>
<p>Welcome, {auth.user?.profile.preferred_username}</p>
<button onClick={() => auth.removeUser()}>Logout</button>
</div>
);
}
7. Integrating OIDC Client with Spring Boot
7.1 Spring Boot OAuth2 Resource Server
Configure Spring Boot as Resource Server — validate JWT tokens from Keycloak:
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
# application.yml
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8080/realms/my-company
jwk-set-uri: http://localhost:8080/realms/my-company/protocol/openid-connect/certs
// SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("admin")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
);
return http.build();
}
// Custom converter để map Keycloak realm_access.roles → Spring Security authorities
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
List<GrantedAuthority> authorities = new ArrayList<>();
// Extract realm roles
Map<String, Object> realmAccess = jwt.getClaimAsMap("realm_access");
if (realmAccess != null) {
List<String> roles = (List<String>) realmAccess.get("roles");
if (roles != null) {
roles.forEach(role ->
authorities.add(new SimpleGrantedAuthority("ROLE_" + role))
);
}
}
// Extract client roles
Map<String, Object> resourceAccess = jwt.getClaimAsMap("resource_access");
if (resourceAccess != null) {
Map<String, Object> clientAccess =
(Map<String, Object>) resourceAccess.get("my-backend-app");
if (clientAccess != null) {
List<String> clientRoles = (List<String>) clientAccess.get("roles");
if (clientRoles != null) {
clientRoles.forEach(role ->
authorities.add(new SimpleGrantedAuthority("ROLE_" + role))
);
}
}
}
return authorities;
});
return converter;
}
}
7.2 Spring Boot OAuth2 Client (Server-side login)
Configure Spring Boot as OAuth2 Client — server-side login flow:
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
# application.yml
spring:
security:
oauth2:
client:
registration:
keycloak:
client-id: my-backend-app
client-secret: ${KEYCLOAK_CLIENT_SECRET}
scope: openid,profile,email
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/keycloak"
provider:
keycloak:
issuer-uri: http://localhost:8080/realms/my-company
user-name-attribute: preferred_username
Keycloak client settings cho Spring Boot OAuth2 Client:
Client ID: my-backend-app
Client authentication: ON (confidential)
Valid redirect URIs: http://localhost:8081/login/oauth2/code/keycloak
Backchannel logout URL: http://localhost:8081/logout/connect/back-channel/keycloak
Web origins: http://localhost:8081
8. Advanced Client Settings
8.1 Tab Advanced
Advanced configurations in the client's Advanced tab:
| Setting | Description | Recommended value |
|---|---|---|
| Access Token Lifespan | Override realm-level token lifespan for this client | Leave blank = use realm level |
| Client Session Idle | Override client session idle timeout | Leave blank = use realm level |
| Client Session Max | Override client session max lifespan | Leave blank = use realm level |
| Client Offline Session Idle | Override offline session idle timeout | Leave blank = use realm level |
| Client Offline Session Max | Override offline session max lifespan | Leave blank = use realm level |
| PKCE Code Challenge Method | Required PKCE method | S256 |
| Pushed Authorization Request Required | Required PAR (RFC 9126) | ON for high-security apps |
| ACR to LoA Mapping | Map ACR values → Level of Assurance | Configuration for step-up auth |
8.2 Tab Credentials (Confidential Clients)
Manage client credentials:
Client Authenticator: Client ID and Secret (default), Signed JWT (client_secret_jwt), Signed JWT with Private Key (private_key_jwt), X.509 Certificate
Client Secret: Regenerate if compromised
Registration access token: Used for Dynamic Client Registration
8.3 Tab Service Account Roles
Assign roles to service account (Client Credentials flow):
Open client → tab Service account roles
Click Assign role
Select realm roles or client roles to assign
# Ví dụ gán role bằng Admin CLI
# Lấy service account user ID
SERVICE_ACCOUNT_ID=$(bin/kcadm.sh get clients/$CLIENT_UUID/service-account-user \
-r my-company --fields id --format csv --noquotes)
# Gán realm role
bin/kcadm.sh add-roles -r my-company \
--uusername service-account-my-service \
--rolename admin
# Gán client role
bin/kcadm.sh add-roles -r my-company \
--uusername service-account-my-service \
--cclientid target-client \
--rolename manage-users
9. Practice exercises
Lab 1: Creating Public Client for React SPA
Create client
react-spa-labwithClient authentication= OFFConfiguration: Valid redirect URIs =
http://localhost:3000/*, Web origins =http://localhost:3000Enable PKCE: Advanced → PKCE Code Challenge Method =
S256Create React app, install
keycloak-js, integrate login/logoutVerify token trong browser DevTools → Application → Network tab
Lab 2: Creating Confidential Client for Spring Boot API
Create client
spring-api-labwithClient authentication= ONEnable
Service accounts rolesAssign role
adminto service accountCreate Spring Boot project with
spring-boot-starter-oauth2-resource-serverImplement endpoint
/api/mereturns user information from JWTTest with
curlsend Bearer token
Lab 3: Client Credentials Flow
Create client
batch-workeronly Client Credentials flowGet tokens via
curlCall API endpoint with newly retrieved token
Check token contents via jwt.io (for development only)
Lab 4: Device Authorization Flow
Create public client
cli-toolwith Device Authorization Grant enabledUse
curlto simulate device flow:- Request device code
- Open verification URI on browser, enter user code
- Poll cho token
Verify token received
# Script test Device Authorization Flow
#!/bin/bash
REALM=my-company
CLIENT_ID=cli-tool
KC_URL=http://localhost:8080
# Bước 1: Request device code
RESPONSE=$(curl -s -X POST \
"$KC_URL/realms/$REALM/protocol/openid-connect/auth/device" \
-d "client_id=$CLIENT_ID")
DEVICE_CODE=$(echo $RESPONSE | jq -r '.device_code')
USER_CODE=$(echo $RESPONSE | jq -r '.user_code')
VERIFY_URI=$(echo $RESPONSE | jq -r '.verification_uri_complete')
INTERVAL=$(echo $RESPONSE | jq -r '.interval')
echo "========================================"
echo "Mở URL sau trên browser:"
echo "$VERIFY_URI"
echo "Hoặc truy cập: $(echo $RESPONSE | jq -r '.verification_uri')"
echo "Nhập code: $USER_CODE"
echo "========================================"
# Bước 2: Polling for token
while true; do
sleep $INTERVAL
TOKEN_RESPONSE=$(curl -s -X POST \
"$KC_URL/realms/$REALM/protocol/openid-connect/token" \
-d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
-d "client_id=$CLIENT_ID" \
-d "device_code=$DEVICE_CODE")
ERROR=$(echo $TOKEN_RESPONSE | jq -r '.error // empty')
if [ -z "$ERROR" ]; then
echo "Xác thực thành công!"
echo "Access Token: $(echo $TOKEN_RESPONSE | jq -r '.access_token' | head -c 50)..."
break
elif [ "$ERROR" = "authorization_pending" ]; then
echo "Đang chờ user xác thực..."
elif [ "$ERROR" = "slow_down" ]; then
INTERVAL=$((INTERVAL + 5))
echo "Slow down, tăng interval lên ${INTERVAL}s"
else
echo "Error: $ERROR"
break
fi
done