[oauth] SSO IdP 세션 도입 - #433
Open
ZaMan0806 wants to merge 4 commits into
Open
Conversation
로그인 상태를 IdP에 보관해 여러 클라이언트 간 재로그인 없이 인증이 이어지도록 한다. - IdpSessionRedisEntity: 로그인 세션 저장 (기본 8시간) - IdpSessionHandoffRedisEntity: BFF가 서버-투-서버로 POST를 호출해 응답에 쿠키를 심을 수 없으므로, 브라우저를 백엔드로 한 번 경유시키는 일회용 티켓 - OauthConsentJpaEntity: 클라이언트별 scope 동의 기록 - IssueAuthorizationCodeService: code 발급 로직을 GET/POST 양쪽에서 재사용하도록 추출 GET /authorize는 세션 쿠키가 유효하고 계정이 ACTIVE이며 미해소 정보 수정 요청이 없고 요청 scope가 동의 기록에 포함될 때만 로그인을 생략한다. 어느 조건이든 어긋나면 기존 로그인 플로우로 폴백해, 정보 수정 강제가 SSO로 우회되지 않도록 한다. POST /authorize의 Location은 SP redirect_uri에서 핸드오프 URL로 변경된다.
There was a problem hiding this comment.
Overview
- IdP(datagsm-server)에 SSO 세션을 도입하는 PR입니다.
- OAuth authorize 성공 시 세션과 일회용 핸드오프 티켓을 생성하고, 브라우저 경유 엔드포인트에서 세션 쿠키를 설정한 뒤 클라이언트로 리다이렉트하도록 변경합니다.
- 기존 “로그인 성공”과 “code 발급”을 분리하고, 세션/동의 상태가 있으면 로그인 없이 code를 발급하는 SSO 분기를 추가합니다.
Intent
- 여러 OAuth 클라이언트 간 재로그인을 줄이고 SSO 경험을 제공한다.
- 서버-투-서버 호출에서 브라우저 쿠키를 직접 심을 수 없는 문제를 일회용 티켓 핸드오프로 우회한다.
- 클라이언트별 scope 동의 기록을 남겨, 세션이 있어도 동의되지 않은 scope는 자동 승인되지 않도록 한다.
Risk
- 높음: OAuth 인증/인가 흐름, 세션, 쿠키, 리다이렉트, 동의 정책 등 보안 핵심 영역 변경.
- 집중 검토 지점:
POST /v1/oauth/authorize응답Location이 핸드오프 URL로 바뀌는 Breaking Change와 프론트/BFF 연동 상태- 세션 쿠키 도메인/Secure/SameSite 설정,
OAUTH_ISSUER_URL등 배포 환경변수 정확성 - 일회용 티켓 생성/소비/만료 처리와 중복 사용 방지
- SSO 우회 방지 조건(계정 상태, 정보수정요청, scope 동의)의 완결성
- 세션/코드 발급/동의 저장 간 트랜잭션·동시성 처리
- 기존 OAuth 플로우와의 호환성 및 회귀 영향
0 inline comment(s)
코드리뷰에서 발견된 보안 결함 3건을 수정한다. 핸드오프 티켓 단독 세션 발급 (세션 고정) - ticket은 URL에 노출되어 로그/Referer/브라우저 히스토리에 남으므로 그것만으로 세션 쿠키를 발급하면 티켓 탈취자가 피해자 세션을 획득한다. - verifier를 함께 발급해 해시(SHA-256)만 저장하고, 소비 시점에 대조한다. - 불일치 시에도 티켓을 즉시 폐기해 무차별 대입을 차단한다. 오픈 리다이렉트 - 저장된 redirectUrl을 검증 없이 Location에 반영하던 것을, 소비 시점에 client.redirectUrls로 재검증하도록 변경한다. - 접두사 비교는 도메인 위조를 허용하므로 구분자까지 확인해 정확히 일치시킨다. state URL 인코딩 누락 - state는 클라이언트가 임의 값을 넣는 CSRF 방어 값이라, 인코딩하지 않으면 '&' 삽입으로 파라미터 조작이 가능하다. - code/state 모두 URLEncoder로 인코딩한다. IssueAuthorizationCodeServiceTest를 신설하고, 핸드오프 테스트에 verifier 불일치/오픈 리다이렉트/접두사 위조 케이스를 추가한다.
snowykte0426
reviewed
Sep 9, 2026
교차 리뷰에서 드러난 두 가지 문제를 바로잡는다. verifier 분리의 한계 보완 - ticket과 verifier가 같은 URL 쿼리로 전달되므로, 로그·Referer·브라우저 히스토리에 노출되는 상황에서는 두 값이 항상 함께 새어나간다. 즉 verifier만으로는 애초 의도한 유출 방어가 성립하지 않는다. - 정상 흐름이 브라우저 최상위 내비게이션이라는 점을 이용해 Sec-Fetch-Site/Sec-Fetch-Mode로 그 형태만 통과시킨다. 나중에 URL을 입수해 fetch나 이미지 로드로 재사용하는 시도가 차단된다. - 헤더를 보내지 않는 구형 브라우저는 기본적으로 통과시켜 로그인을 막지 않으며, 엄격 모드는 설정으로 켤 수 있다. 티켓 삭제 DoS 제거 - verifier 불일치 시 티켓을 삭제하던 동작을 걷어낸다. - ticket만 아는 공격자가 아무 verifier로 요청 한 번을 보내 정상 사용자의 로그인을 무효화할 수 있었다. 탈취보다 적은 정보로 가능한 공격이라 무차별 대입 차단 이득보다 손실이 크다. 핸드오프 서비스가 JPA를 조회하므로 @transactional(readOnly = true)를 명시한다.
리뷰에서 지적된 항목을 반영한다. 세션 쿠키를 host-only로 고정 - 쿠키를 심는 곳과 읽는 곳이 모두 authorization 모듈의 같은 호스트라 Domain 확장이 필요 없다. 상위 도메인으로 넓히면 모든 서브도메인이 요청마다 세션 쿠키를 받게 되어, 하나만 침해돼도 SSO 전체가 넘어간다. - OAUTH_IDP_SESSION_COOKIE_DOMAIN 설정을 제거한다. SSO 경로 rate limit 누락 - GET 경로도 이제 인가 코드를 발급하는데 한도가 걸려 있지 않았다. - 한도 검사를 IssueAuthorizationCodeService로 옮겨 두 경로가 같은 정책을 따르게 한다. 동의 기록 동시성 - 같은 (account, client)로 첫 로그인이 동시에 들어오면 유니크 제약을 위반한다. 이 시점에는 code와 세션이 이미 Redis에 저장돼 롤백되지 않아 사용자만 500을 받았다. - DataIntegrityViolationException을 잡아 재조회 후 병합한다. 핸드오프 검증 순서 - 티켓을 지우기 전에 redirect_uri를 먼저 검증한다. 순서가 반대면 클라이언트 설정이 바뀐 순간 티켓만 소비되고 세션은 쓰이지 못한 채 남았다. - 검증 실패 시 티켓과 세션을 함께 정리한다. 그 외 - verifier 누락을 스프링 기본 400이 아닌 InvalidRequest로 통일 - sha256 해시를 OpaqueTokenHashUtil로 추출해 발급/검증이 갈라지지 않게 함 - matchesRegisteredRedirectUri를 Boolean 반환으로 단순화 - account.id를 requireNotNull로 바꿔 동의 기록이 조용히 누락되지 않게 함 - uk 최좌측 접두사와 중복되는 idx_oauth_consent_account_id 제거
Collaborator
Author
리뷰 반영 완료 (7eb1150)인라인 지적 11건을 모두 확인했습니다. 10건 수정, 1건 보류입니다. 수정한 항목
보류한 항목계정 삭제 시 동의 기록 정리 — 계정 삭제 경로가 코드베이스에 아직 없어 지금 넣을 대상이 없습니다. 놓치지 않도록 PR 본문 후속 과제에 명시했습니다. 검증
특히 verifier가 무의미하다는 지적과 동의 기록 실패 시점에 이미 Redis에 코드가 저장돼 있다는 지적은 제가 놓친 부분이었습니다. 짚어주셔서 감사합니다.
|
snowykte0426
approved these changes
Sep 9, 2026
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.
개요
OAuth 로그인 상태를 IdP(datagsm-server)에 보관해, 여러 클라이언트를 오갈 때 재로그인 없이 인증이 이어지도록 합니다.
기존에는 로그인 성공 결과가 어디에도 남지 않아(요청 1건에 대한 code 발급 후 종료) 다른 클라이언트로 이동하면 항상 다시 로그인해야 했습니다. 이 PR은 "로그인 성공"과 "code 발급"을 분리해 SSO를 가능하게 합니다.
변경 흐름
핸드오프 단계가 필요한 이유
POST /v1/oauth/authorize는 프론트 BFF가 서버-투-서버 fetch로 호출하므로, 이 응답에Set-Cookie를 실어도 브라우저가 아닌 Next.js 서버가 받게 됩니다. 따라서 브라우저가 최상위 내비게이션으로 백엔드를 한 번 경유하도록 일회용 티켓을 두고, 그 지점에서 세션 쿠키를 심습니다.보안 고려
StudentDataEditRequest가 있으면 code를 발급하지 않고 로그인 페이지로 폴백합니다.HttpOnly/Secure/SameSite=Lax/Max-Age적용.주요 변경 파일
신규
IdpSessionRedisEntity/IdpSessionRedisRepository— 로그인 세션 (기본 8시간)IdpSessionHandoffRedisEntity/IdpSessionHandoffRedisRepository— 일회용 핸드오프 티켓OauthConsentJpaEntity/OauthConsentJpaRepository— 클라이언트별 scope 동의 기록IssueAuthorizationCodeService(+Impl)— code 발급 로직을 GET/POST 양쪽에서 재사용하도록 추출CompleteIdpSessionHandoffService(+Impl)— 티켓 소비 및 쿠키 설정수정
StartOauthAuthorizeFlowServiceImpl— SSO 세션 분기 추가 (#431의 스코프 변경과 별개)CompleteOauthAuthorizeFlowServiceImpl— 세션·동의·핸드오프 생성, code 발급 위임OauthController— 쿠키 파라미터, 핸드오프 엔드포인트OauthEnvironment/application.yml— 세션·쿠키 설정 추가POST /v1/oauth/authorize의 응답Location이 변경됩니다.BFF가 이 Location을 가공 없이 그대로 브라우저에 반환하면 정상 동작합니다. 다만 다음에 해당하면 수정이 필요합니다.
redirect: 'follow'가 걸려 있으면 →redirect: 'manual'로 변경 필수 (서버가 리다이렉트를 따라가면 쿠키가 브라우저에 심기지 않습니다)code/state를 파싱해 쓰고 있으면 → 제거프론트 수정은 이 PR 머지 전에 완료될 예정입니다.
배포 환경변수
OAUTH_ISSUER_URL이 잘못되면 핸드오프 리다이렉트가 깨집니다. 나머지(세션 8시간, 쿠키명,Secure=true)는 기본값으로 충분합니다.테스트
후속 작업 (별도 PR)
id_token+openidscope, OIDC discovery 문서코드리뷰 반영 (보안 수정)
셀프 코드리뷰에서 발견한 결함 3건을 이 PR 내에서 수정했습니다.
1. 핸드오프 URL 재사용으로 세션이 탈취되던 문제 (CRITICAL)
ticket은 URL 쿼리라Location헤더·Referer·BFF 로그·브라우저 히스토리에 남습니다. 그것만으로 세션 쿠키를 내주면, 티켓을 입수한 공격자가 만료 전에 먼저 열어 피해자 계정의 SSO 세션을 획득할 수 있었습니다.처음에는
verifier를 추가 발급해 해시만 저장하는 방식으로 접근했으나, 교차 리뷰에서 이 방식이 목적을 달성하지 못한다는 점이 드러났습니다.ticket과verifier가 같은 URL 쿼리에 함께 실리므로, 위 채널들에서 둘은 항상 같이 노출됩니다. 비밀을 둘로 쪼갰지만 같은 봉투에 넣은 셈입니다.BFF가 서버-투-서버라 POST 응답에 쿠키를 심을 수 없다는 제약 때문에, 리다이렉트 URL만으로는 채널 분리가 원리적으로 불가능합니다. 그래서 요청의 형태를 검증하는 방향으로 전환했습니다.
Sec-Fetch-Site/Sec-Fetch-Mode로 브라우저 최상위 내비게이션만 허용fetch·<img>로 재사용하는 시도가 차단됨OAUTH_IDP_SESSION_HANDOFF_REQUIRE_FETCH_METADATA=trueverifier대조는 부분 유출 대비 보조 방어로 유지 (MessageDigest.isEqual)2. 오픈 리다이렉트 (HIGH)
저장된
redirectUrl을 검증 없이Location에 넣고 있었습니다. 화이트리스트 검증이 저장 이전 단계에만 있어 방어가 한 겹뿐이었습니다.client.redirectUrls로 재검증https://example.com.attacker.io같은 도메인 위조를 허용하므로, 이어지는 문자가?/&구분자인지까지 확인3.
stateURL 인코딩 누락 (HIGH)state는 클라이언트가 임의 값을 넣는 CSRF 방어 값이라, 인코딩 없이 붙이면&삽입으로 파라미터를 조작할 수 있었습니다.code/state모두URLEncoder로 인코딩테스트
IssueAuthorizationCodeServiceTest를 신설하고(6개), 핸드오프 테스트에 verifier 불일치·오픈 리다이렉트·접두사 위조·Sec-Fetch 차단 케이스를 추가했습니다. 모듈 전체 129개 테스트 통과.두 방어(Sec-Fetch 검증, 티켓 보존)는 뮤테이션 테스트로 실효성을 확인했습니다 — 검증을 제거하면 각각 6건, 1건이 실패합니다.
2차 교차 리뷰에서 함께 제거한 문제
티켓 삭제 DoS (HIGH) — verifier 불일치 시 티켓을 삭제하던 동작을 걷어냈습니다.
ticket만 아는 공격자가 아무 값이나 verifier로 넣어 요청 한 번으로 정상 사용자의 로그인을 무효화할 수 있었습니다. 탈취(두 값 모두 필요)보다 적은 정보로 가능한 공격이라, 무차별 대입 차단 이득보다 손실이 컸습니다.남은 후속 과제
로그아웃 부재, 비밀번호 변경 시 세션 무효화, 동의 화면 UI는 별도 PR로 진행합니다.
2차 리뷰에서 추가로 확인된 항목(별도 PR 예정):
recordConsent의 동시성 — 인가 코드가 Redis에 발급된 뒤 consent를 기록하는데,@Transactional은 Redis를 롤백하지 않아 동시 요청 시 코드는 발급된 채 500이 반환될 수 있음idpSessionRedisRepository에delete호출이 없어 쿠키 유출 시 8시간 동안 서버에서 끊을 수 없음resolveSsoAccount의 6중 폴백에 진단 로그 부재 (폴백 자체는 안전한 설계로 확인됨)tb_oauth_consent행도 함께 정리해야 함 —accountId가 FK 없는 단순 컬럼이라 자동 정리되지 않음 (현재 계정 삭제 경로 자체가 없어 이번 PR 범위 밖)