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

@@ -154,10 +154,10 @@ create-oauth-client:
echo "Creating new client..." echo "Creating new client..."
CLIENT_SECRET=$(just utils::random-password) CLIENT_SECRET=$(just utils::random-password)
just keycloak::create-client \ just keycloak::create-client \
${KEYCLOAK_REALM} \ realm=${KEYCLOAK_REALM} \
airflow \ client_id=airflow \
"https://${AIRFLOW_HOST}/auth/oauth-authorized/keycloak" \ redirect_url="https://${AIRFLOW_HOST}/auth/oauth-authorized/keycloak" \
"$CLIENT_SECRET" client_secret="$CLIENT_SECRET"
fi fi
if helm status external-secrets -n ${EXTERNAL_SECRETS_NAMESPACE} &>/dev/null; then if helm status external-secrets -n ${EXTERNAL_SECRETS_NAMESPACE} &>/dev/null; then

View File

@@ -159,8 +159,8 @@ delete-credentials:
# Create Keycloak client for Cube # Create Keycloak client for Cube
create-keycloak-client: create-keycloak-client:
@just keycloak::create-client ${KEYCLOAK_REALM} ${CUBE_OIDC_CLIENT_ID} \ @just keycloak::create-client realm=${KEYCLOAK_REALM} client_id=${CUBE_OIDC_CLIENT_ID} \
"http://localhost:${CUBE_OIDC_CALLBACK_PORT}/callback" redirect_url="http://localhost:${CUBE_OIDC_CALLBACK_PORT}/callback"
# Get JWT token using oauth2c # Get JWT token using oauth2c
get-token: get-token:

View File

@@ -115,10 +115,10 @@ create-oauth-client:
# Create confidential client for oauth2-proxy # Create confidential client for oauth2-proxy
CLIENT_SECRET=$(just utils::random-password) CLIENT_SECRET=$(just utils::random-password)
just keycloak::create-client \ just keycloak::create-client \
${KEYCLOAK_REALM} \ realm=${KEYCLOAK_REALM} \
dagster \ client_id=dagster \
"https://${DAGSTER_HOST}/oauth2/callback" \ redirect_url="https://${DAGSTER_HOST}/oauth2/callback" \
"$CLIENT_SECRET" client_secret="$CLIENT_SECRET"
if helm status external-secrets -n ${EXTERNAL_SECRETS_NAMESPACE} &>/dev/null; then if helm status external-secrets -n ${EXTERNAL_SECRETS_NAMESPACE} &>/dev/null; then
echo "External Secrets available. Storing credentials in Vault and recreating ExternalSecret..." echo "External Secrets available. Storing credentials in Vault and recreating ExternalSecret..."

View File

@@ -109,10 +109,10 @@ create-oauth-client:
CLIENT_SECRET=$(just utils::random-password) CLIENT_SECRET=$(just utils::random-password)
just keycloak::create-client \ just keycloak::create-client \
${KEYCLOAK_REALM} \ realm=${KEYCLOAK_REALM} \
datahub \ client_id=datahub \
"https://${DATAHUB_HOST}/callback/oidc" \ redirect_url="https://${DATAHUB_HOST}/callback/oidc" \
"$CLIENT_SECRET" client_secret="$CLIENT_SECRET"
if helm status external-secrets -n ${EXTERNAL_SECRETS_NAMESPACE} &>/dev/null; then if helm status external-secrets -n ${EXTERNAL_SECRETS_NAMESPACE} &>/dev/null; then
echo "External Secrets available. Storing credentials in Vault and recreating ExternalSecret..." echo "External Secrets available. Storing credentials in Vault and recreating ExternalSecret..."

View File

@@ -80,9 +80,9 @@ install root_token='':
just create-namespace just create-namespace
# just k8s::copy-regcred ${JUPYTERHUB_NAMESPACE} # just k8s::copy-regcred ${JUPYTERHUB_NAMESPACE}
just keycloak::create-client ${KEYCLOAK_REALM} ${JUPYTERHUB_OIDC_CLIENT_ID} \ just keycloak::create-client realm=${KEYCLOAK_REALM} client_id=${JUPYTERHUB_OIDC_CLIENT_ID} \
"https://${JUPYTERHUB_HOST}/hub/oauth_callback" \ redirect_url="https://${JUPYTERHUB_HOST}/hub/oauth_callback" \
"" "${JUPYTERHUB_OIDC_CLIENT_SESSION_IDLE}" "${JUPYTERHUB_OIDC_CLIENT_SESSION_MAX}" client_secret="" client_session_idle="${JUPYTERHUB_OIDC_CLIENT_SESSION_IDLE}" client_session_max="${JUPYTERHUB_OIDC_CLIENT_SESSION_MAX}"
just add-helm-repo just add-helm-repo
export JUPYTERHUB_OIDC_CLIENT_ID=${JUPYTERHUB_OIDC_CLIENT_ID} export JUPYTERHUB_OIDC_CLIENT_ID=${JUPYTERHUB_OIDC_CLIENT_ID}
export KEYCLOAK_REALM=${KEYCLOAK_REALM} export KEYCLOAK_REALM=${KEYCLOAK_REALM}

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 Keycloak client for Kubernetes OIDC authentication
create-k8s-client: 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 Keycloak realm
delete-realm realm: delete-realm realm:
@@ -200,6 +200,16 @@ list-clients realm:
export KEYCLOAK_REALM={{ realm }} export KEYCLOAK_REALM={{ realm }}
dotenvx run -q -f ../.env.local -- tsx ./scripts/list-clients.ts 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 # Check if Keycloak client exists
client-exists realm client_id: client-exists realm client_id:
#!/bin/bash #!/bin/bash
@@ -211,19 +221,30 @@ client-exists realm client_id:
dotenvx run -q -f ../.env.local -- tsx ./scripts/client-exists.ts dotenvx run -q -f ../.env.local -- tsx ./scripts/client-exists.ts
# Create Keycloak client # 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 #!/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 set -euo pipefail
export KEYCLOAK_ADMIN_USER=$(just admin-username) export KEYCLOAK_ADMIN_USER=$(just admin-username)
export KEYCLOAK_ADMIN_PASSWORD=$(just admin-password) export KEYCLOAK_ADMIN_PASSWORD=$(just admin-password)
export KEYCLOAK_REALM={{ realm }} while (( "$#" )); do
export KEYCLOAK_CLIENT_ID={{ client_id }} key="KEYCLOAK_$(echo ${1%%=*} | awk '{print toupper($0)}')"
export KEYCLOAK_CLIENT_SECRET={{ client_secret }} value=${1#*=}
export KEYCLOAK_REDIRECT_URL={{ redirect_url }} export ${key}="${value}"
export KEYCLOAK_CLIENT_SESSION_IDLE={{ session_idle }} if [ "${KEYCLOAK_DEBUG:-}" = "true" ]; then
export KEYCLOAK_CLIENT_SESSION_MAX={{ session_max }} env | grep "${key}"
export KEYCLOAK_CLIENT_DIRECT_ACCESS_GRANTS={{ direct_access_grants }} fi
export KEYCLOAK_CLIENT_PKCE_METHOD={{ pkce_method }} shift
done
dotenvx run -q -f ../.env.local -- tsx ./scripts/create-client.ts dotenvx run -q -f ../.env.local -- tsx ./scripts/create-client.ts
# Add audience mapper to existing client # Add audience mapper to existing client

View File

@@ -28,6 +28,7 @@ const main = async () => {
const sessionMax = process.env.KEYCLOAK_CLIENT_SESSION_MAX; const sessionMax = process.env.KEYCLOAK_CLIENT_SESSION_MAX;
const directAccessGrants = process.env.KEYCLOAK_CLIENT_DIRECT_ACCESS_GRANTS; const directAccessGrants = process.env.KEYCLOAK_CLIENT_DIRECT_ACCESS_GRANTS;
const pkceMethod = process.env.KEYCLOAK_CLIENT_PKCE_METHOD; const pkceMethod = process.env.KEYCLOAK_CLIENT_PKCE_METHOD;
const postLogoutRedirectUris = process.env.KEYCLOAK_POST_LOGOUT_REDIRECT_URIS;
const kcAdminClient = new KcAdminClient({ const kcAdminClient = new KcAdminClient({
baseUrl: `https://${keycloakHost}`, baseUrl: `https://${keycloakHost}`,
@@ -86,6 +87,15 @@ const main = async () => {
console.log(`Setting Client Session Max Lifespan: ${sessionMax}`); 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') { if (directAccessGrants === 'true') {
console.log('Enabling Direct Access Grants (Resource Owner Password Credentials)'); 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();

View File

@@ -113,9 +113,10 @@ create-oidc-client:
echo "Creating new public client for PKCE flow..." echo "Creating new public client for PKCE flow..."
# Create public client (no client secret) for PKCE flow # Create public client (no client secret) for PKCE flow
just keycloak::create-client \ just keycloak::create-client \
${KEYCLOAK_REALM} \ realm=${KEYCLOAK_REALM} \
lakekeeper \ client_id=lakekeeper \
"https://${LAKEKEEPER_HOST}/ui/callback" redirect_url="https://${LAKEKEEPER_HOST}/ui/callback" \
post_logout_redirect_uris="https://${LAKEKEEPER_HOST}/ui/logout,https://${LAKEKEEPER_HOST}/ui/,https://${LAKEKEEPER_HOST}/"
fi fi
# Add audience mapper to include 'lakekeeper' in JWT audience # Add audience mapper to include 'lakekeeper' in JWT audience

View File

@@ -96,8 +96,8 @@ install:
--placeholder="e.g., minio-console.example.com" --placeholder="e.g., minio-console.example.com"
) )
fi fi
just keycloak::create-client ${KEYCLOAK_REALM} ${MINIO_OIDC_CLIENT_ID} \ just keycloak::create-client realm=${KEYCLOAK_REALM} client_id=${MINIO_OIDC_CLIENT_ID} \
"https://${MINIO_HOST}/oauth_callback,https://${MINIO_CONSOLE_HOST}/oauth_callback" redirect_url="https://${MINIO_HOST}/oauth_callback,https://${MINIO_CONSOLE_HOST}/oauth_callback"
just add-keycloak-minio-policy just add-keycloak-minio-policy
just create-namespace just create-namespace
just create-root-credentials just create-root-credentials

View File

@@ -25,7 +25,7 @@ setup-for-app app_name app_host app_namespace="default" upstream_service="":
# Generate client secret for confidential client # Generate client secret for confidential client
client_secret=$(just utils::random-password 32) client_secret=$(just utils::random-password 32)
if ! just keycloak::create-client "${KEYCLOAK_REALM}" "${client_id}" "${redirect_url}" "${client_secret}"; then if ! just keycloak::create-client realm="${KEYCLOAK_REALM}" client_id="${client_id}" redirect_url="${redirect_url}" client_secret="${client_secret}"; then
echo "Failed to create Keycloak client" echo "Failed to create Keycloak client"
exit 1 exit 1
fi fi

View File

@@ -242,7 +242,7 @@ setup-oidc-auth:
just keycloak::delete-client "${KEYCLOAK_REALM}" "vault" || true just keycloak::delete-client "${KEYCLOAK_REALM}" "vault" || true
oidc_client_secret=$(just utils::random-password) oidc_client_secret=$(just utils::random-password)
redirect_urls="https://${VAULT_HOST}/ui/vault/auth/oidc/oidc/callback,http://localhost:8250/oidc/callback,http://localhost:8200/ui/vault/auth/oidc/oidc/callback" redirect_urls="https://${VAULT_HOST}/ui/vault/auth/oidc/oidc/callback,http://localhost:8250/oidc/callback,http://localhost:8200/ui/vault/auth/oidc/oidc/callback"
just keycloak::create-client "${KEYCLOAK_REALM}" "vault" "${redirect_urls}" "${oidc_client_secret}" just keycloak::create-client realm="${KEYCLOAK_REALM}" client_id="vault" redirect_url="${redirect_urls}" client_secret="${oidc_client_secret}"
echo "Using client secret: ${oidc_client_secret}" echo "Using client secret: ${oidc_client_secret}"
just keycloak::add-audience-mapper "vault" "vault" just keycloak::add-audience-mapper "vault" "vault"
just keycloak::add-groups-mapper "vault" just keycloak::add-groups-mapper "vault"