콘텐츠로 이동

사용자 정의 OIDC 연동 (배포 플랜)


개요

Guance 배포 플랜은 사용자 정의 OIDC 방식을 통해 타사 ID 공급자(IdP)를 연동할 수 있습니다. 다음 시나리오에 사용됩니다.

  • 표준 구현과 OIDC 흐름 또는 반환 구조가 다른 단일 IdP
  • 여러 IdP가 공존하며 로그인 진입점에서 출처를 구분해야 하는 경우
  • code -> token -> userinfo 과정에서 주소 생성, 계정 정보 변환 또는 계정 통일을 처리해야 하는 경우

이 문서는 "다중 IDP 환경에서의 OIDC 구성 설명"을 기반으로 정리되었으며, 단일 IDP 사용 방식도 함께 설명합니다. 두 시나리오는 동일한 사용자 정의 OIDC 기능을 공유하며, 차이는 주로 프론트엔드 로그인 진입점과 Func 라우팅 로직에 있습니다.

전제 조건

  • Guance 베이스 버전이 1.123.216 이상이어야 합니다.
  • 배포 플랜 Launcher 구성 권한이 있어야 합니다.
  • 내장 Func가 활성화되어 있고 함수 API를 생성할 수 있어야 합니다.
  • 대상 IdP에서 client_id, client_secret, 권한 부여 주소, 토큰 주소, 사용자 정보 주소 등을 확보해야 합니다.
  • 사용자 정보 필드와 Guance 계정 필드 간의 매핑 관계가 명확해야 합니다.

구현 원리

사용자 정의 OIDC 방식의 핵심은 OIDC의 주요 흐름을 Func가 처리하도록 하는 것입니다.

  1. OIDCClientSet.wellKnowURL이 IdP의 .well-known/openid-configuration을 직접 가리키지 않고 Func의 well_know 함수를 가리킵니다.
  2. 로그인 시 Guance 내부에서 authorization_endpoint를 통해 Func의 get_auth_url을 호출하고, 이 함수가 최종 로그인 주소를 반환합니다.
  3. 콜백 시 Guance 내부에서 userinfo_endpoint를 통해 Func의 get_userinfo를 호출하고, 이 함수가 내부적으로 code -> token -> userinfo를 완료합니다.
  4. Func는 여러 IdP의 사용자 정보를 정규화한 후 Guance에 직접 반환하며, 시스템이 계정 매칭 및 로그인을 수행합니다.

단일 IDP와 다중 IDP의 차이점

시나리오 프론트엔드 로그인 진입점 Func 라우팅 방식 콜백 주소
단일 IDP 로그인 버튼 하나만 표시 type을 전달하지 않으면 기본 IdP로 직접 처리 type을 생략하거나 고정값 하나를 포함할 수 있음
다중 IDP 각각 다른 type을 전달하는 여러 로그인 버튼 main_oidctype에 따라 다른 IdP 하위 스크립트로 분기 해당 IdP의 콜백 주소와 일치해야 하며, 일반적으로 type=xxx를 포함함

단일 IDP인 경우 메인 스크립트에서 기본값을 하드코딩하는 것이 좋습니다. 예: xtype = args.get("type") or "keycloak". 이렇게 하면 나중에 다중 IDP로 확장할 때 프론트엔드 진입점과 분기 로직만 추가하면 됩니다.

절차

1. forethought-core/coreOIDCClientSet 구성

Launcher에서 네임스페이스: forethought-core > core 로 이동하여 config.yaml에 다음 구성을 추가하거나 수정합니다.

OIDCClientSet:
  # 사용자 정의 OIDC 활성화
  enableCustomOIDC: true

  # Func의 well_know 함수 API 주소를 가리킴
  wellKnowURL: "<Func well_know API 주소>"

  mapping:
    username: preferred_username
    mobile: mobile
    email: email
    exterId: sub

설명:

  • enableCustomOIDC: true는 사용자 정의 OIDC를 활성화하는 핵심 스위치입니다.
  • wellKnowURL은 IdP의 서비스 검색 주소가 아닌 Func의 well_know 함수를 가리켜야 합니다.
  • mapping의 필드 이름은 Func가 최종 반환하는 사용자 정보 구조와 일치해야 합니다.

2. 프론트엔드 로그인 진입점 구성

Launcher에서 네임스페이스: forethought-webclient > front-web-config 로 이동하여 config.js를 수정합니다.

단일 IDP 예시

window.DEPLOYCONFIG = {
  ...
  paasCustomLoginInfo: [
    {
      label: "OIDC 로그인",
      url: "https://<배포 플랜 Web 도메인>/oidc/login",
      desc: "사용자 정의 OIDC 로그인"
    }
  ],
  paasCustomLoginUrl: "https://<IdP 로그아웃 주소>?redirect_url=https://<배포 플랜 Web 도메인>/oidc/login"
}

다중 IDP 예시

window.DEPLOYCONFIG = {
  ...
  paasCustomLoginInfo: [
    {
      iconUrl: "https://<아이콘 주소>",
      label: "Keycloak 로그인",
      url: "https://<배포 플랜 Web 도메인>/oidc/login?type=keycloak",
      desc: "OIDC Keycloak 로그인"
    },
    {
      iconUrl: "https://<아이콘 주소>",
      label: "Authing 로그인",
      url: "https://<배포 플랜 Web 도메인>/oidc/login?type=authing",
      desc: "OIDC Authing 로그인"
    }
  ]
}

설명:

  • 다중 IDP 시나리오에서는 type 매개변수로 출처를 식별하는 것이 좋습니다.
  • type은 Func로 전달되어 메인 스크립트가 다른 IdP를 구분하는 데 사용됩니다.
  • IdP 콜백 주소가 type에 의존하는 경우, 프론트엔드 로그인 진입점, IdP 구성, Func 하위 스크립트의 redirect_uri 세 곳이 일치해야 합니다.

보충: SSO 로그아웃 주소 구성

사용자가 Guance에서 로그아웃할 때 타사 인증 센터에서도 동시에 로그아웃되도록 하려면 front-web-configconfig.jspaasCustomLoginUrl을 추가해야 합니다.

window.DEPLOYCONFIG = {
  ...
  paasCustomLoginInfo: [
    {
      label: "OIDC 로그인",
      url: "https://<배포 플랜 Web 도메인>/oidc/login",
      desc: "사용자 정의 OIDC 로그인"
    }
  ],
  paasCustomLoginUrl: "https://<IdP end_session_endpoint>?redirect_url=https://<배포 플랜 Web 도메인>/oidc/login"
}

설명:

  • paasCustomLoginUrl은 타사 로그아웃 주소이며, 일반적으로 IdP의 .well-known/openid-configuration에 있는 end_session_endpoint를 참조할 수 있습니다.
  • 사용자 정의 OIDC 방식에서 OIDCClientSet.wellKnowURL은 Func의 well_know 인터페이스를 가리키지만, paasCustomLoginUrl은 Func API 주소가 아닌 최종 타사 IdP의 로그아웃 주소를 입력해야 합니다.
  • PDF 원본 구성 예시에서 타사 로그아웃 주소는 일반적으로 redirect_url 매개변수를 사용하여 https://<배포 플랜 Web 도메인>/oidc/login으로 리디렉션됩니다.
  • IdP가 post_logout_redirect_uri, returnTo 또는 다른 매개변수 이름을 사용하는 경우 해당 IdP의 실제 프로토콜 요구 사항을 따르세요.
  • paasCustomLoginUrl은 하나의 구성 값만 가지므로, 다중 IDP 시나리오에서는 중간 로그아웃 주소를 두고 컨텍스트에 따라 해당 IdP로 리디렉션하는 등 통합 로그아웃 전략을 추가로 설계해야 합니다.
  • 로그인하지 않은 상태에서 사이트에 접속할 때 바로 타사 인증을 시작하려면 paasCustomLoginUrl/oidc/login으로 직접 구성할 수도 있습니다.

3. Web Nginx 전달 규칙 구성

네임스페이스: forethought-webclient > front-web-config 에서 nginx.conf를 수정하여 OIDC 로그인 및 콜백 요청을 inner 서비스로 전달합니다.

location /oidc/login {
    proxy_connect_timeout 5;
    proxy_send_timeout 5;
    proxy_read_timeout 300;
    proxy_http_version 1.1;
    proxy_set_header Connection "keep-alive";
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Headers X-Requested-With;
    add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
    proxy_pass http://inner.forethought-core:5000/api/v1/inner/oidc/login;
}

location /oidc/callback {
    proxy_connect_timeout 5;
    proxy_send_timeout 5;
    proxy_read_timeout 300;
    proxy_http_version 1.1;
    proxy_set_header Connection "keep-alive";
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Headers X-Requested-With;
    add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
    proxy_pass http://inner.forethought-core:5000/api/v1/inner/oidc/callback;
}

이 단계의 목적은 브라우저가 액세스하는 /oidc/login/oidc/callback이 최종적으로 배포 플랜 내부의 OIDC 처리 로직으로 전달되도록 하는 것입니다.

4. Func에서 메인 스크립트 작성

메인 스크립트는 외부에 통합 진입점을 제공하고 type에 따라 다른 IdP 하위 스크립트로 분기합니다. 최소한 다음 세 가지 함수를 포함하는 것이 좋습니다.

  • well_know: OIDCClientSet.wellKnowURL에서 사용하는 서비스 검색 정보를 반환합니다.
  • get_auth_url: IdP로 리디렉션할 최종 인증 주소를 생성하여 반환합니다.
  • get_userinfo: 콜백 매개변수를 받아 내부적으로 code -> token -> userinfo를 완료합니다.

예시:

import __keycloak as keycloak_client
import __authing as authing_client

@DFF.API('OIDC 서비스 검색 인터페이스')
def well_know():
    return {
        "authorization_endpoint": "<Func get_auth_url API 주소>",
        "token_endpoint": "",
        "userinfo_endpoint": "<Func get_userinfo API 주소>"
    }

@DFF.API('로그인 주소 정보 획득')
def get_auth_url(**kwargs):
    args = kwargs.get("args", {})
    xtype = args.get("type") or "keycloak"

    if xtype == "keycloak":
        return keycloak_client.get_auth_url(**kwargs)
    elif xtype == "authing":
        return authing_client.get_auth_url(**kwargs)
    else:
        raise Exception(f"잘못된 값 type=`{xtype}`")

@DFF.API('사용자 정보 획득 인터페이스')
def get_userinfo(**kwargs):
    args = kwargs.get("args", {})
    xtype = args.get("type") or "keycloak"

    if xtype == "keycloak":
        return keycloak_client.get_userinfo(**kwargs)
    elif xtype == "authing":
        return authing_client.get_userinfo(**kwargs)
    else:
        raise Exception(f"잘못된 값 type=`{xtype}`")

설명:

  • well_know가 반환하는 authorization_endpointuserinfo_endpoint는 모두 Func API 주소입니다.
  • 이 방식에서 token_endpoint는 일반적으로 비워둘 수 있습니다. code -> token 과정이 get_userinfo 내부에서 이미 완료되기 때문입니다.
  • 단일 IDP 시나리오에서도 이 메인 스크립트 계층을 유지하는 것이 좋습니다. 나중에 확장이 더 편리합니다.

5. 각 IdP에 대한 하위 스크립트 작성

각 IdP 하위 스크립트는 독립적으로 유지 관리하는 것이 좋습니다. 예: __keycloak.py, __authing.py. 하위 스크립트는 최소한 다음 내용을 구현해야 합니다.

  • OIDC_SET: 현재 IdP의 클라이언트 구성
  • well_know: 현재 IdP의 실제 OIDC 엔드포인트
  • get_auth_url: 인증 주소 생성
  • turn_token: 콜백 code를 기반으로 토큰 교환
  • turn_userinfo: 토큰을 기반으로 사용자 정보 획득
  • get_userinfo: 외부 통합 진입점으로, 내부에서 turn_tokenturn_userinfo를 순차적으로 호출

예시 구조:

OIDC_SET = {
    "client_id": "<OIDC 클라이언트 ID>",
    "client_secret": "<OIDC 클라이언트 시크릿>",
    "scope": "openid profile email address",
    "redirect_uri": "https://<배포 플랜 Web 도메인>/oidc/callback?type=authing",
    "grant_type": "authorization_code"
}

def well_know():
    return {
        "authorization_endpoint": "https://<IdP 인증 주소>",
        "token_endpoint": "https://<IdP 토큰 주소>",
        "userinfo_endpoint": "https://<IdP 사용자 정보 주소>",
        "end_session_endpoint": "https://<IdP 로그아웃 주소>",
        "jwks_uri": "https://<IdP JWKS 주소>"
    }

!!! warning

위의 `OIDC_SET`은 구성 구조 설명을 위한 예시이며, Func 스크립트에 `client_secret`, 토큰 또는 기타 민감한 자격 증명을 하드코딩하지 않는 것이 좋습니다.

키 정보를 관리해야 하는 경우 Func 측의 비밀번호 유형 환경 변수를 통해 주입하고 읽는 것을 우선적으로 고려하세요. 특정 연동 시나리오에서 여전히 `OIDCClientSet.clientSecret`을 사용해야 하는 경우에도 동일한 방식으로 보안 관리하여 스크립트나 구성 예시에 평문으로 작성되지 않도록 하는 것이 좋습니다.

핵심 주의사항:

  • 다중 IDP 시나리오에서 redirect_uri에는 해당 type=xxx 매개변수가 포함되어야 합니다.
  • IdP 백엔드에 등록된 콜백 주소는 OIDC_SET.redirect_uri와 완전히 일치해야 합니다.
  • get_auth_url의 반환 값은 {"url": "..."} 형식으로 고정됩니다.
  • get_userinfo의 반환 값은 계정 정보 JSON이어야 하며, 사용자 속성을 최상위 구조에 직접 배치하는 것이 좋습니다.

SSO 로그아웃 설명

PDF 《001-배포 플랜【OIDC】타사 인증 로그인 구성》의 배포 설명에 따르면, Guance에서 현재 지원하는 SSO 로그아웃 방식은 다음과 같습니다.

  1. 사용자가 Guance 측에서 로그아웃을 클릭합니다.
  2. 시스템이 먼저 로컬 세션을 종료합니다.
  3. 로컬 세션 종료가 완료되면 브라우저가 paasCustomLoginUrl에 지정된 타사 로그아웃 주소로 이동합니다.
  4. 타사 인증 센터가 로그아웃을 완료한 후 매개변수 규칙에 따라 로그인 페이지 또는 지정된 페이지로 리디렉션됩니다.

제한 사항:

  • 현재 시스템은 타사 인증 센터가主动적으로 발행하는 로그아웃 요청을 처리할 수 없습니다.
  • 즉, Guance에서主动적으로 트리거하는 로그아웃 흐름만 지원하며, IdP가 단일 로그아웃을 발행한 후 Guance으로 콜백하여 연동 로그아웃을 완료하는 방식은 지원하지 않습니다.
  • 따라서 연동 단계에서 고객과 "누가 로그아웃을 발행할지"와 "로그아웃 후 어디로 돌아갈지"를 미리 확인하는 것이 좋습니다.

6. 사용자 정보 필드 통일

Func가 최종적으로 Guance에 반환하는 사용자 정보는 OIDCClientSet.mapping과 일대일로 대응되어야 합니다. 최소한 다음 필드가 존재하는 것이 좋습니다.

Guance 계정 필드 IdP 필드 예시
username preferred_username
email email
mobile mobile
exterId sub

여러 IdP에서 동일한 사람이 같은 이메일을 사용하는 경우 Func에서 동일한 계정 식별자로 통일 처리하는 것이 좋습니다. 예:

  • email을 회사 이메일로 통일 설정
  • sub 또는 exterId를 이메일 값으로 통일
  • IdP마다 반환 구조가 다른 경우 Func 내에서 먼저 통일된 JSON으로 변환한 후 반환

이 단계는 다중 IDP 로그인 시 중복 계정이 생성되는 것을 방지하는 핵심입니다.

7. 필요한 경우 기존 OIDC 계정 정리

환경에 이미 기존 OIDC 계정이 있고, 다른 IdP로 인해 동일한 이메일에 여러 계정이 생성된 경우 다음 방법을 고려할 수 있습니다.

-- OIDC 계정에서 동일한 이메일의 중복 계정이 있는지 조회
select email, count(id) as num
from `main_account`
where status = 0 and `exterId` <> ""
group by email
having num > 1;

-- 중복 계정 정리 후 exterId를 email로 통일 가능
update `main_account`
set exterId = email
where status = 0 and `exterId` <> "";

실행 전에 먼저 백업을 완료하고 실제 계정 상태를 고려하여 신중하게 처리하세요.

디버깅 권장 사항

  • 초기 연동 테스트 단계에서는 get_userinfo 마지막에 계정 정보를 출력하고 임시로 예외를 발생시켜 필드가 예상과 일치하는지 먼저 확인한 후 로그인을 허용하세요.
  • 중복 계정이 발생하면 다른 IdP가 반환하는 email, sub, exterId가 이미 통일되었는지 먼저 확인하세요.
  • 콜백 후 로그인에 실패하면 redirect_uri가 IdP 백엔드에 등록된 값과 완전히 일치하는지 먼저 확인하세요.
  • 단일 IDP를 다중 IDP로 전환하는 경우 기본 type을 먼저 유지한 후 점진적으로 새 로그인 진입점과 하위 스크립트를 추가하세요.

자주 묻는 질문

1. wellKnowURL을 IdP의 .well-known 주소로 직접 설정할 수 없는 이유는 무엇인가요?

이 방식의 목표는 표준 OIDC 엔드포인트를 읽는 것뿐만 아니라 로그인 주소 생성, 콜백 처리, 계정 정규화 등의 로직을 Func에 위임하는 것이므로, wellKnowURL은 사용자 정의 well_know 함수를 가리켜야 합니다.

2. 단일 IDP에서도 이 방식을 사용하는 것이 좋은 이유는 무엇인가요?

많은 단일 IDP 시나리오도 본질적으로 "비표준 OIDC" 연동이기 때문입니다. 예를 들어 로그인 주소, 콜백 매개변수, 사용자 정보 구조 또는 계정 기본 키를 수정해야 하는 경우가 있습니다. 이 방식을 사용하면 이러한 호환성 문제를 통일적으로 처리할 수 있습니다.

3. IdP가 반환하는 사용자 정보 구조의 계층이 깊은 경우 어떻게 해야 하나요?

Func에서 먼저 구문 분석하여 평탄화한 후, 최종 결과를 최상위 구조로 정리하여 Guance에 반환하는 것이 좋습니다. 복잡한 중첩 구조를 시스템 계정 매핑에 직접 전달하지 마세요.

문서 평가

이 페이지가 도움이 되었나요?