13년차의 서버실

서버, 인프라, 홈랩 기술 블로그

[태그:] 인증 보안

  • [보안] MFA 도입 문제 해결: 기업 2FA 트러블슈팅 및 운영 방법

    [보안] MFA 도입 문제 해결: 기업 2FA 트러블슈팅 및 운영 방법

    [보안] MFA 도입 문제 해결: 기업 2FA 트러블슈팅 및 운영 방법

    기업에서 MFA 도입 문제를 한 번이라도 겪어보신 분들은 아실 겁니다. 보안팀은 분명 좋은 의도로 다단계 인증을 붙였는데, 현업에서는 갑자기 로그인 실패가 늘고 헬프데스크 티켓이 쏟아지거든요. 저도 처음엔 “보안 강해졌으면 끝 아닌가?” 싶었는데, 실제로 운영해보니 그 다음부터가 시작이더라고요. 특히 재택근무, 개인 휴대폰 사용, 해외 출장, VPN(Virtual Private Network, 가상사설망) 환경이 섞이면 작은 설정 하나가 사용자 불만으로 바로 이어졌습니다.

    이번 글은 특정 벤더 제품 홍보가 아니라, 기업 MFA/2FA 도입 후 실제로 자주 터지는 문제를 어떻게 정리하고 풀어갔는지를 사례 중심으로 정리한 글입니다. 2FA 트러블슈팅이 필요한 분, 사용자 경험 개선과 인증 보안 사이에서 균형점을 찾고 싶은 분들께 도움이 될 겁니다.

    MFA 도입 문제를 설명하는 기업 인증 아키텍처 다이어그램

    사내 IdP(Identity Provider, 인증 제공자), 사용자 디바이스, OTP 앱, VPN, 헬프데스크까지 연결된 전체 흐름을 보여주는 이미지입니다.

    MFA가 왜 이렇게 자주 욕을 먹는가

    MFA(Multi-Factor Authentication, 다단계 인증)는 쉽게 말해 비밀번호 하나만으로는 부족하니 추가 증명 수단을 더 붙이자는 개념이거든요. 보통 아래 조합으로 구성됩니다.

    • 알고 있는 것: 비밀번호, PIN
    • 가지고 있는 것: OTP 앱, 보안키(Security Key, 물리 보안키), 푸시 인증 기기
    • 자기 자신인 것: 생체인증

    문제는 여기서 끝이 아니라는 점입니다. 사용자는 로그인 한 번 더 하면 되는 줄 아는데, 운영자 입장에서는 시간 동기화, 백업 코드, 기기 교체, 예외 정책, 네트워크 지연, 프록시 헤더, 브라우저 호환성까지 챙겨야 하거든요. 실제로 써보니까 보안 기능 자체보다 운영 시나리오 설계가 더 중요했습니다.

    현업에서 많이 나오는 MFA 도입 문제 패턴

    1. OTP 코드는 맞는데 계속 틀렸다고 나오는 경우
    2. 사용자 휴대폰 교체 후 2FA 복구가 안 되는 경우
    3. 푸시 인증이 해외망이나 사내망 제한으로 지연되는 경우
    4. VPN 로그인과 SaaS 로그인에서 정책이 달라 혼선이 생기는 경우
    5. 신규 입사자 초기 등록 과정이 복잡해서 첫날부터 막히는 경우

    여기서 중요한 포인트! 사용자 불만의 대부분은 보안 자체가 아니라 등록과 복구 과정에서 발생합니다. 저도 처음엔 인증 실패 로그만 봤는데, 막상 인터뷰해보니 “어떻게 해야 하는지 모르겠다”가 더 많았어요.

    MFA 인증 흐름 이해: 구성 요소와 장애 지점

    제가 현장에서 설명할 때는 복잡한 용어를 줄이고 이렇게 풀었습니다. 로그인 체인은 대체로 사용자 – 애플리케이션 – IdP(Identity Provider, 인증 제공자) – MFA 수단으로 이어집니다. 여기서 어느 한 군데라도 시간이 안 맞거나 세션 정보가 꼬이거나, 리다이렉트(Redirect, 로그인 후 재이동)가 잘못되면 사용자는 그냥 “로그인이 안 된다”고 느끼게 됩니다.

    구성 요소 역할 문제 발생 시 증상
    IdP 사용자 본인 확인과 정책 적용 로그인 루프, 정책 오적용
    TOTP 시간 기반 OTP 생성 코드 불일치, 시간 오차
    Push MFA 앱 푸시 승인 지연, 알림 미수신
    WebAuthn 브라우저 기반 강한 인증 기기/브라우저 호환성 이슈
    VPN/Proxy 접속 경로와 헤더 전달 위치 기반 정책 오판, 세션 실패
    Helpdesk 복구와 예외 처리 티켓 폭증, 우회 절차 남발

    MFA 도입 문제를 줄이기 위한 사전 설계

    이 단계는 나중에 삽질을 줄여줍니다. 저는 실제로 아래 4가지를 먼저 정리한 뒤부터 장애가 확 줄었습니다.

    1. 등록(Enrollment): 첫 로그인 시 무엇을 먼저 등록하게 할지 정합니다.
    2. 복구(Recovery): 휴대폰 분실, 번호 변경, 앱 삭제 시 절차를 정합니다.
    3. 예외(Exception): 임원, 현장직, 공유 단말 사용자의 예외 정책을 문서화합니다.
    4. 지원(Support): 헬프데스크가 확인할 체크리스트를 만듭니다.

    특히 TOTP(Time-based One-Time Password, 시간 기반 일회용 비밀번호)만 강제하면 단순해 보이지만, 휴대폰 교체 시 복구 경험이 나빠질 수 있습니다. 반대로 WebAuthn(웹인증)이나 보안키를 섞으면 보안은 좋아지는데 초기 등록 안내가 어려워질 수 있죠. 결국 정답은 하나가 아니라 조직의 사용자 층에 맞는 조합입니다.

    실전 구현: 운영자가 먼저 점검해야 할 기본 항목

    제가 직접 해보니 사용자 계정 문제로 보였던 장애의 상당수가 사실은 인프라 설정 문제였습니다. 그래서 헬프데스크에 넘기기 전에 서버 쪽부터 확인하는 루틴을 만들었습니다.

    1. 시간 동기화 확인

    TOTP 기반 2FA 트러블슈팅에서 가장 먼저 볼 것은 시간입니다. 서버 시간이 몇십 초만 틀어져도 인증 실패가 반복되거든요.

    timedatectl status
    chronyc tracking
    chronyc sources -v

    출력에서 시스템 시간과 NTP(Network Time Protocol, 네트워크 시간 동기화) 상태를 확인합니다. 만약 시간 소스가 불안정하다면 아래처럼 서비스 상태도 같이 봅니다.

    systemctl status chronyd
    journalctl -u chronyd --since "-30 min"

    처음엔 사용자 OTP 앱이 이상한 줄 알고 계정만 붙잡고 있었는데, 알고 보니 가상화 환경에서 호스트 시간 드리프트(Time Drift, 시간 밀림)가 있었던 적도 있었습니다. 그때 드디어 됐다! 했던 기억이 아직도 선명하네요.

    2. 리버스 프록시 헤더 확인

    사내 SSO(Single Sign-On, 통합 로그인) 앞단에 Reverse Proxy(리버스 프록시)가 있으면 원본 IP, 프로토콜, 호스트 헤더가 제대로 전달되는지 꼭 봐야 합니다. 위치 기반 정책이나 신뢰 디바이스 정책이 이 값에 의존하는 경우가 많습니다.

    proxy_headers:
      x_forwarded_for: true
      x_forwarded_proto: true
      x_forwarded_host: true
      preserve_host: true
    session:
      cookie_secure: true
      same_site: Lax

    벤더별 설정 형식은 다르지만 핵심은 비슷합니다. HTTPS 종료 지점이 어디인지, 애플리케이션이 실제 외부 URL을 정확히 인지하는지가 중요합니다.

    MFA 도입 문제 해결을 위한 시간 동기화와 OTP 등록 구성 이미지

    인증 실패 원인을 서버 시간, 프록시, 사용자 등록 단계로 나눠 확인하는 흐름을 시각화한 이미지입니다.

    3. 등록 절차를 단계별로 최소화

    사용자 경험 개선 측면에서 제일 효과 있었던 건 화면 개수 줄이기였습니다. 등록 화면에서 한 번에 너무 많은 선택지를 보여주면 사용자들이 멈춥니다. 그래서 저는 보통 이렇게 안내했습니다.

    1. 비밀번호 로그인 완료
    2. 기본 MFA 수단 1개 등록
    3. 백업 수단 1개 추가 등록
    4. 백업 코드 저장 확인

    여기서 백업 코드를 빼먹으면 나중에 헬프데스크가 고생합니다. 진짜 많이요.

    4. 로그에서 먼저 봐야 할 필드

    인증 보안 로그는 길기만 하고 실무에서 바로 못 쓰는 경우가 많습니다. 저는 아래 필드만 먼저 보라고 팀에 공유해뒀습니다.

    • 사용자 ID
    • 인증 방식: TOTP, Push, WebAuthn 등
    • 실패 사유 코드
    • 클라이언트 IP와 User-Agent
    • 정책 매칭 결과
    • 시간대와 서버 시각
    grep "mfa" /var/log/auth.log | tail -n 50

    리눅스 표준 인증 로그만으로는 부족할 수 있지만, 최소한 최근 실패 흐름은 빠르게 볼 수 있습니다.

    ⚠️ 실제로 많이 겪는 2FA 트러블슈팅 사례

    이제부터는 현장에서 자주 만나는 케이스입니다. 혹시 이런 경험 있으신가요? 증상은 비슷한데 원인은 꽤 다르게 나옵니다.

    사례 1. OTP는 맞는데 계속 실패

    증상: 사용자는 앱에 뜬 코드를 정확히 넣었다고 하는데 실패합니다.

    원인: 서버 시간 불일치, 사용자 휴대폰 시간 자동 보정 해제, OTP 등록 시점의 QR 재사용.

    해결:

    1. 서버 NTP 상태 확인
    2. 사용자 휴대폰 시간 자동 설정 확인 안내
    3. 기존 시크릿(Secret, OTP 공유 비밀값) 폐기 후 재등록

    저도 처음엔 사용자가 코드를 잘못 넣는다고 생각했었는데, 실제로는 휴대폰 시간이 수동으로 바뀌어 있던 케이스가 있었어요. 이런 건 로그만 봐서는 잘 안 보입니다.

    사례 2. 휴대폰 교체 후 로그인 불가

    증상: 새 휴대폰으로 OTP 앱을 옮겼는데 코드가 다릅니다.

    원인: 앱 데이터 이전만 하고 실제 계정 재등록을 안 했거나, 클라우드 동기화가 조직 정책과 충돌.

    해결:

    • 백업 코드 또는 보조 인증 수단으로 임시 로그인
    • 기존 MFA 디바이스 해제
    • 신규 기기 재등록
    • 헬프데스크에서 본인 확인 후 복구 승인

    여기서 중요한 포인트! 복구 정책이 없으면 보안은 강해도 운영은 망가집니다. 그래서 저는 최소 2개 수단 등록을 권장했습니다. 예를 들면 TOTP + 백업 코드, 혹은 TOTP + 보안키 같은 조합이죠.

    사례 3. 푸시 인증이 너무 늦게 옴

    증상: 푸시는 오긴 오는데 30초, 1분 뒤에 도착합니다.

    원인: 배터리 최적화, 모바일 OS 알림 제한, 해외망 지연, 기업 MDM(Mobile Device Management, 모바일 단말 관리) 정책 충돌.

    해결:

    1. 푸시만 강제하지 말고 TOTP 대체 수단 제공
    2. 모바일 앱의 알림 권한과 배터리 예외 설정 점검
    3. 네트워크 품질 낮은 지역 사용자에게는 보안키 옵션 검토

    사실 푸시는 편한데, 네트워크 품질에 너무 기대는 면이 있습니다. 특히 글로벌 조직이면 더 그렇고요.

    사례 4. VPN에서는 되는데 SaaS에서는 막힘

    증상: 사내 VPN 로그인은 되는데 메일이나 협업툴 SSO는 실패합니다.

    원인: 애플리케이션별 정책 차이, Conditional Access(조건부 접근) 규칙 불일치, 시간대별 위험 로그인 탐지 민감도 차이.

    해결:

    • 정책을 앱별로 따로 만들었다면 우선순위 재검토
    • 공통 베이스라인 정책 정의
    • 예외 그룹을 줄이고 만료일이 있는 예외만 허용

    이거 진짜 자주 나옵니다. 정책을 빠르게 붙이다 보면 서비스별 예외가 쌓이거든요. 나중엔 운영자도 왜 막히는지 헷갈립니다 ㅎㅎ

    사용자 경험 개선을 위해 제가 바꿨던 것들

    사용자 경험 개선은 거창한 UI 개편보다 작은 문구 수정과 절차 정리에서 시작됐습니다. 실제로 효과 있었던 항목만 적어보겠습니다.

    개선 전 개선 후 체감 효과
    인증 실패: 다시 시도 시간 자동 설정 확인 후 다시 시도 불필요한 문의 감소
    MFA 등록 1개만 요구 기본 수단 + 백업 수단 등록 복구 문의 감소
    예외 정책 무기한 허용 만료일 있는 예외 정책 운영 통제 강화
    헬프데스크 개별 대응 표준 체크리스트 배포 응답 속도 향상

    저는 안내 문구도 꽤 많이 손봤습니다. 예를 들어 “코드가 올바르지 않습니다” 대신 “휴대폰 시간 자동 설정을 확인한 뒤 새 코드를 입력하세요”처럼 다음 행동이 보이도록 바꿨죠. 별거 아닌 것 같은데, 이런 차이가 티켓 수를 꽤 줄여줬습니다.

    MFA 도입 문제 완화를 위한 헬프데스크 체크리스트와 사용자 경험 개선 이미지

    운영자 체크리스트, 복구 절차, 사용자용 안내 문구 개선 포인트를 비교해 보여주는 이미지입니다.

    검증: 도입 후 무엇을 확인해야 하나

    구성이 끝났다고 바로 안심하면 안 됩니다. MFA 도입 문제는 보통 배포 직후보다 2주에서 4주 사이에 많이 드러납니다. 신규 등록, 휴가 후 복귀, 단말 교체, 출장 같은 이벤트가 그때 몰리거든요.

    운영 검증 체크리스트

    1. 성공/실패 로그인 비율 확인
    2. MFA 등록 완료율 확인
    3. 복구 요청 건수와 원인 분류
    4. 예외 정책 사용 빈도 확인
    5. 시간 동기화 경고 이벤트 확인
    # 예시: 최근 인증 실패 건수 집계
    journalctl --since "-24 hours" | grep -i "mfa failed" | wc -l
    
    # 예시: 시간 동기화 관련 경고 확인
    journalctl --since "-24 hours" | grep -Ei "ntp|chrony|time sync"

    실제로 써보니까 성공률 숫자 하나만 보면 안 되더라고요. 성공률은 높아도 복구 요청이 급증하면 이미 사용자 경험은 나빠진 상태일 수 있습니다.

    MFA 도입 문제 검증을 위한 인증 성공률과 복구 요청 대시보드

    인증 성공률과 실패 사유, 복구 요청 변화 추이를 한 번에 볼 수 있는 결과 검증용 대시보드 이미지입니다.

    정리: 보안과 편의성은 대립만 하는 게 아닙니다

    이번 사례에서 제가 배운 건 명확했습니다. MFA 도입 문제는 기능을 켜서 생기는 게 아니라, 등록과 복구, 예외 정책을 설계하지 않아서 커진다는 점입니다. 다단계 인증 자체는 이미 표준에 가깝지만, 사용자가 불편하다고 느끼는 순간은 늘 운영 디테일에서 나옵니다.

    정리하면 이렇습니다.

    • 시간 동기화는 TOTP 운영의 기본입니다.
    • 복구 절차가 없으면 헬프데스크가 병목이 됩니다.
    • 백업 수단 등록은 선택이 아니라 사실상 필수입니다.
    • 정책 일관성이 없으면 VPN과 SaaS 사이에서 혼선이 생깁니다.
    • 안내 문구와 체크리스트만 잘 정리해도 사용자 불만이 크게 줄어듭니다.

    저도 처음엔 보안 정책을 얼마나 강하게 걸지에만 집중했었는데, 결국 오래 버티는 구성은 사용자가 스스로 해결 가능한 구조였습니다. 다음 글에서는 보안키(Security Key)와 TOTP를 함께 운영할 때 어떤 기준으로 그룹을 나누면 좋은지 다뤄볼 예정입니다. 이전 글에서 다뤘던 SSO 정책 정리 방법과 함께 보시면 흐름이 더 잘 잡히실 겁니다.

    MFA 도입 문제 운영 개선 포인트를 정리한 인포그래픽

    등록, 복구, 예외, 검증의 네 축으로 MFA 운영 개선 포인트를 정리한 요약 인포그래픽입니다.

    자주 묻는 질문

    Q1. TOTP만 써도 충분한가요?

    환경에 따라 다릅니다. 기본 수단으로는 충분할 수 있지만, 복구용 백업 수단은 꼭 함께 준비하는 게 좋습니다.

    Q2. 사용자 불만이 많으면 MFA 강제를 늦춰야 하나요?

    무조건 늦추기보다, 파일럿 그룹에서 실패 원인과 복구 절차를 먼저 다듬는 쪽이 낫습니다.

    Q3. 2FA 트러블슈팅에서 제일 먼저 볼 것은 뭔가요?

    저는 항상 시간 동기화, 사용자 등록 상태, 예외 정책 순서로 봅니다. 이 세 가지에서 의외로 많이 걸립니다.

  • [보안] OAuth 2.0 보안 허점 파헤치기: 실제 공격 사례와 방어 전략

    [보안] OAuth 2.0 보안 허점 파헤치기: 실제 공격 사례와 방어 전략

    목차

    [보안] OAuth 2.0 보안 허점 파헤치기: 실제 공격 사례와 방어 전략

    서비스 로그인 붙이다 보면 OAuth 2.0 보안 이야기를 꼭 만나게 됩니다. 처음엔 그냥 소셜 로그인 한 번 붙이면 끝나는 줄 알았거든요. 근데 운영 환경으로 올라가고, 모바일 앱이 붙고, 리버스 프록시(Reverse Proxy, 역방향 프록시) 뒤에 애플리케이션이 숨어버리면 얘기가 완전히 달라집니다. 제가 홈랩과 사내 테스트 환경에서 직접 굴려보니, OAuth 공격은 거창한 제로데이보다도 설정 실수, 검증 누락, 토큰 저장 위치 같은 데서 시작되는 경우가 훨씬 많더라고요. 이번 글에서는 OAuth 2.0 보안을 실제 공격 흐름 중심으로 풀어보고, 현업에서 바로 적용할 수 있는 방어 전략까지 정리해보겠습니다.

    OAuth 2.0 보안 전체 흐름과 공격 지점을 보여주는 아키텍처 다이어그램

    OAuth 2.0 인증 흐름에서 사용자, 애플리케이션, 인증 서버, 리소스 서버 사이의 이동 경로와 주요 공격 지점을 한눈에 보여주는 다이어그램입니다.

    1. 왜 OAuth 2.0 보안이 자꾸 사고로 이어질까요

    쉽게 말해 OAuth 2.0은 비밀번호를 직접 주고받지 않고 권한을 위임하는 방식입니다. 문제는 구조가 유연한 만큼, 잘못 붙이면 공격자에게도 길을 열어준다는 점입니다. 특히 아래 세 가지가 자주 문제를 만듭니다.

    • Redirect URI(리다이렉트 URI) 검증이 느슨한 경우가 많더라고요
    • state 값 검증이 빠져 CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조)가 가능한 경우
    • Access Token(액세스 토큰)을 브라우저에 무방비로 저장하는 경우

    저도 예전에 테스트 앱 하나 만들면서 redirect URI를 와일드카드 비슷하게 처리했다가, “이거 설마 되겠어?” 했던 우회가 진짜 성립하는 걸 보고 식은땀 난 적이 있습니다. 드디어 됐다 싶었던 로그인 연동이, 보안 관점에서는 시작점이었던 거죠.

    2. OAuth 2.0 핵심 개념, 헷갈리는 부분만 딱 짚어보겠습니다

    OAuth 2.0 보안을 보려면 역할을 먼저 분리해서 봐야 합니다.

    구성 요소 역할 보안 포인트
    Resource Owner 사용자 피싱 페이지 유도 방지
    Client 웹/모바일 애플리케이션 state, PKCE, redirect URI 검증
    Authorization Server 인증 및 토큰 발급 정확한 클라이언트 검증
    Resource Server API 서버 토큰 검증과 권한 범위 확인

    여기서 중요한 포인트가 있습니다. 인증(Authentication, 본인 확인)과 인가(Authorization, 권한 부여)를 섞어 생각하면 설계가 꼬입니다. OAuth 2.0은 기본적으로 인가 프레임워크이고, 로그인은 보통 OpenID Connect(OIDC, 인증 레이어)를 함께 붙여 다룹니다. 이 차이를 놓치면 토큰을 너무 많은 곳에서 믿어버리게 되거든요.

    Authorization Code Flow와 PKCE를 기본값으로 봐야 하는 이유

    요즘은 Authorization Code Flow(인가 코드 흐름)에 PKCE(Proof Key for Code Exchange, 코드 교환용 증명 키)를 붙이는 구성이 사실상 기본입니다. 예전엔 Implicit Flow(암시적 흐름)도 많이 보였는데, 실제로 써보니까 브라우저 노출 면이 넓고 운영상 통제가 까다롭더라고요. 그래서 저는 브라우저 기반 앱도 가능하면 백엔드 연계나 BFF(Backend For Frontend, 프론트엔드 전용 백엔드) 구조를 함께 검토합니다.

    3. 실제 OAuth 공격 사례, 패턴으로 보면 훨씬 잘 보입니다

    특정 회사 사고를 끌어와 자극적으로 보기보다, 반복되는 공격 패턴으로 보는 게 실무에는 훨씬 도움이 되거든요. 아래 세 가지는 제가 테스트 환경에서 재현도 해봤고, 보안 리뷰 때도 정말 자주 보는 유형입니다.

    사례 1. state 누락으로 인한 로그인 CSRF

    애플리케이션이 인증 요청을 보낼 때 state를 만들지 않거나, 만들어도 응답에서 검증하지 않으면 공격자가 자기 계정으로 발급한 인가 응답을 피해자 세션에 주입할 수 있습니다. 겉으로는 로그인 성공처럼 보이는데, 실제로는 피해자가 공격자 계정에 연결된 상태가 되는 거죠.

    • 증상: 사용자가 모르는 계정으로 연결됨
    • 원인: state 생성 또는 검증 누락
    • 방어: 요청별 고유 state 생성, 서버 세션과 비교, 1회성 사용

    사례 2. Redirect URI 오검증으로 인가 코드 탈취

    이건 진짜 위험합니다. Redirect URI를 정확히 일치시키지 않고 접두어(prefix) 비교나 부분 문자열 비교로 처리하면, 공격자가 비슷한 주소를 만들어 인가 코드나 토큰을 가로챌 수 있습니다. 처음엔 “도메인만 같으면 괜찮지 않나” 싶었는데, 하위 경로나 쿼리 문자열까지 엮이면 생각보다 쉽게 틈이 생기더라고요.

    • 증상: 정상 로그인 후 공격자 페이지로 코드 전달
    • 원인: 느슨한 redirect URI 검증
    • 방어: 사전 등록된 URI와 정확히 일치 비교

    사례 3. PKCE 미적용으로 인가 코드 가로채기 위험

    특히 퍼블릭 클라이언트(public client)인 모바일 앱, SPA(Single Page Application, 단일 페이지 애플리케이션) 계열에서 PKCE가 없으면 코드가 중간에서 노출됐을 때 교환 방어가 어렵습니다. PKCE는 code_verifier와 code_challenge를 사용해서, 코드를 훔쳐도 토큰으로 못 바꾸게 막는 장치입니다.

    • 증상: 코드 탈취 후 토큰 교환 시도
    • 원인: PKCE 미적용
    • 방어: S256 기반 PKCE 강제
    OAuth 2.0 보안에서 state 누락과 redirect URI, PKCE 문제를 비교한 공격 흐름 이미지

    세 가지 대표적인 OAuth 공격 패턴이 어디서 시작되고 어느 지점에서 차단할 수 있는지 비교하는 흐름도입니다.

    4. 실전 구현: 안전한 OAuth 2.0 보안 체크포인트를 코드로 붙여보겠습니다

    여기서는 Python Flask(플라스크) 예시로 보겠습니다. 프레임워크는 달라도 핵심은 같습니다. state 검증, PKCE 적용, redirect URI 고정, 이 세 개는 기본값으로 가져가셔야 합니다.

    Step 1. state 값을 만들고 세션에 묶기

    1. 로그인 시작 시 난수 기반 state 생성
    2. 서버 세션 또는 안전한 저장소에 보관
    3. 콜백에서 반드시 동일성 검증
    import secrets
    from flask import Flask, redirect, request, session, abort
    from urllib.parse import urlencode
    
    app = Flask(__name__)
    app.secret_key = "replace-with-secure-secret"
    
    AUTHORIZATION_ENDPOINT = "https://auth.example.com/oauth2/authorize"
    CLIENT_ID = "demo-client"
    REDIRECT_URI = "https://app.example.com/callback"
    
    @app.route("/login")
    def login():
        state = secrets.token_urlsafe(32)
        session["oauth_state"] = state
    
        params = {
            "response_type": "code",
            "client_id": CLIENT_ID,
            "redirect_uri": REDIRECT_URI,
            "scope": "openid profile email",
            "state": state,
        }
        return redirect(f"{AUTHORIZATION_ENDPOINT}?{urlencode(params)}")
    
    @app.route("/callback")
    def callback():
        returned_state = request.args.get("state")
        expected_state = session.pop("oauth_state", None)
    
        if not returned_state or returned_state != expected_state:
            abort(400, "invalid state")
    
        code = request.args.get("code")
        if not code:
            abort(400, "missing code")
    
        return "state validation passed"
    

    이 코드는 단순하지만 중요합니다. 제가 직접 해보니 state를 검증하는 순간, 재현되던 로그인 CSRF 시나리오가 바로 막히더라고요.

    Step 2. PKCE 붙이기

    PKCE는 생각보다 어렵지 않습니다. code_verifier를 만들고, 그걸 SHA-256으로 해시해서 code_challenge를 보내면 됩니다.

    import base64
    import hashlib
    import secrets
    
    
    def generate_pkce_pair():
        code_verifier = secrets.token_urlsafe(64)
        digest = hashlib.sha256(code_verifier.encode("ascii")).digest()
        code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
        return code_verifier, code_challenge
    
    code_verifier, code_challenge = generate_pkce_pair()
    session["code_verifier"] = code_verifier
    
    params = {
        "response_type": "code",
        "client_id": CLIENT_ID,
        "redirect_uri": REDIRECT_URI,
        "scope": "openid profile email",
        "state": session["oauth_state"],
        "code_challenge": code_challenge,
        "code_challenge_method": "S256",
    }
    

    토큰 교환 시에는 저장한 code_verifier를 함께 보내야 합니다. 이 부분 빠뜨리면 “분명 PKCE 넣었는데 왜 안 되지?” 하고 삽질 좀 하게 됩니다 ㅎㅎ

    Step 3. Redirect URI는 정확히 고정하기

    운영 환경에서 제일 많이 깨지는 부분이기도 합니다. 프록시 뒤에서 http로 인식하거나, path가 달라지거나, 포트가 틀어지면 인증 서버와 애플리케이션의 인식이 어긋납니다.

    oauth:
      client_id: demo-client
      redirect_uri: https://app.example.com/callback
      allowed_redirect_uris:
        - https://app.example.com/callback
    

    여기서 중요한 포인트! 부분 일치 금지, 와일드카드 지양, 환경별 URI 명확 분리입니다.

    Step 4. 로컬에서 공격 시나리오 점검하기

    제가 홈랩에서 자주 쓰는 방식은 curl로 콜백을 강제로 때려보는 겁니다. 최소한의 재현만 해도 어디서 검증이 빠졌는지 바로 보입니다.

    curl -i "https://app.example.com/callback?code=test-code&state=wrong-state"
    
    curl -i "https://app.example.com/callback?code=test-code"
    

    정상이라면 둘 다 거절돼야 합니다. 하나라도 통과하면 바로 수정해야 합니다.

    OAuth 2.0 보안 구현에서 state와 PKCE 설정을 설명하는 구성 이미지

    안전한 OAuth 클라이언트 구현에서 state, PKCE, redirect URI 고정이 각각 어디에 들어가는지 보여주는 구성 이미지입니다.

    5. OAuth 2.0 보안 운영 팁: 코드보다 설정에서 더 많이 무너집니다

    사실 OAuth 공격은 코드보다 운영 설정에서 더 자주 터집니다. 애플리케이션 보안 관점에서 특히 아래 항목은 꼭 챙기셔야 합니다.

    • Access Token 저장 위치: 브라우저의 localStorage는 XSS에 취약할 수 있으니 신중히 접근
    • HTTPS 강제: 토큰과 코드가 평문으로 흘러가면 끝입니다
    • Scope 최소화: 꼭 필요한 권한만 요청
    • 토큰 수명 짧게: 장기 유효 토큰은 사고 반경을 키웁니다
    • Refresh Token 보호: 서버 측 저장과 회전(rotation) 전략 검토
    • 로그 마스킹: 토큰, 인가 코드, 사용자 식별자 노출 금지

    이건 제가 정말 여러 번 봤는데요. 개발 편의 때문에 디버그 로그에 전체 콜백 URL을 남기면, 거기에 code나 token이 그대로 찍히는 경우가 있습니다. 테스트 때는 편한데, 운영에서는 그대로 사고 증거가 되더라고요.

    6. ⚠️ 트러블슈팅: 제가 실제로 자주 겪었던 문제들

    문제 1. 프록시 뒤에서 redirect URI가 http로 바뀌는 경우

    Nginx나 Ingress(인그레스, 외부 트래픽 진입점) 뒤에 둘 때 앱이 원래 스킴을 못 알아차리면 https:// 대신 http://로 redirect URI를 만들어버립니다.

    • 해결: X-Forwarded-Proto 신뢰 설정
    • 해결: 프레임워크별 proxy headers 설정 확인
    • 해결: 인증 서버 등록 URI와 앱 생성 URI를 동일하게 맞춤

    문제 2. state를 세션에 저장했는데 콜백에서 사라지는 경우

    도메인, 서브도메인, SameSite 쿠키 정책이 엮이면 세션이 유지되지 않을 수 있습니다. 저도 처음엔 코드만 의심했는데, 알고 보니 쿠키 속성이 문제였던 적이 많았습니다.

    • 해결: 세션 쿠키 도메인과 SameSite 정책 점검
    • 해결: 로드밸런서 환경에서 세션 일관성 확인
    • 해결: 다중 인스턴스면 중앙 세션 저장소 사용

    문제 3. 모바일 앱에서 PKCE는 넣었는데 교환 실패

    대부분은 code_verifier를 로그인 시작 시점과 토큰 교환 시점에 다르게 쓰거나, Base64 URL 인코딩 처리가 어긋난 경우였습니다.

    • 해결: 최초 생성한 code_verifier를 그대로 유지
    • 해결: code_challenge_method=S256 확인
    • 해결: URL-safe Base64 처리와 패딩 제거 확인

    7. 검증과 결과: 무엇을 통과해야 “안전하다”고 말할 수 있을까요

    보안은 느낌으로 끝내면 안 됩니다. 체크리스트로 검증해야 합니다. 저는 아래 항목을 통과해야 최소 기준은 넘겼다고 봅니다.

    1. state가 없거나 틀리면 콜백이 거절된다
    2. 등록되지 않은 redirect URI는 인증 서버에서 거절된다
    3. PKCE 없이 또는 잘못된 verifier로는 토큰 교환이 실패한다
    4. 토큰과 code가 애플리케이션 로그에 남지 않는다
    5. 브라우저 개발자 도구에서 민감한 토큰 노출 면이 최소화돼 있다
    6. 권한 범위(scope)가 필요한 수준으로 제한돼 있다

    이렇게 점검하고 나면 OAuth 2.0 보안이 훨씬 단단해집니다. 화려한 기능 추가는 없어 보여도, 실제로는 장애와 사고를 줄이는 가장 값진 작업이더라고요.

    OAuth 2.0 보안 검증 결과와 체크리스트를 보여주는 대시보드 이미지

    state 검증, PKCE, redirect URI 검사, 로그 마스킹 여부를 통과/실패로 시각화한 결과 이미지입니다.

    8. OAuth 공격 방어 전략 한눈에 정리

    공격 유형 주요 원인 방어 전략
    로그인 CSRF state 검증 누락 고유 state 생성, 세션 바인딩, 1회성 사용
    인가 코드 탈취 redirect URI 오검증 정확 일치 비교, 사전 등록 URI만 허용
    코드 재사용/가로채기 PKCE 미적용 S256 PKCE 강제
    토큰 유출 부적절한 저장/로그 출력 로그 마스킹, 저장 위치 최소화, HTTPS 강제
    과도한 권한 넓은 scope 요청 최소 권한 원칙 적용

    혹시 지금 운영 중인 서비스가 있다면, 위 표 기준으로 하나씩 점검해보세요. 특히 OAuth 2.0 보안은 한 군데만 잘한다고 끝나는 게 아니라, 클라이언트와 인증 서버, 프록시와 브라우저 저장소까지 같이 봐야 합니다.

    OAuth 2.0 보안 공격 유형과 방어 전략을 요약한 인포그래픽

    대표적인 OAuth 공격 유형과 대응책을 좌우 비교 형태로 정리한 인포그래픽입니다.

    9. 마무리: OAuth 2.0 보안은 기능이 아니라 운영 습관입니다

    정리해보면, OAuth 2.0 보안의 핵심은 복잡한 암호 기술보다도 기본기입니다. state 검증, PKCE 적용, redirect URI 정확 일치, 토큰 노출 최소화. 이 네 가지만 제대로 해도 공격면이 눈에 띄게 줄어듭니다. 저도 처음엔 문서만 보고 붙였다가 왜 자꾸 이상한 케이스가 생기나 했었는데, 결국 문제는 대부분 “설마 이 정도는 괜찮겠지”에서 시작됐습니다.

    다음 글에서는 OAuth와 함께 자주 등장하는 OpenID Connect(OIDC, 인증 레이어)의 ID Token 검증 포인트를 따로 다뤄볼 예정입니다. 이전 글에서 다룬 리버스 프록시와 쿠키 보안 설정을 함께 보시면 더 이해가 잘 되실 겁니다.

    FAQ. 자주 헷갈리는 질문

    Q1. SPA에서도 OAuth를 안전하게 쓸 수 있나요?

    가능합니다. 다만 PKCE를 기본으로 하고, 토큰 저장 전략과 XSS 대응을 같이 설계해야 합니다.

    Q2. state와 nonce는 같은 건가요?

    완전히 같지는 않습니다. state는 요청 위조 방지에, nonce는 주로 재생 공격 방지와 토큰 검증 맥락에서 쓰입니다.

    Q3. redirect URI를 여러 개 등록해도 괜찮나요?

    괜찮습니다. 다만 각각을 명시적으로 등록하고 정확히 일치 비교해야 합니다. 범용 패턴 허용은 조심하셔야 합니다.