diff --git a/keycloak/justfile b/keycloak/justfile index 8f537e4..5ad6ab5 100644 --- a/keycloak/justfile +++ b/keycloak/justfile @@ -149,7 +149,7 @@ uninstall-operator: kubectl delete -f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/${KEYCLOAK_OPERATOR_VERSION}/kubernetes/keycloaks.k8s.keycloak.org-v1.yml --ignore-not-found # Create Keycloak realm -create-realm create-client-for-k8s='true' access_token_lifespan='3600' refresh_token_lifespan='14400' sso_session_idle_timeout='7200': +create-realm create-client-for-k8s='true' access_token_lifespan='43200' refresh_token_lifespan='86400' sso_session_idle_timeout='7200': #!/bin/bash set -euo pipefail export KEYCLOAK_ADMIN_USER=$(just admin-username) @@ -268,7 +268,6 @@ delete-client realm client_id: export KEYCLOAK_CLIENT_ID={{ client_id }} dotenvx run -q -f ../.env.local -- tsx ./scripts/delete-client.ts - # Add attribute mapper for Keycloak client add-attribute-mapper client_id attribute_name display_name='' claim_name='' options='' default_value='' mapper_name='' view_perms='admin,user' edit_perms='admin': #!/bin/bash @@ -528,16 +527,32 @@ default-admin-password: show-realm-token-settings 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/show-realm-token-settings.ts # Update realm token settings (access token lifespan, refresh token lifespan, etc.) -update-realm-token-settings realm access_token_lifespan='3600' refresh_token_lifespan='1800': +update-realm-token-settings realm access_token_lifespan='' refresh_token_lifespan='': #!/bin/bash set -euo pipefail - export KEYCLOAK_REALM={{ realm }} export ACCESS_TOKEN_LIFESPAN={{ access_token_lifespan }} export REFRESH_TOKEN_LIFESPAN={{ refresh_token_lifespan }} + while [ -z "${ACCESS_TOKEN_LIFESPAN}" ]; do + ACCESS_TOKEN_LIFESPAN=$( + gum input --prompt="Access token lifespan (in seconds): " --width=100 \ + --placeholder="e.g., 43200 for 12 hours" --value="43200" + ) + done + while [ -z "${REFRESH_TOKEN_LIFESPAN}" ]; do + REFRESH_TOKEN_LIFESPAN=$( + gum input --prompt="Refresh token lifespan (in seconds): " --width=100 \ + --placeholder="e.g., 86400 for 24 hours" --value="86400" + ) + done + 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/update-realm-token-settings.ts # Create Keycloak client role @@ -627,3 +642,25 @@ remove-user-from-client-role realm username client_id role_name: export KEYCLOAK_CLIENT_ID={{ client_id }} export KEYCLOAK_ROLE_NAME={{ role_name }} dotenvx run -q -f ../.env.local -- tsx ./scripts/remove-user-from-client-role.ts + +# Create client scope +create-client-scope realm scope_name description='': + #!/bin/bash + set -euo pipefail + export KEYCLOAK_ADMIN_USER=$(just admin-username) + export KEYCLOAK_ADMIN_PASSWORD=$(just admin-password) + export KEYCLOAK_REALM={{ realm }} + export SCOPE_NAME={{ scope_name }} + export DESCRIPTION="{{ description }}" + dotenvx run -q -f ../.env.local -- tsx ./scripts/create-client-scope.ts + +# Add scope to client +add-scope-to-client realm client_id scope_name: + #!/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 SCOPE_NAME={{ scope_name }} + dotenvx run -q -f ../.env.local -- tsx ./scripts/add-scope-to-client.ts diff --git a/keycloak/scripts/add-scope-to-client.ts b/keycloak/scripts/add-scope-to-client.ts new file mode 100644 index 0000000..8eb2a4e --- /dev/null +++ b/keycloak/scripts/add-scope-to-client.ts @@ -0,0 +1,69 @@ +#!/usr/bin/env tsx + +import KcAdminClient from '@keycloak/keycloak-admin-client'; +import invariant from 'tiny-invariant'; + +const main = async () => { + const KEYCLOAK_HOST = process.env.KEYCLOAK_HOST; + const KEYCLOAK_ADMIN_USER = process.env.KEYCLOAK_ADMIN_USER; + const KEYCLOAK_ADMIN_PASSWORD = process.env.KEYCLOAK_ADMIN_PASSWORD; + const realm = process.env.KEYCLOAK_REALM; + const clientId = process.env.KEYCLOAK_CLIENT_ID; + const scopeName = process.env.SCOPE_NAME; + + invariant(KEYCLOAK_HOST, 'KEYCLOAK_HOST environment variable is required'); + invariant(KEYCLOAK_ADMIN_USER, 'KEYCLOAK_ADMIN_USER environment variable is required'); + invariant(KEYCLOAK_ADMIN_PASSWORD, 'KEYCLOAK_ADMIN_PASSWORD environment variable is required'); + invariant(realm, 'KEYCLOAK_REALM environment variable is required'); + invariant(clientId, 'KEYCLOAK_CLIENT_ID environment variable is required'); + invariant(scopeName, 'SCOPE_NAME environment variable is required'); + + const kcAdminClient = new KcAdminClient({ + baseUrl: `https://${KEYCLOAK_HOST}`, + realmName: 'master', + }); + + try { + await kcAdminClient.auth({ + username: KEYCLOAK_ADMIN_USER, + password: KEYCLOAK_ADMIN_PASSWORD, + grantType: 'password', + clientId: 'admin-cli', + }); + + console.log('Authentication successful.'); + + // Set target realm + kcAdminClient.setConfig({ + realmName: realm, + }); + + // Find the client + const clients = await kcAdminClient.clients.find({ clientId }); + if (clients.length === 0) { + throw new Error(`Client '${clientId}' not found`); + } + const client = clients[0]; + + // Find the client scope + const clientScopes = await kcAdminClient.clientScopes.find(); + const scope = clientScopes.find(s => s.name === scopeName); + if (!scope) { + throw new Error(`Client scope '${scopeName}' not found`); + } + + // Add scope to client as default scope + await kcAdminClient.clients.addDefaultClientScope({ + id: client.id!, + clientScopeId: scope.id!, + }); + + console.log(`Client scope '${scopeName}' added to client '${clientId}' as default scope.`); + + } catch (error) { + console.error('Error adding scope to client:', error); + process.exit(1); + } +}; + +main(); \ No newline at end of file diff --git a/keycloak/scripts/create-client-scope.ts b/keycloak/scripts/create-client-scope.ts new file mode 100644 index 0000000..8613de0 --- /dev/null +++ b/keycloak/scripts/create-client-scope.ts @@ -0,0 +1,65 @@ +#!/usr/bin/env tsx + +import KcAdminClient from '@keycloak/keycloak-admin-client'; +import invariant from 'tiny-invariant'; + +const main = async () => { + const KEYCLOAK_HOST = process.env.KEYCLOAK_HOST; + const KEYCLOAK_ADMIN_USER = process.env.KEYCLOAK_ADMIN_USER; + const KEYCLOAK_ADMIN_PASSWORD = process.env.KEYCLOAK_ADMIN_PASSWORD; + const realm = process.env.KEYCLOAK_REALM; + const scopeName = process.env.SCOPE_NAME; + const description = process.env.DESCRIPTION || ''; + + invariant(KEYCLOAK_HOST, 'KEYCLOAK_HOST environment variable is required'); + invariant(KEYCLOAK_ADMIN_USER, 'KEYCLOAK_ADMIN_USER environment variable is required'); + invariant(KEYCLOAK_ADMIN_PASSWORD, 'KEYCLOAK_ADMIN_PASSWORD environment variable is required'); + invariant(realm, 'KEYCLOAK_REALM environment variable is required'); + invariant(scopeName, 'SCOPE_NAME environment variable is required'); + + const kcAdminClient = new KcAdminClient({ + baseUrl: `https://${KEYCLOAK_HOST}`, + realmName: 'master', + }); + + try { + await kcAdminClient.auth({ + username: KEYCLOAK_ADMIN_USER, + password: KEYCLOAK_ADMIN_PASSWORD, + grantType: 'password', + clientId: 'admin-cli', + }); + + console.log('Authentication successful.'); + + // Set target realm + kcAdminClient.setConfig({ + realmName: realm, + }); + + // Check if scope already exists + const existingScopes = await kcAdminClient.clientScopes.find(); + const existingScope = existingScopes.find(scope => scope.name === scopeName); + + if (existingScope) { + console.log(`Client scope '${scopeName}' already exists.`); + return; + } + + // Create client scope + const result = await kcAdminClient.clientScopes.create({ + name: scopeName, + description: description || `${scopeName} scope`, + protocol: 'openid-connect', + includeInTokenScope: true, + }); + + console.log(`Client scope '${scopeName}' created successfully with ID: ${result.id}`); + + } catch (error) { + console.error('Error creating client scope:', error); + process.exit(1); + } +}; + +main(); \ No newline at end of file