Skip to content
Published on

OAuth 2.1 마이그레이션 가이드 — PKCE 의무화 시대의 인증 설계

Authors

들어가며

OAuth 2.0(RFC 6749)이 발행된 지 14년이 지났습니다. 그동안 OAuth 2.0은 사실상 모든 API 인가의 표준이 되었지만, 동시에 수많은 보안 사고의 원인이 되기도 했습니다. Implicit flow에서 액세스 토큰이 URL fragment로 노출되고, 인가 코드가 가로채기 공격(authorization code interception)으로 탈취되고, redirect URI의 느슨한 매칭으로 토큰이 공격자 서버로 흘러가는 사고가 반복되었습니다.

이 문제들을 해결하기 위해 IETF OAuth 워킹그룹은 보안 권고(Security BCP)를 계속 갱신해 왔고, 2025년 1월 그 결정판인 RFC 9700이 발행되었습니다. 그리고 이 모든 교훈을 단일 스펙으로 통합한 것이 바로 OAuth 2.1 draft입니다.

2026년 현재 OAuth 2.1은 아직 IETF draft 상태이지만, 업계에서는 이미 사실상의 표준입니다. 특히 AI 에이전트 시대가 본격화되면서 그 위상이 더 단단해졌습니다. MCP(Model Context Protocol)의 authorization 스펙이 OAuth 2.1을 기반으로 작성되었고, Keycloak 26.6은 OAuth Client ID Metadata Document(CIMD)를 실험적으로 지원하면서 MCP authorization server 역할까지 수행할 수 있게 되었습니다. 새로 만드는 시스템이라면 OAuth 2.0의 레거시 패턴을 답습할 이유가 전혀 없습니다.

이 글에서는 OAuth 2.0의 문제점, OAuth 2.1이 통합한 변경 사항, PKCE의 동작 원리, 그리고 기존 애플리케이션을 OAuth 2.1 호환으로 마이그레이션하는 구체적인 체크리스트를 다룹니다.

OAuth 2.0의 문제점 — 왜 2.1이 필요했나

스펙이 흩어져 있었다

OAuth 2.0을 "제대로" 구현하려면 읽어야 할 문서가 너무 많았습니다.

문서내용발행
RFC 6749OAuth 2.0 핵심 프레임워크2012
RFC 6750Bearer Token 사용법2012
RFC 7636PKCE2015
RFC 8252네이티브 앱 BCP2017
RFC 9700Security BCP (구 draft-ietf-oauth-security-topics)2025

문제는 RFC 6749만 읽고 구현한 시스템이 너무 많았다는 점입니다. 2012년 기준으로는 합법이었던 Implicit flow, password grant, 와일드카드 redirect URI가 그대로 프로덕션에 남았습니다. OAuth 2.1은 이 다섯 문서 중 핵심 세 가지 — RFC 6749 + RFC 7636(PKCE) + RFC 9700(Security BCP) — 를 하나로 통합하고, 위험한 옵션을 스펙 차원에서 삭제했습니다.

구체적인 공격 시나리오들

OAuth 2.0 시대에 실제로 반복된 공격을 정리하면 다음과 같습니다.

공격 1: Implicit flow 토큰 유출
  - access_token이 URL fragment(#access_token=...)로 전달됨
  - 브라우저 히스토리, Referer 헤더, 프록시 로그에 토큰이 남음
  - 악성 스크립트가 location.hash를 읽어 토큰 탈취

공격 2: Authorization Code Interception (모바일)
  - 커스텀 URL 스킴(myapp://callback)을 악성 앱이 동일하게 등록
  - OS가 악성 앱으로 인가 코드를 전달
  - 공격자가 코드를 토큰으로 교환 (PKCE 없으면 막을 수 없음)

공격 3: Redirect URI 부분 매칭 악용
  - 등록: https://app.example.com/*
  - 공격: https://app.example.com/redirect?url=https://evil.com
  - 오픈 리다이렉터를 경유해 코드/토큰이 공격자에게 전달

공격 4: CSRF를 통한 계정 연결 조작
  - state 파라미터 미사용 시 공격자의 인가 응답을
    피해자 세션에 주입 가능

공격 5: Refresh token 탈취 후 무기한 사용
  - rotation이 없으면 탈취된 refresh token이
    만료 시까지(혹은 무기한) 유효

이 중 1번과 2번은 flow 자체의 설계 결함이고, 3~5번은 스펙이 "권고"에 그쳤던 부분입니다. OAuth 2.1은 이 모두를 의무(MUST)로 격상하거나 해당 기능 자체를 제거했습니다.

OAuth 2.1의 핵심 변경 사항

OAuth 2.1 draft의 변경점을 한 표로 요약하면 다음과 같습니다.

항목OAuth 2.0OAuth 2.1
PKCE선택 (RFC 7636, 주로 모바일)모든 authorization code flow에서 의무
Implicit grant허용제거
Resource Owner Password Credentials허용제거
Redirect URI 매칭구현체 재량 (부분 매칭 흔함)정확한 문자열 비교(exact matching) 의무
Refresh token (public client)제약 없음sender-constrained 또는 rotation 의무
Bearer token in query string허용금지
state 파라미터권고CSRF 방어는 PKCE가 흡수, state는 앱 상태용

하나씩 자세히 살펴보겠습니다.

Implicit grant 제거 — 왜, 그리고 무엇으로 대체하나

Implicit flow는 2012년 당시 브라우저의 한계(CORS 미지원) 때문에 만들어진 타협안입니다. 토큰 엔드포인트 호출 없이 인가 응답에서 곧바로 access token을 fragment로 받는 구조였습니다.

[Implicit flow - 제거됨]
Browser ──> /authorize?response_type=token ──> IdP
Browser <── 302 Location: https://app/#access_token=eyJ... <── IdP
            └─ 토큰이 URL에 그대로 노출

문제점은 명확합니다.

  1. 토큰이 URL에 노출되어 히스토리/로그/Referer로 유출됩니다.
  2. 토큰 엔드포인트를 거치지 않으므로 클라이언트 인증이 불가능합니다.
  3. 응답이 클라이언트에 직접 오기 때문에 토큰 교체(rotation)나 sender-constraining을 적용할 수 없습니다.

대체 패턴은 단순합니다. 이제 모든 브라우저가 CORS를 지원하므로, SPA도 authorization code flow + PKCE를 사용하면 됩니다. 토큰은 token 엔드포인트에서 POST 응답 본문으로 전달되고, URL에는 일회용 인가 코드만 잠깐 나타납니다.

ROPC(password grant) 제거 — 왜, 그리고 무엇으로 대체하나

Resource Owner Password Credentials grant는 사용자의 아이디/비밀번호를 클라이언트 앱이 직접 받아 토큰 엔드포인트로 전송하는 방식입니다.

POST /token HTTP/1.1
Host: idp.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=password&username=alice&password=hunter2&client_id=legacy-app

이 방식은 OAuth의 존재 이유 자체를 부정합니다. OAuth는 "비밀번호를 제3자 앱에 넘기지 않기 위해" 만들어진 프로토콜인데, ROPC는 정확히 그 반대를 합니다. 또한 다음을 모두 불가능하게 만듭니다.

  • MFA, passkeys 등 강화된 인증 수단 적용
  • IdP 주도의 위험 기반 인증(risk-based authentication)
  • 피싱 내성 — 앱이 가짜라면 비밀번호가 그대로 털립니다
  • SSO — 비밀번호가 IdP가 아닌 개별 앱을 경유합니다

대체 패턴은 용도별로 다릅니다.

ROPC를 쓰던 이유대체 패턴
자사 모바일 앱의 네이티브 로그인 UIAuthorization code + PKCE (시스템 브라우저/Custom Tab)
서버 간 통신, 배치 작업client_credentials grant
CLI 도구 로그인Device authorization grant (RFC 8628) 또는 PKCE + 루프백 리다이렉트
테스트 자동화테스트 전용 client_credentials 또는 토큰 사전 발급

Redirect URI exact matching

OAuth 2.1은 redirect URI를 단순 문자열 비교(simple string comparison) 로 검증할 것을 요구합니다. 와일드카드, 부분 매칭, 정규식 매칭은 모두 금지입니다.

등록된 URI:  https://app.example.com/callback

요청된 URI                                          판정
https://app.example.com/callback                    허용
https://app.example.com/callback/                   거부 (trailing slash)
https://app.example.com/callback?next=/home         거부 (쿼리 추가)
https://app.example.com.evil.com/callback           거부
http://app.example.com/callback                     거부 (scheme 다름)

Keycloak에서 흔히 보이는 안티패턴이 Valid Redirect URIs에 와일드카드를 넣는 것입니다.

나쁜 설정:  https://app.example.com/*        ← 오픈 리다이렉터와 결합 시 토큰 유출
나쁜 설정:  *                                 ← 절대 금지
좋은 설정:  https://app.example.com/auth/callback   (필요한 경로를 전부 명시)

유일한 예외는 네이티브 앱의 루프백 리다이렉트(127.0.0.1)로, 포트 번호만 가변을 허용합니다(RFC 8252).

PKCE 동작 원리 상세

PKCE(Proof Key for Code Exchange, RFC 7636)는 OAuth 2.1의 심장입니다. "픽시"라고 읽습니다.

핵심 아이디어

인가 코드를 요청한 주체와 토큰으로 교환하는 주체가 동일한지를 증명하는 메커니즘입니다. 클라이언트는 매 인가 요청마다 일회용 비밀값(code_verifier)을 생성하고, 그 해시(code_challenge)만 먼저 보냅니다. 코드 교환 시 원본을 제출하면, 서버가 해시를 다시 계산해 대조합니다.

1) 클라이언트: code_verifier 생성
   - 43~128자의 무작위 문자열 (A-Z, a-z, 0-9, -._~)
   - 최소 256비트 엔트로피의 CSPRNG 사용

2) 클라이언트: code_challenge 계산
   code_challenge = BASE64URL( SHA256( code_verifier ) )

3) 인가 요청 (front channel)
   GET /authorize?response_type=code
       &client_id=spa-client
       &redirect_uri=https://app.example.com/callback
       &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
       &code_challenge_method=S256
       &scope=openid profile
       &state=af0ifjsldkj

4) IdP: code_challenge를 인가 코드와 함께 저장 후 코드 발급

5) 토큰 요청 (back channel)
   POST /token
   grant_type=authorization_code
   &code=SplxlOBeZQQYbYS6WxSbIA
   &redirect_uri=https://app.example.com/callback
   &client_id=spa-client
   &code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

6) IdP: BASE64URL(SHA256(받은 code_verifier)) == 저장된 code_challenge ?
   - 일치: 토큰 발급
   - 불일치: invalid_grant 에러

공격자가 인가 코드를 가로채더라도 code_verifier를 모르므로 토큰 교환에 실패합니다. code_verifier는 front channel(브라우저 리다이렉트)에 절대 노출되지 않고, TLS로 보호되는 back channel로만 전송됩니다.

code_challenge_method: S256 vs plain

method계산식비고
S256BASE64URL(SHA256(verifier))의무 지원, 항상 사용해야 함
plainverifier 그대로인가 요청이 노출되면 무력화, 사용 금지

OAuth 2.1에서 plain은 "S256을 기술적으로 사용할 수 없는 경우"에만 허용되는데, 현실적으로 그런 환경은 없습니다.

다운그레이드 공격과 방어

PKCE에도 다운그레이드 공격 벡터가 있습니다. RFC 9700이 상세히 다루는 부분입니다.

시나리오: PKCE 다운그레이드
  1. 공격자가 피해자의 인가 요청에서 code_challenge를 제거하거나
     plain으로 바꿔치기 시도
  2. 서버가 "PKCE는 선택"으로 동작하면, challenge 없는 요청도 수락
  3. 공격자가 가로챈 코드를 verifier 없이 교환 성공

방어 (서버 측):
  - 인가 요청에 code_challenge가 있었으면, 토큰 요청에
    code_verifier가 반드시 있어야 함 (없으면 거부)
  - 인가 요청에 code_challenge가 없었는데 토큰 요청에
    code_verifier가 오면 거부
  - 클라이언트 정책으로 PKCE를 강제 (Keycloak:
    Advanced Settings > PKCE Code Challenge Method = S256)

Keycloak에서는 클라이언트별로 PKCE를 강제할 수 있습니다.

# kcadm으로 PKCE S256 강제 설정
kcadm.sh update clients/CLIENT-UUID -r myrealm \
  -s 'attributes."pkce.code.challenge.method"=S256'

코드 예제 — verifier/challenge 생성

// SPA (브라우저) - Web Crypto API
function base64url(buffer) {
  return btoa(String.fromCharCode(...new Uint8Array(buffer)))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

async function createPkcePair() {
  const random = crypto.getRandomValues(new Uint8Array(32));
  const verifier = base64url(random.buffer); // 43 chars
  const digest = await crypto.subtle.digest(
    'SHA-256', new TextEncoder().encode(verifier)
  );
  const challenge = base64url(digest);
  return { verifier, challenge };
}
// Java - Spring Security 없이 직접 구현 시
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;

public final class Pkce {
    private static final SecureRandom RANDOM = new SecureRandom();
    private static final Base64.Encoder B64 =
        Base64.getUrlEncoder().withoutPadding();

    public static String newVerifier() {
        byte[] bytes = new byte[32];
        RANDOM.nextBytes(bytes);
        return B64.encodeToString(bytes);
    }

    public static String challengeOf(String verifier) throws Exception {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        byte[] digest = md.digest(verifier.getBytes("US-ASCII"));
        return B64.encodeToString(digest);
    }
}

Spring Security를 쓴다면 직접 구현할 필요 없이 설정 한 줄로 끝납니다.

# application.yml - Spring Security 6.x는 public client에 PKCE 자동 적용
spring:
  security:
    oauth2:
      client:
        registration:
          keycloak:
            client-id: my-spa
            authorization-grant-type: authorization_code
            redirect-uri: "http://localhost:8080/login/oauth2/code/keycloak"
            scope: openid,profile
            client-authentication-method: none   # public client → PKCE 자동
        provider:
          keycloak:
            issuer-uri: https://idp.example.com/realms/myrealm

Refresh Token 처리 강화

OAuth 2.1은 public client(SPA, 모바일)의 refresh token에 대해 다음 중 하나를 요구합니다.

  1. Sender-constrained: DPoP(RFC 9449)나 mTLS(RFC 8705)로 토큰을 특정 클라이언트 키에 바인딩
  2. Rotation: refresh token을 사용할 때마다 새 토큰을 발급하고 이전 토큰을 무효화

rotation의 핵심은 재사용 감지(reuse detection) 입니다.

정상 흐름:
  RT1 사용 → AT2 + RT2 발급, RT1 무효화
  RT2 사용 → AT3 + RT3 발급, RT2 무효화

탈취 시나리오:
  공격자가 RT1 탈취 → 정상 사용자가 RT1 사용 → RT2 발급
  공격자가 RT1 재사용 시도
  → 서버: "RT1은 이미 사용됨" = 탈취 신호
  → 해당 토큰 패밀리(RT1에서 파생된 모든 토큰) 전체 무효화
  → 사용자 재로그인 유도

Keycloak 설정은 realm 단위로 합니다.

# Realm Settings > Sessions / Tokens
kcadm.sh update realms/myrealm \
  -s revokeRefreshToken=true \
  -s refreshTokenMaxReuse=0

refresh token 수명주기 설계는 별도 글(리프레시 토큰 로테이션과 세션 관리)에서 더 깊이 다룹니다.

클라이언트 유형별 권장 패턴

SPA (브라우저 단독)

권장: Authorization Code + PKCE (public client)
      또는 BFF 패턴 (confidential client, 토큰을 브라우저에 두지 않음)

┌─────────┐  1. /authorize (+ code_challenge)   ┌─────────┐
│ Browser │ ──────────────────────────────────> │   IdP   │
│  (SPA)  │ <── 2. 302 + code ───────────────── │         │
│         │ ── 3. /token (+ code_verifier) ───> │         │
│         │ <── 4. AT + RT (rotation 적용) ──── │         │
└─────────┘                                     └─────────┘
  • 토큰 저장은 메모리(JS 변수)가 기본. localStorage는 XSS에 취약합니다.
  • 더 높은 보안이 필요하면 BFF(Backend-for-Frontend)로 토큰을 서버에 두고 브라우저에는 HttpOnly 세션 쿠키만 발급합니다.

모바일/데스크톱 네이티브 앱

권장: Authorization Code + PKCE + 시스템 브라우저 (RFC 8252)
  - iOS: ASWebAuthenticationSession
  - Android: Custom Tabs
  - redirect: HTTPS 기반 App Links / Universal Links 권장
             (커스텀 스킴보다 가로채기 내성이 높음)
  - WebView 내장 로그인 금지 (피싱 + 쿠키 격리 문제)

서버사이드 웹 앱

권장: Authorization Code + PKCE (confidential client)
  - client_secret 또는 private_key_jwt로 클라이언트 인증
  - PKCE는 confidential client에도 적용 (OAuth 2.1 의무)
    → authorization code injection 방어
  - 세션 쿠키: HttpOnly + Secure + SameSite=Lax 이상

confidential client인데 PKCE가 왜 필요한지 의아할 수 있습니다. client_secret은 "토큰 요청을 보낸 것이 그 클라이언트"임을 증명하지만, "그 인가 코드가 그 세션에서 시작된 요청의 결과물"임은 증명하지 못합니다. 공격자가 자신의 인가 코드를 피해자 세션에 주입하는 code injection 공격은 PKCE(또는 OIDC nonce)로만 막을 수 있습니다.

서버 간 통신 (사용자 없음)

권장: client_credentials grant
  - 인증: private_key_jwt 또는 mTLS > client_secret
  - 2026 트렌드: 워크로드 아이덴티티(SPIFFE/SVID)와 연계,
    Keycloak 26.6의 Federated client authentication으로
    쿠버네티스 서비스 어카운트 토큰을 클라이언트 인증에 사용 가능

기존 앱 마이그레이션 체크리스트

실무에서 바로 쓸 수 있는 체크리스트입니다. 순서대로 진행하면 됩니다.

1단계 — 현황 조사

  • 모든 OAuth 클라이언트 목록화 (IdP 어드민 콘솔에서 export)
  • grant type별 분류: implicit / password / authorization_code / client_credentials
  • redirect URI에 와일드카드나 http(비 TLS)가 있는지 점검
  • PKCE 미적용 authorization_code 클라이언트 식별
  • refresh token 정책(rotation 여부, 수명) 확인

Keycloak이라면 다음과 같이 조사할 수 있습니다.

# implicit flow가 켜진 클라이언트 찾기
kcadm.sh get clients -r myrealm --fields clientId,implicitFlowEnabled \
  | jq '.[] | select(.implicitFlowEnabled == true) | .clientId'

# Direct Access Grants(ROPC)가 켜진 클라이언트 찾기
kcadm.sh get clients -r myrealm --fields clientId,directAccessGrantsEnabled \
  | jq '.[] | select(.directAccessGrantsEnabled == true) | .clientId'

2단계 — 위험도 높은 것부터 제거

  • Implicit flow 클라이언트를 code + PKCE로 전환 (라이브러리 교체 수준)
  • ROPC 클라이언트를 용도에 따라 code+PKCE / client_credentials / device flow로 전환
  • 와일드카드 redirect URI를 명시적 목록으로 교체
  • 쿼리 스트링으로 access token을 전달하는 API 호출 제거 (Authorization 헤더로)

3단계 — PKCE 및 rotation 적용

  • 모든 authorization_code 클라이언트에 PKCE S256 강제 (IdP 정책)
  • public client에 refresh token rotation + reuse detection 활성화
  • 클라이언트 라이브러리를 인증 표준 라이브러리로 통일 (AppAuth, oidc-client-ts, Spring Security 등)

4단계 — 검증

  • challenge 없는 인가 요청이 거부되는지 테스트
  • 잘못된 verifier로 토큰 교환이 실패하는지 테스트
  • redirect URI 변형(trailing slash, 쿼리 추가)이 거부되는지 테스트
  • refresh token 재사용 시 패밀리 무효화가 동작하는지 테스트
# PKCE 강제 확인: challenge 없이 인가 요청 → 에러가 나야 정상
curl -s -o /dev/null -w "%{http_code}\n" \
  "https://idp.example.com/realms/myrealm/protocol/openid-connect/auth?client_id=my-spa&response_type=code&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback"

흔한 함정

함정증상해결
verifier를 localStorage에 저장탭 간 충돌, XSS 노출sessionStorage 또는 메모리
code 재사용invalid_grant 간헐 발생콜백 핸들러 멱등 처리, React StrictMode 이중 실행 주의
시계 오차토큰이 즉시 만료 처리됨NTP 동기화, clock skew 허용치 설정
프록시가 쿼리 로그 기록인가 코드가 로그에 남음코드 수명 60초 이하 + 일회용 보장 확인
ROPC 제거 후 테스트 깨짐CI 인증 실패테스트용 client_credentials 클라이언트 분리

AI 에이전트 시대와 OAuth 2.1 — MCP 맥락

2025~2026년에 OAuth 2.1이 단숨에 "사실상 표준"이 된 결정적 계기는 AI 에이전트입니다.

MCP(Model Context Protocol)는 LLM 에이전트가 외부 도구/데이터 서버에 접근하는 표준 프로토콜인데, 그 authorization 스펙이 OAuth 2.1 기반으로 작성되었습니다. MCP 서버는 OAuth 2.1 resource server로, MCP 클라이언트(에이전트 호스트)는 OAuth 2.1 클라이언트로 동작합니다. PKCE는 당연히 의무이고, dynamic client registration(RFC 7591)과 protected resource metadata(RFC 9728)가 적극 활용됩니다.

여기서 흥미로운 것이 Keycloak 26.6의 OAuth Client ID Metadata Document(CIMD) 실험적 지원입니다. 에이전트처럼 사전 등록이 어려운 클라이언트가 자신의 메타데이터 문서 URL 자체를 client_id로 사용하는 방식으로, Keycloak이 MCP authorization server 역할을 수행할 수 있게 합니다. 사람이 아닌 주체(non-human identity)가 폭발적으로 늘어나는 환경에서, "모든 클라이언트를 어드민이 수동 등록한다"는 전제가 무너지고 있다는 신호입니다.

에이전트가 사용자를 대신해 행동하는 시나리오에서는 Token Exchange(RFC 8693)와 결합한 위임 체인 설계도 중요해집니다. Keycloak 26.6의 JWT Authorization Grant 지원이 이런 시나리오를 겨냥합니다. 요컨대 OAuth 2.1은 "사람의 로그인"을 넘어 "에이전트의 위임받은 행동"까지 포괄하는 기반 계층이 되었습니다.

운영 베스트 프랙티스

  1. IdP 정책으로 강제하세요. 클라이언트 개발자의 선의에 기대지 말고, IdP에서 PKCE S256 의무화, implicit/ROPC 비활성화를 realm 기본값으로 설정합니다. Keycloak 26.6은 FAPI 2.0 Security Profile을 Final 수준으로 지원하므로, 고보안 환경이라면 FAPI 2.0 client policy를 그대로 적용하는 것도 좋은 선택입니다.
  2. 토큰 수명은 짧게. access token 5~15분, refresh token은 rotation과 함께 idle 30일 / max 90일 수준에서 시작해 조정합니다.
  3. 표준 라이브러리만 사용하세요. OAuth flow를 직접 구현하는 것은 안티패턴입니다. oidc-client-ts(SPA), AppAuth(모바일), Spring Security OAuth2 Client(Java), golang.org/x/oauth2(Go)처럼 검증된 라이브러리를 씁니다.
  4. 모니터링 지표를 잡으세요. invalid_grant 비율, PKCE 검증 실패 수, refresh token 재사용 감지 수는 공격 시도의 조기 신호입니다.
  5. 점진적 마이그레이션. 신규 클라이언트부터 OAuth 2.1 정책을 적용하고, 레거시는 deprecation 일정을 공지한 뒤 단계적으로 차단합니다.

마치며

OAuth 2.1은 새로운 기능을 추가한 스펙이 아니라, 14년간의 보안 사고에서 얻은 교훈을 기본값으로 만든 스펙입니다. 정리하면 다음과 같습니다.

  • PKCE는 모든 곳에: public이든 confidential이든, authorization code flow에는 항상 S256 PKCE를 적용합니다.
  • Implicit과 ROPC는 잊으세요: SPA는 code+PKCE 또는 BFF, 서버 간은 client_credentials, CLI는 device flow입니다.
  • redirect URI는 정확히: 와일드카드 없는 exact matching만 사용합니다.
  • refresh token은 rotation + reuse detection: 탈취를 전제로 한 수명주기를 설계합니다.
  • MCP와 AI 에이전트 시대의 기본 계층: 새 시스템은 처음부터 OAuth 2.1 + 필요시 FAPI 2.0으로 설계하는 것이 미래 비용을 아끼는 길입니다.

draft가 RFC 번호를 받는 시점은 중요하지 않습니다. 그 내용은 이미 RFC 9700으로 의무화되어 있고, 주요 IdP와 라이브러리가 모두 구현을 마쳤기 때문입니다. 지금 마이그레이션을 시작하세요.

참고 자료