보안

JWT, OAuth 2.0, OpenID Connect: 보안 취약점과 방어 전략 심층 분석

강코의 코딩 일기 2026. 4. 1. 21:23

JWT, OAuth 2.0, OpenID Connect를 안전하게 구현하기 위한 필수 보안 취약점과 효과적인 방어 전략을 심층 분석합니다. 개발자라면 반드시 알아야 할 보안 가이드.

인증과 인가는 현대 웹 애플리케이션의 핵심 요소입니다. 사용자 신원을 확인하고 권한을 부여하는 과정은 서비스의 신뢰도를 결정짓는 중요한 부분이죠. 특히 JWT (JSON Web Token), OAuth 2.0, OpenID Connect (OIDC)는 분산 환경에서 널리 사용되는 강력한 도구들이지만, 잘못 구현할 경우 심각한 보안 취약점을 야기할 수 있습니다. 과연 여러분의 서비스는 이러한 위협으로부터 안전하게 보호되고 있을까요?

이 글에서는 JWT, OAuth 2.0, OpenID Connect 각각의 구현 시 발생할 수 있는 주요 보안 취약점을 심층 분석하고, 실질적인 방어 전략을 제시합니다. 각각의 장단점을 살펴보며, 안전하고 견고한 인증 및 인가 시스템을 구축하기 위한 필수 지식들을 탐구해 보겠습니다.

JWT, OAuth 2.0, OpenID Connect 구현 시 보안 취약점과 방어 전략 - firearm, revolver, bullet, gun, weapon, handgun, crime, danger, shot, shoot, crime scene, shooting, security, criminal, murder, dangerous, defense, brown security, brown gun

Image by stevepb on Pixabay

인증/인가 프로토콜의 중요성과 보안 위협

웹 서비스의 복잡성이 증가하고 마이크로서비스 아키텍처가 확산되면서, 중앙 집중식 세션 관리 방식의 한계가 명확해졌습니다. 이에 따라 등장한 것이 토큰 기반 인증 방식인 JWT, 그리고 권한 위임을 위한 표준 프레임워크인 OAuth 2.0, 그리고 그 위에 신원 확인 계층을 추가한 OpenID Connect입니다. 이 기술들은 사용자 경험을 향상시키고 시스템 확장성을 높이는 데 기여했지만, 동시에 새로운 형태의 보안 위협을 초래했습니다.

이러한 기술들이 가진 유연성은 강력한 장점이지만, 그 유연성만큼이나 구현 시 개발자의 책임이 커집니다. 잘못된 설정 하나가 전체 시스템을 위협할 수 있기 때문입니다. 예를 들어, 토큰 탈취, 권한 상승, 서비스 거부(DoS) 등 다양한 공격 시나리오에 노출될 수 있습니다. 따라서 각 프로토콜의 작동 방식을 정확히 이해하고, 발생 가능한 취약점에 대한 깊이 있는 분석과 방어 전략 수립이 필수적입니다.

JWT (JSON Web Token) 보안 취약점과 방어 전략

JWT는 클라이언트와 서버 간 정보를 안전하게 전달하기 위한 간결하고 자체 포함적인 방법으로 각광받고 있습니다. 그러나 그 구조적 특성 때문에 몇 가지 고질적인 보안 취약점을 가지고 있습니다.

서명 검증 우회 및 키 노출

JWT의 가장 치명적인 취약점 중 하나는 서명 검증 우회입니다. 특히 alg: "none" 알고리즘을 허용하는 서버가 있다면, 공격자는 서명 없이 임의의 페이로드를 생성하여 서버에 전송할 수 있습니다. 서버가 이를 검증 없이 신뢰한다면, 권한 상승이나 임의 코드 실행으로 이어질 수 있습니다.

또한, JWT의 서명에 사용되는 비밀 키(Secret Key) 노출은 모든 토큰을 무력화할 수 있는 심각한 위협입니다. 공격자가 비밀 키를 알게 되면, 유효한 JWT를 무한정 생성하여 시스템에 접근할 수 있게 됩니다.

방어 전략:

  • 강력한 서명 알고리즘 사용: HS256, RS256, ES256과 같은 강력한 암호화 알고리즘을 사용하고, alg: "none"은 절대 허용하지 않도록 서버에서 명시적으로 거부해야 합니다.
  • 보안 키 관리: 비밀 키는 외부에 노출되지 않도록 환경 변수, 키 관리 서비스(KMS) 또는 안전한 볼트(Vault)에 저장해야 합니다. 주기적인 키 교체(Key Rotation)도 중요합니다.
  • 서명 검증의 철저함: 모든 수신 JWT에 대해 발급자(iss), 수신자(aud), 만료 시간(exp), 서명(signature) 등을 포함한 모든 필드를 철저히 검증해야 합니다.

토큰 탈취 및 재사용 공격 (Replay Attack)

JWT는 한 번 발급되면 만료되기 전까지 유효합니다. 만약 공격자가 유효한 JWT를 탈취한다면, 해당 토큰이 만료될 때까지 합법적인 사용자처럼 행세하며 서비스에 접근할 수 있습니다. 이는 세션 하이재킹과 유사한 형태로, 주로 XSS (Cross-Site Scripting) 공격을 통해 토큰이 탈취되거나, 안전하지 않은 통신 채널을 통해 가로채질 수 있습니다.

방어 전략:

  • 짧은 만료 시간 설정: Access Token의 만료 시간을 5분~30분 정도로 짧게 설정하여 탈취 시 공격자가 활동할 수 있는 시간을 최소화합니다.
  • Refresh Token 활용: 만료 시간이 짧은 Access Token과 만료 시간이 긴 Refresh Token을 함께 사용합니다. Refresh Token은 HttpOnly 속성의 쿠키에 저장하여 XSS 공격으로부터 보호하고, 사용 시마다 재사용 여부를 검증해야 합니다.
  • 토큰 블랙리스트/폐기: 사용자가 로그아웃하거나 비밀번호를 변경했을 때, 또는 의심스러운 활동이 감지될 경우 해당 토큰을 즉시 무효화할 수 있는 블랙리스트(Blacklist) 또는 폐기(Revocation) 메커니즘을 구현해야 합니다. 이는 Redis와 같은 인메모리 데이터베이스를 활용하여 빠르게 검증할 수 있습니다.
  • HTTPS 강제: 모든 통신은 반드시 HTTPS를 통해 이루어져야 합니다. HTTP 통신은 중간자 공격(Man-in-the-Middle)에 취약하여 토큰이 쉽게 가로채질 수 있습니다.
// Express.js 예시: JWT 토큰 폐기 미들웨어 (Redis 사용 가정)
const jwt = require('jsonwebtoken');
const redisClient = require('./redisClient'); // Redis 클라이언트 설정

async function revokeToken(req, res, next) {
    const token = req.headers.authorization?.split(' ')[1];

    if (!token) {
        return res.status(401).send('No token provided.');
    }

    try {
        // 토큰이 블랙리스트에 있는지 확인
        const isBlacklisted = await redisClient.get(`blacklist:${token}`);
        if (isBlacklisted) {
            return res.status(401).send('Token has been revoked.');
        }

        // 토큰 디코딩하여 만료 시간 확인
        const decoded = jwt.decode(token);
        if (!decoded || !decoded.exp) {
            return res.status(400).send('Invalid token structure.');
        }

        // 남은 만료 시간 계산 (초 단위)
        const expiresIn = decoded.exp - Math.floor(Date.now() / 1000);

        // 토큰을 블랙리스트에 추가하고 남은 만료 시간 동안 유지
        await redisClient.set(`blacklist:${token}`, 'true', 'EX', expiresIn);
        res.status(200).send('Token revoked successfully.');

    } catch (error) {
        console.error('Token revocation error:', error);
        res.status(500).send('Internal server error.');
    }
}

토큰 저장 위치의 취약점

JWT를 클라이언트 측에 저장하는 방식에 따라 XSS, CSRF 등의 공격에 취약해질 수 있습니다. 일반적으로 localStorageHttpOnly 쿠키가 비교 대상이 됩니다.

특징 localStorage HttpOnly Cookie
접근 방식 JavaScript를 통해 접근 가능 JavaScript 접근 불가, HTTP 요청 시 자동 전송
XSS 취약성 높음 (스크립트 삽입 시 토큰 탈취 용이) 낮음 (JavaScript로 토큰 읽기 불가)
CSRF 취약성 낮음 (요청 헤더에 수동 추가 필요) 높음 (자동 전송되므로 CSRF 토큰 필요)
보안 권장 사항 Access Token만 저장, 강력한 XSS 방어 필수 Refresh Token 저장에 적합, CSRF 토큰과 함께 사용

방어 전략:

  • HttpOnly 쿠키 사용 (권장): Refresh Token은 HttpOnly, Secure, SameSite=Lax 또는 Strict 속성을 가진 쿠키에 저장하는 것이 XSS 공격으로부터 가장 안전합니다. Access Token은 localStorage에 저장하거나, 더 안전하게는 메모리에 저장하고 매 요청 시 쿠키에서 Refresh Token을 이용해 Access Token을 재발급받는 방법을 고려할 수 있습니다.
  • CSRF 토큰 사용: HttpOnly 쿠키 사용 시 CSRF 공격에 대비하여 모든 상태 변경 요청에 CSRF 토큰을 포함시켜야 합니다.

OAuth 2.0 보안 취약점과 방어 전략

OAuth 2.0은 권한 위임을 위한 프레임워크로, 사용자 대신 특정 리소스에 접근할 수 있는 권한을 제3자 애플리케이션에 부여합니다. 유연하고 강력하지만, 다양한 플로우(Grant Type)와 복잡성 때문에 잘못 구현하면 심각한 취약점을 노출할 수 있습니다.

리다이렉트 URI 조작

OAuth 2.0의 핵심은 리다이렉트 URI (Redirect URI)를 통해 인증 코드나 액세스 토큰을 클라이언트에게 전달하는 것입니다. 공격자가 이 리다이렉트 URI를 조작하여 인증 코드나 토큰을 가로챌 수 있다면, 이는 인가 코드 가로채기(Authorization Code Interception) 공격으로 이어집니다.

방어 전략:

  • 엄격한 리다이렉트 URI 등록 및 검증: 인가 서버(Authorization Server)는 클라이언트 애플리케이션이 등록한 리다이렉트 URI 목록을 매우 엄격하게 관리하고, 인가 요청 시 전달된 URI가 등록된 목록에 정확히 일치하는지 완전 일치 방식으로 검증해야 합니다. 와일드카드나 부분 일치는 위험합니다.
  • HTTPS 강제: 리다이렉트 URI는 반드시 HTTPS 프로토콜을 사용해야 합니다.

클라이언트 자격 증명 노출

client_idclient_secret은 클라이언트 애플리케이션을 인가 서버에 식별하고 인증하는 데 사용됩니다. 클라이언트 시크릿(Client Secret)이 노출될 경우, 공격자는 해당 클라이언트인 것처럼 가장하여 토큰을 발급받거나 민감한 정보를 탈취할 수 있습니다.

방어 전략:

  • 안전한 클라이언트 시크릿 관리: 서버 측 애플리케이션(Confidential Client)의 경우, 클라이언트 시크릿을 안전한 환경 변수나 키 관리 시스템에 저장해야 합니다.
  • PKCE (Proof Key for Code Exchange) 사용: 모바일 앱이나 SPA(Single Page Application)와 같은 공개 클라이언트(Public Client)는 클라이언트 시크릿을 안전하게 저장할 수 없으므로, 인가 코드 플로우(Authorization Code Flow)와 함께 PKCE를 반드시 사용해야 합니다. PKCE는 인가 코드 가로채기 공격을 방지하여, 탈취된 인가 코드를 악의적인 클라이언트가 액세스 토큰으로 교환하는 것을 막아줍니다.
// PKCE 흐름 (간략화된 코드 예시)
// 클라이언트 측
const generateRandomString = (length) => { /* ... */ }; // 랜덤 문자열 생성
const sha256 = (plain) => { /* ... */ }; // SHA256 해시 함수
const base64UrlEncode = (buffer) => { /* ... */ }; // Base64 URL 인코딩

const code_verifier = generateRandomString(128);
const code_challenge = base64UrlEncode(sha256(code_verifier));
const code_challenge_method = 'S256';

// 인가 요청 시
// GET /authorize?response_type=code&client_id=...&redirect_uri=...&scope=...
// &code_challenge=CODE_CHALLENGE&code_challenge_method=S256

// 토큰 요청 시
// POST /token?grant_type=authorization_code&client_id=...&redirect_uri=...
// &code=AUTHORIZATION_CODE&code_verifier=CODE_VERIFIER

Implicit Flow의 위험성 (현재는 권장하지 않음)

이전에는 SPA에서 Implicit Flow를 사용하여 액세스 토큰을 직접 받는 경우가 있었으나, 이는 토큰이 URL 파편(Fragment)을 통해 전달되므로 브라우저 히스토리, 로그 등에 노출될 위험이 큽니다. XSS 공격에 취약하며, Refresh Token을 사용할 수 없다는 단점도 있습니다.

방어 전략:

  • Authorization Code Flow with PKCE 사용: SPA 및 모바일 애플리케이션은 Implicit Flow 대신 PKCE를 적용한 Authorization Code Flow를 사용하는 것이 강력히 권장됩니다. 이는 인가 코드가 백엔드 서버를 통해 교환되므로 프런트엔드 노출 위험이 줄어듭니다.
JWT, OAuth 2.0, OpenID Connect 구현 시 보안 취약점과 방어 전략 - toddler hand, child's hand, hand, trust, hands, closeness, affection, hold tight, security, keep, support, prop up, connection, contact, close, relationship, connect, together, love, connect competition, trust, trust, trust, trust, trust, security, security, security, support, support, connection

Image by Myriams-Fotos on Pixabay

OpenID Connect (OIDC) 보안 취약점과 방어 전략

OpenID Connect는 OAuth 2.0 위에 구축된 신원 확인(Identity Layer) 프로토콜입니다. 사용자의 신원을 클라이언트 애플리케이션이 안전하게 확인할 수 있도록 ID 토큰(ID Token)을 제공하며, JWT 형식으로 인코딩됩니다. OIDC는 OAuth 2.0의 취약점을 상당 부분 공유하며, ID 토큰과 관련된 고유한 취약점도 가집니다.

ID 토큰 위변조 및 재사용

ID 토큰은 사용자의 신원 정보를 담고 있으므로, 이 토큰이 위변조되거나 재사용될 경우 심각한 보안 문제를 야기합니다. 공격자가 ID 토큰의 서명을 위조하거나, 탈취한 ID 토큰을 다른 세션에서 재사용하려 할 수 있습니다.

방어 전략:

  • 철저한 ID 토큰 검증: 클라이언트 애플리케이션은 수신한 ID 토큰에 대해 다음과 같은 사항을 반드시 검증해야 합니다.
    • 서명(Signature) 검증: 발급자(Issuer)의 공개 키를 사용하여 서명을 검증합니다.
    • 발급자(iss) 검증: ID 토큰을 발급한 주체가 예상한 주체인지 확인합니다.
    • 수신자(aud) 검증: ID 토큰이 해당 클라이언트 애플리케이션을 위해 발급되었는지 확인합니다.
    • 만료 시간(exp) 검증: 토큰이 만료되지 않았는지 확인합니다.
    • 발급 시간(iat) 검증: 토큰이 너무 오래된 것은 아닌지 확인합니다.
    • Nonce 검증: 인가 요청 시 전달한 nonce 파라미터가 ID 토큰의 nonce 클레임과 일치하는지 확인합니다. 이는 재사용 공격(Replay Attack)을 방지하는 데 필수적입니다.
  • PKCE와 Nonce의 조합: OAuth 2.0의 PKCE와 OIDC의 Nonce를 함께 사용하여 인가 코드 및 ID 토큰의 가로채기 및 재사용 공격에 대한 방어력을 극대화해야 합니다.
// Node.js 예시: OIDC ID 토큰 검증 (oidc-client-js 라이브러리 사용 가정)
const { UserManager } = require('oidc-client-js');

async function validateIdToken(idToken, expectedNonce) {
    const userManager = new UserManager({
        // ... OIDC 설정
    });

    try {
        const decodedToken = await userManager.decodeJwt(idToken);

        // 1. 서명 검증 (라이브러리 내부에서 처리)
        // 2. iss, aud, exp 검증 (라이브러리 내부에서 처리)
        // 3. nonce 검증
        if (decodedToken.nonce !== expectedNonce) {
            throw new Error("Nonce mismatch: possible replay attack.");
        }

        console.log("ID Token is valid.");
        return decodedToken;

    } catch (error) {
        console.error("ID Token validation failed:", error);
        throw error;
    }
}

약한 암호화 알고리즘 사용

JWT와 마찬가지로 ID 토큰도 서명에 약한 암호화 알고리즘을 사용하거나 alg: "none"을 허용할 경우 위변조에 취약해집니다. 이는 OIDC의 신뢰성을 근본적으로 훼손할 수 있습니다.

방어 전략:

  • 강력한 서명 알고리즘 강제: OIDC Provider(IdP)와 클라이언트 모두 RS256, ES256과 같은 비대칭 암호화 알고리즘을 사용하여 ID 토큰에 서명하고 검증해야 합니다. alg: "none"은 절대 허용해서는 안 됩니다.
  • JWKS (JSON Web Key Set) 엔드포인트 활용: OIDC Provider는 공개 키를 JWKS 엔드포인트를 통해 제공해야 하며, 클라이언트는 이 엔드포인트를 통해 최신 공개 키를 동적으로 가져와 서명을 검증해야 합니다. 이는 키 교체 시 유연성을 제공하고 보안을 강화합니다.
JWT, OAuth 2.0, OpenID Connect 구현 시 보안 취약점과 방어 전략 - man, police, g20, guard, security, police, police, police, police, police, g20, g20, g20

Image by fsHH on Pixabay

공통 보안 전략 및 구현 시 주의사항

JWT, OAuth 2.0, OpenID Connect는 각각 고유한 취약점을 가지지만, 안전한 인증/인가 시스템을 구축하기 위해서는 공통적으로 적용해야 할 보안 전략들이 있습니다.

전반적인 보안 강화

  • 입력 값 검증: 모든 사용자 입력 및 외부에서 수신하는 데이터를 엄격하게 검증하여 SQL Injection, XSS, Command Injection 등 일반적인 웹 취약점을 방어해야 합니다.
  • 로깅 및 모니터링: 인증 및 인가 관련 모든 실패 및 성공 이벤트를 기록하고, 비정상적인 활동(예: 과도한 로그인 시도, 비정상적인 토큰 요청)을 감지할 수 있도록 실시간 모니터링 시스템을 구축해야 합니다.
  • 최소 권한의 원칙: 각 서비스나 사용자는 필요한 최소한의 권한만을 가져야 합니다.
  • 정기적인 보안 감사 및 테스트: 주기적으로 보안 전문가의 감사를 받거나, 침투 테스트(Penetration Test)를 수행하여 잠재적인 취약점을 발견하고 개선해야 합니다.
  • 보안 패치 및 종속성 관리: 사용 중인 라이브러리나 프레임워크의 보안 취약점을 항상 최신 상태로 유지하고, 발견된 취약점에 대한 패치를 즉시 적용해야 합니다.

프로토콜 비교 및 선택 가이드

각 프로토콜의 목적과 특징을 이해하는 것은 올바른 구현 전략을 수립하는 데 중요합니다.

특징 JWT OAuth 2.0 OpenID Connect
주요 목적 정보 교환을 위한 안전한 토큰 형식 권한 위임 프레임워크 (인가) 신원 확인 계층 (인증)
독립성 단독으로 사용 가능 (인증/인가 정보 포함) 권한 위임만을 다루며, 인증은 별도 처리 OAuth 2.0 위에 구축되어 OAuth 2.0 필요
주요 토큰 JWT (Access Token) Access Token, Refresh Token ID Token (JWT), Access Token, Refresh Token
핵심 취약점 서명 우회, 키 노출, 토큰 탈취/재사용 리다이렉트 URI 조작, 클라이언트 시크릿 노출 ID 토큰 위변조/재사용, Nonce 미검증
권장 사용처 API 인증, 서버 간 정보 교환 타사 서비스 연동 (SNS 로그인, 결제 등) 통합 로그인(SSO), 사용자 신원 확인

결론: 안전한 인증/인가 시스템 구축을 위한 로드맵

JWT, OAuth 2.0, OpenID Connect는 현대 웹 개발에서 빼놓을 수 없는 강력한 도구입니다. 하지만 그 복잡성과 유연성 뒤에는 잠재적인 보안 취약점들이 숨어 있습니다. 이 글에서 다룬 내용들을 바탕으로, 개발자 여러분은 각 프로토콜의 작동 원리를 깊이 이해하고, 발생 가능한 위협에 대한 명확한 인식을 가지며, 견고한 방어 전략을 수립해야 합니다.

핵심은 다층 방어(Defense in Depth) 접근 방식입니다. 하나의 방어선이 뚫리더라도 다른 방어선이 위협을 막아낼 수 있도록 설계해야 합니다. JWT의 짧은 만료 시간과 Refresh Token의 조합, OAuth 2.0의 PKCE 적용, OIDC의 철저한 ID 토큰 검증과 Nonce 활용, 그리고 전반적인 시스템의 보안 강화는 선택이 아닌 필수입니다.

안전한 인증/인가 시스템 구축은 지속적인 노력과 학습을 요구합니다. 끊임없이 변화하는 보안 위협에 대응하기 위해 최신 보안 동향을 주시하고, 사용 중인 라이브러리와 프레임워크를 최신 상태로 유지하는 것이 중요합니다. 이 글이 여러분의 서비스 보안 강화에 실질적인 도움이 되기를 바랍니다. 여러분의 서비스는 어떤 보안 전략을 사용하고 계신가요? 댓글로 경험을 공유해 주세요!

📌 함께 읽으면 좋은 글

  • [보안] DevSecOps 도입: CI/CD 파이프라인에 보안 검증 자동화 통합 전략
  • [클라우드 인프라] GitOps로 쿠버네티스 배포 자동화: Argo CD vs Flux CD 심층 비교 전략
  • [개발 도구] Fuzzy Finder fzf: 터미널 생산성을 극대화하는 대화형 검색 도구 활용 가이드

이 글이 도움이 되셨다면 공감(♥)댓글로 응원해 주세요!
궁금한 점이나 다루었으면 하는 주제가 있다면 댓글로 남겨주세요.