기업 인증 연동 · SSO 연동 실전 · 이론
OIDC 와 SAML — 무엇을 검증해야 안전한가
한 줄 요약
OIDC 든 SAML 이든 안전을 만드는 것은 프로토콜이 아니라 수신측의 검증 목록이다 — 서명·iss·aud·exp·nonce 중 하나만 빠져도 그 구멍으로 들어올 수 있다.
왜 이게 문제인가
연동은 검증을 빼도 정상 흐름에서는 완벽하게 동작한다. 로그인이 되고 화면이 뜨고 사용자 이름도 맞게 나온다. 그래서 개발 중에도, 통합 테스트에서도 아무 신호가 없다.
빠진 검증이 드러나는 시점은 둘뿐이다 — 보안 진단을 받을 때, 또는 사고가 났을 때. 서명을 안 보면 누구나 페이로드를 고쳐 아무 사용자로 들어올 수 있고, aud 를 안 보면 다른 클라이언트용으로 발급된 토큰이 우리 앱에서 통한다. 이건 구현 난이도의 문제가 아니라 목록을 알고 있느냐의 문제다.
OIDC Authorization Code 흐름
브라우저가 있는 웹 애플리케이션의 표준 흐름이다. 다섯 걸음이다.
1. 사용자가 우리 앱의 보호된 페이지 요청2. 앱 → 브라우저를 IdP 로 리다이렉트 GET /authorize?response_type=code&client_id=labhub-web &redirect_uri=https://app.example.com/callback &scope=openid profile email &state=<임의값>&nonce=<임의값>3. IdP 가 로그인 처리 후 브라우저를 우리 앱으로 리다이렉트 302 Location: https://app.example.com/callback?code=<인가코드>&state=<임의값>4. 앱(서버)이 백채널로 토큰 교환 POST /token grant_type=authorization_code&code=...&redirect_uri=... → { access_token, id_token, refresh_token, expires_in }5. 앱이 id_token 을 검증하고 세션을 만든다왜 코드를 한 번 거치는가. 토큰을 브라우저 URL 로 직접 주면
브라우저 히스토리·리퍼러·로그에 토큰이 남는다. 인가 코드는 일회용이고 수명이 짧고,
실제 토큰은 서버 대 서버(백채널) 로 받는다. 그래서 안전하다.
state 와 nonce 는 역할이 다르다. 자주 헷갈린다.
state— CSRF 방지. 요청을 시작한 그 브라우저 세션으로 돌아왔는지 확인.nonce— 토큰 재생(replay) 방지. ID 토큰 안에 같은 값이 들어 있는지 본다.
콜백에서 받은 state 가 내가 보낸 값과 같은지 본다.
누가 예전에 발급된 ID 토큰을 주워 와도 nonce 가 다르니 거절된다.
ID 토큰에서 반드시 검증할 것
ID 토큰은 JWT 다. 헤더.페이로드.서명 세 부분이 점으로 이어져 있고
각각 base64url 이다. 디코딩은 누구나 할 수 있다. 서명 검증이 안전의 전부다.
| 검증 항목 | 안 하면 무슨 일이 나는가 |
| --- | --- |
| 서명 | 누구나 페이로드를 고쳐 관리자로 로그인 |
| iss (발급자) | 공격자가 자기 IdP 로 발급한 토큰이 통과 |
| aud (대상) | 다른 앱용으로 발급된 토큰이 통과 (토큰 치환) |
| exp (만료) | 만료된 토큰이 영원히 유효 |
| nonce | 예전 토큰 재사용 |
| alg | alg: none 이나 알고리즘 혼동 공격 |
마지막 줄이 유명한 함정이다. JWT 라이브러리가 토큰이 스스로 밝힌 알고리즘을
믿고 검증하면, 공격자가 alg: none 으로 바꿔 서명 없이 통과시킬 수 있다.
검증 코드는 우리가 기대하는 알고리즘을 고정해야 한다.
시계 문제도 실무에서 반드시 만난다. exp/iat 검증에는
60~120초 정도의 clock skew 허용치를 둔다. NTP 가 어긋난 서버 한 대가
간헐적 로그인 실패를 만드는 장애는 원인 파악이 오래 걸린다.
SAML 2.0 — XML 이고, 서명이 전부다
SAML 은 XML 문서 하나(Response 안에 Assertion)를 브라우저 POST 로 전달한다.
구조는 이렇게 생겼다.
<samlp:Response Destination="https://app.example.com/saml/acs" InResponseTo="_req123" IssueInstant="..."> <saml:Issuer>https://idp.example.com/</saml:Issuer> <samlp:Status><samlp:StatusCode Value="...:Success"/></samlp:Status> <saml:Assertion> <saml:Issuer>https://idp.example.com/</saml:Issuer> <ds:Signature>...</ds:Signature> ← 서명 <saml:Subject> <saml:NameID Format="...emailAddress">hong@labhub.co.kr</saml:NameID> <saml:SubjectConfirmation>...</saml:SubjectConfirmation> </saml:Subject> <saml:Conditions NotBefore="..." NotOnOrAfter="..."> <saml:AudienceRestriction> <saml:Audience>https://app.example.com/sp</saml:Audience> </saml:AudienceRestriction> </saml:Conditions> <saml:AttributeStatement> <saml:Attribute Name="department"><saml:AttributeValue>개발팀</saml:AttributeValue></saml:Attribute> </saml:AttributeStatement> </saml:Assertion></samlp:Response>SP(우리 시스템)가 검증해야 할 것은 OIDC 와 대응된다.
1. 서명 — Response 또는 Assertion 에 유효한 서명이 있는가.
그리고 서명된 범위 안에 우리가 신뢰하는 데이터가 들어 있는가
2. Issuer — 우리가 등록한 IdP 인가
3. Audience — 우리 SP 를 대상으로 발급됐는가
4. NotBefore / NotOnOrAfter — 유효 시간인가 (clock skew 허용)
5. InResponseTo — 우리가 보낸 요청에 대한 응답인가
6. Destination — 우리 ACS URL 로 온 것인가
7. 재사용 방지 — 같은 Assertion ID 를 이미 쓴 적 있는가
1번의 두 번째 문장이 XML Signature Wrapping(XSW) 공격의 핵심이다.
공격자가 원본 Assertion 을 문서 어딘가에 숨겨 두고, 서명되지 않은 가짜 Assertion 을
파서가 읽는 위치에 넣는다. 서명 검증은 통과하는데 앱이 읽는 데이터는 가짜다.
그래서 "서명이 유효한가"만이 아니라 "내가 읽는 그 노드가 서명된 노드인가"를
확인해야 한다. 이게 어렵기 때문에 SAML 은 직접 구현하지 말고 검증된 라이브러리를 써야 한다.
SAML 인증서 만료 — 조용히 오는 전면 장애
SAML 은 IdP 의 서명 인증서를 SP 가 미리 등록해 둔다. 그 인증서가 만료되면
모든 사용자의 로그인이 동시에 실패한다. 실제로 대형 전환 프로젝트에서
서명 인증서 만료로 47분간 전면 로그인 장애가 난 사례가 있다.
방어는 단순하다. 인증서 만료 감시 목록에 SAML 서명 인증서를 반드시 넣는다.
서버 TLS 인증서만 감시하는 목록이 대부분이라 이게 빠진다.
OIDC vs SAML — 실무 선택
| 항목 | SAML 2.0 | OIDC |
| --- | --- | --- |
| 데이터 형식 | XML | JSON (JWT) |
| 전달 경로 | 주로 프론트채널(브라우저 POST) | 프론트+백채널 |
| 모바일/SPA | 불편 | 적합 (PKCE) |
| 메타데이터 교환 | XML 메타데이터 파일 | discovery 문서(JSON) |
| 국내 대기업/공공 | 여전히 다수 | 신규 도입 증가 |
| 구현 난도 | 높음 (XML 서명) | 중간 |
신규면 OIDC, 상대가 이미 SAML 이면 SAML. 그리고 재미있는 사실 하나 —
Keycloak 같은 IdP 에 OIDC 클라이언트와 SAML 클라이언트를 함께 등록하면,
같은 SSO 세션에서 한쪽은 JWT 를, 다른 쪽은 XML Assertion 을 받는다.
IdP 가 프로토콜 변환 허브 역할을 하는 것이다.
레거시와 신규가 공존하는 SI 환경에서 이 성질이 이행 전략의 핵심이 된다.