From 0142034535cb8cc3649837869a071bc1f03d41e5 Mon Sep 17 00:00:00 2001 From: Masaki Yatsu Date: Sun, 9 Nov 2025 15:46:59 +0900 Subject: [PATCH] feat(keycloak): add keycloak::add-groups-mapper-to-scope --- keycloak/justfile | 10 +++ .../scripts/add-groups-mapper-to-scope.ts | 82 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 keycloak/scripts/add-groups-mapper-to-scope.ts diff --git a/keycloak/justfile b/keycloak/justfile index 51b3a93..dccf5fe 100644 --- a/keycloak/justfile +++ b/keycloak/justfile @@ -269,6 +269,16 @@ add-audience-mapper-to-scope realm scope_name audience: export KEYCLOAK_AUDIENCE={{ audience }} dotenvx run -q -f ../.env.local -- tsx ./scripts/add-audience-mapper-to-scope.ts +# Add groups mapper to client scope +add-groups-mapper-to-scope realm 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 SCOPE_NAME={{ scope_name }} + dotenvx run -q -f ../.env.local -- tsx ./scripts/add-groups-mapper-to-scope.ts + # Delete Keycloak client delete-client realm client_id: #!/bin/bash diff --git a/keycloak/scripts/add-groups-mapper-to-scope.ts b/keycloak/scripts/add-groups-mapper-to-scope.ts new file mode 100644 index 0000000..bd895a0 --- /dev/null +++ b/keycloak/scripts/add-groups-mapper-to-scope.ts @@ -0,0 +1,82 @@ +#!/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; + invariant(KEYCLOAK_HOST, "KEYCLOAK_HOST environment variable is required"); + + const KEYCLOAK_ADMIN_USER = process.env.KEYCLOAK_ADMIN_USER; + invariant(KEYCLOAK_ADMIN_USER, "KEYCLOAK_ADMIN_USER environment variable is required"); + + const KEYCLOAK_ADMIN_PASSWORD = process.env.KEYCLOAK_ADMIN_PASSWORD; + invariant(KEYCLOAK_ADMIN_PASSWORD, "KEYCLOAK_ADMIN_PASSWORD environment variable is required"); + + const realm = process.env.KEYCLOAK_REALM; + invariant(realm, "KEYCLOAK_REALM environment variable is required"); + + const scopeName = process.env.SCOPE_NAME; + 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."); + + kcAdminClient.setConfig({ + realmName: realm, + }); + + 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"); + + const mapperName = "groups"; + const existingMappers = await kcAdminClient.clientScopes.listProtocolMappers({ id: scope.id }); + + if ( + existingMappers.some( + (mapper) => mapper.name === mapperName || mapper.config?.["claim.name"] === "groups" + ) + ) { + console.warn(`Groups mapper already exists in scope '${scopeName}'.`); + return; + } + + const groupsMapper = { + name: mapperName, + protocol: "openid-connect", + protocolMapper: "oidc-group-membership-mapper", + config: { + "claim.name": "groups", + "full.path": "false", + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true", + }, + }; + + await kcAdminClient.clientScopes.addProtocolMapper({ id: scope.id }, groupsMapper); + console.log(`Groups mapper added to client scope '${scopeName}'.`); + } catch (error) { + console.error("Error adding groups mapper to scope:", error); + process.exit(1); + } +}; + +main();