LabHub
배우기 러닝패스 코스

Enterprise Authentication Integration

OIDC and SAML — What You Must Verify to Be Safe

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

OIDC 든 SAML 이든 안전을 만드는 것은 프로토콜이 아니라 수신측의 검증 목록이다 — 서명·iss·aud·exp·nonce 중 하나만 빠져도 그 구멍으로 들어올 수 있다.

Flow map: 수신측의 검증 목록 · 정상 흐름에서는 완벽하게 동작한다. · 목록을 알고 있느냐 · 왜 코드를 한 번 거치는가.

왜 이게 문제인가

연동은 검증을 빼도 정상 흐름에서는 완벽하게 동작한다. 로그인이 되고 화면이 뜨고 사용자 이름도 맞게 나온다. 그래서 개발 중에도, 통합 테스트에서도 아무 신호가 없다.

빠진 검증이 드러나는 시점은 둘뿐이다 — 보안 진단을 받을 때, 또는 사고가 났을 때. 서명을 안 보면 누구나 페이로드를 고쳐 아무 사용자로 들어올 수 있고, 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 로 직접 주면 브라우저 히스토리·리퍼러·로그에 토큰이 남는다. 인가 코드는 일회용이고 수명이 짧고, 실제 토큰은 서버 대 서버(백채널) 로 받는다. 그래서 안전하다.

statenonce 는 역할이 다르다. 자주 헷갈린다.

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 환경에서 이 성질이 이행 전략의 핵심이 된다.