JWT vs. OpenID Connect

JWT(JSON Web Token)와 OpenID Connect(OIDC)는 모두 현대적인 인증 및 권한 부여 시스템에서 중요한 역할을 하는 기술이다. 이 두 기술은 서로 밀접한 관계가 있지만, 목적과 기능 면에서 중요한 차이점을 가지고 있다.

JWT(JSON Web Token)

JWT는 당사자 간에 안전하게 정보를 전송하기 위한 개방형 표준(RFC 7519)으로, 컴팩트하고 자체 포함적인 방식으로 정보를 안전하게 전달한다.

기본 구조

JWT는 점(.)으로 구분된 세 부분으로 구성된다:

  1. 헤더(Header): 토큰 유형과 사용된 암호화 알고리즘 정보
  2. 페이로드(Payload): 클레임(사용자 ID, 만료 시간 등) 정보
  3. 서명(Signature): 토큰의 무결성을 보장하는 디지털 서명

예시:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

주요 특징

  • 자체 포함성(Self-contained): 필요한 모든 정보를 토큰 자체에 포함
  • 무상태(Stateless): 서버 측에서 세션 상태를 유지할 필요가 없음
  • 컴팩트(Compact): URL로 전송 가능한 크기
  • 서명 기반(Signature-based): 무결성 보장
  • 다양한 암호화 알고리즘 지원: HMAC, RSA, ECDSA 등

주요 용도

  • 인증(Authentication)
  • 정보 교환(Information Exchange)
  • 권한 부여(Authorization)

OpenID Connect (OIDC)

OpenID Connect는 OAuth 2.0 프로토콜 위에 구축된 인증 계층으로, 클라이언트가 사용자의 신원을 확인하고 기본적인 프로필 정보를 얻을 수 있도록 하는 표준이다.

구성 요소

  1. ID 토큰: JWT 형식으로 사용자 신원 정보를 포함
  2. UserInfo 엔드포인트: 사용자 프로필 정보 제공
  3. 인증 엔드포인트: 사용자 인증 처리
  4. 토큰 엔드포인트: 토큰 발급 및 갱신
  5. 검색 문서(Discovery Document): 서비스 설정 정보

주요 특징

  • 표준화된 인증 프로토콜: OAuth 2.0 확장
  • 다양한 인증 흐름: 권한 부여 코드 흐름, 암시적 흐름 등
  • 중앙 집중식 로그인: 단일 ID 제공자를 통한 다중 서비스 접근
  • 클레임 기반 신원 확인: 표준화된 사용자 속성
  • 연합 인증(Federated Authentication): 여러 신원 제공자 지원

주요 용도

  • 단일 로그인(Single Sign-On)
  • 신원 확인 및 프로필 정보 제공
  • 엔터프라이즈 인증 시스템 통합

심층 분석

주요 차이점

  1. 개념적 차이:
    • JWT는 정보를 안전하게 전송하는 데 사용되는 토큰 형식이다.
    • OIDC는 사용자 인증 및 신원 확인을 위한 프로토콜이다.
  2. 범위와 목적:
    • JWT는 다양한 용도로 사용될 수 있는 범용 토큰 형식이다.
    • OIDC는 특별히 사용자 인증과 신원 확인을 위해 설계된 프로토콜이다.
  3. 구현 복잡성:
    • JWT는 상대적으로 구현이 간단하다(토큰 생성 및 검증).
    • OIDC는 여러 엔드포인트와 흐름을 포함하는 더 복잡한 프로토콜이다.
  4. 기능 세트:
    • JWT는 기본적인 정보 전달과 서명 검증에 중점을 둔다.
    • OIDC는 사용자 세션 관리, 토큰 갱신, 사용자 정보 조회 등 다양한 기능을 제공한다.

JWT의 작동 방식

  1. 토큰 생성:

    1
    2
    3
    4
    5
    6
    7
    
    // Node.js 예시
    const jwt = require('jsonwebtoken');
    const token = jwt.sign(
      { userId: '123', role: 'admin' },  // 페이로드
      'secretKey',                       // 서명 키
      { expiresIn: '1h' }                // 옵션
    );
    
  2. 토큰 검증:

    1
    2
    3
    4
    5
    6
    
    try {
      const decoded = jwt.verify(token, 'secretKey');
      console.log(decoded.userId); // '123'
    } catch(err) {
      console.error('토큰 검증 실패');
    }
    
  3. 인증 흐름:

    • 사용자가 로그인하면 서버는 JWT를 생성하여 반환
    • 클라이언트는 이후 요청 시 Authorization 헤더에 JWT 포함
    • 서버는 JWT 서명을 검증하고 포함된 정보를 사용

OpenID Connect의 작동 방식

  1. 권한 부여 코드 흐름(가장 일반적):

    • 사용자가 클라이언트 애플리케이션에 접근
    • 클라이언트는 사용자를 ID 제공자(IdP)의 인증 엔드포인트로 리디렉션
    • 사용자가 인증한 후, IdP는 권한 부여 코드와 함께 사용자를 클라이언트로 리디렉션
    • 클라이언트는 이 코드를 IdP의 토큰 엔드포인트에 제출하여 ID 토큰과 접근 토큰 획득
    • ID 토큰(JWT 형식)은 사용자의 신원을 확인하는 데 사용
    • 접근 토큰은 UserInfo 엔드포인트에서 추가 사용자 정보를 조회하는 데 사용 가능
  2. ID 토큰(JWT 형태)의 예시 클레임:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    {
      "iss": "https://auth.example.com",
      "sub": "24400320",
      "aud": "s6BhdRkqt3",
      "exp": 1311281970,
      "iat": 1311280970,
      "auth_time": 1311280969,
      "name": "홍길동",
      "email": "gildong@example.com"
    }
    
  3. 검색 문서(Discovery Document) - OIDC 구성 정보:

    1
    2
    3
    4
    5
    6
    7
    8
    
    {
      "issuer": "https://auth.example.com",
      "authorization_endpoint": "https://auth.example.com/auth",
      "token_endpoint": "https://auth.example.com/token",
      "userinfo_endpoint": "https://auth.example.com/userinfo",
      "jwks_uri": "https://auth.example.com/jwks.json",
      ...
    }
    

장단점 분석

JWT의 장단점

장점:

  • 간단하고 경량적인 구조
  • 무상태(Stateless) 특성으로 확장성이 우수
  • 다양한 언어 및 플랫폼에서 지원
  • 서명을 통한 데이터 무결성 보장
  • 자체 포함적 특성으로 추가 데이터베이스 조회 감소

단점:

  • 저장된 정보 암호화가 기본적으로 없음(인코딩만 됨)
  • 일단 발급된 토큰은 만료될 때까지 취소하기 어려움
  • 토큰 크기가 클 수 있음(많은 정보 포함 시)
  • 보안 키 관리의 중요성
  • 구현 실수 시 보안 취약점 발생 가능

OpenID Connect의 장단점

장점:

  • 표준화된 사용자 인증 프로토콜
  • 다양한 인증 흐름 지원(다양한 애플리케이션 유형)
  • 사용자 프로필 정보 표준화
  • 다중 요소 인증, 토큰 철회 등 고급 보안 기능
  • 상호 운용성(다양한 ID 제공자와 호환)

단점:

  • 구현 복잡성이 높음
  • 전체 흐름 이해 및 올바른 구현의 어려움
  • 추가적인 네트워크 요청 발생
  • 설정 및 유지 관리의 복잡성
  • 서드파티 의존성(ID 제공자가 다운되면 인증 불가)

사용 시나리오 및 선택 가이드

JWT가 적합한 경우

  • 간단한 API 인증
  • 마이크로서비스 간 통신
  • 상태 정보 전달이 필요한 경우
  • 단일 도메인 내 인증 시스템
  • 서버 리소스 최적화가 중요한 경우
  • 빠른 개발이 필요한 소규모 프로젝트

OpenID Connect가 적합한 경우

  • 소셜 로그인 구현
  • 다중 애플리케이션 단일 로그인(SSO)
  • 엔터프라이즈 인증 시스템
  • 사용자 신원 확인이 중요한 경우
  • 다중 요소 인증이 필요한
  • 다양한 클라이언트 유형(웹, 모바일, SPA) 지원
  • 글로벌 사용자 기반 서비스

통합 및 상호보완적 사용

JWT와 OpenID Connect는 상호 배타적이지 않으며, 실제로 OIDC는 JWT를 ID 토큰 형식으로 사용한다.

다음과 같은 통합 패턴이 일반적이다:

  1. OIDC로 인증, JWT로 권한 부여:
    • OIDC를 통해 사용자 인증 및 ID 토큰 획득
    • 인증된 세션에 대해 JWT 기반 접근 토큰 발급
    • API 호출 시 JWT 사용
  2. 마이크로서비스 아키텍처에서의 활용:
    • 경계(게이트웨이) 레벨에서 OIDC 인증
    • 내부 서비스 간 통신에 JWT 활용
  3. 하이브리드 인증 시스템:
    • 웹 애플리케이션은 OIDC 흐름 사용
    • API는 JWT 베어러 토큰 인증 사용
    • 두 시스템이 동일한 사용자 정보 및 권한 공유

보안 고려사항

JWT 보안

  1. 비밀 키 보호: 서명 키를 안전하게 관리하고 정기적으로 순환
  2. 토큰 수명 제한: 짧은 만료 시간 설정
  3. 민감한 정보 제외: 중요 데이터는 토큰에 포함하지 않음
  4. 적절한 서명 알고리즘 사용: 최소 HS256 또는 RS256 권장
  5. 안전한 저장소 사용: HttpOnly 쿠키 또는 세션 스토리지 활용

OpenID Connect 보안

  1. 클라이언트 인증: 클라이언트 시크릿 보호
  2. PKCE(Proof Key for Code Exchange): 모바일/SPA 애플리케이션용 추가 보안
  3. 상태 파라미터 검증: CSRF 공격 방지
  4. ID 토큰 검증: 발급자, 대상, 서명, 만료 시간 확인
  5. TLS/SSL 사용: 모든 엔드포인트와 통신

JWT vs. OpenID Connect 비교

특성JWT(JSON Web Token)OpenID Connect
정의정보를 안전하게 전송하기 위한 표준 형식OAuth 2.0 위에 구축된 인증 프로토콜
범주토큰 형식인증 프로토콜
관계OIDC에서 ID 토큰 형식으로 사용됨JWT를 ID 토큰으로 활용
주요 목적정보 전달, 인증, 권한 부여사용자 인증 및 신원 확인
구성 요소헤더, 페이로드, 서명ID 토큰, 접근 토큰, UserInfo 엔드포인트 등
복잡성낮음 (단순한 형식)높음 (프로토콜 및 흐름)
표준화RFC 7519OpenID Foundation
규모경량 (단일 토큰)광범위 (전체 프로토콜)
상태 관리무상태(Stateless)일부 상태 유지 가능(세션 관리 등)
확장성제한적 (사용자 정의 클레임)고급 (표준 클레임, 동적 클라이언트 등록)
인증 주체단일 서버 또는 서비스신뢰할 수 있는 ID 제공자
주요 사용 사례API 인증, 정보 교환SSO, 소셜 로그인, 엔터프라이즈 인증
검증 방식서명 검증서명 검증 + 프로토콜 규칙 준수
부가 기능제한적세션 관리, 동적 등록, 사용자 정보 조회
구현 복잡성낮음중간~높음
보안 기능기본적 (서명, 암호화)고급 (세션 관리, 철회, 다중 요소 인증 등)
상호운용성다양한 구현체인증 제공자 간 호환성

구현 예시

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
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
app.use(express.json());

const SECRET_KEY = 'your-secret-key';  // 실제 환경에서는 안전하게 관리

// 로그인 및 JWT 발급
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  
  // 실제 구현에서는 데이터베이스 조회 등으로 사용자 검증
  if (username === 'user' && password === 'pass') {
    const token = jwt.sign(
      { userId: '12345', username: 'user', role: 'admin' },
      SECRET_KEY,
      { expiresIn: '1h' }
    );
    
    return res.json({ token });
  }
  
  return res.status(401).json({ message: '인증 실패' });
});

// JWT 검증 미들웨어
function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];
  
  if (!token) return res.status(401).json({ message: '토큰 없음' });
  
  jwt.verify(token, SECRET_KEY, (err, user) => {
    if (err) return res.status(403).json({ message: '토큰 무효' });
    
    req.user = user;
    next();
  });
}

// 보호된 엔드포인트
app.get('/protected', authenticateToken, (req, res) => {
  res.json({ 
    message: '보호된 리소스에 접근 성공', 
    user: req.user 
  });
});

app.listen(3000, () => {
  console.log('서버 시작: http://localhost:3000');
});

OpenID Connect 클라이언트 구현 예시 (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
const express = require('express');
const { Issuer, generators } = require('openid-client');
const session = require('express-session');

const app = express();

// 세션 설정
app.use(session({
  secret: 'session-secret',
  resave: false,
  saveUninitialized: true
}));

// OIDC 설정 (구글 예시)
let client;
Issuer.discover('https://accounts.google.com')
  .then(issuer => {
    client = new issuer.Client({
      client_id: 'YOUR_CLIENT_ID',
      client_secret: 'YOUR_CLIENT_SECRET',
      redirect_uris: ['http://localhost:3000/callback'],
      response_types: ['code']
    });
    console.log('OIDC 클라이언트 초기화 완료');
  })
  .catch(err => {
    console.error('OIDC 설정 오류:', err);
  });

// 로그인 시작
app.get('/login', (req, res) => {
  const code_verifier = generators.codeVerifier();
  const code_challenge = generators.codeChallenge(code_verifier);
  
  req.session.code_verifier = code_verifier;
  
  const authUrl = client.authorizationUrl({
    scope: 'openid email profile',
    code_challenge,
    code_challenge_method: 'S256'
  });
  
  res.redirect(authUrl);
});

// 콜백 처리
app.get('/callback', async (req, res) => {
  try {
    const params = client.callbackParams(req);
    const tokenSet = await client.callback(
      'http://localhost:3000/callback',
      params,
      { code_verifier: req.session.code_verifier }
    );
    
    // ID 토큰 (JWT 형식)
    const idToken = tokenSet.id_token;
    
    // ID 토큰 클레임 확인
    const claims = tokenSet.claims();
    
    // 사용자 정보 (추가 정보 필요 시)
    const userinfo = await client.userinfo(tokenSet);
    
    // 세션에 사용자 정보 저장
    req.session.user = userinfo;
    
    res.redirect('/profile');
  } catch (err) {
    console.error('인증 오류:', err);
    res.status(500).send('인증 처리 중 오류 발생');
  }
});

// 사용자 프로필
app.get('/profile', (req, res) => {
  if (!req.session.user) {
    return res.redirect('/login');
  }
  
  res.json({
    message: '인증된 사용자 정보',
    user: req.session.user
  });
});

app.listen(3000, () => {
  console.log('서버 시작: http://localhost:3000');
});

용어 정리

용어설명

참고 및 출처