Skip to content

feat(oidc): role 변경 시 서명된 Security Event Token(SET) 발행 - #81

Merged
mack-erel merged 1 commit into
mainfrom
feat/oidc-role-change-set
Jul 8, 2026
Merged

feat(oidc): role 변경 시 서명된 Security Event Token(SET) 발행#81
mack-erel merged 1 commit into
mainfrom
feat/oidc-role-change-set

Conversation

@mack-erel

Copy link
Copy Markdown
Owner

배경

RP 는 로그인 시 IdP roles 클레임을 세션에 스냅샷해 저장한다. 그래서 KeyStone 에서 role 을 바꿔도 사용자가 다시 로그인하기 전까지 RP 에 반영되지 않아, 강등 시 최대 세션 수명(14일) 동안 낡은 권한이 유지되는 보안 이슈가 있었다.

이를 back-channel logout 과 동일한 메커니즘(같은 서명키·JWKS·SET 봉투)으로 push 해, 세션을 끊지 않고 RP 의 members.role 만 갱신한다. RP 는 매 요청 DB role 을 읽으므로 재로그인 없이 다음 요청부터 반영된다. 새 서명 스킴·시크릿은 필요 없다.

변경 사항

  • schema: oidcClientsrole_change_uri(text, nullable) 추가 — sqlite/pg/mysql 3방언 + 마이그레이션(d1 0029, pg/mysql/sqlite 0012). URI 설정된 클라이언트만 SET 수신.
  • src/lib/server/oidc/role-change.ts (신규, logout.ts 미러):
    • getRoleChangeTarget — 배정 대상 클라이언트 1곳만 조회(enabled + role_change_uri).
    • sendRoleChangeSet — RS256, typ=secevent+jwt, iss/aud(=clientId)/sub/iat/jti/events, nonce 금지, form role_change_token=. 오류 삼킴(재시도 없음).
    • events = { "https://idp.hyochan.site/event/role-change": { roles } } — RP 상수와 바이트 일치.
  • admin/user-actions/service.ts: addAssignment(→[role.key]) / revokeAssignment(→[]) DB 변경 직후 fire-and-forget 발행. 변경 후 getActiveAssignment권위 있는 최종 roles 스냅샷(로그인 token/userinfo 와 동일 경로). 전송 성공/실패를 role_change_set_sent audit(outcome)로 기록.
  • admin UI: oidc-clients 생성/수정 폼에 Role Change URI 입력 + validateSingleUri 검증, load 쿼리·컬럼 반영.
  • test: test/integration/role-change-set.test.ts 6케이스 — 계약/서명 검증, roles [key]/[], URI 미설정·saml skip, 전송 실패 삼킴, audit 기록.

계약 (RP 수신 측과 일치)

POST https://<rp>/auth/oidc/role-change, application/x-www-form-urlencoded, body role_change_token=<JWT>. audclientId 문자열(uuid 아님). 부여→roles: [role.key], 회수→roles: [](RP 가 user 로 강등).

알려진 한계

  • 만료(expiresAt) 기반 강등은 push 하지 않음(관리자 명시 변경 add/revoke 에만 발행) — 후속 과제.
  • RP 반영은 Hyperdrive 읽기 캐시로 최대 ~60초 지연 가능(정상).
  • 전송은 best-effort(재시도 없음, logout 과 동일).

검증

  • 전체 테스트 241+6 통과, bun run check 0 errors, prettier+eslint 통과, back-channel logout 회귀 없음.
  • pg 마이그레이션은 이미 적용 완료(role_change_uri 컬럼 추가).

배포 후 남은 작업

  • 드론원스톱 OIDC 클라이언트(clientId 34be2cd7512f458f9a59)의 Role Change URI 를 admin UI 에서 https://drone.devday.one/auth/oidc/role-change 로 등록.
  • d1/mysql/sqlite 사용 환경은 각 마이그레이션 적용 필요(원격 D1 은 수동 적용 규칙).

🤖 Generated with Claude Code

관리자가 사용자의 OIDC 서비스 role 을 부여/회수하면, back-channel logout 과
동일한 서명키·JWKS·SET 봉투로 서명된 토큰을 대상 RP 의 role_change_uri 로 push
한다. RP 는 세션을 끊지 않고 members.role 만 갱신하므로 재로그인 없이 다음 요청부터
반영된다(강등 시 낡은 권한 유지 보안 이슈 해소).

- schema: oidcClients 에 role_change_uri(nullable) 추가 (sqlite/pg/mysql 3방언 + 마이그레이션)
- oidc/role-change.ts: getRoleChangeTarget + sendRoleChangeSet (logout.ts 미러)
  - RS256, typ=secevent+jwt, iss/aud(=clientId)/sub/iat/jti/events, nonce 금지
  - events = { "https://idp.hyochan.site/event/role-change": { roles } }
- user-actions/service.ts: addAssignment(→[role.key]) / revokeAssignment(→[]) 직후
  fire-and-forget 발행. 변경 후 getActiveAssignment 로 권위 있는 최종 roles 스냅샷.
  전송 성공/실패를 role_change_set_sent audit(outcome)로 기록.
- admin UI: oidc-clients 생성/수정 폼에 Role Change URI 입력 + validateSingleUri 검증
- test: role-change-set.test.ts (계약/서명/roles/skip/실패삼킴/audit 6케이스)

발행은 best-effort(재시도 없음), oidc 배정에만, role_change_uri 미설정 시 skip.
만료 기반 강등은 push 하지 않음(후속 과제).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mack-erel
mack-erel merged commit 45c3c50 into main Jul 8, 2026
6 checks passed
@mack-erel
mack-erel deleted the feat/oidc-role-change-set branch July 8, 2026 04:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant