From 79278ef5b2bf197604ebf7f87cdd50fa183e733d Mon Sep 17 00:00:00 2001 From: Masaki Yatsu Date: Fri, 19 Sep 2025 03:10:59 +0900 Subject: [PATCH] feat(keycloak): set PKCE method and fix creating audience mapper --- keycloak/justfile | 3 +- keycloak/scripts/add-audience-mapper.ts | 9 ++-- keycloak/scripts/create-client.ts | 16 ++++-- keycloak/scripts/get-client-details.ts | 71 +++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 8 deletions(-) create mode 100755 keycloak/scripts/get-client-details.ts diff --git a/keycloak/justfile b/keycloak/justfile index 234aeb1..e59d9b5 100644 --- a/keycloak/justfile +++ b/keycloak/justfile @@ -217,12 +217,11 @@ create-client realm client_id redirect_url client_secret='' session_idle='' sess dotenvx run -q -f ../.env.local -- tsx ./scripts/create-client.ts # Add audience mapper to existing client -add-audience-mapper realm client_id audience: +add-audience-mapper client_id audience: #!/bin/bash set -euo pipefail export KEYCLOAK_ADMIN_USER=$(just admin-username) export KEYCLOAK_ADMIN_PASSWORD=$(just admin-password) - export KEYCLOAK_REALM={{ realm }} export KEYCLOAK_CLIENT_ID={{ client_id }} export KEYCLOAK_AUDIENCE={{ audience }} dotenvx run -q -f ../.env.local -- tsx ./scripts/add-audience-mapper.ts diff --git a/keycloak/scripts/add-audience-mapper.ts b/keycloak/scripts/add-audience-mapper.ts index b118472..55081ac 100644 --- a/keycloak/scripts/add-audience-mapper.ts +++ b/keycloak/scripts/add-audience-mapper.ts @@ -17,6 +17,9 @@ const main = async () => { const clientId = process.env.KEYCLOAK_CLIENT_ID; invariant(clientId, "KEYCLOAK_CLIENT_ID environment variable is required"); + const audience = process.env.KEYCLOAK_AUDIENCE; + invariant(audience, "KEYCLOAK_AUDIENCE environment variable is required"); + const kcAdminClient = new KcAdminClient({ baseUrl: `https://${keycloakHost}`, realmName: "master", @@ -40,14 +43,14 @@ const main = async () => { const client = clients[0]; invariant(client.id, "Client ID is not set"); - const mapperName = `aud-mapper-${clientId}`; + const mapperName = `aud-mapper-${audience}`; const audienceMapper = { name: mapperName, protocol: "openid-connect", protocolMapper: "oidc-audience-mapper", config: { - "included.client.audience": clientId, - "id.token.claim": "true", + "included.client.audience": audience, + "id.token.claim": "false", "access.token.claim": "true", }, }; diff --git a/keycloak/scripts/create-client.ts b/keycloak/scripts/create-client.ts index de6b3f3..3820ca5 100644 --- a/keycloak/scripts/create-client.ts +++ b/keycloak/scripts/create-client.ts @@ -50,24 +50,34 @@ const main = async () => { return; } + const isPublicClient = !clientSecret || clientSecret === ''; const clientConfig: any = { clientId: clientId, secret: clientSecret, enabled: true, redirectUris: redirectUris, - publicClient: clientSecret && clientSecret !== '' ? false : true, + publicClient: isPublicClient, directAccessGrantsEnabled: directAccessGrants === 'true', }; + // Only set PKCE for public clients + if (isPublicClient) { + clientConfig.attributes = { + 'pkce.code.challenge.method': 'S256' + }; + console.log('Setting PKCE Code Challenge Method to S256 for public client'); + } else { + clientConfig.attributes = {}; + console.log('Creating confidential client without PKCE'); + } + // Add session timeout settings if provided if (sessionIdle && sessionIdle !== '') { - clientConfig.attributes = clientConfig.attributes || {}; clientConfig.attributes['client.session.idle.timeout'] = sessionIdle; console.log(`Setting Client Session Idle Timeout: ${sessionIdle}`); } if (sessionMax && sessionMax !== '') { - clientConfig.attributes = clientConfig.attributes || {}; clientConfig.attributes['client.session.max.lifespan'] = sessionMax; console.log(`Setting Client Session Max Lifespan: ${sessionMax}`); } diff --git a/keycloak/scripts/get-client-details.ts b/keycloak/scripts/get-client-details.ts new file mode 100755 index 0000000..945c3b8 --- /dev/null +++ b/keycloak/scripts/get-client-details.ts @@ -0,0 +1,71 @@ +#!/usr/bin/env tsx + +import KcAdminClient from "@keycloak/keycloak-admin-client"; +import invariant from "tiny-invariant"; + +async function main() { + const kcAdminClient = new KcAdminClient({ + baseUrl: `https://${process.env.KEYCLOAK_HOST}`, + realmName: "master", + }); + + await kcAdminClient.auth({ + username: process.env.KEYCLOAK_ADMIN_USER!, + password: process.env.KEYCLOAK_ADMIN_PASSWORD!, + grantType: "password", + clientId: "admin-cli", + }); + + const realm = process.env.KEYCLOAK_REALM!; + const clientId = process.env.KEYCLOAK_CLIENT_ID!; + + invariant(realm, "KEYCLOAK_REALM is required"); + invariant(clientId, "KEYCLOAK_CLIENT_ID is required"); + + try { + // Find the client + const clients = await kcAdminClient.clients.find({ realm, clientId }); + if (clients.length === 0) { + throw new Error(`Client '${clientId}' not found in realm '${realm}'`); + } + + const client = clients[0]; + + // Get full client details + const clientDetails = await kcAdminClient.clients.findOne({ + realm, + id: client.id!, + }); + + console.log("=== Client Configuration ==="); + console.log(`Client ID: ${clientDetails?.clientId}`); + console.log(`Access Type: ${clientDetails?.publicClient ? 'public' : 'confidential'}`); + console.log(`Client Authenticator: ${clientDetails?.clientAuthenticatorType}`); + console.log(`Standard Flow Enabled: ${clientDetails?.standardFlowEnabled}`); + console.log(`Direct Access Grants: ${clientDetails?.directAccessGrantsEnabled}`); + console.log(`Service Accounts Enabled: ${clientDetails?.serviceAccountsEnabled}`); + console.log(`Valid Redirect URIs: ${JSON.stringify(clientDetails?.redirectUris, null, 2)}`); + console.log(`Base URL: ${clientDetails?.baseUrl || 'Not set'}`); + console.log(`Root URL: ${clientDetails?.rootUrl || 'Not set'}`); + console.log(`Web Origins: ${JSON.stringify(clientDetails?.webOrigins, null, 2)}`); + + // Get client secret if confidential + if (!clientDetails?.publicClient) { + try { + const clientSecret = await kcAdminClient.clients.getClientSecret({ + realm, + id: client.id!, + }); + console.log(`Client Secret: ${clientSecret.value}`); + } catch (error) { + console.log(`Client Secret: Error retrieving - ${error}`); + } + } + + } catch (error) { + console.error(`Error retrieving client details: ${error}`); + process.exit(1); + } +} + +main().catch(console.error); \ No newline at end of file