feat(keycloak): keycloak::create-client now receives named arguments

This commit is contained in:
Masaki Yatsu
2025-09-19 12:11:48 +09:00
parent 8499e52892
commit f4a73377c3
12 changed files with 198 additions and 34 deletions

View File

@@ -165,7 +165,7 @@ create-realm create-client-for-k8s='true' access_token_lifespan='3600' refresh_t
# Create Keycloak client for Kubernetes OIDC authentication
create-k8s-client:
@just create-client ${KEYCLOAK_REALM} ${K8S_OIDC_CLIENT_ID} "http://localhost:8000,http://localhost:18000"
@just create-client realm=${KEYCLOAK_REALM} client_id=${K8S_OIDC_CLIENT_ID} redirect_url="http://localhost:8000,http://localhost:18000"
# Delete Keycloak realm
delete-realm realm:
@@ -200,6 +200,16 @@ list-clients realm:
export KEYCLOAK_REALM={{ realm }}
dotenvx run -q -f ../.env.local -- tsx ./scripts/list-clients.ts
# Get detailed Keycloak client configuration
get-client realm client_id:
#!/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 }}
dotenvx run -q -f ../.env.local -- tsx ./scripts/get-client.ts
# Check if Keycloak client exists
client-exists realm client_id:
#!/bin/bash
@@ -211,19 +221,30 @@ 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' pkce_method='':
[positional-arguments]
create-client *args:
#!/bin/bash
# realm: Keycloak realm name
# client_id: Keycloak client ID (required)
# redirect_url: Redirect URL for the client (required)
# client_secret: Keycloak client secret (empty for public clients)
# client_session_idle: Session idle timeout in seconds
# client_session_max: Session max lifespan in seconds
# client_direct_access_grants: Whether to enable direct access grants (true/false)
# client_pkce_method: PKCE method ('S256', 'plain' or empty)
# post_logout_redirect_uris: Post logout redirect URIs (comma-separated input, converted to Keycloak ## format)
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_CLIENT_SECRET={{ client_secret }}
export KEYCLOAK_REDIRECT_URL={{ redirect_url }}
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 }}
while (( "$#" )); do
key="KEYCLOAK_$(echo ${1%%=*} | awk '{print toupper($0)}')"
value=${1#*=}
export ${key}="${value}"
if [ "${KEYCLOAK_DEBUG:-}" = "true" ]; then
env | grep "${key}"
fi
shift
done
dotenvx run -q -f ../.env.local -- tsx ./scripts/create-client.ts
# Add audience mapper to existing client

View File

@@ -28,6 +28,7 @@ const main = async () => {
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 postLogoutRedirectUris = process.env.KEYCLOAK_POST_LOGOUT_REDIRECT_URIS;
const kcAdminClient = new KcAdminClient({
baseUrl: `https://${keycloakHost}`,
@@ -86,6 +87,15 @@ const main = async () => {
console.log(`Setting Client Session Max Lifespan: ${sessionMax}`);
}
// Add post logout redirect URIs if provided
if (postLogoutRedirectUris && postLogoutRedirectUris !== '') {
// Split comma-separated URIs and set as array in attributes using ## separator (Keycloak format)
const postLogoutUris = postLogoutRedirectUris.split(',').map(uri => uri.trim());
clientConfig.attributes = clientConfig.attributes || {};
clientConfig.attributes['post.logout.redirect.uris'] = postLogoutUris.join('##');
console.log(`Setting Post Logout Redirect URIs: ${postLogoutUris.join(', ')}`);
}
if (directAccessGrants === 'true') {
console.log('Enabling Direct Access Grants (Resource Owner Password Credentials)');
}

View File

@@ -0,0 +1,132 @@
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 clientId = process.env.KEYCLOAK_CLIENT_ID;
invariant(clientId, "KEYCLOAK_CLIENT_ID 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({ clientId });
if (clients.length === 0) {
console.log(`Client '${clientId}' not found in realm '${realmName}'`);
process.exit(1);
}
const client = clients[0];
console.log(`=== Client Details: ${clientId} ===`);
console.log(`ID: ${client.id}`);
console.log(`Client ID: ${client.clientId}`);
console.log(`Name: ${client.name || 'N/A'}`);
console.log(`Description: ${client.description || 'N/A'}`);
console.log(`Enabled: ${client.enabled}`);
console.log(`Protocol: ${client.protocol}`);
console.log(`Public Client: ${client.publicClient}`);
console.log(`Bearer Only: ${client.bearerOnly}`);
console.log(`Standard Flow Enabled: ${client.standardFlowEnabled}`);
console.log(`Direct Access Grants Enabled: ${client.directAccessGrantsEnabled}`);
console.log(`Service Accounts Enabled: ${client.serviceAccountsEnabled}`);
console.log(`Front Channel Logout: ${client.frontchannelLogout}`);
console.log(`Always Display in Console: ${client.alwaysDisplayInConsole}`);
console.log("");
if (client.rootUrl) {
console.log(`Root URL: ${client.rootUrl}`);
}
if (client.baseUrl) {
console.log(`Base URL: ${client.baseUrl}`);
}
if (client.adminUrl) {
console.log(`Admin URL: ${client.adminUrl}`);
}
console.log("");
if (client.redirectUris && client.redirectUris.length > 0) {
console.log("Redirect URIs:");
client.redirectUris.forEach((uri, index) => {
console.log(` ${index + 1}. ${uri}`);
});
console.log("");
}
if (client.webOrigins && client.webOrigins.length > 0) {
console.log("Web Origins:");
client.webOrigins.forEach((origin, index) => {
console.log(` ${index + 1}. ${origin}`);
});
console.log("");
}
if (client.attributes && Object.keys(client.attributes).length > 0) {
console.log("Attributes:");
Object.entries(client.attributes).forEach(([key, value]) => {
console.log(` ${key}: ${value}`);
});
console.log("");
}
if (client.defaultClientScopes && client.defaultClientScopes.length > 0) {
console.log("Default Client Scopes:");
client.defaultClientScopes.forEach((scope, index) => {
console.log(` ${index + 1}. ${scope}`);
});
console.log("");
}
if (client.optionalClientScopes && client.optionalClientScopes.length > 0) {
console.log("Optional Client Scopes:");
client.optionalClientScopes.forEach((scope, index) => {
console.log(` ${index + 1}. ${scope}`);
});
console.log("");
}
// Get protocol mappers
const protocolMappers = await kcAdminClient.clients.listProtocolMappers({ id: client.id! });
if (protocolMappers.length > 0) {
console.log("Protocol Mappers:");
protocolMappers.forEach((mapper, index) => {
console.log(` ${index + 1}. ${mapper.name} (${mapper.protocolMapper})`);
if (mapper.config) {
Object.entries(mapper.config).forEach(([key, value]) => {
console.log(` ${key}: ${value}`);
});
}
});
console.log("");
}
} catch (error) {
console.error("An error occurred:", error);
process.exit(1);
}
};
main();