feat: IdP 3라운드 개선 — 보안·기능·테스트·운영 마감 - #77
Merged
Merged
Conversation
…·issuer fail-closed - 로그인 타이밍 균등화: 미존재/비활성/무크레덴셜 경로에도 동일 비용 더미 scrypt 수행 (사용자 열거 오라클 제거) - 계정 단위 잠금: login:user:<username> 키 슬라이딩 윈도우(10회/15분, 실패만 카운트, IP 제한과 병행) - users lookup API: ?id= 경로에도 tenant 스코프 강제 (?tenant= 미지정 시 default 폴백으로 무회귀) - authorization_code 교환 시 grant.sessionId 세션의 revoked/expired 검사 (invalid_grant) - 프로덕션에서 IDP_ISSUER_URL 미설정 시 host fallback 대신 503 fail-closed, 부트스트랩 필수 env fail-fast - i18n ko/en 키 확장(후속 커밋의 email/admin 키 포함 일괄 반영) Co-Authored-By: Claude Fable 5 <[email protected]>
…rification_tokens·CI 드리프트 체크 - legacy oidc_grants.code 컬럼+uidx 3방언 drop (codeHash 전환 완료분 정리) - client_skins.createdAt sqlite 초→ms 통일 + 기존 값 ×1000 보정 UPDATE(재적용 안전 가드) - email_verification_tokens 테이블 3방언 추가 (password_reset_tokens 패턴) - d1 offline generate 침묵 버그 수정: d1-http driver 우회(CLI 플래그), 적용(db:migrate)은 config 유지 - CI에 마이그레이션 드리프트 체크 스텝(db:generate:all 후 git diff) - 4트랙 생성물: d1 0021·0022 / pg·mysql·sqlite 각 0004·0005 — 적용은 별도(사용자) Co-Authored-By: Claude Fable 5 <[email protected]>
- d1/sqlite=batch, pg/mysql=transaction 복붙 분기를 runAtomic()으로 추출 - refresh 토큰 회전을 claim+insert 원자 단위로 (claim 성공·insert 실패 시 세션 유실 창 제거, 동시 회전 패자 stray는 family 폐기로 무효화 — RFC 6819 §5.2.2.3) - getActiveSigningKey/getPublicJwks tenant별 globalThis 캐시(60s TTL, null 미캐시) + invalidateSigningKeyCache - totp enroll confirm도 runAtomic 치환(동작 무변경) Co-Authored-By: Claude Fable 5 <[email protected]>
- runExpiredDataGc: sessions(만료+30일 유예)·oidc_grants·refresh/reset/email 토큰·webauthn challenges·rate_limits·saml 3종 일괄 purge, 테이블별 에러 격리 - sessions 유예 30일 근거: revoke-on-logout 불변식(폐기 세션의 refresh 토큰은 전부 revoke) — 조기 삭제로 인한 세션 검사 우회 방지 - Workers: 요청 1% 확률 waitUntil 발사(adapter-cloudflare가 scheduled 미지원이라 cron 대신), Node: 1시간 setInterval(unref+중복 가드) - orphan이던 기존 purge 3함수 호출 연결 Co-Authored-By: Claude Fable 5 <[email protected]>
- 가입 시 인증 메일(SHA-256 해시 저장·24h TTL·rate-limit) → verify-email 라우트(GET 비소진, POST 원자 소진) → emailVerifiedAt 세팅으로 email_verified 클레임 정상화(기존엔 항상 false) - 재발송 액션 + 프로필 미인증 배너 (프로필 저장은 default→named save 액션 전환) - 보안 알림 9종(비번 변경/재설정·TOTP 등록/삭제/백업코드·패스키 등록/삭제·admin 초기화/잠금) — waitUntil best-effort 격리, users.locale 기반 ko/en - 발송 실패는 본 동작에 영향 없음, 이메일 없는 계정 스킵 Co-Authored-By: Claude Fable 5 <[email protected]>
- 신규 8파일: crypto-keys(22)·password(19)·oidc-grant(6)·oidc-refresh(8)·ratelimit(7)·email-verification(6)·session(9)·gc(7) — WebCrypto 독립 교차검증, mock DB 실계약 준수(총 151 테스트) - schema-parity 강화: nullable·타입 계열·인덱스/unique 비교, 방언별 정당한 차이는 명시적 예외 목록(signing_keys mysql partial-unique 1건) - @vitest/coverage-v8 + test:coverage / typecheck(tsc) / lint:fix / db:check(4방언) 스크립트 - lefthook pre-commit에 eslint 추가(svelte config 직렬화 이슈로 --cache 미사용) Co-Authored-By: Claude Fable 5 <[email protected]>
…회전 절차
- admin/users/[id]/+page.server.ts 677→181줄: user-actions/{profile,org,service,security} 4모듈로 순수 이동(11개 액션 계약·audit kind 보존, forceLogout 신설 포함)
- admin 강제 로그아웃 액션: 세션+refresh 폐기, tenant 가드, confirm UI
- admin 서버 에러 98곳 i18n 전환(adminError/requireFormId 헬퍼, ko/en 605키 대칭) — UI 라벨 영어·에러 한국어 혼재 해소. 잔존: crud-factory 스키마 문구(테스트 계약)·saml-sps/[id]·프로토콜 API(후속)
- 보안 감사 문서 2건 repo 제거(E9), improvement-report 정오표, SECRET_ROTATION.md 신설(7용도·재암호화 3종·무중단 불가 명시), 전수 개선 감사 계획/투두 문서
Co-Authored-By: Claude Fable 5 <[email protected]>
- users.status enum 에 deletion_pending 추가 + deletion_scheduled_at 컬럼(soft-delete 유예 만료 시각) 3방언 - invite_tokens 테이블 신설(SHA-256 해시·72h TTL·1회용, email_verification_tokens 패턴) 3방언 - 4트랙 마이그레이션 생성물: d1 0023(invite)·0024(deletion) / pg·mysql·sqlite 각 0006·0007 — 적용은 별도(사용자) - 후속 P7(초대)·P8(계정삭제) 기능이 공유하는 스키마 기반이라 한 커밋으로 묶음(journal/snapshot 얽힘 회피) Co-Authored-By: Claude Fable 5 <[email protected]>
- listActiveSessions: revokedAt IS NULL·미만료 세션을 최근 활동순 반환(셀프서비스 세션 관리 화면용) - revokeSessionById: sessionId+userId 동시 일치 세션만 폐기(IDOR 방지 — 소유·활성 가드를 select·update 양쪽에, 멱등) - /account/sessions 라우트: 현재 세션 하이라이트, 개별/타 세션 일괄 폐기 액션 Co-Authored-By: Claude Fable 5 <[email protected]>
- issueInvite: 관리자가 비밀번호 없이 계정 선생성 → invite_token(72h·1회용) 발급 → sendInviteEmail - /accept-invite 라우트: 토큰 검증(isNull(usedAt)+미만료) 후 최초 비밀번호 설정, 원자 소비(usedAt 세팅) - admin/users: 초대 액션 + "초대중" 배지(유효 invite_token 보유자만 판정 — 연합 사용자 오표시 해소) - email.ts: sendInviteEmail(안전 URL 가드·72h 안내 문구) Co-Authored-By: Claude Fable 5 <[email protected]>
- /account/danger-zone: 셀프서비스 탈퇴 요청 → status=deletion_pending + deletionScheduledAt(유예 30일), 세션·refresh 폐기 - authenticatePendingDeletionUser + 로그인 복구 흐름: 유예 내 재로그인 시 2단계 확인 후 active 환원(deletionScheduledAt=null), 유예 경과 시 거부 - gc: deletion_pending & deletionScheduledAt<now 계정만 하드삭제(FK cascade 자식 정리·audit set null 보존, 활성 계정 오삭제 불가) - guards/profile: status 유니온에 deletion_pending 반영, security-notify: account_deletion_requested 알림 종류 추가 Co-Authored-By: Claude Fable 5 <[email protected]>
- RuntimeConfig.signingKeySecrets([current] 또는 [current,previous]): 발급/암호화는 current만, 복호/검증은 tryWithSecrets 로 current→previous 순차 시도 - crypto/keys: tryWithSecrets·tryWithSecretsNullable 헬퍼, getActiveSigningKey(secrets[]) 시그니처 전환, IDP_SIGNING_KEY_SECRET_PREVIOUS 주입 - 소비처 전량 전환: oidc(token/introspect/userinfo/end-session)·saml(sso/slo)·totp/webauthn verify·mfa·logout·login(ldap bindPassword) - scripts/reencrypt-secrets.ts(3종 재암호화)·SECRET_ROTATION.md 갱신, crypto-keys 회전 테스트 - 회전 소비처 프로토콜 라우트는 signingKeySecrets 변경과 잔존 한국어 에러 i18n 이 라인 단위로 얽혀 있어 파일 단위로 본 커밋에 포함(i18n 키는 P10 JSON 에 추가 — 최종 트리에서 정합) Co-Authored-By: Claude Fable 5 <[email protected]>
- 프로토콜 라우트 에러 문구 i18n: oidc(authorize/revoke)·saml(metadata)·totp enroll·webauthn(authenticate/register/passkey) + P9 에서 함께 전환한 회전 소비처 라우트의 키 - admin 라우트: ldap-providers·saml-sps(목록/[id]) 서버 에러 i18n - validation·admin/schemas·crud-factory: zod 메시지 i18n 화, admin-zod·crud-factory 테스트 계약 갱신 - i18n ko/en JSON: 프로토콜/admin/기능 키 + mfa_login 5키 대칭 추가(본 후속 라운드 전체가 참조하는 키를 일괄 반영) Co-Authored-By: Claude Fable 5 <[email protected]>
- account-deletion·invite-accept·secret-rotation·session-refresh-cascade·oidc-flow 통합 테스트 - test/stubs/app-environment: 통합 하니스용 App.Platform/환경 스텁, vitest.config 에 integration 포함 Co-Authored-By: Claude Fable 5 <[email protected]>
- followup-round2/PLAN.md·TODO.md: P6~P11 페이즈 설계와 페이즈별 실행 투두 기록 Co-Authored-By: Claude Fable 5 <[email protected]>
- users: deletion_pending 하드삭제 배치 조회용 인덱스 추가(방언별 부분/일반 인덱스) — d1 0025 / pg·mysql·sqlite 0008 - pendingEmail·pendingEmailRequestedAt 컬럼 + email_change_tokens 테이블 신설(SHA-256·1회용·만료, 이메일 변경 플로우 기반) — d1 0026 / *0009 - oidcClients.organizationClaimConfig(JSON text) 컬럼 — organization 클레임 클라이언트별 노출 토글 저장(null=전량 노출 하위호환) — d1 0027 / *0010 - P12(users 인덱스)·P13(이메일 변경)·P17(organization config) 세 페이즈가 공유하는 스키마 기반이라 한 커밋으로 묶음(journal/snapshot 얽힘 회피). 적용은 별도(사용자). Co-Authored-By: Claude Fable 5 <[email protected]>
- email.*: 트랜잭션 메일(find-id/비밀번호 재설정/이메일 인증/초대) locale 인지용 제목·본문·버튼·푸터 키 - security_alert.*: 세션 철회(session_revoked/sessions_revoked_all)·이메일 변경(email_change_requested)·백업코드(backup_codes_low/depleted) 알림 키 - profile.*: 이메일 변경 UI + 생년월일 형식 에러, sessions.*: 세션 지정/조회 에러, accept_invite.credential_label, skins.placeholder_* 3종 - 다수 페이즈(P12~P17)가 공유하는 키라 JSON hunk 분리 대신 일괄 반영(직전 라운드 c1fa642 관례). 코드 참조는 각 페이즈 커밋에서. Co-Authored-By: Claude Fable 5 <[email protected]>
- gc: invite_tokens·email_change_tokens purge 추가(만료 또는 소진분 정리, 미만료·미소진 보존) + users 하드삭제를 배치(id 조회→IN 삭제 루프)로 전환해 대량 cascade 락 방지 + GC 결과 구조화(JSON) 로깅 - guards: assertNotLastAdmin 이 "로그인 가능한(credential/identities 보유)" admin 만 카운트 — 미수락 초대 admin 이 마지막 관리자 삭제 보호를 잘못 완화하는 문제 차단 - accept-invite: lookupToken 조인에 status='active' 강제(비활성/삭제예정 계정에 credential 심기 차단), credential label i18n - health: 경량 SELECT 1 readiness 체크 — DB 불가 시 503 - gc·schema-parity 테스트 보강 - gc.ts 는 email_change purge(P13)·구조화 로깅(P17)이 같은 파일에 얽혀 한 커밋으로 통합. 배치 삭제는 방언 이식성 위해 DELETE…LIMIT 대신 2단계 루프. Co-Authored-By: Claude Fable 5 <[email protected]>
…(P15)
- ratelimit/store.ts 신설: RateLimitStore{increment/peek} 인터페이스 + DbRateLimitStore(4방언 upsert 캡슐화)·MemoryRateLimitStore(Map, 자체 evict)·resolveRateLimitStore(Workers=DB, Node=memory)
- ratelimit/index.ts: checkRateLimit(store,…)·peekRateLimit(store,…) 로 시그니처 전환, 슬라이딩 윈도우 산식(evaluate)만 남기고 상태 연산은 store 위임. 알고리즘 byte-identical
- hooks: 요청당 locals.rateLimitStore 해석, app.d.ts Locals 확장, guards.requireDbContext 가 store 반환
- 호출부 전량 이관(checkRateLimit(db)→store): login(중복 accountLockStatus 를 peekRateLimit 로 대체)·admin-login·signup·find-id·find-password·reset-password·verify-email·accept-invite·mfa·profile(재발송)·token(×2)·totp(×2)·webauthn(×3)·oidc(authorize/introspect/revoke)·saml-sso
- .env.example: Node in-memory 단일 인스턴스 한계·Redis 확장 경로 문서화(A3)
- breaking 시그니처라 24개 호출부·인프라를 한 커밋에 묶음. token 조직클레임(P17)·find*/signup/profile 메일locale(P13)·mfa 백업(P17) 등 동일 파일의 타 페이즈 hunk 는 후속 커밋으로 분리.
Co-Authored-By: Claude Fable 5 <[email protected]>
- FormError.svelte 신설: role="alert"+aria-live="assertive"+실패 시 포커스 이동 공용 에러 배너 - 8개 인증 폼(login/signup/mfa/reset-password/find-id/find-password/accept-invite/verify-email)에 FormError 적용 + use:enhance 로딩 상태(제출 중 버튼 disabled·스피너, SSR 폴백 유지) - login: 스킨 렌더 조건에 복구(soft-delete) 케이스 예외 — 스킨 로그인에서도 계정 복구 패널 도달(U3) - 순수 UI 변경(progressive enhancement, 시각 동작 무회귀). profile.svelte 가 FormError 를 참조하므로 P13 앞에 배치. Co-Authored-By: Claude Fable 5 <[email protected]>
- email.ts: 트랜잭션 메일 전량 수신자 locale 인지 — 제목·본문·CTA·푸터·lang 속성 i18n(baseHtml/ctaBody 공통화). 발송 헬퍼(issueEmailVerification·issueInvite)·호출부(find-id·find-password·signup·admin/users 초대)에서 대상 locale 전달, i18n core.normalizeLocale 공용화 - 세션 철회 보안 알림: security-notify 에 session_revoked·sessions_revoked_all·email_change_requested kind 추가, account/sessions 개별/일괄 철회에 dispatchSecurityAlert(본인 직접 철회도 발송) + 하드코딩 한국어 2건 i18n - 프로필 이메일 변경(F3): email-change.ts(신규, email_change_tokens 1회용)·confirm-email-change 라우트(신규), profile changeEmail 액션 — 현 비밀번호 재인증→중복 검증→pendingEmail 저장→새 주소 확인 메일→기존 주소 변경시도 알림, profile.svelte 이메일 변경 UI(대기 배너·FormError) - email-verification 테스트를 새 locale 시그니처로 갱신. gc.ts 의 email_change purge 는 P12 커밋에 통합됨. sessions i18n·security-notify backup kind 는 이 커밋과 P17 이 공유해 여기 통합. - 생년월일 형식 에러 i18n(P12-6)은 profile 파일 분할을 줄이려 이 커밋에 편승. Co-Authored-By: Claude Fable 5 <[email protected]>
- harness 확장: DbRateLimitStore 를 locals 에 주입(프로덕션 Workers 경로 재현), seedSamlSp·seedIdentityProvider, 쿠키 잼 헬퍼, SAML 서명 fixture(makeKeyCert/AuthnRequest 서명·검증) 공용화 - saml-sso: SP-initiated POST 바인딩 풀플로우(서명·audience·ACS 검증, 권한 게이트 실패, AuthnRequest replay 가드) - login-mfa: 같은 쿠키로 login→idp_mfa_pending→mfa(TOTP)→세션 발급 체이닝, 오답 거부·백업코드 경로 - ldap-login: seedIdentityProvider 기반 LDAP 분기, organization id_token 일치·이메일 변경·rate-limit store 신규 로직 통합 - p12-logic·session-revoke-notify: P12 버그수정·세션 철회 알림 통합 검증. 전부 실 DB/실 라우트 구동 - harness 의 DbStore 주입은 P15/P16 공유이나 테스트 헬퍼 분할 위험을 피해 확장분과 함께 본 커밋에 통합. Co-Authored-By: Claude Fable 5 <[email protected]>
- claims.ts: buildOrganizationClaims·parseOrganizationClaimConfig 공용화 — token(id_token)/userinfo 가 동일 로직·동일 config 적용. config null=전량 노출(하위호환), 필드별 false=해당 클레임 생략 - token: organization scope 요청 시 id_token 에도 조직 클레임 매핑(과거 누락 버그 B2 수정), buildTokens 에 organizationClaimConfig 전달 - userinfo: 인라인 매핑을 buildOrganizationClaims 로 대체(중복 제거), 발급 클라이언트 config 조회 - admin/oidc-clients/[id]: updateOrganizationClaims 액션 + 체크박스 UI(4필드 토글, 전량 켜짐=null 저장) - oidc-claims 유닛 테스트(config off/부분/하위호환 15케이스) - token 파일은 P15 rate-limit hunk 를 앞선 커밋에서 분리, 여기서는 조직 클레임 hunk 만. Co-Authored-By: Claude Fable 5 <[email protected]>
- docs/ADMIN_GUIDE.md 신설: 조직(dept/team/part/position) 관리, OIDC/SAML 클라이언트 등록, scope/role, 스킨 placeholder 6종, 서명키 회전, 감사로그 - admin/skins·skins/guide: placeholder 문서에 IDP_REGISTERED·IDP_PASSWORD_RESET·IDP_FLASH_MSG 3종 보강(실제값 정합) - mfa: 백업코드 통과 시 잔량 계산 → 저잔량(≤2) 경고·소진(0) 알림 메일(D4, TOTP 통과 시 미검사로 오탐 방지). rate-limit hunk(P15)와 분리해 백업 hunk 만. Co-Authored-By: Claude Fable 5 <[email protected]>
- followup-round3/PLAN.md·TODO.md: P12~P17 페이즈 설계와 페이즈별 실행 투두·검증 기록 Co-Authored-By: Claude Fable 5 <[email protected]>
…eat/saml-email-followup
- login-mfa 레이트리밋 테스트는 11회 순차 로그인마다 scrypt 검증 + 실패 타이밍 균등화 지연이 누적되어 CI 러너에서 기본 5s 를 초과하므로 per-test timeout 30s 로 상향 - gitleaks generic-api-key 오탐 3건(.gitleaksignore): 테스트 시드 비밀번호 · GCM 도메인 분리 라벨 · 테스트 상수 Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_0128wqZti9tnyoa4dyiX29tT
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
IdP 전반을 세 라운드(전수 감사 → 후속 → 마감)에 걸쳐 조사·기획·구현·독립 검증한 결과입니다. 각 페이즈는 작성/검증 레인을 분리해 독립 Opus 에이전트 검증을 통과했으며, 스키마 변경은 마이그레이션 생성까지만 하고 실제 적용은 분리했습니다.
요약
test·check(0 errors) ·lint·build·typecheck·db:check(4방언) 그린db:generate:allno-op(스키마=마이그레이션 정합)1차 — 전수 개선 감사
runAtomic원자화 · 서명키 캐시email_verified클레임 정상화) · 보안 알림 메일 9종 · admin 강제 로그아웃users/[id]분리 · admin 에러 i18n2차 — 후속
3차 — 마감
배포 전 필요 작업
docs/SECRET_ROTATION.md의 무중단 절차(IDP_SIGNING_KEY_SECRET_PREVIOUS병기 → 배포 →scripts/reencrypt-secrets.ts --apply→ PREVIOUS 제거).로드맵(미포함, 계획 문서에 기록)
DCR/PAR/Device Flow, Playwright e2e, Redis rate-limit store, HMAC 도메인 분리 —
docs/plans/**참조.🤖 Generated with Claude Code