웹 OIDC(SSO) 로그인 설정
원천 · framework/evomind/web/workflow-project-evo-service기준 · 릴리스 v0.12.0커밋 · 2e32c6cc파일 · docs/operations/web-oidc.md기준선 · 2026-11-12
로그인 화면의 SSO 버튼으로 Keycloak 등 OIDC 제공자에 로그인한다. 로그인에 성공하면 서비스가 사용자·그룹 소속을 동기화하고
자체 토큰을 발급한다. 이후 API 호출은 이 자체 토큰으로 인증한다(IdP 토큰을 API 가 직접 검증하지 않는다).
evo-dispatch 웹 연동을 켜면 로그인 때 받은 사용자 본인의 Keycloak 토큰을 메모리에만 보관해 dispatch 호출에 쓴다(dispatch.md).
기본은 꺼짐이다. 꺼진 상태에서는 SSO 버튼이 없고 기존 로컬 로그인만 동작한다.
브라우저 ─ SSO 버튼 ─▶ <evo-api>/oauth2/authorization/oidc ─▶ IdP 로그인IdP ─▶ <evo-api>/login/oauth2/code/oidc ─▶ 사용자·그룹 동기화 ─▶ <화면>/oidc/callback?code=… ─▶ 자체 토큰 교환1. IdP(Keycloak) 준비
섹션 제목: “1. IdP(Keycloak) 준비”realm(예: evo)에 웹 로그인용 클라이언트를 하나 만든다. 조직용·일반용 인스턴스를 따로 쓰면 인스턴스마다 자기 Keycloak(개발서버는 둘 다 렐름 evo)에서 이 절을 한 번씩 수행하고, 리디렉션 URI·Web origins 는 그 인스턴스의 주소로 넣는다(instances.md). 로그인 과정은 모두 백엔드(evo-api)가 수행하므로 프론트용·백엔드용을 따로 만들지 않는다. 이 절은 Keycloak 관리자에게 그대로 전달해 작업을 맡길 수 있게 썼다.
1.1 클라이언트 생성 (Clients → Create client)
섹션 제목: “1.1 클라이언트 생성 (Clients → Create client)”| 항목 | 값 |
|---|---|
| Client type | OpenID Connect |
| Client ID | evo-web (다른 이름이면 그 값을 OIDC_CLIENT_ID에 넣는다) |
| Client authentication | On (기밀 클라이언트) |
| Authorization | Off |
| Standard flow | On |
| Direct access grants / Implicit flow / Service accounts roles | Off |
| OAuth 2.0 Device Authorization Grant | Off (CLI용 클라이언트가 따로 있다: cli-session.md) |
| Root URL / Home URL | https://<evo-web 주소> |
| Valid redirect URIs | https://<evo-api 주소>/login/oauth2/code/oidc — 콜백은 화면이 아니라 API 주소이며, * 없이 정확히 이 한 줄만 넣는다 |
| Web origins | https://<evo-web 주소> |
| PKCE Method | 비워 두거나 S256 (앱은 S256을 보낸다) |
1.2 토큰 claim (Clients → evo-web → Client scopes → evo-web-dedicated → Add mapper)
섹션 제목: “1.2 토큰 claim (Clients → evo-web → Client scopes → evo-web-dedicated → Add mapper)”앱은 로그인 직후 ID 토큰과 userinfo의 claim으로 사용자와 그룹을 읽는다. 기본 설정에서는 access token에만 들어가는 항목이 있으므로 이 클라이언트에서 ID 토큰과 userinfo에도 들어가게 한다. 역할(realm_access.roles) claim은 더 이상 읽지 않는다.
| claim | 설정 |
|---|---|
groups |
Mapper type Group Membership, Token Claim Name groups, Full group path Off(전체 경로가 아닌 그룹 이름), Add to ID token / access token / userinfo 모두 On. 경로는 OIDC_GROUPS_CLAIM |
preferred_username, name, email |
기본 profile·email 스코프를 Default로 둔다(기본값) |
로그인 자격은 그룹 소속이다. Keycloak 에 앱의 기본 그룹과 같은 이름의 그룹 user, admin 을 만들고(소문자, 접두 없이 글자 그대로) 사용 대상자를 넣는다. user 또는 admin 그룹에 속하지 않은 사용자는 계정만 있어도 로그인이 거부된다(/login?error=oidc_role, 화면 문구 “접근 권한이 없습니다.”). 이때 앱에는 새 계정이 만들어지지 않는다.
- 로그인할 때마다 토큰의
groups중 앱에 같은 이름 그룹이 있는 것만 그 계정의 소속이 되고 나머지 소속은 모두 제거된다(Keycloak 이 기준). 앱에 없는 이름은 무시하고 그룹을 자동으로 만들지 않는다. 앱 그룹은 앱에서 먼저 만든다. - 그룹이 없는 사용자의 토큰에는
groupsclaim 이 빈 배열이 아니라 항목 자체가 없다(Keycloak 26.1.5 확인). 앱은 이를 ’그룹 없음’으로 처리하므로, 매퍼가 빠지면 소속이 사라져 로그인이 거부된다(조용한 오동작이 아니라 눈에 띄는 장애). - SSO 계정의 소속은 기본적으로 Keycloak 에서만 바꾼다. 서비스 계정(6절)을 설정하지 않았다면 앱 관리 화면에서 SSO 계정을 그룹에 넣거나 빼려 할 때 “SSO 계정은 Keycloak 에서 관리합니다”로 거부된다(로컬 계정은 앱에서 관리한다). 소속이 바뀌면 그 사람의 다음 로그인(또는 CLI 세션 요청)에 반영된다.
1.3 설정 확인
섹션 제목: “1.3 설정 확인”- Clients → evo-web → Client scopes → Evaluate에서 시험 사용자(그룹
user소속)를 고른다. - Generated ID token과 Generated user info 양쪽에
groups가 그룹 이름만으로(경로/없이) 들어 있고user가 보이는지 확인한다. 렐름 역할(realm_access.roles)은 앱이 읽지 않으므로 확인하지 않아도 된다. - Credentials 탭에서 Client secret이 발급됐는지만 확인한다.
1.4 관리자가 돌려줄 것
섹션 제목: “1.4 관리자가 돌려줄 것”- Client ID(
evo-web에서 바꿨다면 바꾼 값)와 1.3 확인 결과(통과 / 빠진 claim). - Client secret은 채팅·이슈·문서에 적지 않는다. 요청자가 정한 안전한 경로로만 전달하고, 받은 쪽은
.env(개발서버는 CI/CD 변수DEPLOY_DEV_ORG_ENV_FILE)의OIDC_CLIENT_SECRET에만 넣는다.
2. 환경변수 (.env)
섹션 제목: “2. 환경변수 (.env)”| 이름 | 기본값 | 설명 |
|---|---|---|
OIDC_ENABLED |
false |
true 로 켠다. |
OIDC_ISSUER_URI |
(비움) | https://<keycloak>/realms/<realm>. 기동 시 Discovery 로 확인한다. |
OIDC_CLIENT_ID / OIDC_CLIENT_SECRET |
(비움) | 위 기밀 클라이언트 값. 시크릿은 .env 에만 둔다. |
OIDC_SCOPES |
openid,profile,email |
요청할 scope. |
OIDC_GROUPS_CLAIM |
groups |
그룹 claim 경로(점 표기). 역할 claim 설정(OIDC_ROLES_CLAIM)은 없어졌다. |
OIDC_GROUP_PREFIX |
(비움) | 설정하면 이 접두사로 시작하는 그룹만 동기화하고 접두사는 뗀다. |
OIDC_SUCCESS_REDIRECT_URI |
(비움) | 로그인 후 돌아갈 화면 주소. 화면과 API 주소가 다르면 반드시 화면 주소를 넣는다. 비우면 같은 주소로 돌아간다. |
.env 를 바꾼 뒤 앱을 재기동한다. 켜면 OIDC_ISSUER_URI 에 닿지 못할 때 앱이 기동하지 못할 수 있으니 주소를 먼저 확인한다.
HTTPS 발급자만 사용한다. 시크릿·내부 주소를 저장소·이슈·로그에 남기지 않는다.
3. 알아 둘 점
섹션 제목: “3. 알아 둘 점”- 로그아웃과 계정 전환: 앱의 로그아웃은 자체 refresh 세션을 폐기하고 브라우저의 토큰만 지우며 Keycloak SSO 세션은 끝내지 않는다. 대신 OIDC 인증 요청에
prompt=login을 붙여, Keycloak 이 SSO 세션이 있어도 로그인 자격 증명을 다시 묻는다. 그래서 로그아웃 뒤 같은 브라우저에서 OIDC 로 다른 계정을 골라 로그인할 수 있다. SSO 세션을 재사용한 자동 로그인은 하지 않는다. Keycloak 세션 자체를 끝내는 RP-initiated logout(end_session)은 지금 범위가 아니다. - IdP 의 역할 claim 은 쓰지 않는다. 관리자 여부는 앱이
admin그룹 소속(유효 역할)으로 판정하며, 사용자 역할 표시값(users.role)은admin그룹 소속 여부에 맞춰 둔다. - 같은 이름의 로컬 계정과 SSO 계정은 별개 계정이다. 자세한 동작은 4절을 본다.
- CLI 로그인(
evo login)은 별도 설정이다:cli-session.md.
4. 계정 분리와 Keycloak 삭제 시 동작
섹션 제목: “4. 계정 분리와 Keycloak 삭제 시 동작”같은 이름의 로컬 계정과 SSO 계정
섹션 제목: “같은 이름의 로컬 계정과 SSO 계정”- SSO 계정은 사용자 이름이 아니라 발급자(issuer)와
sub로 식별한다. 이름이 같아도 로컬 계정과 합쳐지지 않고 별개 계정으로 공존한다. 계정 병합 기능은 없다. - 처음 SSO 로그인할 때 같은 이름이 이미 있으면 SSO 쪽 이름 뒤에
_oidc(그것도 있으면_oidc2,_oidc3)가 붙는다. 예: 로컬hong이 있으면 SSOhong은hong_oidc가 된다. - 두 계정은 대화 기록·설정·CLI 키·API 키가 각각 따로다. 같은 사람이 두 계정을 쓰면 데이터가 나뉜다. 운영에서는 SSO 로그인을 쓰고 로컬 계정은 비상용 관리자 계정만 남기는 것을 권한다.
Keycloak 에서 계정이나 그룹을 삭제해도 앱에는 반영되지 않는다
섹션 제목: “Keycloak 에서 계정이나 그룹을 삭제해도 앱에는 반영되지 않는다”evo-api 는 Keycloak 의 삭제·변경을 알리는 연동이 없다. 사용자가 로그인하거나 CLI 세션을 요청할 때 받는 토큰만 본다.
| Keycloak 에서 한 일 | evo-api 에 일어나는 일 |
|---|---|
| 계정 삭제 | 그 사람은 새로 로그인하거나 CLI 세션을 받을 수 없다. 그러나 evo-api 의 계정(대화·설정·그룹 소속)은 활성 상태로 그대로 남는다. |
| 그룹 삭제 | evo-api 의 그룹과 그 그룹의 허용 모델·권한 설정은 그대로 남는다. 그 뒤 로그인(또는 CLI 세션 요청)하는 사용자만 소속이 빠지고, 로그인하지 않은 사용자의 소속은 그대로다. |
이미 발급된 것은 만료될 때까지 쓸 수 있다.
| 발급된 것 | 유효 기간(기본값) | Keycloak 삭제만으로는 |
|---|---|---|
| 웹 access 토큰 | 15분(JWT_ACCESS_TTL) |
만료까지 유효 |
| 웹 refresh 토큰 | 7일(JWT_REFRESH_TTL) |
앱이 자체 발급한 토큰이라 갱신이 계속 된다(앱 계정이 활성인 동안) |
| CLI 키(evo-lite) | 12시간(CLI_KEY_TTL) |
만료까지 유효 |
| API 게이트웨이 키 | 폐기 전까지 | 소유 계정이 활성이면 유효 |
접근을 끊으려면 앱에서 계정을 비활성화한다
섹션 제목: “접근을 끊으려면 앱에서 계정을 비활성화한다”관리자 설정의 사용자 화면에서 해당 계정의 활성 을 해제한다. 계정과 데이터까지 지우려면 비활성화한 뒤 삭제 한다(, 비활성화와 삭제의 차이는 roles-groups.md 8절).
- 로그인과 토큰 갱신(refresh)이 거부된다. 이미 발급된 웹 access 토큰도 다음 요청부터 401 로 거부된다(요청마다 계정 상태를 확인한다). refresh 토큰 행은 폐기되지 않고 남지만 갱신 요청에서 거부된다.
- SSO 로그인과 CLI 세션 요청이 거부되고, 그 사용자의 CLI 키는 evo-lite 에서 삭제된다.
- 그 사용자의 API 게이트웨이 키는 소유 계정이 비활성이므로 인증에 실패한다.
- 계정 행·대화 기록은 남는다.
- 이 순서로 정리한다: Keycloak 에서 계정을 삭제 → 앱에서 같은 사람의 계정을 비활성화(SSO 계정 이름은
_oidc가 붙었을 수 있으니 사용자 목록의 인증 열로 확인한다) → 데이터도 지울 때만 앱에서 삭제. - 앱에서 삭제해도 Keycloak 계정은 지워지지 않는다(evo-api 는 Keycloak 에 쓰지 않는다). Keycloak 계정이 남아 있으면 그 사람이 다시 SSO 로그인할 때 빈 새 계정이 만들어진다.
5. 확인 방법
섹션 제목: “5. 확인 방법”.env를 채우고 재기동한다.GET <evo-api>/api/auth/config의oidcEnabled가true인지 확인한다.- 로그인 화면에 SSO 버튼이 보이고, 눌렀을 때 IdP 로그인 화면으로 이동하는지 확인한다. 로그인 후
/oidc/callback을 거쳐 채팅 화면으로 들어오면 성공이다. - 로그아웃한 뒤 SSO 버튼을 다시 누른다. IdP 가 자격 증명을 다시 묻고, 같은 계정 또는 다른 계정으로 로그인할 수 있어야 한다.
6. 앱에서 한 SSO 계정의 그룹 소속 변경을 Keycloak 에 반영
섹션 제목: “6. 앱에서 한 SSO 계정의 그룹 소속 변경을 Keycloak 에 반영”기본은 SSO 계정의 그룹 소속을 Keycloak 에서만 바꾼다(앱 화면에서는 “SSO 계정은 Keycloak 에서 관리합니다”로 거부). 서비스 계정을 설정하면 앱 관리 화면에서 SSO 계정을 그룹에 넣거나 빼는 것이 가능해지고, 앱이 Keycloak 의 같은 이름 그룹에도 반영한다. 로컬 계정은 Keycloak 을 거치지 않고 앱에서만 바꾼다. 그룹과 사용자 자체는 Keycloak 에 만들지 않는다(그룹은 앱에서 만들고 Keycloak 담당이 같은 이름으로 따로 만든다).
6.1 Keycloak 준비 (담당자에게 전달)
섹션 제목: “6.1 Keycloak 준비 (담당자에게 전달)”렐름마다 서비스 계정 클라이언트를 하나 만든다(조직용·일반용은 서로 다른 secret).
| 항목 | 값 |
|---|---|
| Client ID | evo-api-sync |
| 유형 | 비공개(client authentication On), Service accounts roles On |
| 로그인 방식 | 클라이언트 자격 증명만. Standard flow·Direct access·Implicit·Device grant 는 끈다. |
| 서비스 계정 역할 | realm-management 의 view-users, manage-users 두 개만 |
| secret | 채팅·이슈·문서에 적지 않고 파일로만 전달한다. |
manage-users 는 사용자 수정·삭제·비밀번호 재설정까지 허용하므로 그룹 소속만으로 좁힐 수 없다(26.1.5 기준, 그룹 한정 권한은 preview 기능). 그래서 앱은 그룹 소속 추가·제거 호출만 하도록 코드로 제한하고, secret 은 인스턴스별로 .env(개발서버는 CI/CD 변수)에만 둔다. 운영에 쓰기 전에 그룹 한정 권한이나 안정판 도입을 다시 검토한다.
6.2 환경변수
섹션 제목: “6.2 환경변수”| 이름 | 설명 |
|---|---|
KEYCLOAK_SYNC_CLIENT_ID |
서비스 계정 클라이언트 ID(예: evo-api-sync). 비우면 꺼짐. |
KEYCLOAK_SYNC_CLIENT_SECRET |
그 client secret. 둘 다 있고 발급자(OIDC_ISSUER_URI, 비면 CLI_OIDC_ISSUER_URI)를 알아야 켜진다. |
6.3 동작
섹션 제목: “6.3 동작”- 앱에서 SSO 계정을 그룹에 넣거나 빼면 Keycloak 에 먼저 반영하고(사용자
sub로 같은 이름 그룹에 추가·제거) 성공하면 앱 DB 와 CLI 키를 바꾼다. Keycloak 에 이미 그 상태면 호출 없이 앱만 맞춘다. - 실패하면(같은 이름 그룹 없음, 사용자 없음, 서비스 계정 권한·인증 오류, Keycloak 오류·시간 초과, 다른 렐름 계정)
502“Keycloak 에 반영하지 못해 변경하지 않았습니다. …” 이고 앱 DB 는 바뀌지 않는다. 관리 화면은 오류를 표시하고 화면 값을 변경 전으로 돌린다. - Keycloak 에 반영한 뒤 뒤따르는 단계(CLI 키 갱신 등)가 실패해 변경이 롤백되면 반대 호출로 Keycloak 을 원래대로 되돌린다. 반영·실패·복원은 감사 로그(
group.keycloak)에 남고 secret·토큰은 남지 않는다. - 한계: 이미 발급된 토큰에는 새 소속이 들어 있지 않다. 그 토큰으로 로그인·CLI 세션을 다시 요청하면 동기화가 앱의 변경을 되돌릴 수 있고, 새 토큰이 발급되면(다시 로그인하면) 맞춰진다.
답변은 문서를 근거로 만들지만 틀릴 수 있습니다. 출처 문서를 함께 확인하세요.