diff --git a/airflow/justfile b/airflow/justfile index 09b1871..612d6fa 100644 --- a/airflow/justfile +++ b/airflow/justfile @@ -154,10 +154,10 @@ create-oauth-client: echo "Creating new client..." CLIENT_SECRET=$(just utils::random-password) just keycloak::create-client \ - ${KEYCLOAK_REALM} \ - airflow \ - "https://${AIRFLOW_HOST}/auth/oauth-authorized/keycloak" \ - "$CLIENT_SECRET" + realm=${KEYCLOAK_REALM} \ + client_id=airflow \ + redirect_url="https://${AIRFLOW_HOST}/auth/oauth-authorized/keycloak" \ + client_secret="$CLIENT_SECRET" fi if helm status external-secrets -n ${EXTERNAL_SECRETS_NAMESPACE} &>/dev/null; then diff --git a/custom-example/cube/justfile b/custom-example/cube/justfile index 203c06f..a03587f 100644 --- a/custom-example/cube/justfile +++ b/custom-example/cube/justfile @@ -159,8 +159,8 @@ delete-credentials: # Create Keycloak client for Cube create-keycloak-client: - @just keycloak::create-client ${KEYCLOAK_REALM} ${CUBE_OIDC_CLIENT_ID} \ - "http://localhost:${CUBE_OIDC_CALLBACK_PORT}/callback" + @just keycloak::create-client realm=${KEYCLOAK_REALM} client_id=${CUBE_OIDC_CLIENT_ID} \ + redirect_url="http://localhost:${CUBE_OIDC_CALLBACK_PORT}/callback" # Get JWT token using oauth2c get-token: diff --git a/dagster/justfile b/dagster/justfile index 77e7072..fc3f0d5 100644 --- a/dagster/justfile +++ b/dagster/justfile @@ -115,10 +115,10 @@ create-oauth-client: # Create confidential client for oauth2-proxy CLIENT_SECRET=$(just utils::random-password) just keycloak::create-client \ - ${KEYCLOAK_REALM} \ - dagster \ - "https://${DAGSTER_HOST}/oauth2/callback" \ - "$CLIENT_SECRET" + realm=${KEYCLOAK_REALM} \ + client_id=dagster \ + redirect_url="https://${DAGSTER_HOST}/oauth2/callback" \ + client_secret="$CLIENT_SECRET" if helm status external-secrets -n ${EXTERNAL_SECRETS_NAMESPACE} &>/dev/null; then echo "External Secrets available. Storing credentials in Vault and recreating ExternalSecret..." diff --git a/datahub/justfile b/datahub/justfile index 01980c3..387a0de 100644 --- a/datahub/justfile +++ b/datahub/justfile @@ -109,10 +109,10 @@ create-oauth-client: CLIENT_SECRET=$(just utils::random-password) just keycloak::create-client \ - ${KEYCLOAK_REALM} \ - datahub \ - "https://${DATAHUB_HOST}/callback/oidc" \ - "$CLIENT_SECRET" + realm=${KEYCLOAK_REALM} \ + client_id=datahub \ + redirect_url="https://${DATAHUB_HOST}/callback/oidc" \ + client_secret="$CLIENT_SECRET" if helm status external-secrets -n ${EXTERNAL_SECRETS_NAMESPACE} &>/dev/null; then echo "External Secrets available. Storing credentials in Vault and recreating ExternalSecret..." diff --git a/jupyterhub/justfile b/jupyterhub/justfile index 1756744..a6a299f 100644 --- a/jupyterhub/justfile +++ b/jupyterhub/justfile @@ -80,9 +80,9 @@ install root_token='': just create-namespace # just k8s::copy-regcred ${JUPYTERHUB_NAMESPACE} - just keycloak::create-client ${KEYCLOAK_REALM} ${JUPYTERHUB_OIDC_CLIENT_ID} \ - "https://${JUPYTERHUB_HOST}/hub/oauth_callback" \ - "" "${JUPYTERHUB_OIDC_CLIENT_SESSION_IDLE}" "${JUPYTERHUB_OIDC_CLIENT_SESSION_MAX}" + just keycloak::create-client realm=${KEYCLOAK_REALM} client_id=${JUPYTERHUB_OIDC_CLIENT_ID} \ + redirect_url="https://${JUPYTERHUB_HOST}/hub/oauth_callback" \ + client_secret="" client_session_idle="${JUPYTERHUB_OIDC_CLIENT_SESSION_IDLE}" client_session_max="${JUPYTERHUB_OIDC_CLIENT_SESSION_MAX}" just add-helm-repo export JUPYTERHUB_OIDC_CLIENT_ID=${JUPYTERHUB_OIDC_CLIENT_ID} export KEYCLOAK_REALM=${KEYCLOAK_REALM} diff --git a/keycloak/justfile b/keycloak/justfile index bbdb783..5092955 100644 --- a/keycloak/justfile +++ b/keycloak/justfile @@ -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 diff --git a/keycloak/scripts/create-client.ts b/keycloak/scripts/create-client.ts index ed646ee..b356a8c 100644 --- a/keycloak/scripts/create-client.ts +++ b/keycloak/scripts/create-client.ts @@ -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)'); } diff --git a/keycloak/scripts/get-client.ts b/keycloak/scripts/get-client.ts new file mode 100644 index 0000000..499f13a --- /dev/null +++ b/keycloak/scripts/get-client.ts @@ -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(); \ No newline at end of file diff --git a/lakekeeper/justfile b/lakekeeper/justfile index a95c0e4..9a3842a 100644 --- a/lakekeeper/justfile +++ b/lakekeeper/justfile @@ -113,9 +113,10 @@ create-oidc-client: echo "Creating new public client for PKCE flow..." # Create public client (no client secret) for PKCE flow just keycloak::create-client \ - ${KEYCLOAK_REALM} \ - lakekeeper \ - "https://${LAKEKEEPER_HOST}/ui/callback" + realm=${KEYCLOAK_REALM} \ + client_id=lakekeeper \ + redirect_url="https://${LAKEKEEPER_HOST}/ui/callback" \ + post_logout_redirect_uris="https://${LAKEKEEPER_HOST}/ui/logout,https://${LAKEKEEPER_HOST}/ui/,https://${LAKEKEEPER_HOST}/" fi # Add audience mapper to include 'lakekeeper' in JWT audience diff --git a/minio/justfile b/minio/justfile index 676077d..5944f22 100644 --- a/minio/justfile +++ b/minio/justfile @@ -96,8 +96,8 @@ install: --placeholder="e.g., minio-console.example.com" ) fi - just keycloak::create-client ${KEYCLOAK_REALM} ${MINIO_OIDC_CLIENT_ID} \ - "https://${MINIO_HOST}/oauth_callback,https://${MINIO_CONSOLE_HOST}/oauth_callback" + just keycloak::create-client realm=${KEYCLOAK_REALM} client_id=${MINIO_OIDC_CLIENT_ID} \ + redirect_url="https://${MINIO_HOST}/oauth_callback,https://${MINIO_CONSOLE_HOST}/oauth_callback" just add-keycloak-minio-policy just create-namespace just create-root-credentials diff --git a/oauth2-proxy/justfile b/oauth2-proxy/justfile index 447f05e..96bd50e 100644 --- a/oauth2-proxy/justfile +++ b/oauth2-proxy/justfile @@ -25,7 +25,7 @@ setup-for-app app_name app_host app_namespace="default" upstream_service="": # Generate client secret for confidential client 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" exit 1 fi diff --git a/vault/justfile b/vault/justfile index 0279958..4347c45 100644 --- a/vault/justfile +++ b/vault/justfile @@ -242,7 +242,7 @@ setup-oidc-auth: just keycloak::delete-client "${KEYCLOAK_REALM}" "vault" || true 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" - 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}" just keycloak::add-audience-mapper "vault" "vault" just keycloak::add-groups-mapper "vault"