본문으로 건너뛰기
김신건의 로그

[Auth] OAuth 2.0 / 2.1: Authorization Code + PKCE

· 수정 · 📖 약 3분 · 972자/단어 #oauth #auth #security #backend
OAuth 2.0, OAuth 2.1, Authorization Code, PKCE, Client Credentials, Implicit flow, Device flow

정의

OAuth 2.0 (RFC 6749, 2012) 은 제3자 앱이 사용자 동의로 자원에 접근 하게 하는 권한 위임 프레임워크. 인증 (authentication) 이 아니라 인가 (authorization).

OAuth 2.1 은 진행 중인 draft. 옛 flow 정리 + PKCE 의무화.

핵심 4 역할

flowchart LR
    User["Resource Owner<br/>사용자"] -->|동의| Auth["Authorization Server<br/>예: Google"]
    Client["Client App<br/>예: 우리 앱"] -->|토큰 요청| Auth
    Auth -->|access_token| Client
    Client -->|"토큰 + 요청"| Resource["Resource Server<br/>예: Google Drive API"]

Authorization Code Flow + PKCE (표준)

가장 일반적이고 OAuth 2.1 에서 유일하게 권장 되는 사용자 흐름.

sequenceDiagram
    autonumber
    participant U as User
    participant C as Client
    participant Auth as Auth Server
    participant API as Resource Server

    C->>C: code_verifier (랜덤 43-128자) 생성
    C->>C: code_challenge = SHA256(verifier) -> base64url
    C->>U: 브라우저 -> Auth 로 redirect<br/>?response_type=code&client_id=...<br/>&code_challenge=...&code_challenge_method=S256
    U->>Auth: 로그인 + 동의
    Auth-->>U: redirect to Client<br/>?code=ABC
    U-->>C: code=ABC 전달
    C->>Auth: POST /token<br/>code=ABC&code_verifier=<원본>&client_id=...
    Auth->>Auth: SHA256(verifier) == challenge?
    Auth-->>C: { access_token, refresh_token }
    C->>API: Authorization: Bearer <token>
    API-->>C: 자원

PKCE 가 왜 필수?

OAuth 2.0 의 옛날 implicit flow + public client (모바일 / SPA) 에서 intercept 공격. code_verifier동적 공유 secret 처럼 작용.

IMPORTANT

모든 OAuth 클라이언트는 PKCE 필수. 옛 라이브러리도 반드시 최신 버전.

다른 Flow

Flow사용상태
Authorization Code + PKCE사용자 로그인 (웹/모바일)권장
Client Credentials서버 대 서버 (사용자 없음)권장
Device AuthorizationTV / CLI / 헤드리스권장
Refresh Tokenaccess_token 갱신권장
Implicit(옛 SPA)폐기 (OAuth 2.1)
Resource Owner Passwordpassword 직접 전달폐기 (OAuth 2.1)

Client Credentials (서버끼리)

sequenceDiagram
    Service A->>Auth: POST /token<br/>client_id + client_secret<br/>grant_type=client_credentials
    Auth-->>Service A: { access_token, expires_in }
    Service A->>Service B: Bearer <token>
    Service B-->>Service A: 자원

Device Flow

sequenceDiagram
    Device->>Auth: POST /device/code
    Auth-->>Device: { device_code, user_code, verification_uri }
    Device->>User: "다른 기기로 https://x.com/code 가서 ABC123 입력"
    User->>Auth: 브라우저에서 ABC123 입력 + 동의
    loop poll
        Device->>Auth: POST /token (device_code)
        Auth-->>Device: 200 (성공) 또는 authorization_pending
    end

Apple TV, Roku, GitHub CLI (gh auth login) 같은 키보드 없는 기기.

OAuth 2.0 vs OAuth 2.1

항목2.02.1
Implicit flow허용제거
Resource owner password허용제거
PKCE권장필수
Bearer token in URL허용 (?token=)제거
Refresh token rotation옵션권장
HTTPS권장필수

Scope

scope=read:files write:profile 같이 권한 범위 명시. Auth Server 가 사용자 동의 화면에 표시.

scope=openid profile email     # OpenID Connect
scope=read:items write:orders  # 자원 권한

State Parameter (CSRF 방어)

GET /authorize?...&state=<random>

Auth 가 같은 state 를 redirect 에 포함. 다른 사이트가 위조한 redirect 를 클라이언트가 거절.

CAUTION

state 검증 누락 = CSRF 공격에 OAuth 흐름 위조 가능. 모든 클라이언트 필수.

PKCE 구현 예시

// TypeScript (browser)
async function generatePKCE() {
  const verifier = crypto.randomUUID().replace(/-/g, '')
    + crypto.randomUUID().replace(/-/g, '');   // 64자

  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const digest = await crypto.subtle.digest('SHA-256', data);

  const challenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');

  return { verifier, challenge };
}

// 사용 예
const { verifier, challenge } = await generatePKCE();
sessionStorage.setItem('pkce_verifier', verifier);

const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authUrl.searchParams.set('code_challenge', challenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('state', crypto.randomUUID());
window.location.href = authUrl.toString();

토큰 수명과 갱신

토큰수명용도
access_token5-60분 (짧을수록 좋음)자원 접근에 사용
refresh_token수일-수개월access_token 재발급
id_token (OIDC)보통 access_token 과 동일사용자 정보 포함 JWT

Refresh Token Rotation (OAuth 2.1 권장):

  • refresh_token 을 한 번 사용하면 새 refresh_token 발급, 이전 무효화
  • 탈취 탐지: 만료된 refresh_token 재사용 시 경보 + 세션 전체 무효화
POST /token
grant_type=refresh_token
refresh_token=<old_token>

-> { access_token: "...", refresh_token: "<new_token>", expires_in: 3600 }

Token Introspection / Revocation

# RFC 7662: Token Introspection
POST /introspect
token=<opaque_token>

-> { active: true, sub: "user123", scope: "read", exp: 1234567890 }

# RFC 7009: Token Revocation (로그아웃)
POST /revoke
token=<refresh_token>
token_type_hint=refresh_token

opaque token (불투명 토큰) 은 서버 DB 조회 필요. JWT 는 서명 검증으로 로컬 검증 가능하지만 revoke 가 어려움.

JWT vs Opaque Token

JWTOpaque Token
검증로컬 (서명 확인)서버 introspect 필요
Revocation어려움 (TTL 만료 대기)즉시 (DB 삭제)
크기큰 편 (클레임 포함)짧음
용도API Gateway, statelessAuth Server 중심

OAuth vs OpenID Connect

OAuth 2.0OIDC
목적권한 위임 (자원 접근)인증 (누구인지)
토큰access_token+ id_token (JWT)
User info자원 API 로 별도 조회id_token 안에
표준화기본OAuth 2.0 위

자세한 건 OpenID Connect 참고.

흔한 함정

WARNING

  1. redirect_uri 검증 부재: open redirect 공격으로 코드 탈취.
  2. state 미사용: CSRF.
  3. PKCE 없이 public client: intercept 공격.
  4. access_token 을 URL 에: 로그 / referrer 로 노출.
  5. refresh_token 영구: 탈취 시 영구 침해. rotation (한 번 쓰면 새 발급 + 옛 무효화).
  6. scope 최소화 무시: 필요한 scope 만 요청 (principle of least privilege).

보안 체크리스트

구현 전 점검:

항목세부 사항
PKCE모든 public client (SPA, 모바일) 에 적용
redirect_uri정확한 값만 서버에 등록, prefix 매칭 금지
state요청마다 새 random 값, callback 에서 검증
access_token 저장localStorage 보다 메모리 또는 HttpOnly cookie
refresh_token 저장HttpOnly cookie (XSS 접근 불가)
HTTPSAuth Server 및 Client 모두 필수
scope최소 필요 scope 만 요청
token rotationrefresh_token 사용 후 즉시 교체

실전 라이브러리

플랫폼라이브러리
SPA (React/Vue)@auth0/auth0-react, oidc-client-ts
Next.jsnext-auth (Auth.js v5)
Node.js 서버passport-oauth2, openid-client
Springspring-security-oauth2-client
Pythonauthlib, python-social-auth
Mobile (iOS)AppAuth-iOS
Mobile (Android)AppAuth-Android

관련 위키

이 글의 용어 (6개)
[Auth] CSRF: 위조 요청 공격과 방어auth-security
정의 CSRF (Cross-Site Request Forgery) 는 사용자가 로그인된 상태 에서 공격자가 만든 페이지 가 피해자 브라우저로 위조 요청 을 보내는 공격. 쿠키 기…
[Auth] JWT: 구조, 서명, 만료, refresh tokenauth-security
정의 JWT (JSON Web Token) (RFC 7519) 은 base64url 인코딩된 JSON + 서명 형태의 self-contained token. 서버가 세션 저장 없…
[Auth] mTLS (Mutual TLS): 양방향 인증서 인증auth-security
정의 mTLS (Mutual TLS) 는 서버뿐 아니라 클라이언트도 TLS 인증서로 자기 신분 증명. 일반 TLS = 서버 인증만, mTLS = 양방향. 활용: 서비스 메시 (I…
[Auth] OpenID Connect (OIDC): OAuth 위의 인증 레이어auth-security
정의 OpenID Connect (OIDC) 는 OAuth 2.0 위에 얹은 인증 (authentication) 표준. OAuth 가 권한 위임 (authorization), O…
[Auth] SAML 2.0: XML 기반 기업 SSOauth-security
정의 SAML 2.0 (Security Assertion Markup Language) 는 XML 기반 기업 SSO 표준. 2005년 OASIS 표준화. 모바일 친화도 / 단순성…
[Auth] Session Cookie: HttpOnly, SameSite, Secure flagauth-security
정의 Session Cookie 는 서버가 발급한 임의 ID 를 쿠키에 보관. 매 요청 자동 첨부. 서버 측 세션 store (Redis, DB) 와 짝. [!IMPORTANT]…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기