🔒 보안

OAuth 2.1

OAuth 2.0의 보안 강화 버전

📖 상세 설명

OAuth 2.1은 OAuth 2.0의 보안 모범 사례(BCP)를 통합하고, 더 이상 안전하지 않은 기능을 제거한 최신 인가(Authorization) 프레임워크입니다. RFC 6749(OAuth 2.0)를 기반으로 하지만, Implicit Grant와 Resource Owner Password Credentials Grant를 완전히 제거하고 PKCE를 필수로 요구합니다.

OAuth 2.1의 핵심 변경사항은 PKCE(Proof Key for Code Exchange) 필수화입니다. Authorization Code Flow에서 code_verifier와 code_challenge를 사용하여 인가 코드 가로채기(Authorization Code Interception) 공격을 방지합니다. 이전에는 public client에만 권장되었지만, 이제 모든 클라이언트에 필수입니다.

Implicit Grant(response_type=token) 제거는 토큰이 URL fragment에 노출되어 브라우저 히스토리, 리퍼러 헤더로 유출될 수 있기 때문입니다. 대신 Authorization Code + PKCE를 사용합니다. Password Grant 제거는 클라이언트가 사용자 비밀번호를 직접 다루는 것이 보안상 위험하기 때문입니다.

Refresh Token Rotation도 권장됩니다. Refresh Token 사용 시 새 Refresh Token을 발급하고 이전 것을 무효화하여, 탈취된 토큰의 악용을 제한합니다. Access Token은 더 짧은 만료 시간(분 단위)을 권장하며, Refresh Token으로 갱신합니다.

💻 코드 예제

// OAuth 2.1 Authorization Code Flow with PKCE

// 1. PKCE code_verifier 생성 (43-128자 랜덤 문자열)
function generateCodeVerifier(): string {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  return base64URLEncode(array);
}

// 2. code_challenge 생성 (S256 방식 권장)
async function generateCodeChallenge(verifier: string): Promise<string> {
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const hash = await crypto.subtle.digest('SHA-256', data);
  return base64URLEncode(new Uint8Array(hash));
}

// 3. Authorization 요청 시작
async function startOAuth() {
  const codeVerifier = generateCodeVerifier();
  const codeChallenge = await generateCodeChallenge(codeVerifier);
  const state = crypto.randomUUID(); // CSRF 방지

  // 세션/로컬 스토리지에 저장 (토큰 교환 시 필요)
  sessionStorage.setItem('oauth_code_verifier', codeVerifier);
  sessionStorage.setItem('oauth_state', state);

  const params = new URLSearchParams({
    response_type: 'code',           // Implicit(token) 대신 항상 code
    client_id: 'YOUR_CLIENT_ID',
    redirect_uri: 'https://app.example.com/callback',
    scope: 'openid profile email',
    state: state,
    code_challenge: codeChallenge,   // PKCE 필수
    code_challenge_method: 'S256'    // S256 권장, plain은 비권장
  });

  window.location.href = `https://auth.example.com/authorize?${params}`;
}

// 4. Callback에서 토큰 교환
async function handleCallback() {
  const params = new URLSearchParams(window.location.search);
  const code = params.get('code');
  const state = params.get('state');

  // State 검증 (CSRF 방지)
  if (state !== sessionStorage.getItem('oauth_state')) {
    throw new Error('Invalid state - possible CSRF attack');
  }

  const codeVerifier = sessionStorage.getItem('oauth_code_verifier');

  // 토큰 엔드포인트에 code + code_verifier 전송
  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    // PKCE 검증
    })
  });

  const tokens = await response.json();
  // { access_token, refresh_token, id_token, expires_in }

  // 정리
  sessionStorage.removeItem('oauth_code_verifier');
  sessionStorage.removeItem('oauth_state');

  return tokens;
}

// 5. Refresh Token Rotation
async function refreshTokens(refreshToken: string) {
  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: refreshToken,
      client_id: 'YOUR_CLIENT_ID'
    })
  });

  // 새 access_token + 새 refresh_token 반환 (Rotation)
  return response.json();
}

🗣️ 실무 대화 예시

보안 검토에서:

"기존 OAuth 2.0 구현에 Implicit Flow 쓰고 있는데, OAuth 2.1로 마이그레이션해야 합니다. PKCE 추가하고 Implicit 제거하세요. SPA도 Authorization Code + PKCE로 바꿔야 해요."

기술 면접에서:

"PKCE가 왜 필요한가요?" - "Authorization Code가 탈취되어도 code_verifier 없이는 토큰 교환이 안 됩니다. 특히 모바일 앱에서 custom URL scheme 리다이렉트 시 다른 앱이 코드를 가로챌 수 있는데, PKCE가 이를 방지합니다."

API 설계에서:

"Access Token 만료 시간은 어떻게 설정하나요?" - "OAuth 2.1에선 짧게 설정(5-15분)하는 게 권장돼요. Refresh Token으로 갱신하고, Refresh Token Rotation으로 탈취 영향을 최소화합니다."

⚠️ 주의사항

🔗 관련 용어

FIDO2 SAML JWT Token-based Auth OpenID Connect

📚 더 배우기

📄 IETF - OAuth 2.1 Draft Specification 📄 OAuth.net - OAuth 2.1 Overview 📄 RFC 7636 - PKCE Specification