From 3873df7b693768ac8afa5757c36a288b743e739d Mon Sep 17 00:00:00 2001 From: Masaki Yatsu Date: Fri, 19 Sep 2025 11:10:26 +0900 Subject: [PATCH] feat(keycloak): setting PKCE method for clients --- keycloak/justfile | 12 +++++- keycloak/scripts/create-client.ts | 20 ++++++---- keycloak/scripts/list-clients.ts | 64 +++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 keycloak/scripts/list-clients.ts diff --git a/keycloak/justfile b/keycloak/justfile index e59d9b5..bbdb783 100644 --- a/keycloak/justfile +++ b/keycloak/justfile @@ -191,6 +191,15 @@ delete-realm realm: export KEYCLOAK_REALM_TO_DELETE={{ realm }} dotenvx run -q -f ../.env.local -- tsx ./scripts/delete-realm.ts +# List all Keycloak clients in realm +list-clients realm: + #!/bin/bash + set -euo pipefail + export KEYCLOAK_ADMIN_USER=$(just admin-username) + export KEYCLOAK_ADMIN_PASSWORD=$(just admin-password) + export KEYCLOAK_REALM={{ realm }} + dotenvx run -q -f ../.env.local -- tsx ./scripts/list-clients.ts + # Check if Keycloak client exists client-exists realm client_id: #!/bin/bash @@ -202,7 +211,7 @@ client-exists realm client_id: dotenvx run -q -f ../.env.local -- tsx ./scripts/client-exists.ts # Create Keycloak client -create-client realm client_id redirect_url client_secret='' session_idle='' session_max='' direct_access_grants='false': +create-client realm client_id redirect_url client_secret='' session_idle='' session_max='' direct_access_grants='false' pkce_method='': #!/bin/bash set -euo pipefail export KEYCLOAK_ADMIN_USER=$(just admin-username) @@ -214,6 +223,7 @@ create-client realm client_id redirect_url client_secret='' session_idle='' sess export KEYCLOAK_CLIENT_SESSION_IDLE={{ session_idle }} export KEYCLOAK_CLIENT_SESSION_MAX={{ session_max }} export KEYCLOAK_CLIENT_DIRECT_ACCESS_GRANTS={{ direct_access_grants }} + export KEYCLOAK_CLIENT_PKCE_METHOD={{ pkce_method }} dotenvx run -q -f ../.env.local -- tsx ./scripts/create-client.ts # Add audience mapper to existing client diff --git a/keycloak/scripts/create-client.ts b/keycloak/scripts/create-client.ts index 3820ca5..ed646ee 100644 --- a/keycloak/scripts/create-client.ts +++ b/keycloak/scripts/create-client.ts @@ -27,6 +27,7 @@ const main = async () => { const sessionIdle = process.env.KEYCLOAK_CLIENT_SESSION_IDLE; const sessionMax = process.env.KEYCLOAK_CLIENT_SESSION_MAX; const directAccessGrants = process.env.KEYCLOAK_CLIENT_DIRECT_ACCESS_GRANTS; + const pkceMethod = process.env.KEYCLOAK_CLIENT_PKCE_METHOD; const kcAdminClient = new KcAdminClient({ baseUrl: `https://${keycloakHost}`, @@ -60,15 +61,18 @@ const main = async () => { 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'); + // Configure PKCE based on environment variable + // KEYCLOAK_CLIENT_PKCE_METHOD can be: 'S256', 'plain', or unset/empty (no PKCE) + clientConfig.attributes = {}; + + if (pkceMethod && (pkceMethod === 'S256' || pkceMethod === 'plain')) { + clientConfig.attributes['pkce.code.challenge.method'] = pkceMethod; + console.log(`Setting PKCE Code Challenge Method to ${pkceMethod}`); + } else if (pkceMethod && pkceMethod !== '') { + console.warn(`Invalid PKCE method '${pkceMethod}'. Valid options: S256, plain, or empty for no PKCE`); + console.log('Creating client without PKCE'); } else { - clientConfig.attributes = {}; - console.log('Creating confidential client without PKCE'); + console.log('Creating client without PKCE'); } // Add session timeout settings if provided diff --git a/keycloak/scripts/list-clients.ts b/keycloak/scripts/list-clients.ts new file mode 100644 index 0000000..0838e4a --- /dev/null +++ b/keycloak/scripts/list-clients.ts @@ -0,0 +1,64 @@ +import KcAdminClient from "@keycloak/keycloak-admin-client"; +import invariant from "tiny-invariant"; + +const main = async () => { + const keycloakHost = process.env.KEYCLOAK_HOST; + invariant(keycloakHost, "KEYCLOAK_HOST environment variable is required."); + + const adminUsername = process.env.KEYCLOAK_ADMIN_USER; + invariant(adminUsername, "KEYCLOAK_ADMIN_USER environment variable is required."); + + const adminPassword = process.env.KEYCLOAK_ADMIN_PASSWORD; + invariant(adminPassword, "KEYCLOAK_ADMIN_PASSWORD environment variable is required"); + + const realmName = process.env.KEYCLOAK_REALM; + invariant(realmName, "KEYCLOAK_REALM environment variable is required"); + + const kcAdminClient = new KcAdminClient({ + baseUrl: `https://${keycloakHost}`, + realmName: "master", + }); + + try { + await kcAdminClient.auth({ + username: adminUsername, + password: adminPassword, + grantType: "password", + clientId: "admin-cli", + }); + + kcAdminClient.setConfig({ realmName }); + + const clients = await kcAdminClient.clients.find(); + + console.log(`Found ${clients.length} clients in realm '${realmName}':`); + console.log(""); + + clients.forEach((client, index) => { + const clientType = client.publicClient ? "Public" : "Confidential"; + const status = client.enabled ? "Enabled" : "Disabled"; + const protocol = client.protocol || "unknown"; + + console.log(`${(index + 1).toString().padStart(2, ' ')}. ${client.clientId}`); + console.log(` ID: ${client.id}`); + console.log(` Type: ${clientType}`); + console.log(` Protocol: ${protocol}`); + console.log(` Status: ${status}`); + + if (client.redirectUris && client.redirectUris.length > 0) { + console.log(` Redirect URIs: ${client.redirectUris.join(', ')}`); + } + + if (client.webOrigins && client.webOrigins.length > 0) { + console.log(` Web Origins: ${client.webOrigins.join(', ')}`); + } + + console.log(""); + }); + } catch (error) { + console.error("An error occurred:", error); + process.exit(1); + } +}; + +main(); \ No newline at end of file