infra
Platform

모듈 맵

[Infra Ops] SAML/OAuth 기반 싱글사인온 구성과 장애 분석

0 / 52 완료

펼치기
0 / 52 완료0%

Infra-ops · 27 / 52

[Infra Ops] SAML/OAuth 기반 싱글사인온 구성과 장애 분석

IdP/SP 구조, SAML ACS URL/Entity ID 설정, OAuth 2.0 흐름, SSO redirect loop 분석까지

🚨INCIDENT ALERT
HIGH

전사 SSO 도입 프로젝트에 투입됐습니다. AD FS를 IdP로 쓰고 우리 서비스가 SP 역할을 해야 합니다. SAML 설정을 잡았는데 로그인 후 계속 루프가 돌고, 분명 인증은 됐는데 앱에 로그인이 안 됩니다. 로그에는 아무 에러도 없습니다. 한편 모바일 앱 팀은 OAuth 2.0으로 소셜 로그인을 구현하는데 authorization code와 access_token이 왜 다른 단계에서 전달되는지 이해가 안 된다고 합니다.

이 모듈에서는 SAML 2.0 SP-initiated 흐름, SAML Response 분석 방법, OAuth 2.0 Authorization Code Flow, 그리고 SSO 장애의 대표 패턴을 다룹니다.

이번 챕터에서 배울 것
  • 1IdP/SP 역할과 SAML 2.0 SP-initiated 흐름 전체를 단계별로 설명할 수 있다
  • 2SAML Response를 base64 디코딩해 NameID, Attribute, 서명 정보를 분석할 수 있다
  • 3OAuth 2.0 Authorization Code Flow와 토큰 교환 과정을 설명할 수 있다
  • 4SSO redirect loop의 원인과 SameSite 쿠키 설정으로 해결할 수 있다
  • 5No InResponseTo 오류가 IdP-initiated 흐름에서 발생하는 이유를 설명할 수 있다

SAML 2.0 — 엔터프라이즈 SSO의 표준

💡개념

IdP/SP 구조와 SP-initiated 흐름

SAML(Security Assertion Markup Language)은 엔터프라이즈 SSO에서 가장 많이 쓰이는 표준입니다. 금융, 공공기관, 대기업의 내부 시스템 연동에 자주 사용됩니다. 처음 보면 용어가 낯설지만, 핵심은 "IdP가 인증하고, 그 결과를 서명된 XML로 SP에 전달한다"는 흐름입니다.

IdP/SP 구조와 SP-initiated 흐름

전사 SSO 도입 직후, 정상적으로 로그인됐다는 IdP 로그가 찍히는데 우리 앱에는 세션이 생기지 않아 무한 리다이렉트가 반복되는 장애가 납니다. 개발팀도, 보안팀도, AD 담당자도 각자 "우리 쪽 문제 아니다"라고 합니다. 원인을 찾으려면 IdP와 SP가 어떻게 신뢰를 주고받는지부터 이해해야 합니다. SAML은 XML 기반 어설션으로 인증 결과를 전달하는 표준이고, SP는 그 어설션을 검증해 세션을 만듭니다. IdP(Identity Provider)는 사용자를 인증하는 서버이고, SP(Service Provider)는 그 결과를 받아 서비스를 제공하는 우리 앱입니다.

SP-initiated SAML 흐름 (가장 일반적인 방식):

1. 사용자가 SP 서비스 접근 (https://our-app.com/dashboard)
       │
       ▼
2. SP가 미인증 감지 → AuthnRequest 생성 → IdP로 리다이렉트
   https://idp.company.com/saml/sso?SAMLRequest=BASE64_ENCODED_XML
       │
       ▼
3. 사용자가 IdP에서 로그인 (ID/PW 입력)
       │
       ▼
4. IdP가 SAML Response(어설션) 생성 → SP ACS URL로 POST
   POST https://our-app.com/saml/acs
   SAMLResponse=BASE64_ENCODED_XML (서명된 XML)
       │
       ▼
5. SP가 서명 검증 → 세션 생성 → 원래 URL로 리다이렉트
   → 사용자가 /dashboard에 접근 성공

SAML 설정 시 SP가 IdP에 제공해야 할 정보:

항목예시설명
Entity IDhttps://our-app.com/saml/metadataSP의 고유 식별자 (URI 형식)
ACS URLhttps://our-app.com/saml/acsSAML Response를 받을 엔드포인트
SLO URLhttps://our-app.com/saml/slo싱글 로그아웃 엔드포인트
SP 인증서X.509 공개키IdP가 SP로 보내는 어설션 암호화용

IdP가 SP에 제공하는 정보:

항목설명
IdP Entity IDIdP 고유 식별자
SSO URLSP가 AuthnRequest를 보낼 IdP 로그인 URL
IdP 인증서SP가 SAML Response 서명 검증에 사용
메타데이터 XML위 정보 전체를 담은 파일
💡개념

SAML 핵심 설정 — Spring Security SAML 예시

SAML 연동에서 인증이 안 된다는 장애의 절반 이상은 SP 설정의 Entity ID나 ACS URL이 IdP에 등록된 값과 조금이라도 다른 것이 원인입니다. IdP는 어설션을 보낼 대상을 이 두 값으로 엄격하게 검증하기 때문에, 공백 한 칸이나 마지막 슬래시 하나 차이도 전체 인증 실패로 이어집니다. 특히 Nginx 리버스 프록시 앞단이 있는 환경에서는 X-Forwarded-Proto 헤더가 없으면 ACS URL이 https:// 대신 http://로 생성돼 불일치가 발생하는 패턴이 매우 흔합니다.

YAML
# application.yml (Spring Boot + Spring Security SAML)
spring:
  security:
    saml2:
      relyingparty:
        registration:
          adfs:
            entity-id: https://our-app.com/saml/metadata   # SP Entity ID
            asserting-party:
              metadata-uri: https://idp.company.com/FederationMetadata/2007-06/FederationMetadata.xml
            acs:
              location: "{baseUrl}/saml/acs"                # ACS URL
            signing:
              credentials:
                - private-key-location: classpath:saml/sp-private.key
                  certificate-location: classpath:saml/sp-cert.crt

Nginx에서 SAML ACS 경로 프록시 설정:

Nginx
# SP 앞에 Nginx가 있는 경우 ACS URL이 정확히 프록시 되어야 함
location /saml/ {
    proxy_pass http://backend:8080;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    # X-Forwarded-Proto를 SP가 읽어 ACS URL에 https를 붙여야 함
    # 미설정 시 ACS URL이 http://로 생성되어 IdP와 불일치
}

OAuth 2.0 Authorization Code 흐름

SAML Response 분석

💡개념

base64 디코딩으로 SAML Response 내용 확인하기

SAML Response는 base64로 인코딩된 XML입니다. 로그인 문제를 디버깅할 때 브라우저 개발자 도구의 Network 탭에서 ACS URL로의 POST 요청을 찾아 SAMLResponse 값을 디코딩하면 실제 어설션 내용을 볼 수 있습니다.

SAML Response 분석 명령어:

로컬 터미널
# 브라우저 Network 탭에서 ACS POST 요청의 SAMLResponse 값을 복사 후 분석
# URL 인코딩된 경우 먼저 디코딩 필요

SAML_RESPONSE="BASE64_ENCODED_SAML_RESPONSE_HERE"

# base64 디코딩 후 XML 포맷팅
echo "$SAML_RESPONSE" | base64 -d | xmllint --format - 2>/dev/null | head -80

# NameID (사용자 식별자) 확인
echo "$SAML_RESPONSE" | base64 -d | xmllint --format - 2>/dev/null \
  | grep -E "NameID|NameIdentifier"
# 출력 예: <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">
#            user@company.com
#          </saml:NameID>

# Attribute (역할, 부서 등 추가 정보) 확인
echo "$SAML_RESPONSE" | base64 -d | xmllint --format - 2>/dev/null \
  | grep -A 3 "Attribute"
# 출력 예: <saml:Attribute Name="role">
#            <saml:AttributeValue>admin</saml:AttributeValue>
#          </saml:Attribute>

# InResponseTo 확인 (SP-initiated는 있어야 함)
echo "$SAML_RESPONSE" | base64 -d | xmllint --format - 2>/dev/null \
  | grep "InResponseTo"

# 어설션 유효 시간 확인 (서버 시간 차이 문제 진단)
echo "$SAML_RESPONSE" | base64 -d | xmllint --format - 2>/dev/null \
  | grep -E "NotBefore|NotOnOrAfter"
# NotBefore와 NotOnOrAfter 사이여야 유효
# 서버 시간이 5분 이상 차이나면 어설션이 expired로 거부됨

서버 시간 동기화 확인:

로컬 터미널
# NTP 동기화 상태 확인
timedatectl status
# "System clock synchronized: yes"가 있어야 함

# IdP와 SP 서버 시간 차이 허용 범위: 보통 ±5분
# 차이가 크면 "Assertion not yet valid" 또는 "Assertion expired" 에러
date -u     # SP 서버 UTC 시간

OAuth 2.0 — API 연동과 소셜 로그인

💡개념

Authorization Code Flow — 왜 두 단계인가

OAuth 2.0은 SAML보다 가볍고 REST API 친화적인 인가 프레임워크입니다. 소셜 로그인(Google, Kakao, Naver), 모바일 앱 인증, API 연동에 광범위하게 사용됩니다.

Authorization Code Flow 전체 흐름:

1. 사용자가 "구글로 로그인" 클릭
       │
       ▼
2. 앱 백엔드가 Authorization URL로 리다이렉트
   https://accounts.google.com/oauth/authorize
     ?client_id=CLIENT_ID
     &redirect_uri=https://our-app.com/oauth/callback
     &response_type=code
     &scope=openid email profile
     &state=RANDOM_STATE_VALUE   ← CSRF 방지용
       │
       ▼
3. 사용자가 구글에서 로그인 + 권한 동의
       │
       ▼
4. 구글이 authorization code를 redirect_uri로 전달
   GET https://our-app.com/oauth/callback?code=AUTH_CODE&state=STATE
       │ (authorization code는 단기 유효, 1회용)
       ▼
5. 백엔드 서버가 code + client_secret으로 token 교환
   POST https://accounts.google.com/oauth/token
   { code, client_id, client_secret, redirect_uri, grant_type }
       │ (client_secret은 서버에만 있어 안전)
       ▼
6. Token 서버가 access_token + refresh_token 반환
       │
       ▼
7. 백엔드가 access_token으로 사용자 정보 조회
   GET https://www.googleapis.com/userinfo/v2/me
   Authorization: Bearer ACCESS_TOKEN

token 교환 curl 예시:

로컬 또는 서버
# 5단계: authorization code → access_token 교환
curl -X POST https://auth.example.com/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=AUTH_CODE_FROM_STEP4" \
  -d "client_id=my-client-id" \
  -d "client_secret=my-client-secret" \
  -d "redirect_uri=https://our-app.com/oauth/callback"

# 응답 예시:
# {
#   "access_token": "eyJhbGciOiJ...",
#   "token_type": "Bearer",
#   "expires_in": 3600,
#   "refresh_token": "1//0eGxH...",
#   "scope": "openid email profile"
# }

# access_token으로 사용자 정보 조회
curl https://www.googleapis.com/userinfo/v2/me \
  -H "Authorization: Bearer ACCESS_TOKEN"

# refresh_token으로 access_token 갱신 (만료 후)
curl -X POST https://auth.example.com/oauth/token \
  -d "grant_type=refresh_token" \
  -d "refresh_token=REFRESH_TOKEN" \
  -d "client_id=my-client-id" \
  -d "client_secret=my-client-secret"

access_token vs refresh_token:

항목access_tokenrefresh_token
유효 기간짧음 (1시간~수일)김 (30일~무기한)
용도API 호출 시 Bearer 토큰access_token 갱신
저장 위치메모리 또는 HTTP-only 쿠키안전한 서버 저장소
노출 시 위험API 권한 탈취장기 권한 탈취

Keycloak 기초 — 오픈소스 IdP

💡개념

Keycloak으로 로컬 IdP 구성하기

Keycloak은 Red Hat이 만든 오픈소스 IdP로, SAML과 OAuth/OIDC를 모두 지원합니다. 사내 SSO, 개발 환경 테스트용 IdP로 많이 사용됩니다.

Keycloak Docker 실행:

Docker
# 개발/테스트용 Keycloak 실행
docker run -p 8080:8080 \
  -e KEYCLOAK_ADMIN=admin \
  -e KEYCLOAK_ADMIN_PASSWORD=admin \
  quay.io/keycloak/keycloak:latest \
  start-dev

# http://localhost:8080/admin 접속 후 설정:
# 1. Realm 생성 (예: my-company)
# 2. Client 생성 → Client type: SAML 또는 OpenID Connect
# 3. Valid redirect URIs: https://our-app.com/*
# 4. 메타데이터 URL 확인: http://localhost:8080/realms/my-company/protocol/saml/descriptor

SAML 메타데이터 조회:

로컬 또는 서버
# IdP 메타데이터 XML 확인 (SP 설정에 필요한 IdP 정보 포함)
curl -s http://localhost:8080/realms/my-company/protocol/saml/descriptor \
  | xmllint --format - | grep -E "EntityID|SingleSignOn|X509Certificate"

# OIDC Well-known 설정 확인 (OAuth/OIDC 설정에 필요)
curl -s http://localhost:8080/realms/my-company/.well-known/openid-configuration \
  | python3 -m json.tool | grep -E "issuer|authorization_endpoint|token_endpoint"

실습 — SAML Response 분석과 OAuth Token 교환

1SAML Response base64 디코딩 분석

브라우저 개발자 도구에서 복사한 SAMLResponse 값을 분석합니다.

로컬 터미널
# 브라우저 Network 탭 → ACS URL POST 요청 → Form Data → SAMLResponse 값 복사

SAML_RESPONSE="여기에_복사한_BASE64_값_붙여넣기"

# NameID와 Attribute 확인
echo "$SAML_RESPONSE" | base64 -d | xmllint --format - 2>/dev/null \
  | grep -E "NameID|Attribute|NotBefore|NotOnOrAfter|InResponseTo"

# 어설션 유효 시간 확인
echo "$SAML_RESPONSE" | base64 -d | xmllint --format - 2>/dev/null \
  | grep -E "NotBefore|NotOnOrAfter"
# 현재 서버 시간이 이 범위 안에 있어야 로그인 성공
date -u   # 현재 UTC 시간 확인
echo 'BASE64_STRING' | base64 -d | xmllint --format - | grep -E 'NameID|Attribute'
🔍실행 후 확인할 것
  • base64 디코딩 후 유효한 XML이 출력되는가
  • NameID에 사용자 이메일 또는 사용자명이 들어있는가
  • NotBefore와 NotOnOrAfter 범위에 현재 시간이 포함되는가
  • SP-initiated 흐름이면 InResponseTo 속성이 있는가
2OAuth token 교환 curl 테스트

OAuth Authorization Code를 access_token으로 교환합니다. 실제 OAuth 서버(Keycloak 또는 소셜 로그인 제공자) 정보로 교체합니다.

로컬 또는 서버
# Keycloak 로컬 테스트용 token 교환
curl -X POST http://localhost:8080/realms/my-company/protocol/openid-connect/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=AUTH_CODE" \
  -d "client_id=my-client" \
  -d "client_secret=my-secret" \
  -d "redirect_uri=https://our-app.com/oauth/callback"

# access_token JWT 디코딩 (내용 확인)
ACCESS_TOKEN="받은_ACCESS_TOKEN"
echo "$ACCESS_TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool
# payload에 sub(사용자 ID), exp(만료시간), scope 등 확인
curl -X POST https://auth.example.com/oauth/token -d 'grant_type=authorization_code&code=AUTH_CODE&client_id=CLIENT&redirect_uri=...'
🔍실행 후 확인할 것
  • token 교환 응답에 access_token이 포함됐는가
  • expires_in 값이 적절한가 (3600 = 1시간)
  • access_token JWT의 payload를 디코딩해서 sub(사용자 ID)가 보이는가
  • scope가 요청한 권한(email profile 등)을 포함하는가

트러블슈팅

원인: SP가 로그인 완료 후 세션 쿠키를 브라우저에 저장했지만, 다음 요청 시 브라우저가 쿠키를 전달하지 않아 SP가 미인증으로 판단합니다. 주로 IdP가 다른 도메인에 있을 때 Cross-Site 리다이렉트 후 쿠키의 SameSite 속성 문제로 발생합니다.

로컬 터미널
# 브라우저 개발자 도구 Application → Cookies에서 세션 쿠키 확인
# SameSite=Strict 또는 SameSite=Lax이면 cross-site 리다이렉트 후 쿠키 미전달

# 앱 서버 세션 쿠키 설정 확인 (Tomcat 예시)
grep -r "SameSite\|Secure\|HttpOnly" /opt/tomcat/conf/context.xml

# Spring Boot의 경우 application.yml 확인
grep -A 5 "cookie" application.yml

# 해결: SameSite=None; Secure 설정 (HTTPS 필수)
# Tomcat context.xml
# <CookieProcessor sameSiteCookies="none" />

# Spring Boot application.yml
# server:
#   servlet:
#     session:
#       cookie:
#         same-site: none
#         secure: true

# Nginx에서 Set-Cookie 헤더에 SameSite 추가 (앱 수정 없이)
location /saml/acs {
    proxy_pass http://backend:8080;
    proxy_cookie_flags ~ secure samesite=none;
}

# 변경 후 브라우저 쿠키 삭제하고 재시도

원인: IdP-initiated 흐름으로 SAML Response가 왔는데 SP가 SP-initiated만 허용하도록 설정된 경우입니다. SP-initiated 흐름에서는 SP가 생성한 AuthnRequest의 ID가 Response의 InResponseTo에 담겨 오는데, IdP-initiated 응답에는 이 값이 없습니다. SP가 InResponseTo를 필수로 검증하면 거부합니다.

로컬 터미널
# SP 로그에서 에러 확인
grep -r "InResponseTo\|Replay\|unsolicited" /var/log/app/ | tail -20

# SAML Response에 InResponseTo 있는지 확인
echo "$SAML_RESPONSE" | base64 -d | xmllint --format - 2>/dev/null \
  | grep "InResponseTo"
# 없으면 IdP-initiated 응답

# 해결 방법 1: SP에서 IdP-initiated 허용 설정
# Spring Security SAML의 경우
# saml2.relyingparty.registration.adfs.asserting-party.want-authn-requests-signed=false
# 또는 unsolicited response 허용 설정

# 해결 방법 2: IdP에서 SP-initiated 흐름만 사용하도록 설정 변경
# (IdP 관리자와 협의)

# 해결 방법 3: SP가 AuthnRequest 생성 시 ID를 세션에 저장하고
# Response 수신 시 매핑 검증하는 로직 확인
💼
실무 맥락
현업 패턴

실제 업무에서 이 지식이 쓰이는 상황:

SSO 연계는 개발팀 혼자 해결하기 어렵습니다. IdP 관리자(AD 담당자, 정보보안팀), SP 개발팀, 인프라 팀이 함께 설정을 맞춰야 합니다.

SSO 연계 신규 구성 시 체크리스트:

로컬 또는 서버
# 1. SP 메타데이터 준비 및 IdP 담당자에게 전달
# Entity ID, ACS URL, SLO URL, SP 인증서

# 2. IdP 메타데이터 수신 후 SP 설정
curl -s https://idp.company.com/FederationMetadata/2007-06/FederationMetadata.xml \
  | xmllint --format - | grep -E "EntityID|SingleSignOnService|X509Certificate"

# 3. 서버 시간 동기화 확인 (SAML 어설션 유효 시간에 영향)
timedatectl status | grep "System clock synchronized"

# 4. SP 설정: Entity ID가 IdP에 등록된 것과 정확히 일치하는지
# 공백, 슬래시 끝 여부까지 동일해야 함

# 5. ACS URL이 HTTPS이고 외부에서 접근 가능한지 확인
curl -vI https://our-app.com/saml/acs 2>&1 | grep -E "HTTP/|SSL"

OAuth/OIDC 연동 디버깅:

로컬 또는 서버
# IdP Well-known 설정에서 정확한 엔드포인트 확인
curl -s https://idp.example.com/.well-known/openid-configuration \
  | python3 -m json.tool | grep endpoint

# state 값 불일치 시 (CSRF 방지 파라미터)
# 로그인 시작 시 생성한 state와 callback에서 받은 state 비교
# 불일치하면 CSRF 공격으로 간주하여 차단 → 세션 스토어 확인 필요

SSO는 한 번 잘 구성하면 사용자 경험이 크게 좋아지지만, 설정 불일치에 의한 장애는 원인 파악이 어렵습니다. SAML Response를 직접 디코딩해서 내용을 확인하는 능력만 있어도 대부분의 SSO 장애를 빠르게 해결할 수 있습니다. 다음 모듈에서는 SMTP 메일 발송과 SMS 게이트웨이 연동 — 운영 알림 채널 구성 실무를 다룹니다.

지식 확인

퀴즈 — 4문제

Q1

SAML 2.0에서 IdP와 SP의 역할을 올바르게 설명한 것은?

Q2

SSO 로그인 시 redirect loop가 발생하는 가장 흔한 원인은?

Q3

OAuth 2.0 Authorization Code Flow에서 access_token을 브라우저(프론트엔드)로 직접 보내지 않는 이유는?

Q4

SP-initiated와 IdP-initiated SAML 흐름의 차이는?

0 / 4 답변

🧪 실습으로 확인하기

Nginx 설치 및 기동

초급

Linux 서버에 Nginx를 설치하고 systemd 서비스로 등록하여 80포트에서 응답하는 상태까지 만든다.

30📋 3단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요

infra-ops입문 · 50
[Infra Ops] SFTP/배치 파일 송수신과 외부 기관 연계 실무
인프라 서비스 운영 트랙 계속
linux입문 · 30
[Linux] 개발자가 왜 리눅스 서버와 커맨드라인을 반드시 배워야 하는가
Linux 트랙 시작점