Theory
Security
Oauth Grant Type

OAuth 2.0 Grant Types 완벽 가이드

결론

OAuth 2.0은 인증 및 권한 부여를 위한 산업 표준 프로토콜로, 애플리케이션 유형과 보안 요구사항에 따라 여러 Grant Type을 제공합니다. RFC 6749 (opens in a new tab)

RFC 6749에서 "The OAuth 2.0 authorization framework enables a third-party application to obtain limited access to an HTTP service"라고 명시되어 있습니다.

2025년 기준 권장 사항:

  • 서버 사이드 웹 앱: Authorization Code Grant
  • SPA 및 모바일 앱: Authorization Code + PKCE (필수)
  • 서버 간 통신: Client Credentials Grant
  • 입력 제한 기기: Device Code Grant
  • 사용 금지: Implicit Grant, Resource Owner Password Credentials (OAuth 2.1에서 제거)

OAuth 2.0 공식 사이트 (opens in a new tab)에서 "PKCE is now recommended for all OAuth clients, including web apps using the authorization code flow"라고 명시되어 있습니다.

OAuth 2.0 주요 개념

핵심 용어

  • Resource Owner: 보호된 리소스의 소유자 (일반적으로 사용자)
  • Client: 리소스에 접근하려는 애플리케이션
  • Authorization Server: 인증 및 권한 부여를 처리하는 서버
  • Resource Server: 보호된 리소스를 호스팅하는 서버
  • Access Token: 리소스 접근에 사용되는 토큰
  • Refresh Token: Access Token 갱신에 사용되는 토큰
  • Authorization Code: 일회성 코드 (Access Token으로 교환)

1. Authorization Code Grant

개요

가장 일반적이고 안전한 OAuth Grant Type입니다. 서버 사이드 웹 애플리케이션에 최적화되어 있습니다. OAuth 2.0 Authorization Code (opens in a new tab)

OAuth.net에서 "The authorization code grant type is used to obtain both access tokens and refresh tokens and is optimized for confidential clients"라고 명시되어 있습니다.

흐름도

주요 파라미터

Authorization 요청:

  • response_type=code: Authorization Code를 요청
  • client_id: 클라이언트 식별자
  • redirect_uri: 콜백 URL
  • scope: 요청 권한 범위
  • state: CSRF 방지용 랜덤 값

Token 요청:

  • grant_type=authorization_code
  • code: 받은 Authorization Code
  • client_id, client_secret: 클라이언트 인증
  • redirect_uri: Authorization 요청 시와 동일해야 함

보안 취약점

1. Authorization Code Interception

탈취되는 값: authorization_code

발생 시나리오:

  • 리다이렉트 시 HTTP 사용
  • 네트워크 스니핑
  • 브라우저 히스토리 접근

영향: 공격자가 code를 access token으로 교환 가능

해결책:

  • HTTPS 필수 사용
  • 짧은 code 수명 (보통 10분)
  • code는 일회성 사용만 허용

2. CSRF 공격

탈취되는 값: 세션 조작 (값 탈취는 아님)

발생 시나리오: 공격자가 자신의 authorization code를 피해자에게 사용하게 만듦

영향: 피해자가 공격자의 계정으로 작업 수행 (예: 결제 정보 추가)

해결책: state 파라미터 사용 및 검증

OAuth 2.0 Security Best Practices (opens in a new tab)

위 문서에서 "State parameter for CSRF protection: Always use in GET redirects"라고 명시되어 있습니다.

3. Redirect URI 조작

탈취되는 값: authorization_code

발생 시나리오:

  • Redirect URI 검증이 약함 (예: 와일드카드 허용)
  • 공격자가 자신의 도메인으로 code 전달

영향: 공격자가 code 획득 후 token 교환

해결책:

  • 정확한 redirect_uri 매칭 (와일드카드 금지)
  • 등록된 URI만 허용
  • HTTPS만 허용

4. Client Secret 노출

탈취되는 값: client_secret

발생 시나리오:

  • SPA나 모바일 앱에 하드코딩
  • 버전 관리 시스템에 커밋
  • 디컴파일로 추출

영향: 공격자가 클라이언트를 완전히 사칭 가능

해결책:

  • Public client로 등록
  • PKCE 사용
  • Secret을 프론트엔드에 절대 포함하지 않음

사용 시나리오

  • 서버 사이드 웹 애플리케이션 (Node.js, Django, Spring Boot 등)
  • Client Secret을 안전하게 저장할 수 있는 환경
  • Refresh Token이 필요한 경우
  • 높은 보안이 요구되는 애플리케이션

2. Authorization Code with PKCE

개요

Authorization Code Grant에 PKCE (Proof Key for Code Exchange)를 추가한 확장 버전입니다. 모바일 앱과 SPA에서 Client Secret 없이도 안전하게 사용할 수 있습니다. RFC 7636 (opens in a new tab)

PKCE in OAuth 2.0 (opens in a new tab)

Authgear 문서에서 "PKCE prevents authorization code interception attacks by requiring the client to prove it initiated the authorization request"라고 명시되어 있습니다.

흐름도

PKCE 핵심 메커니즘

PKCE는 클라이언트와 Authorization Server 간의 암호학적 검증을 통해 보안을 강화합니다. 핵심은 **클라이언트만 알고 있는 비밀 값(code_verifier)**과 **공개적으로 전송 가능한 해시 값(code_challenge)**을 사용하는 것입니다.

단계별 상세 흐름

1단계: 클라이언트가 Code Verifier 생성 (브라우저/앱에서 실행)

// 위치: 클라이언트 (브라우저/모바일 앱)
// 시점: Authorization 요청 직전
// 43-128자의 랜덤 문자열 생성
const codeVerifier = generateRandomString(128);
// 예: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
 
// ⚠️ 이 값은 클라이언트에만 저장하고 절대 서버로 보내지 않습니다!
sessionStorage.setItem('code_verifier', codeVerifier);

2단계: 클라이언트가 Code Challenge 생성 (브라우저/앱에서 실행)

// 위치: 클라이언트 (브라우저/모바일 앱)
// 시점: Authorization 요청 직전
// code_verifier를 SHA256으로 해싱
const hash = await crypto.subtle.digest('SHA-256',
  new TextEncoder().encode(codeVerifier)
);
const codeChallenge = base64UrlEncode(hash);
// 예: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
 
// 이 해시 값은 공개되어도 원본(code_verifier)을 알 수 없음 (일방향 해시)

3단계: Authorization 요청 시 Code Challenge 전송

위치: 클라이언트 → Authorization Server
시점: 사용자가 로그인 버튼을 클릭했을 때

GET https://auth.example.com/authorize?
  response_type=code&
  client_id=CLIENT_ID&
  redirect_uri=https://app.example.com/callback&
  scope=read write&
  state=xyz&
  code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
  code_challenge_method=S256

⚠️ code_challenge는 URL에 노출되어도 안전 (해시 값이므로 역산 불가능)
⚠️ code_verifier는 절대 전송하지 않음!

4단계: Authorization Server가 Code Challenge 저장

위치: Authorization Server
시점: Authorization 요청을 받았을 때

저장 형식:
{
  "authorization_code": "abc123...",
  "code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
  "code_challenge_method": "S256",
  "client_id": "CLIENT_ID",
  "expires_at": "2025-11-25T12:30:00Z"
}

이 code_challenge는 나중에 검증할 때 사용됩니다.

5단계: Token 요청 시 Code Verifier 전송

위치: 클라이언트 → Authorization Server
시점: Authorization Code를 받은 직후 (콜백 처리 시)

POST https://auth.example.com/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=AUTHORIZATION_CODE&
client_id=CLIENT_ID&
redirect_uri=https://app.example.com/callback&
code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

⚠️ 이제야 원본 code_verifier를 전송합니다 (HTTPS로 암호화됨)

6단계: Authorization Server가 검증 (서버에서 실행)

// 위치: Authorization Server
// 시점: Token 요청을 받았을 때
 
// 1. 저장된 code_challenge 가져오기
const storedChallenge = database.getCodeChallenge(authorizationCode);
// "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
 
// 2. 받은 code_verifier를 SHA256으로 해싱
const receivedVerifier = request.body.code_verifier;
// "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
 
const computedChallenge = base64UrlEncode(
  sha256(receivedVerifier)
);
// "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
 
// 3. 비교
if (computedChallenge === storedChallenge) {
  // ✅ 검증 성공: 같은 클라이언트임이 확인됨
  return issueAccessToken();
} else {
  // ❌ 검증 실패: 공격자가 중간에 가로챈 것으로 의심
  return { error: 'invalid_grant' };
}

핵심 보안 원리

  1. 클라이언트만 code_verifier를 알고 있음

    • Authorization 요청 시에는 해시 값(code_challenge)만 전송
    • Token 요청 시에만 원본(code_verifier) 전송
  2. 공격자가 authorization_code를 가로채도 무용지물

    공격자가 가질 수 있는 정보:
    - authorization_code (가로챘음)
    - code_challenge (공개된 값)
    
    공격자가 가질 수 없는 정보:
    - code_verifier (클라이언트만 알고 있음)
    
    → Token 요청 시 code_verifier가 없으면 검증 실패
  3. SHA256 해시의 일방향성

    • code_challenge에서 code_verifier를 역산할 수 없음
    • 따라서 code_challenge가 공개되어도 안전
  4. 검증은 Authorization Server에서 수행

    • 클라이언트가 보낸 code_verifier를 서버에서 해싱
    • 저장된 code_challenge와 비교
    • 일치해야만 같은 클라이언트임이 증명됨

보안 개선

Authorization Code Interception 방어:

Common OAuth Vulnerabilities (opens in a new tab)

Doyensec 블로그에서 "With PKCE, the interception of the Authorization Response will not allow the attack since attackers would only be able to access the authorization_code but it won't be possible for them to get the code_verifier value required in the Access Token Request"라고 명시되어 있습니다.

  • 공격자가 authorization_code를 가로채도 code_verifier 없이는 token 교환 불가
  • code_challenge는 공개되지만 code_verifier를 역산할 수 없음 (SHA256 해시의 일방향성)
  • Client Secret 불필요 → 프론트엔드에서 안전

2025년 필수 요구사항

OAuth 2.0 Security Best Practices (opens in a new tab)

위 문서에서 "Use PKCE: Mandatory for public clients"라고 명시되어 있습니다.

  • 모든 public client에서 PKCE 필수
  • OAuth 2.1 draft에서 공식 요구사항
  • SPA, 모바일 앱은 반드시 PKCE 사용

사용 시나리오

  • Single Page Application (React, Vue, Angular 등)
  • 모바일 앱 (iOS, Android, React Native 등)
  • Desktop 앱 (Electron 등)
  • Client Secret을 안전하게 저장할 수 없는 모든 환경
  • Public client

3. Implicit Grant (Deprecated)

개요

⚠️ 더 이상 사용하지 않는 방식입니다. OAuth 2.1에서 완전히 제거되었습니다.

과거에는 SPA에서 사용되었으나, 현재는 Authorization Code + PKCE로 대체되었습니다. OAuth.net (opens in a new tab)

OAuth.net에서 "It is recommended that clients no longer use the Implicit Grant"이라고 명시되어 있습니다.

흐름도

보안 취약점 (Deprecated 이유)

1. Access Token이 URL에 노출

탈취되는 값: access_token

발생 시나리오:

  • 브라우저 히스토리에 저장
  • 서버 로그에 기록
  • Referer 헤더로 누출
  • 브라우저 확장 프로그램 접근

영향: 직접적인 리소스 접근 권한 탈취

2. XSS 공격에 취약

탈취되는 값: access_token

발생 시나리오:

  • JavaScript에서 token 직접 처리
  • XSS 공격으로 token 탈취

영향: 공격자가 사용자의 모든 리소스 접근

3. Refresh Token 없음

영향:

  • Access Token 만료 시 전체 인증 흐름 재시작
  • 사용자 경험 저하

4. Token 유효성 검증 불가

  • Client 인증 없음 (Client Secret 사용 안 함)
  • 누구나 token을 사용 가능

현재 권장 사항

절대 사용하지 말 것

대신 사용: Authorization Code + PKCE

4. Client Credentials Grant

개요

사용자 컨텍스트 없이 서버 간(machine-to-machine) 통신에 사용됩니다. OAuth 2.0 Client Credentials (opens in a new tab)

OAuth.net에서 "The Client Credentials grant type is used by clients to obtain an access token outside of the context of a user"라고 명시되어 있습니다.

흐름도

주요 특징

  • 사용자 없음: Machine-to-Machine 인증
  • 클라이언트가 Resource Owner: 자신의 리소스에 접근
  • 간단한 흐름: 리다이렉트나 사용자 동의 불필요
  • Refresh Token 없음: 만료 시 새 Access Token 요청

Token 요청 예시

방법 1: Request Body에 자격 증명 포함

POST https://auth.example.com/token
Content-Type: application/x-www-form-urlencoded
 
# 아래 파라미터들은 HTTP Request Body에 포함됩니다
grant_type=client_credentials&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
scope=api.read api.write

방법 2: Authorization Header 사용 (권장)

POST https://auth.example.com/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic BASE64(CLIENT_ID:CLIENT_SECRET)
 
# Request Body에는 grant_type과 scope만 포함
grant_type=client_credentials&
scope=api.read api.write

RFC 6749에서 "Clients in possession of a client password MAY use the HTTP Basic authentication. Alternatively, the authorization server MAY support including the client credentials in the request-body"라고 명시되어 있습니다.

두 방식의 차이:

  • Request Body 방식: client_id와 client_secret을 body에 평문으로 포함 (HTTPS 필수)
  • Authorization Header 방식: Basic Authentication을 사용하여 header에 포함 (더 표준적이고 안전)

보안 권장사항:

  • 두 방식 모두 반드시 HTTPS 사용
  • Authorization Header 방식이 더 표준적이고 많은 라이브러리에서 기본 지원
  • 로그에 body가 기록되는 환경에서는 Header 방식이 더 안전

보안 고려사항

1. Client Secret 보호

탈취되는 값: client_secret

영향: 공격자가 클라이언트를 완전히 사칭 가능

해결책:

# 환경 변수 사용
export CLIENT_SECRET="your-secret-here"
 
# 비밀 관리 시스템 사용
# - AWS Secrets Manager
# - HashiCorp Vault
# - Azure Key Vault
# - Google Secret Manager

2. Scope 제한

최소 권한 원칙 적용:

{
  "scope": "api.read",  // ✅ 읽기만 필요한 경우
  "scope": "api.*"      // ❌ 과도한 권한
}

3. Token 수명 제한

{
  "access_token": "...",
  "expires_in": 3600,  // 1시간
  "token_type": "Bearer"
}

사용 시나리오

  • 마이크로서비스 간 통신

    Order Service → Auth Server → Inventory Service
  • 백엔드 배치 작업

    Cron Job → Auth Server → Database API
  • API 게이트웨이

    API Gateway → Auth Server → Backend Services
  • 서버 간 데이터 동기화

  • CLI 도구 (사용자 컨텍스트가 없는 경우)

5. Resource Owner Password Credentials (Deprecated)

개요

⚠️ 더 이상 권장하지 않는 방식입니다. OAuth 2.1에서 제거 예정입니다.

사용자가 클라이언트에 직접 username과 password를 제공합니다. OAuth 2.0 Password Grant (opens in a new tab)

OAuth.net에서 "The Resource Owner Password Credentials grant should not be used"이라고 명시되어 있습니다.

흐름도

보안 취약점 (권장하지 않는 이유)

1. 자격 증명 직접 노출

탈취되는 값: username, password

발생 시나리오:

  • 클라이언트가 비밀번호 저장 (악의적 또는 실수)
  • 네트워크 스니핑
  • 클라이언트 앱 취약점

영향:

  • 사용자 계정 완전 탈취
  • 다른 서비스에서도 사용 가능 (비밀번호 재사용 시)
  • OAuth의 기본 원칙 위배 (비밀번호 공유 방지)

2. 피싱 위험 증가

사용자가 제3자 앱에 비밀번호를 입력하는 습관 형성

3. MFA 지원 불가

2단계 인증, 생체 인증 등 고급 인증 흐름 불가능

4. 클라이언트 신뢰 필요

클라이언트가 비밀번호를 악용하지 않을 것이라는 전제 (OAuth의 철학과 반대)

매우 제한적인 사용 시나리오

오직 다음 경우에만:

  • ✅ 자사(first-party) 애플리케이션
  • ✅ 레거시 시스템 마이그레이션 중 임시 사용
  • ✅ 다른 OAuth 흐름을 지원할 수 없는 경우

절대 금지:

  • ❌ 제3자 애플리케이션
  • ❌ 새로운 시스템 개발

현재 권장 사항

가능한 사용하지 말 것

대신 사용: Authorization Code + PKCE

6. Device Code Grant

개요

입력이 제한된 디바이스 (스마트 TV, IoT 기기 등)를 위한 Grant Type입니다. RFC 8628 (opens in a new tab)

사용자는 다른 디바이스 (스마트폰, PC)에서 인증을 완료합니다. OAuth 2.0 Device Flow (opens in a new tab)

흐름도

주요 파라미터

Device Authorization 요청:

POST https://auth.example.com/device/code
Content-Type: application/x-www-form-urlencoded
 
client_id=CLIENT_ID&
scope=read write

응답:

{
  "device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
  "user_code": "WDJB-MJHT",
  "verification_uri": "https://example.com/device",
  "verification_uri_complete": "https://example.com/device?user_code=WDJB-MJHT",
  "expires_in": 1800,
  "interval": 5
}

Token 폴링:

POST https://auth.example.com/token
Content-Type: application/x-www-form-urlencoded
 
grant_type=urn:ietf:params:oauth:grant-type:device_code&
device_code=GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS&
client_id=CLIENT_ID

보안 고려사항

1. User Code 추측 공격

탈취되는 값: user_code

발생 시나리오:

  • 짧은 user_code (예: 4자리)는 무작위 대입 가능
  • 공격자가 모든 가능한 조합 시도

영향: 공격자가 다른 사용자의 인증 가로채기

해결책:

  • 충분히 긴 user_code (6-8자 권장)
  • 시도 횟수 제한 (rate limiting)
  • 짧은 유효 시간 (15-30분)

2. Device Code 노출

탈취되는 값: device_code

발생 시나리오:

  • 네트워크 스니핑
  • 디바이스 로그 접근

영향: 공격자가 폴링하여 token 획득

해결책:

  • HTTPS 필수
  • 짧은 유효 시간 (보통 5-10분)
  • device_code는 일회성 사용

3. Phishing

발생 시나리오:

  • 가짜 verification_uri로 유도
  • 사용자가 악의적인 사이트에 로그인

해결책:

  • 공식 URL 명확히 표시
  • SSL 인증서 확인
  • 도메인 사용자 교육

사용 시나리오

  • 스마트 TV (Netflix, YouTube 로그인)
  • IoT 기기 (스마트 홈 기기)
  • 게임 콘솔 (PlayStation, Xbox)
  • 프린터
  • 셋톱박스
  • CLI 도구 (사용자 컨텍스트가 있는 경우)

실제 예시: YouTube TV 로그인

  1. TV 화면: "youtube.com/activate 방문하여 코드 입력: ABCD-EFGH"
  2. 사용자가 스마트폰으로 해당 URL 방문
  3. 코드 입력 및 Google 계정 로그인
  4. TV 자동 로그인 완료

보안 Best Practices (2025)

1. PKCE 필수 사용

OAuth 2.0 Security Best Practices (opens in a new tab)

위 문서에서 "Use PKCE: Mandatory for public clients"라고 명시되어 있습니다.

// ✅ 올바른 방법
const codeVerifier = generateRandomString(128);
const codeChallenge = base64UrlEncode(sha256(codeVerifier));
 
// Authorization 요청에 포함
authorizeUrl += `&code_challenge=${codeChallenge}`;
authorizeUrl += `&code_challenge_method=S256`;

2. 짧은 수명의 토큰

{
  "access_token": "...",
  "expires_in": 900,  // ✅ 15분
  "refresh_token": "...",
  "refresh_token_expires_in": 2592000  // 30일
}

OAuth 2.0 Security Best Practices (opens in a new tab)

위 문서에서 "Short-lived tokens & refresh token rotation: Prevent long-term access"라고 명시되어 있습니다.

3. Refresh Token Rotation

// Token 갱신 시
const response = await fetch('https://auth.example.com/token', {
  method: 'POST',
  body: new URLSearchParams({
    grant_type: 'refresh_token',
    refresh_token: oldRefreshToken,
    client_id: CLIENT_ID
  })
});
 
const { access_token, refresh_token } = await response.json();
 
// ✅ 새로운 refresh_token 저장
// ✅ 이전 refresh_token 무효화

재사용 감지:

  • 이전 refresh_token 재사용 시도 감지
  • 해당 사용자의 모든 토큰 즉시 무효화

4. State 파라미터 (CSRF 방지)

// Authorization 요청 전
const state = generateRandomString(32);
sessionStorage.setItem('oauth_state', state);
 
// 콜백 처리 시
const returnedState = urlParams.get('state');
const savedState = sessionStorage.getItem('oauth_state');
 
if (returnedState !== savedState) {
  throw new Error('CSRF attack detected');
}

5. Strict Redirect URI 검증

서버 측 설정:

{
  "client_id": "abc123",
  "redirect_uris": [
    "https://app.example.com/callback"  // ✅ 정확한 매칭
  ]
}

금지 사항:

{
  "redirect_uris": [
    "https://*.example.com/callback",  // ❌ 와일드카드
    "http://localhost:3000/callback"   // ❌ HTTP (개발 환경 제외)
  ]
}

6. 안전한 Token 저장

웹 애플리케이션

// ❌ 절대 금지
localStorage.setItem('access_token', token);  // XSS 취약
sessionStorage.setItem('access_token', token);  // XSS 취약
 
// ✅ 권장 방법 1: HttpOnly Cookie (서버에서 설정)
Set-Cookie: access_token=...; HttpOnly; Secure; SameSite=Strict
 
// ✅ 권장 방법 2: 메모리에만 저장
let accessToken = null;  // 변수에만 저장, 새로고침 시 재인증

모바일 애플리케이션

// iOS: Keychain
let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrAccount as String: "access_token",
    kSecValueData as String: token.data(using: .utf8)!
]
SecItemAdd(query as CFDictionary, nil)
// Android: Encrypted SharedPreferences
val masterKey = MasterKey.Builder(context)
    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
    .build()
 
val sharedPreferences = EncryptedSharedPreferences.create(
    context,
    "secret_shared_prefs",
    masterKey,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
 
sharedPreferences.edit().putString("access_token", token).apply()

7. HTTPS 필수

# Nginx 설정
server {
    listen 443 ssl http2;
    server_name auth.example.com;
    
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
    
    # HTTP 요청을 HTTPS로 리다이렉트
    if ($scheme != "https") {
        return 301 https://$server_name$request_uri;
    }
}

8. Scope 최소화

// ❌ 과도한 권한
scope: "read write delete admin"
 
// ✅ 필요한 권한만
scope: "read:posts write:posts"

9. Token Validation

// Resource Server에서 토큰 검증
async function validateToken(token) {
  // 1. 토큰 서명 검증 (JWT 사용 시)
  const decoded = jwt.verify(token, PUBLIC_KEY);
  
  // 2. 만료 시간 확인
  if (decoded.exp < Date.now() / 1000) {
    throw new Error('Token expired');
  }
  
  // 3. Scope 확인
  if (!decoded.scope.includes('required:permission')) {
    throw new Error('Insufficient permissions');
  }
  
  // 4. 토큰 무효화 목록 확인 (옵션)
  const isRevoked = await checkRevokedTokens(decoded.jti);
  if (isRevoked) {
    throw new Error('Token revoked');
  }
  
  return decoded;
}

2025년 최신 보안 취약점

1. Okta auth0/nextjs-auth0 OAuth Parameter Injection

Okta Auth0 OAuth Injection Vulnerability (opens in a new tab)

WebProNews에서 "A critical OAuth parameter injection vulnerability in Okta's auth0/nextjs-auth0 library, discovered by Joshua Rogers in October 2025, exposes risks from 'AI slop'—hastily generated code lacking security, enabling authentication hijacking and token leaks"라고 보도했습니다.

발견 시기: 2025년 10월

영향:

  • Next.js 애플리케이션의 OAuth 구현
  • 인증 하이재킹 가능
  • Token 유출

원인:

  • AI가 생성한 코드의 보안 결함
  • 적절한 입력 검증 부재

해결책:

  • 최신 버전으로 업데이트
  • AI 생성 코드에 대한 보안 리뷰 강화

2. workers-oauth-provider PKCE Bypass

Common OAuth Vulnerabilities (opens in a new tab)

Doyensec 블로그에서 "A vulnerability in the OAuth implementation of workers-oauth-provider allows an attacker to bypass the Proof Key for Code Exchange (PKCE) protection mechanism"이라고 보고했습니다.

발견 시기: 2025년

영향:

  • MCP 프레임워크의 OAuth 구현
  • PKCE 보호 메커니즘 완전 우회 가능

해결책:

  • GitHub PR #27 패치 적용
  • PKCE 구현 검증 강화

Grant Type 선택 가이드

빠른 참조표

시나리오추천 Grant TypePKCEClient Secret
서버 사이드 웹 앱Authorization Code선택필수
Single Page App (React, Vue)Authorization Code + PKCE필수❌ 사용 안 함
모바일 앱 (iOS, Android)Authorization Code + PKCE필수❌ 사용 안 함
Desktop 앱 (Electron)Authorization Code + PKCE필수❌ 사용 안 함
마이크로서비스 간 통신Client Credentials❌ 불필요필수
배치 작업Client Credentials❌ 불필요필수
스마트 TV, IoTDevice Code❌ 불필요선택
CLI 도구 (사용자 없음)Client Credentials❌ 불필요필수
CLI 도구 (사용자 있음)Device Code❌ 불필요선택

실제 구현 예시

Authorization Code + PKCE (React SPA)

// 1. PKCE 생성 함수
function generateRandomString(length) {
  const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
  const randomValues = new Uint8Array(length);
  crypto.getRandomValues(randomValues);
  return Array.from(randomValues)
    .map(v => charset[v % charset.length])
    .join('');
}
 
async function sha256(plain) {
  const encoder = new TextEncoder();
  const data = encoder.encode(plain);
  const hash = await crypto.subtle.digest('SHA-256', data);
  return hash;
}
 
function base64UrlEncode(arrayBuffer) {
  const bytes = new Uint8Array(arrayBuffer);
  const binary = String.fromCharCode(...bytes);
  const base64 = btoa(binary);
  return base64
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=/g, '');
}
 
// 2. Authorization 요청
async function initiateOAuth() {
  // PKCE 파라미터 생성
  const codeVerifier = generateRandomString(128);
  const codeChallenge = base64UrlEncode(await sha256(codeVerifier));
  const state = generateRandomString(32);
  
  // 세션 저장
  sessionStorage.setItem('code_verifier', codeVerifier);
  sessionStorage.setItem('oauth_state', state);
  
  // Authorization URL 생성
  const params = new URLSearchParams({
    response_type: 'code',
    client_id: 'YOUR_CLIENT_ID',
    redirect_uri: 'https://app.example.com/callback',
    scope: 'openid profile email',
    state: state,
    code_challenge: codeChallenge,
    code_challenge_method: 'S256'
  });
  
  // 리다이렉트
  window.location.href = `https://auth.example.com/authorize?${params}`;
}
 
// 3. 콜백 처리
async function handleCallback() {
  const urlParams = new URLSearchParams(window.location.search);
  const code = urlParams.get('code');
  const state = urlParams.get('state');
  
  // State 검증 (CSRF 방지)
  const savedState = sessionStorage.getItem('oauth_state');
  if (state !== savedState) {
    throw new Error('State mismatch - possible CSRF attack');
  }
  
  // Code Verifier 가져오기
  const codeVerifier = sessionStorage.getItem('code_verifier');
  
  // Token 교환
  const response = await fetch('https://auth.example.com/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code: code,
      redirect_uri: 'https://app.example.com/callback',
      client_id: 'YOUR_CLIENT_ID',
      code_verifier: codeVerifier
    })
  });
  
  const tokens = await response.json();
  
  // 토큰 저장 (메모리에만 - 보안)
  // sessionStorage/localStorage 사용 금지!
  window.accessToken = tokens.access_token;
  window.refreshToken = tokens.refresh_token;
  
  // 세션 정리
  sessionStorage.removeItem('code_verifier');
  sessionStorage.removeItem('oauth_state');
  
  return tokens;
}
 
// 4. API 요청
async function fetchUserData() {
  const response = await fetch('https://api.example.com/user', {
    headers: {
      'Authorization': `Bearer ${window.accessToken}`
    }
  });
  
  if (response.status === 401) {
    // Token 만료 - refresh 시도
    await refreshAccessToken();
    return fetchUserData(); // 재시도
  }
  
  return response.json();
}
 
// 5. Token 갱신
async function refreshAccessToken() {
  const response = await fetch('https://auth.example.com/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: window.refreshToken,
      client_id: 'YOUR_CLIENT_ID'
    })
  });
  
  const tokens = await response.json();
  window.accessToken = tokens.access_token;
  window.refreshToken = tokens.refresh_token; // Rotation
}

Client Credentials (Node.js 마이크로서비스)

// oauth-client.js
const axios = require('axios');
 
class OAuthClient {
  constructor(config) {
    this.clientId = config.clientId;
    this.clientSecret = config.clientSecret;
    this.tokenEndpoint = config.tokenEndpoint;
    this.accessToken = null;
    this.expiresAt = null;
  }
  
  async getAccessToken() {
    // 유효한 토큰이 있으면 재사용
    if (this.accessToken && Date.now() < this.expiresAt) {
      return this.accessToken;
    }
    
    // 새 토큰 요청
    const response = await axios.post(
      this.tokenEndpoint,
      new URLSearchParams({
        grant_type: 'client_credentials',
        client_id: this.clientId,
        client_secret: this.clientSecret,
        scope: 'api.read api.write'
      }),
      {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
        }
      }
    );
    
    const { access_token, expires_in } = response.data;
    
    this.accessToken = access_token;
    // 만료 5분 전에 갱신 (버퍼)
    this.expiresAt = Date.now() + (expires_in - 300) * 1000;
    
    return this.accessToken;
  }
  
  async makeAuthenticatedRequest(url, options = {}) {
    const token = await this.getAccessToken();
    
    return axios({
      ...options,
      url,
      headers: {
        ...options.headers,
        'Authorization': `Bearer ${token}`
      }
    });
  }
}
 
// 사용 예시
const oauthClient = new OAuthClient({
  clientId: process.env.CLIENT_ID,
  clientSecret: process.env.CLIENT_SECRET,
  tokenEndpoint: 'https://auth.example.com/token'
});
 
// API 호출
const userData = await oauthClient.makeAuthenticatedRequest(
  'https://api.example.com/users/123',
  { method: 'GET' }
);

참고 자료

공식 명세

보안 가이드

구현 가이드

최신 취약점

결론 및 권장사항

OAuth 2.0은 강력하고 유연한 인증/인가 프레임워크이지만, 올바르게 구현하지 않으면 심각한 보안 취약점이 발생할 수 있습니다.

핵심 원칙

  1. 2025년 기준 PKCE는 필수: 모든 public client (SPA, 모바일)에서 반드시 사용
  2. Implicit Grant 사용 금지: OAuth 2.1에서 제거됨
  3. 짧은 수명의 토큰: Access Token 15-60분, Refresh Token Rotation 적용
  4. HTTPS 필수: 모든 OAuth 통신은 HTTPS로만
  5. State 파라미터: CSRF 공격 방지를 위해 항상 사용
  6. 최소 권한 원칙: 필요한 scope만 요청
  7. 안전한 저장소: 웹은 HttpOnly Cookie, 모바일은 Secure Storage
  8. Strict Redirect URI: 정확한 매칭, 와일드카드 금지

Grant Type 선택

  • 서버 사이드 웹 앱 → Authorization Code Grant
  • SPA, 모바일 앱 → Authorization Code + PKCE (필수)
  • 서버 간 통신 → Client Credentials Grant
  • 입력 제한 기기 → Device Code Grant
  • 레거시 시스템 → 가능한 빨리 다른 방식으로 마이그레이션

보안 체크리스트

  • PKCE 구현 (public client)
  • State 파라미터 사용
  • HTTPS 적용
  • Redirect URI 정확히 등록
  • Client Secret 안전하게 보관 (환경 변수, Vault)
  • 짧은 Token 수명 설정
  • Refresh Token Rotation 구현
  • Token을 안전한 저장소에 보관
  • Scope 최소화
  • Rate Limiting 적용
  • 로깅 및 모니터링 구현
  • 정기적인 보안 감사

OAuth 2.0을 올바르게 구현하면 안전하고 확장 가능한 인증 시스템을 구축할 수 있습니다.