[Auth] OAuth 2.0 / 2.1: Authorization Code + PKCE
정의
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 Authorization | TV / CLI / 헤드리스 | 권장 |
| Refresh Token | access_token 갱신 | 권장 |
| Implicit | (옛 SPA) | 폐기 (OAuth 2.1) |
| Resource Owner Password | password 직접 전달 | 폐기 (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.0 | 2.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_token | 5-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
| JWT | Opaque Token | |
|---|---|---|
| 검증 | 로컬 (서명 확인) | 서버 introspect 필요 |
| Revocation | 어려움 (TTL 만료 대기) | 즉시 (DB 삭제) |
| 크기 | 큰 편 (클레임 포함) | 짧음 |
| 용도 | API Gateway, stateless | Auth Server 중심 |
OAuth vs OpenID Connect
| OAuth 2.0 | OIDC | |
|---|---|---|
| 목적 | 권한 위임 (자원 접근) | 인증 (누구인지) |
| 토큰 | access_token | + id_token (JWT) |
| User info | 자원 API 로 별도 조회 | id_token 안에 |
| 표준화 | 기본 | OAuth 2.0 위 |
자세한 건 OpenID Connect 참고.
흔한 함정
WARNING
- redirect_uri 검증 부재: open redirect 공격으로 코드 탈취.
- state 미사용: CSRF.
- PKCE 없이 public client: intercept 공격.
- access_token 을 URL 에: 로그 / referrer 로 노출.
- refresh_token 영구: 탈취 시 영구 침해. rotation (한 번 쓰면 새 발급 + 옛 무효화).
- 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 접근 불가) |
| HTTPS | Auth Server 및 Client 모두 필수 |
| scope | 최소 필요 scope 만 요청 |
| token rotation | refresh_token 사용 후 즉시 교체 |
실전 라이브러리
| 플랫폼 | 라이브러리 |
|---|---|
| SPA (React/Vue) | @auth0/auth0-react, oidc-client-ts |
| Next.js | next-auth (Auth.js v5) |
| Node.js 서버 | passport-oauth2, openid-client |
| Spring | spring-security-oauth2-client |
| Python | authlib, python-social-auth |
| Mobile (iOS) | AppAuth-iOS |
| Mobile (Android) | AppAuth-Android |
관련 위키
- JWT
- OpenID Connect
- SAML
- mTLS
- CSRF
- Session Cookie - cookie 기반 세션과 토큰 방식 비교
이 글의 용어 (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]…
이 개념을 다룬 위키 페이지 (9)
- wiki[Auth] JWT: 구조, 서명, 만료, refresh token
- wiki[Auth] mTLS (Mutual TLS): 양방향 인증서 인증
- wiki[Auth] OpenID Connect (OIDC): OAuth 위의 인증 레이어
- wiki[Auth] SAML 2.0: XML 기반 기업 SSO
- wiki[AWS] IAM: User, Role, Policy, STS
- wiki[AWS] STS / AssumeRole: 임시 자격, cross-account
- wiki[Pattern] API Gateway: BFF, 라우팅, auth, rate limit
- wiki[DRF] Authentication: Session, Token, JWT, OAuth2
- wiki[K8s] RBAC: Role, ClusterRole, ServiceAccount, RoleBinding
💬 댓글