Chuyển đến nội dung chính

Lesson 6: OpenID Connect Clients - Configuration from A to Z

Learn in detail about OIDC client types (public, confidential, bearer-only), create and configure clients via Admin Console, OIDC auth flows (Authorization Code, Implicit, Client Credentials, Device Authorization, CIBA), set up PKCE, CIBA policy and practical integration with React and Spring Boot.

🔒 DevSecOps — Lesson 6 Lesson 6: OpenID Connect Clients - Configuration from A to Z

Keycloak from Basic to Advanced

Part 2: SSO Protocols - OpenID Connect and SAML

xdev.asia

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:

EndpointURL PatternMục đích
Authorization/realms/{realm}/protocol/openid-connect/authKhởi tạo authentication flow
Token/realms/{realm}/protocol/openid-connect/tokenLấy/refresh tokens
UserInfo/realms/{realm}/protocol/openid-connect/userinfoLấ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/introspectKiểm tra token validity
Token Revocation/realms/{realm}/protocol/openid-connect/revokeThu hồi token
JWKS/realms/{realm}/protocol/openid-connect/certsPublic keys cho JWT verification
Device Authorization/realms/{realm}/protocol/openid-connect/auth/deviceDevice 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

CharacteristicPublicConfidentialBearer-only (deprecated)
Client authenticationOFFONN/A
Client secretNoYesNo
Can initialize loginYesYesNo
Redirect URIRequiredRequiredNone
PKCERequiredOptionalN/A
Use main caseSPA, MobileServer appPure API

3. Create OIDC Client via Admin Console

3.1 Steps to create Client

  1. Access Admin Console → select realm → Clients → Create client

  2. 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
  3. Capability config:

    • Client authentication: ON (confidential) or OFF (public)
    • Authorization: ON if fine-grained authorization
    • is needed
    • Authentication flow: select appropriate flows
  4. 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 admin

Tạ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=false

Tạ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

SettingDescriptionNote
Client IDUnique identifier for clientCannot be changed after creation
NameDisplay nameSupport localization key: ${my-client-name}
DescriptionClient description
Always displayed in UIAlways displayed on Account ConsoleUsed for internal tools

4.2 Access Settings

SettingDescriptionExample
Root URLRoot URL, prepend to relative URLshttp://localhost:3000
Home URLDefault URL when redirecting to client/dashboard
Valid redirect URIsList of valid redirect URIs (wildcard *)http://localhost:3000/*
Valid post logout redirect URIsValid post logout URIs+ (inherit redirect URIs)
Web originsCORS allowed origins+ (inherits redirect URIs)
Admin URLURL cho backchannel operationsURL backend (logout, policy enforcement)

Security note for redirect URIs:

  • NEVER uses the wildcard * as a redirect URI in production — this is a vulnerability Open Redirect

  • Declare 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

SettingDescriptionWhen to turn on
Client authenticationON = confidential, OFF = publicON cho server apps
AuthorizationFine-grained authorization (UMA)When resource-based permissions are needed
Standard flowAuthorization Code FlowMost use cases
Direct access grantsResource Owner Password CredentialsLegacy apps (not recommended)
Implicit flowImplicit Grant (deprecated)Not recommended
Service accounts rolesClient Credentials GrantMachine-to-machine auth
OAuth 2.0 Device Authorization GrantDevice Code FlowSmart TV, CLI tools
OIDC CIBA GrantClient-Initiated Backchannel AuthBanking, telecom

4.4 Login Settings

SettingDescription
Login themeTheme for this client's login page
Consent requiredShow consent screen to user
Display client on screenDisplay client name on consent screen
Client consent screen textCustom text for consent

4.5 Logout Settings

SettingDescription
Front channel logoutLogout qua browser redirect (OpenID Connect Front-Channel Logout)
Backchannel logout URLURL receives backchannel logout requests from Keycloak
Backchannel logout session requiredInclude session ID in logout token
Backchannel logout revoke offline sessionsRevoke 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:

  1. Client creates code_verifier (random string 43-128 characters)

  2. Client calculates code_challenge = Base64URL(SHA256(code_verifier))

  3. Send code_challenge in authorization request

  4. Send code_verifier in token request — Keycloak verify by hashing and comparing

PKCE configuration in Keycloak:

Go to client → tab Advanced → Advanced Settings:

SettingValueDescription
Proof Key for Code Exchange Code Challenge MethodS256Mandatory PKCE with SHA-256 (recommended)
plainPKCE 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: ON

Request (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:

  1. Create Confidential Client (Client authentication = ON)

  2. Enable Service accounts roles in Capability Config

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

  1. Client → Capability Config → enable OAuth 2.0 Device Authorization Grant

  2. 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:

  1. Client → Capability Config → enable OIDC CIBA Grant

  2. Realm Settings → Authentication → tab CIBA Policy:

SettingDescriptionDefault value
Backchannel Token Delivery Modepoll, ping, or pushpoll
Expires InAuthentication request expiration time120 seconds
IntervalInterval between polling requests5 seconds
Authentication Requested User HintUser hint type: login_hint, login_hint_token, id_token_hintlogin_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:

SettingDescriptionRecommended value
Access Token LifespanOverride realm-level token lifespan for this clientLeave blank = use realm level
Client Session IdleOverride client session idle timeoutLeave blank = use realm level
Client Session MaxOverride client session max lifespanLeave blank = use realm level
Client Offline Session IdleOverride offline session idle timeoutLeave blank = use realm level
Client Offline Session MaxOverride offline session max lifespanLeave blank = use realm level
PKCE Code Challenge MethodRequired PKCE methodS256
Pushed Authorization Request RequiredRequired PAR (RFC 9126)ON for high-security apps
ACR to LoA MappingMap ACR values ​​→ Level of AssuranceConfiguration 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):

  1. Open client → tab Service account roles

  2. Click Assign role

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

  1. Create client react-spa-lab with Client authentication = OFF

  2. Configuration: Valid redirect URIs = http://localhost:3000/*, Web origins = http://localhost:3000

  3. Enable PKCE: Advanced → PKCE Code Challenge Method = S256

  4. Create React app, install keycloak-js, integrate login/logout

  5. Verify token trong browser DevTools → Application → Network tab

Lab 2: Creating Confidential Client for Spring Boot API

  1. Create client spring-api-lab with Client authentication = ON

  2. Enable Service accounts roles

  3. Assign role admin to service account

  4. Create Spring Boot project with spring-boot-starter-oauth2-resource-server

  5. Implement endpoint /api/me returns user information from JWT

  6. Test with curl send Bearer token

Lab 3: Client Credentials Flow

  1. Create client batch-worker only Client Credentials flow

  2. Get tokens via curl

  3. Call API endpoint with newly retrieved token

  4. Check token contents via jwt.io (for development only)

Lab 4: Device Authorization Flow

  1. Create public client cli-tool with Device Authorization Grant enabled

  2. Use curl to simulate device flow:

    • Request device code
    • Open verification URI on browser, enter user code
    • Poll cho token
  3. 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