JWT vs. OAuth 2.0

기본 개념

  • JWT (JSON Web Token)
    JWT는 당사자 간에 안전하게 정보를 JSON 객체로 전송하기 위한 컴팩트하고 자체 완결적인 방식이다. 이 정보는 디지털 서명되어 있어 신뢰할 수 있다. JWT는 주로 인증(Authentication)과 정보 교환을 위해 사용된다.
  • OAuth 2.0
    OAuth 2.0은 사용자가 자신의 정보에 대한 접근 권한을 제3자 애플리케이션에 부여할 수 있게 해주는 인가(Authorization) 프레임워크이다. 사용자가 비밀번호를 공유하지 않고도 제한된 접근 권한을 제3자에게 제공할 수 있다.

주요 목적과 용도

  1. JWT의 목적

    • 사용자 인증(Authentication)
    • 인증된 사용자 간의 정보 교환
    • 서명된 토큰을 통한 정보의 무결성 검증
    • Stateless 서버 아키텍처 구현
  2. OAuth 2.0의 목적

    • 제3자 애플리케이션에 대한 권한 부여(Authorization)
    • 리소스 소유자를 대신한 제한된 접근 제공
    • 서비스 간 안전한 권한 위임
    • 다양한 애플리케이션 유형(웹, 모바일, IoT 등)에 대한 인가 흐름 제공

3. 작동 원리

  • JWT 작동 원리

    1. 사용자가 로그인하면 서버는 JWT를 생성한다.
    2. JWT는 헤더(알고리즘, 토큰 타입), 페이로드(클레임), 서명의 세 부분으로 구성된다.
    3. 이 토큰을 클라이언트에게 반환한다.
    4. 클라이언트는 이후 요청 시 이 토큰을 Authorization 헤더에 포함시킨다.
    5. 토큰을 검증하고 페이로드의 정보를 기반으로 요청을 처리한다.
  • OAuth 2.0 작동 원리

    1. 클라이언트 애플리케이션이 사용자에게 특정 리소스에 대한 권한을 요청한다.
    2. 사용자가 동의하면 인가 서버는 인가 코드를 발급한다.
    3. 클라이언트는 이 인가 코드를 사용하여 인가 서버에 액세스 토큰을 요청한다.
    4. 인가 서버는 액세스 토큰(및 선택적으로 리프레시 토큰)을 발급한다.
    5. 클라이언트는 이 액세스 토큰을 사용하여 리소스 서버에 보호된 리소스를 요청한다.
    6. 리소스 서버는 토큰을 검증하고 요청된 리소스를 제공한다.

주요 구성 요소

  • JWT 구성 요소
    1. 헤더(Header): 토큰 타입과 사용된 서명 알고리즘 정보
    2. 페이로드(Payload): 등록된 클레임, 공개 클레임, 비공개 클레임 등 사용자와 추가 데이터를 포함
    3. 서명(Signature): 토큰의 무결성을 검증하기 위한 서명 부분
  • OAuth 2.0 구성 요소
    1. 리소스 소유자(Resource Owner): 보호된 리소스에 대한 접근 권한을 부여할 수 있는 사용자
    2. 클라이언트(Client): 리소스 소유자의 리소스에 접근하려는 애플리케이션
    3. 리소스 서버(Resource Server): 보호된 리소스를 호스팅하는 서버
    4. 인가 서버(Authorization Server): 사용자 인증 후 토큰을 발급하는 서버
    5. 액세스 토큰(Access Token): 리소스 접근을 위한 자격 증명
    6. 리프레시 토큰(Refresh Token): 새로운 액세스 토큰을 얻기 위한 토큰

주요 차이점

  1. 목적과 기능
    • JWT: 주로 인증과 정보 교환에 중점
    • OAuth 2.0: 인가(권한 부여)에 중점
  2. 범위
    • JWT: 토큰 형식과 서명 방법을 정의하는 표준
    • OAuth 2.0: 전체 인가 프레임워크와 프로토콜 정의
  3. 복잡성
    • JWT: 상대적으로 단순한 구조와 구현
    • OAuth 2.0: 여러 흐름과 역할이 포함된 복잡한 프레임워크
  4. 토큰 내용
    • JWT: 토큰 자체에 사용자 정보와 메타데이터를 포함
    • OAuth 2.0: 액세스 토큰은 일반적으로 참조 토큰이지만, JWT 형식의 토큰을 사용할 수도 있음
  5. 상태 관리
    • JWT: 일반적으로 상태 비저장(Stateless) 방식
    • OAuth 2.0: 일반적으로 상태 저장(Stateful) 방식, 특히 리프레시 토큰 관리 시

사용 사례

  1. JWT 적합한 사용 사례
    • 단일 도메인 내 사용자 인증
    • 마이크로서비스 간 정보 전달
    • 일회성 이메일 확인이나 비밀번호 재설정
    • 상태 비저장 API 인증
  2. OAuth 2.0 적합한 사용 사례
    • 소셜 로그인(페이스북, 구글 등을 통한 로그인)
    • 제3자 애플리케이션 API 인가
    • 여러 서비스 간의 권한 위임
    • 사용자의 특정 리소스에 대한 제한된 접근 제공

장단점

JWT

  1. 장점
    • 자체 포함적: 필요한 모든 정보를 토큰 자체에 포함
    • 서버 부하 감소: 세션 저장소가 필요 없음
    • 확장성: 분산 시스템에서 효율적
    • 간단한 구현: 대부분의 프로그래밍 언어에서 지원
  2. 단점
    • 토큰 크기: 클레임이 많을수록 토큰 크기 증가
    • 보안 위험: 클라이언트에 저장된 정보는 탈취될 수 있음
    • 토큰 무효화: 발급된 토큰을 즉시 무효화하기 어려움
    • 상태 저장 불가: 로그아웃 등의 기능 구현이 복잡

OAuth 2.0

  1. 장점
    • 높은 보안성: 사용자 자격 증명을 직접 공유하지 않음
    • 세분화된 권한 부여: 특정 권한(scope)에 대해서만 접근 허용
    • 다양한 인가 흐름: 다양한 클라이언트 유형에 적합한 흐름 제공
    • 토큰 갱신 메커니즘: 리프레시 토큰을 통한 장기 접근 지원
  2. 단점
    • 구현 복잡성: 전체 흐름 구현이 복잡함
    • 다양한 해석: 명세의 유연성으로 인한 구현 차이
    • 오버헤드: 여러 요청과 응답 교환이 필요
    • 중앙화된 인가 서버: 단일 장애 지점이 될 수 있음

상호 관계

JWT와 OAuth 2.0은 경쟁 관계가 아니라 보완적인 관계에 있다. OAuth 2.0은 인가 프레임워크로, JWT는 토큰 형식으로 사용될 수 있다. 실제로 많은 OAuth 2.0 구현에서는 액세스 토큰과 ID 토큰으로 JWT 형식을 사용한다. 이러한 결합을 OpenID Connect(OIDC)라고 하며, OAuth 2.0을 확장하여 인증 레이어를 추가한 프로토콜이다.

JWT vs. OAuth 2.0

특성JWTOAuth 2.0
주요 목적인증(Authentication) 및 정보 교환인가(Authorization) 및 권한 위임
형태토큰 형식 표준인가 프레임워크 및 프로토콜
복잡성상대적으로 단순복잡한 여러 흐름
상태 관리Stateless (상태 비저장)일반적으로 Stateful (상태 저장)
토큰 내용자체 포함적 (사용자 정보 포함)일반적으로 참조 토큰 (JWT 사용 가능)
토큰 유효성서명을 통한 로컬 검증인가 서버를 통한 검증 (일반적으로)
토큰 취소어려움 (블랙리스트 필요)상대적으로 쉬움
스케일링높음 (서버에 상태 저장 안 함)중간 (토큰 저장소 필요)
구현 난이도낮음높음
사용 사례단일 서비스 인증, API 인증소셜 로그인, API 인가, 서비스 간 권한 위임
클라이언트 타입제한 없음다양한 클라이언트별 흐름 제공
리소스 접근직접적 (토큰 내 정보 사용)간접적 (토큰으로 리소스 서버 접근)
토큰 갱신기본 지원 없음리프레시 토큰 지원
권한 범위토큰 내 클레임으로 정의Scope 파라미터로 정의
표준화RFC 7519RFC 6749, RFC 6750
보안 강점무결성 보장 (서명)자격 증명 비공개 (권한 위임)
확장성JSON 클레임으로 확장 가능다양한 확장 명세 존재 (OIDC 등)
서드파티 지원직접적으로 지원하지 않음핵심 기능

실제 구현 예시

JWT 구현 예시 (Node.js)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// JWT 인증 구현 예시
const jwt = require('jsonwebtoken');
const express = require('express');
const app = express();

// 비밀키 - 실제로는 환경 변수로 관리해야 함
const SECRET_KEY = 'your_jwt_secret';

app.use(express.json());

// 로그인 라우트
app.post('/login', (req, res) => {
  // 사용자 인증 로직 (데이터베이스 조회 등)
  const user = { id: 1, username: 'test_user', role: 'admin' };
  
  // JWT 토큰 생성
  const token = jwt.sign(
    { 
      userId: user.id,
      username: user.username,
      role: user.role 
    },
    SECRET_KEY,
    { 
      expiresIn: '1h',  // 토큰 만료 시간
      issuer: 'myapp'   // 발행자
    }
  );
  
  // 클라이언트에 토큰 반환
  res.json({ token });
});

// 인증 미들웨어
function authenticateToken(req, res, next) {
  // 헤더에서 토큰 추출
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];  // "Bearer TOKEN" 형식
  
  if (!token) {
    return res.status(401).json({ message: '인증 토큰이 필요합니다' });
  }
  
  // 토큰 검증
  jwt.verify(token, SECRET_KEY, (err, decoded) => {
    if (err) {
      // 토큰 검증 실패
      return res.status(403).json({ message: '유효하지 않은 토큰입니다' });
    }
    
    // 검증 성공 시 요청 객체에 사용자 정보 추가
    req.user = decoded;
    next();
  });
}

// 보호된 라우트
app.get('/protected', authenticateToken, (req, res) => {
  res.json({ 
    message: '인증된 사용자입니다!', 
    user: req.user 
  });
});

app.listen(3000, () => {
  console.log('서버가 3000번 포트에서 실행 중입니다');
});

OAuth 2.0 구현 예시 (Node.js - 인가 코드 흐름)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
// OAuth 2.0 인가 코드 흐름 구현 예시
const express = require('express');
const axios = require('axios');
const crypto = require('crypto');
const app = express();

// OAuth 설정 - 실제로는 환경 변수로 관리해야 함
const CLIENT_ID = 'your_client_id';
const CLIENT_SECRET = 'your_client_secret';
const REDIRECT_URI = 'http://localhost:3000/callback';
const AUTH_SERVER_URL = 'https://authorization-server.com';

// 상태 값 저장을 위한 임시 저장소 (실제로는 Redis 등 사용)
const stateSessions = {};

app.get('/', (req, res) => {
  // CSRF 방지를 위한 상태 값 생성
  const state = crypto.randomBytes(16).toString('hex');
  
  // 상태 값을 세션에 저장
  stateSessions[state] = { created: new Date() };
  
  // 인가 서버로 리디렉션할 URL 생성
  const authorizationUrl = new URL(`${AUTH_SERVER_URL}/authorize`);
  authorizationUrl.searchParams.append('response_type', 'code');
  authorizationUrl.searchParams.append('client_id', CLIENT_ID);
  authorizationUrl.searchParams.append('redirect_uri', REDIRECT_URI);
  authorizationUrl.searchParams.append('scope', 'read profile');
  authorizationUrl.searchParams.append('state', state);
  
  // 사용자를 인가 서버로 리디렉션
  res.redirect(authorizationUrl.toString());
});

// 콜백 처리
app.get('/callback', async (req, res) => {
  const { code, state, error } = req.query;
  
  // 에러 처리
  if (error) {
    return res.status(400).json({ error: error });
  }
  
  // 상태 값 검증
  if (!state || !stateSessions[state]) {
    return res.status(400).json({ error: '유효하지 않은 상태 값입니다' });
  }
  
  // 상태 값 세션 삭제 (1회성)
  delete stateSessions[state];
  
  try {
    // 인가 코드를 액세스 토큰으로 교환
    const tokenResponse = await axios.post(`${AUTH_SERVER_URL}/token`, {
      grant_type: 'authorization_code',
      code: code,
      redirect_uri: REDIRECT_URI,
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET
    }, {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      }
    });
    
    // 토큰 정보 추출
    const { access_token, refresh_token, expires_in } = tokenResponse.data;
    
    // 사용자 정보 요청 (액세스 토큰 사용)
    const userResponse = await axios.get(`${AUTH_SERVER_URL}/userinfo`, {
      headers: {
        'Authorization': `Bearer ${access_token}`
      }
    });
    
    // 사용자 정보와 토큰 반환
    res.json({
      user: userResponse.data,
      tokens: {
        access_token,
        refresh_token,
        expires_in
      }
    });
    
  } catch (error) {
    res.status(500).json({
      error: '토큰 교환 중 오류가 발생했습니다',
      details: error.message
    });
  }
});

// 토큰 갱신 엔드포인트
app.post('/refresh-token', async (req, res) => {
  const { refresh_token } = req.body;
  
  if (!refresh_token) {
    return res.status(400).json({ error: '리프레시 토큰이 필요합니다' });
  }
  
  try {
    // 리프레시 토큰을 사용하여 새 액세스 토큰 요청
    const response = await axios.post(`${AUTH_SERVER_URL}/token`, {
      grant_type: 'refresh_token',
      refresh_token: refresh_token,
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET
    }, {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      }
    });
    
    // 새 토큰 정보 반환
    res.json(response.data);
    
  } catch (error) {
    res.status(500).json({
      error: '토큰 갱신 중 오류가 발생했습니다',
      details: error.message
    });
  }
});

app.listen(3000, () => {
  console.log('OAuth 클라이언트가 3000번 포트에서 실행 중입니다');
});

용어 정리

용어설명

참고 및 출처