Keycloak

[Keycloak] JWT에 Custom 스코프 추가하기

kyoulho 2025. 3. 26. 19:28
728x90

이 글에서는 최신 버전인 Keycloak 26을 기준으로 JWT에 사용자가 소속된 그룹 정보를 {id, name} 형태로 추가하기 위한 Client Scope 설정 및 Custom Mapper를 구현하는 방법을 자세히 다룹니다.

1. 요구사항 정의

Keycloak에서 발급한 JWT의 Payload에 사용자가 속한 그룹의 id와 name 정보를 배열 형태로 추가합니다.

예상되는 Payload 구조:

{
  "sub": "12345678-1234-1234-1234-123456789abc",
  "preferred_username": "johndoe",
  "group_info": [
    {"id": "group-1-id", "name": "Admins"},
    {"id": "group-2-id", "name": "Users"}
  ]
}

2. Client Scope 생성

Keycloak 관리 콘솔에서 아래 단계를 수행합니다.

  • Client Scopes 메뉴로 이동 후, Create client scope 클릭
  • 이름을 custom-groups-scope로 설정하고 저장

3. Custom Protocol Mapper 구현 (SPI)

Keycloak 기본 mapper로는 위 형태를 구현할 수 없으므로, Java로 커스텀 SPI를 구현합니다.

프로젝트 설정 (Gradle 예시)

plugins {
    java
}

group = "com.kyoulho"
version = "1.0.0"

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    compileOnly ("org.keycloak:keycloak-core:26.0.0")
    compileOnly ("org.keycloak:keycloak-server-spi:26.0.0")
    compileOnly ("org.keycloak:keycloak-services:26.0.0")
}

Mapper 구현

GroupInfoProtocolMapper.java를 생성합니다:

package com.kyoulho;

import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.oidc.mappers.*;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.IDToken;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class GroupInfoProtocolMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper {

    // 매퍼 식별자
    public static final String PROVIDER_ID = "group-info-protocol-mapper";
    // 매퍼 설정 시 노출되는 프로퍼티 목록
    private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<>();

    static {
        // 토큰 클레임 이름 등 기본 설정 추가
        OIDCAttributeMapperHelper.addTokenClaimNameConfig(CONFIG_PROPERTIES);
        OIDCAttributeMapperHelper.addJsonTypeConfig(CONFIG_PROPERTIES);
        // 어떤 토큰(Access/ID/UserInfo)에 포함할지 설정
        OIDCAttributeMapperHelper.addIncludeInTokensConfig(CONFIG_PROPERTIES, GroupInfoProtocolMapper.class);
        // ✅ multivalued 설정 추가 (리스트 구조 보존용)
        ProviderConfigProperty multiValuedProp = new ProviderConfigProperty();
        multiValuedProp.setName("multivalued");
        multiValuedProp.setLabel("Multivalued");
        multiValuedProp.setType(ProviderConfigProperty.BOOLEAN_TYPE);
        multiValuedProp.setDefaultValue("true");
        multiValuedProp.setHelpText("If true, a list of values will remain an array in the token.");
        CONFIG_PROPERTIES.add(multiValuedProp);
    }

    @Override
    public String getId() {
        return PROVIDER_ID;
    }

    @Override
    public String getDisplayCategory() {
        // 관리자 UI에서의 카테고리 이름
        return TOKEN_MAPPER_CATEGORY;
    }

    @Override
    public String getDisplayType() {
        // 관리자 UI에서 이 매퍼를 선택할 때 표시될 이름
        return "Group Info (ID & Name)";
    }

    @Override
    public String getHelpText() {
        return "Adds an array of {id, name} objects for each group the user is in.";
    }

    @Override
    public List<ProviderConfigProperty> getConfigProperties() {
        return CONFIG_PROPERTIES;
    }

    /**
     * 그룹 정보를 토큰에 담는 핵심 로직
     */
    @Override
    protected void setClaim(IDToken token,
                            ProtocolMapperModel mappingModel,
                            UserSessionModel userSession,
                            KeycloakSession keycloakSession,
                            ClientSessionContext clientSessionCtx) {

        // 사용자가 속한 그룹마다 {id, name} 맵 구조 생성
        List<Map<String, String>> groupInfoList = userSession.getUser().getGroupsStream()
                .map(groupModel -> {
                    Map<String, String> groupMap = new HashMap<>();
                    groupMap.put("id", groupModel.getId());
                    groupMap.put("name", groupModel.getName());
                    return groupMap;
                })
                .collect(Collectors.toList());

        // 매퍼가 지정한 클레임 이름(ex: group_info)으로 groupInfoList를 매핑
        OIDCAttributeMapperHelper.mapClaim(token, mappingModel, groupInfoList);
    }
}

SPI 등록

resources/META-INF/services에 org.keycloak.protocol.ProtocolMapper 파일을 생성합니다.

파일에 클래스 명을 추가합니다:

com.kyoulho.GroupInfoProtocolMapper

4. 배포 및 설정

  • 작성한 모듈을 Jar로 빌드한 뒤, Keycloak의 /providers 디렉토리에 복사하고 Keycloak을 재시작합니다.
FROM gradle:8.12-jdk17 AS builder
WORKDIR /app

COPY build.gradle.kts settings.gradle.kts /app/
COPY src /app/src


RUN gradle clean build --no-daemon

FROM bitnami/keycloak:26

USER root

COPY theme /opt/bitnami/keycloak/themes/yeosu-theme
COPY --from=builder /app/build/libs/*.jar /opt/bitnami/keycloak/providers/

USER 1001

ENTRYPOINT [ "/opt/bitnami/scripts/keycloak/entrypoint.sh" ]
CMD [ "/opt/bitnami/scripts/keycloak/run.sh", "start", "--optimized" ]

5. Mapper 적용하기

  • Keycloak 관리 콘솔에서 custom-groups-scope의 Mapper 유형으로 방금 생성한 Group Info (ID & Name)을 선택하고, 클레임 이름(group_info)을 설정한 후 저장합니다.

6. Client Scope 연결

  • JWT에 그룹 정보를 추가하고자 하는 클라이언트의 설정에서 Client Scopes 탭으로 이동하여, 방금 만든 custom-groups-scope를 추가합니다.
728x90