diff --git a/keycloak/justfile b/keycloak/justfile index 28abda9..32c86eb 100644 --- a/keycloak/justfile +++ b/keycloak/justfile @@ -258,6 +258,17 @@ add-audience-mapper client_id audience: export KEYCLOAK_AUDIENCE={{ audience }} dotenvx run -q -f ../.env.local -- tsx ./scripts/add-audience-mapper.ts +# Add audience mapper to client scope +add-audience-mapper-to-scope realm scope_name audience: + #!/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 KEYCLOAK_AUDIENCE={{ audience }} + dotenvx run -q -f ../.env.local -- tsx ./scripts/add-audience-mapper-to-scope.ts + # Delete Keycloak client delete-client realm client_id: #!/bin/bash diff --git a/keycloak/scripts/add-audience-mapper-to-scope.ts b/keycloak/scripts/add-audience-mapper-to-scope.ts new file mode 100755 index 0000000..e60f382 --- /dev/null +++ b/keycloak/scripts/add-audience-mapper-to-scope.ts @@ -0,0 +1,80 @@ +#!/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 audience = process.env.KEYCLOAK_AUDIENCE; + + 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'); + invariant(audience, 'KEYCLOAK_AUDIENCE 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 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`); + } + + invariant(scope.id, 'Client scope ID is not set'); + + // Check if mapper already exists + const mapperName = `aud-mapper-${audience}`; + const existingMappers = await kcAdminClient.clientScopes.listProtocolMappers({ id: scope.id }); + + if (existingMappers.some((mapper) => mapper.name === mapperName)) { + console.warn(`Audience mapper '${mapperName}' already exists in scope '${scopeName}'.`); + return; + } + + // Create audience mapper + const audienceMapper = { + name: mapperName, + protocol: 'openid-connect', + protocolMapper: 'oidc-audience-mapper', + config: { + 'included.client.audience': audience, + 'id.token.claim': 'false', + 'access.token.claim': 'true', + }, + }; + + await kcAdminClient.clientScopes.addProtocolMapper({ id: scope.id }, audienceMapper); + console.log(`Audience mapper '${mapperName}' added to client scope '${scopeName}'.`); + + } catch (error) { + console.error('Error adding audience mapper to scope:', error); + process.exit(1); + } +}; + +main();