본문으로 건너뛰기
2026. 8. 22
© WONKOOK LEE

이 토큰은 누구를 위한 것인가요?

하나의 토큰과 서로 다른 수신자

이 블로그에는 GitHub 계정으로 로그인해야 열리는 비공개 페이지가 있습니다. 로그인 콜백을 처리하는 엣지 함수는 GitHub에서 액세스 토큰을 받은 뒤 GET /user를 한 번 호출합니다. 사용자 계정과 접근 허용 여부를 확인하고 나면 그 토큰은 버립니다. 브라우저에는 대신 제가 서명한 짧은 수명의 세션 쿠키를 보냅니다.

처음 보면 같은 일을 두 번 하는 것처럼 보입니다. 이미 토큰을 받았는데 왜 새 쿠키를 만들까요?

이 질문에 답하려면 토큰의 모양보다 수신자를 먼저 봐야 합니다. 액세스 토큰은 GitHub API를 위한 자격 증명이고, 세션 쿠키는 이 블로그의 게이트를 위한 자격 증명입니다. 둘 다 긴 문자열이지만 읽는 쪽과 맡은 일이 다릅니다.

OAuth, OpenID Connect(OIDC), JWT, PKCE, BFF가 뒤엉켜 보일 때도 같은 방법이 통합니다.

이 자격 증명은 누가 발급했고, 누가 읽으며, 어디까지 허용하는가?

이 글은 그 질문을 따라 웹 인증 구조를 정리합니다. 기준은 OAuth 2.0 Security Best Current Practice(RFC 9700), OAuth 2.0 for Browser-Based Applications(RFC 10017), OpenID Connect Core입니다. OAuth 2.1은 2026년 8월 현재 아직 IETF 초안이므로, 확정 표준처럼 부르지 않습니다.


이 글에서 다루는 내용

세션에서 출발해 OAuth와 OIDC의 경계를 나눈 뒤, 토큰이 놓이는 위치에 따라 웹 아키텍처가 어떻게 달라지는지 살펴봅니다.

  1. 로그인과 권한 검사는 왜 다른가
  2. OAuth가 풀려던 문제는 무엇인가
  3. Authorization Code와 PKCE는 어떻게 이어지는가
  4. 액세스 토큰, 리프레시 토큰, ID 토큰은 누가 읽는가
  5. OIDC 로그인은 무엇을 검증해야 하는가
  6. 브라우저에 토큰을 두면 무엇이 달라지는가
  7. 세션, BFF, SPA 중 무엇을 고를 것인가
  8. SSO와 Identity Federation은 어디에 놓이는가
  9. 실제 서비스에서는 어떻게 결정할 것인가



1. 로그인과 권한 검사는 왜 다른가

인증(Authentication)은 사용자가 누구인지 확인하는 일입니다. 인가(Authorization)는 확인된 사용자가 지금 하려는 일을 허용할지 판단하는 일입니다.

로그인은 인증이 일어나는 대표적인 순간입니다. 반면 인가는 요청마다 반복됩니다. 같은 사용자도 급여 명세서는 읽을 수 있지만 다른 직원의 계좌 정보를 수정할 권한은 없을 수 있습니다. 로그인에 성공했다는 사실만으로 모든 API가 열리지는 않습니다.

HTTP 상태 코드도 이 구분을 반영합니다. 401 Unauthorized는 이름과 달리 인증 자격 증명이 없거나 유효하지 않을 때 쓰고, 403 Forbidden은 사용자를 알아봤지만 해당 작업은 허용하지 않을 때 씁니다. 클라이언트는 이 차이로 로그인 화면을 열지, 권한 안내를 보여줄지 결정합니다.

세션은 여전히 좋은 출발점입니다

전통적인 서버 세션은 이 두 문제를 분리해 다룹니다.

  1. 사용자가 로그인하면 서버가 세션 레코드를 만듭니다.
  2. 브라우저에는 그 레코드를 가리키는 세션 ID만 쿠키로 보냅니다.
  3. 이후 서버는 쿠키로 사용자를 찾고, 매 요청의 권한을 따로 판단합니다.

진짜 로그인 상태는 서버에 있습니다. 브라우저가 가진 세션 ID는 그 상태를 가리키는 포인터입니다. 서버는 세션을 지우거나 만료시켜 접근을 바로 끊을 수 있습니다.

쿠키 속성도 이 구조에서 이해하면 외우기 쉽습니다.

  • HttpOnly는 JavaScript가 세션 ID를 직접 읽는 경로를 닫습니다.
  • Secure는 HTTPS 연결에서만 쿠키를 보내게 합니다.
  • SameSite는 다른 사이트에서 시작된 요청에 쿠키를 보낼 범위를 제한합니다.

쿠키는 브라우저가 자동으로 보내주기 때문에 편합니다. 하지만 그 편리함이 CSRF의 출발점이 되기도 합니다. 공격자 사이트가 우리 서버로 요청을 만들었는데 브라우저가 세션 쿠키를 붙이면 서버에는 정상 사용자의 요청처럼 보입니다. SameSite 설정과 CSRF 토큰, Origin 검사 같은 방어가 필요한 이유입니다.

세션을 오래된 방식으로 취급할 필요는 없습니다. 프론트엔드와 API가 한 서비스 경계 안에 있고 제3자 API를 대신 호출할 일도 없다면, 서버 세션은 지금도 좋은 기본값입니다.

Recap

로그인은 사용자를 확인하는 한 번의 과정이고, 인가는 요청마다 내리는 권한 판단입니다. 서버 세션에서는 로그인 상태가 서버에 있고 브라우저에는 포인터만 남습니다. 이 구조만으로 충분한 서비스에 OAuth를 얹으면 해결되는 문제보다 관리할 요소가 더 늘 수 있습니다.




2. OAuth가 풀려던 문제는 무엇인가

OAuth를 소셜 로그인 기술로 먼저 접하면 출발점을 놓치기 쉽습니다. OAuth가 처음 풀려던 문제는 로그인이 아니라 권한 위임입니다.

예를 들어 일정 관리 서비스가 사용자의 Google Calendar를 읽어야 한다고 해보겠습니다. 사용자에게 Google 비밀번호를 받아 보관하면 캘린더뿐 아니라 그 계정 전체가 위험해집니다. 캘린더 읽기 권한만 회수할 수도 없고, 비밀번호 변경이나 다중 인증과도 충돌합니다.

필요한 것은 비밀번호가 아니라 다음과 같은 제한된 허가입니다.

이 애플리케이션이 이 사용자의 캘린더를 읽도록 허용합니다.

OAuth는 사용자의 비밀번호를 클라이언트에 넘기지 않고, 범위와 수명이 제한된 자원 접근 권한을 건넵니다.

OAuth 역할일정 서비스의 예맡은 일
Resource Owner사용자자원 접근을 허용합니다.
Client일정 서비스권한을 위임받아 API를 호출합니다.
Authorization ServerGoogle의 인가 서버사용자를 확인하고 토큰을 발급합니다.
Resource ServerCalendar API토큰을 검사하고 자원을 제공합니다.

여기서 Client는 사용자가 아니라 권한을 빌려 쓰는 애플리케이션입니다. React 애플리케이션이 직접 Client가 될 수도 있고, 서버가 Client가 될 수도 있습니다. 어느 쪽을 고르느냐에 따라 토큰이 머무는 곳과 공격 표면이 달라집니다.

Authorization Server와 Resource Server도 역할이 다릅니다. 같은 회사가 둘 다 운영하더라도 앞쪽은 토큰을 발급하고, 뒤쪽은 토큰이 자기 API를 위한 것인지 확인합니다. API 서버는 사용자가 비밀번호로 로그인했는지, 패스키를 썼는지 알 필요가 없습니다.

OAuth가 필요하지 않은 경우도 있습니다

OAuth는 제3자 API나 여러 서비스 경계를 넘나드는 접근 권한을 위임할 때 유용합니다. 반대로 프론트엔드와 API가 같은 도메인에 있고 둘이 하나의 제품으로 움직인다면 액세스 토큰으로 세션을 흉내 낼 이유가 적습니다.

RFC 10017 §7.1은 이런 애플리케이션이 OIDC로 로그인만 외부에 맡긴 뒤, 자체 서버 세션으로 로그인 상태를 유지할 수 있다고 설명합니다. 인증을 외부에 맡기는 것과 우리 API 접근까지 OAuth로 다루는 것은 별개의 결정입니다.

Recap

OAuth는 로그인 프로토콜이 아니라 제한된 API 권한을 위임하는 프로토콜입니다. 어떤 기술을 고르기 전에 먼저 제3자 자원에 접근해야 하는지, 서로 독립된 서비스 경계를 건너야 하는지 확인해야 합니다.




3. Authorization Code와 PKCE는 어떻게 이어지는가

오늘날 브라우저 기반 애플리케이션의 기본 흐름은 Authorization Code Grant와 PKCE의 조합입니다. 이름은 길지만 생각할 것은 두 가지뿐입니다.

  • 토큰을 브라우저 리다이렉트에 실어 보내지 않습니다.
  • 인가를 시작한 클라이언트만 코드를 토큰으로 바꿀 수 있게 묶습니다.

이 그림에서 Client의 위치는 아키텍처마다 다릅니다. 서버 애플리케이션이나 BFF에서는 Client가 서버에 있고, 브라우저 전용 SPA에서는 JavaScript가 Client입니다.

그래서 토큰 요청을 흔히 "백채널 교환"이라고 부르더라도 항상 서버 간 통신이라는 뜻은 아닙니다. 중요한 것은 통신 주체가 서버인지가 아니라, 토큰이 브라우저 리다이렉트에 실리지 않고 Token Endpoint의 직접 응답으로 돌아온다는 점입니다. SPA는 CORS가 허용된 Token Endpoint에 브라우저에서 직접 POST할 수 있습니다.

흐름에 등장하는 주소와 값을 구분합니다

요소가리키는 곳주의할 점
Authorization Endpoint사용자가 로그인하고 동의하는 곳브라우저가 이동합니다.
Token Endpoint코드를 토큰으로 바꾸는 곳Client가 직접 호출합니다.
redirect_uri인가 응답이 돌아올 Client의 주소등록값과 정확히 일치해야 합니다.
scopeClient가 요청하는 권한의 범위애플리케이션 내부 역할과는 다릅니다.

redirect_uri는 로그인 뒤 아무 곳으로나 보내는 편의 기능이 아닙니다. 인가 서버는 미리 등록된 URI와 요청값을 정확한 문자열로 비교해야 합니다. 네이티브 앱의 localhost 포트 예외를 빼면 와일드카드나 느슨한 부분 일치는 코드가 공격자 주소로 빠질 틈을 만듭니다. 로그인 뒤 돌아갈 화면 같은 애플리케이션 문맥은 redirect_uri를 바꾸기보다 검증된 state에 묶어 관리합니다.

scope도 "사용자는 관리자다" 같은 우리 서비스의 역할을 뜻하지 않습니다. calendar.read처럼 Client가 Resource Server에서 위임받으려는 작업의 범위입니다. 토큰에 scope가 있어도 실제 리소스와 테넌트에 대한 최종 인가는 Resource Server가 다시 판단합니다.

코드가 토큰보다 먼저 오는 이유

Authorization Code는 수명이 짧고 한 번만 쓸 수 있는 교환권입니다. 브라우저 리다이렉트를 거치는 구간에는 이 교환권만 노출하고, 실제 액세스 토큰은 Token Endpoint에서 받습니다.

과거 Implicit Grant는 액세스 토큰을 인가 응답의 URL fragment에 담아 바로 돌려줬습니다. fragment는 일반적인 HTTP 요청이나 Referer 헤더로 서버에 전송되지는 않습니다. 그래도 브라우저 실행 환경과 히스토리, 리다이렉트 처리 코드에 토큰이 직접 나타나고 재생 공격을 막기도 어렵습니다. 현대 브라우저에는 CORS와 History API가 있으므로 이 우회 방식을 유지할 이유가 사라졌습니다.

RFC 9700은 Implicit Grant를 사용하지 않도록 권고하고, RFC 10017은 브라우저 클라이언트가 Authorization Code Grant를 사용하도록 요구합니다. OAuth 2.1 초안에서도 Implicit Grant와 Resource Owner Password Credentials Grant는 빠져 있습니다.

PKCE는 코드를 시작한 흐름에 묶습니다

인가 코드의 수명이 짧아도 공격자가 먼저 교환하면 끝입니다. PKCE(Proof Key for Code Exchange)는 이 문제를 막습니다.

  1. 클라이언트가 예측하기 어려운 code_verifier를 만듭니다.
  2. 그 값을 SHA-256으로 해시한 code_challenge를 인가 요청에 보냅니다.
  3. 콜백에서 코드를 받은 뒤 Token Endpoint에 원본 code_verifier를 보냅니다.
  4. 인가 서버가 처음 받은 challenge와 대조합니다.

코드만 가로챈 쪽은 verifier를 모르므로 토큰을 받을 수 없습니다. challenge 방식은 S256을 사용합니다. 평문 verifier를 인가 요청에 싣는 plain 방식은 PKCE의 이점을 크게 줄입니다.

PKCE를 클라이언트 인증으로 이해하면 안 됩니다. 브라우저에 포함한 client_secret은 누구나 꺼내 볼 수 있으므로 비밀이 아닙니다. PKCE가 증명하는 것은 "등록된 앱의 정체"가 아니라 이 코드 교환을 시도한 쪽이 같은 인가 흐름을 시작했다는 사실입니다.

state는 여전히 흐름의 문맥을 지킵니다

state는 요청을 시작한 브라우저의 문맥과 콜백을 연결합니다. 콜백에서 저장해 둔 값과 정확히 비교하고 한 번 사용한 뒤 폐기합니다. 로그인 뒤 돌아갈 경로를 담더라도 임의의 외부 URL을 그대로 신뢰해서는 안 됩니다.

RFC 9700은 인가 서버가 PKCE를 지원하고 반드시 사용하게 한다는 사실이 확인됐다면 PKCE를 CSRF 방어 수단으로 삼을 수 있다고 설명합니다. OIDC의 nonce도 조건에 따라 같은 공격을 막습니다. 그래도 대부분의 검증된 SDK는 state, PKCE, OIDC nonce를 각자 관리합니다. 직접 하나를 빼서 단순화하기보다 라이브러리의 전체 흐름을 유지하는 편이 안전합니다.

여러 Issuer를 지원하면 mix-up을 막아야 합니다

Google과 Microsoft처럼 둘 이상의 인가 서버를 지원한다면 어느 서버로 인가 요청을 보냈는지도 흐름에 묶어야 합니다. 공격자가 콜백을 다른 인가 서버의 응답처럼 보이게 만들면 Client가 코드를 엉뚱한 Token Endpoint로 보낼 수 있습니다. 이를 인가 서버 혼동 공격(Authorization Server Mix-Up)이라고 합니다.

Client는 인가 요청을 시작할 때 선택한 Issuer를 state가 가리키는 서버 측 트랜잭션에 저장하고, 콜백의 Issuer와 대조해야 합니다. RFC 9207iss 응답 파라미터를 쓰거나 Issuer마다 서로 다른 Redirect URI를 두는 방법이 있습니다. 콜백에서 넘어온 값만 보고 Token Endpoint와 JWKS를 고르면 공격자가 검증 대상을 고르게 됩니다.

Recap

Authorization Code Grant는 브라우저 리다이렉트에 토큰 대신 짧은 교환권을 싣습니다. PKCE는 그 교환권을 인가를 시작한 흐름에 묶습니다. SPA도 직접 Token Endpoint를 호출할 수 있으므로, "백채널"을 무조건 서버 간 통신으로 이해해서는 안 됩니다.




4. 액세스 토큰, 리프레시 토큰, ID 토큰은 누가 읽는가

세 토큰의 차이는 내용보다 누가 읽도록 발급됐는지에 있습니다.

토큰주된 수신자대답하는 질문
Access TokenResource Server이 API에서 무엇을 할 수 있는가?
Refresh TokenAuthorization Server의 Token Endpoint새 액세스 토큰을 발급해도 되는가?
ID TokenOIDC Client사용자가 언제, 어디서 인증됐는가?

이 표의 수신자는 서로 바꿀 수 없습니다. 특히 ID 토큰을 비즈니스 API의 Authorization: Bearer 헤더에 넣어 인가에 사용하면 안 됩니다. ID 토큰은 로그인 결과를 받은 Client를 위한 문서이지, Resource Server에 API 권한을 증명하는 자격 증명이 아닙니다.

Access Token은 API를 위한 자격 증명입니다

액세스 토큰은 API 호출 권한을 나타냅니다. 보통 scope는 허용된 작업을 좁히고, audienceresource는 토큰을 사용할 API를 제한합니다.

클라이언트는 액세스 토큰의 내부 형식에 의존하지 않는 편이 좋습니다. 어떤 제공자는 JWT를 쓰고, 어떤 제공자는 클라이언트가 해석할 수 없는 임의 문자열을 씁니다. JWT처럼 보여도 클라이언트가 payload를 읽어 화면 권한을 결정하면 제공자가 형식을 바꾸거나 클레임의 의미를 조정할 때 깨집니다. 액세스 토큰을 해석하고 최종 권한을 판단할 책임은 Resource Server에 있습니다.

Access Token은 로그인 증명서가 아닙니다

액세스 토큰은 "이 사용자가 누구다"라는 주장을 Client에 전달하려고 만든 문서가 아닙니다. 애초에 Client용으로 발급된 것이 아닐 수 있고, 사용자 인증 시점이나 방법이 들어 있다는 보장도 없습니다. 서비스용 토큰처럼 사용자 자체가 없는 액세스 토큰도 있습니다.

그렇다고 액세스 토큰을 받아 사용자 API를 호출하는 모든 로그인이 틀렸다는 뜻은 아닙니다. GitHub처럼 OIDC를 제공하지 않는 OAuth 연동에서는, 서버가 자신이 시작한 인가 흐름으로 토큰을 받고 공급자의 사용자 API를 호출해 계정을 확인할 수 있습니다. 이 블로그의 비공개 페이지가 그렇게 동작합니다.

중요한 경계는 이렇습니다.

  • 서버가 state를 검증하고 코드를 직접 교환합니다.
  • 토큰은 브라우저가 임의로 제출한 값이 아닙니다.
  • 사용자 API 응답은 해당 공급자와 맺은 계약 안에서만 해석합니다.
  • 확인이 끝나면 우리 서비스의 세션을 따로 발급합니다.

반대로 브라우저가 가져온 액세스 토큰을 서버가 그대로 받아 /me 호출 결과만으로 로그인을 처리한다면, 그 인가 흐름과 사용자를 묶을 근거가 약합니다. OIDC를 지원하는 공급자라면 로그인에는 ID 토큰과 OIDC 검증 절차를 쓰는 편이 명확합니다.

Refresh Token은 더 오래 사는 권한입니다

액세스 토큰을 짧게 발급하면 유출 피해 시간을 줄일 수 있습니다. 대신 사용자가 몇 분마다 로그인하지 않도록 리프레시 토큰으로 새 액세스 토큰을 받습니다. 리프레시 토큰이 유출되면 유효한 동안 공격자도 액세스 토큰을 계속 발급받을 수 있으므로 보관 기준은 액세스 토큰보다 엄격해야 합니다.

브라우저 같은 공개 클라이언트에 리프레시 토큰을 발급한다면 RFC 9700 §2.2.2은 sender-constrained token이나 refresh token rotation을 요구합니다. 앞의 방식은 특정 키를 증명하는 Client만 토큰을 쓸 수 있게 묶고, 뒤의 방식은 토큰을 쓸 때마다 이전 토큰을 폐기하고 새 토큰으로 바꿉니다. 이미 사용한 리프레시 토큰이 다시 나타나면 토큰 계열 전체를 끊어 탈취를 감지할 수 있습니다.

회전만으로 충분한 것은 아닙니다. 공격자가 최신 토큰을 먼저 쓰고 정상 애플리케이션의 저장값까지 지우면 재사용 흔적이 바로 나타나지 않을 수 있습니다. 최대 수명, 유휴 만료, 최소 scope, 특정 Resource Server로의 audience 제한을 함께 적용해야 합니다.

토큰의 내용을 읽는 것과 상태를 묻는 것은 다릅니다

액세스 토큰은 JWT일 수도 있고 자체적으로 해석할 수 없는 문자열인 불투명 토큰(Opaque Token)일 수도 있습니다. 불투명 토큰을 받은 Resource Server는 문자열만 보고 토큰의 상태나 권한을 알아낼 수 없습니다. Authorization Server의 Introspection Endpoint에 물어 토큰이 활성 상태인지와 어떤 권한을 가졌는지 확인합니다.

반대로 JWT처럼 필요한 정보를 토큰 안에 모두 담으면 Resource Server가 서명과 클레임을 로컬에서 검증할 수 있어 매 요청마다 원격 조회하지 않아도 됩니다. 대신 만료 전에 강제로 회수하려면 별도 설계가 필요합니다. 짧은 수명, 폐기 목록, 보안 이벤트 전파 또는 Introspection을 조합하는 이유입니다.

수단호출하는 쪽맡은 일
Revocation EndpointClient더는 쓰지 않을 토큰의 폐기를 Authorization Server에 요청합니다.
Introspection EndpointResource Server토큰의 현재 활성 상태와 메타데이터를 조회합니다.
로컬 JWT 검증Resource Server네트워크 조회 없이 서명, 시간, audience, scope를 검사합니다.

Revocation은 "로그아웃 URL을 한 번 호출하면 모든 시스템의 토큰이 즉시 사라진다"는 뜻이 아닙니다. 어느 범위의 연관 토큰까지 함께 폐기하는지, Resource Server가 상태를 캐시하는지, 이미 만들어진 애플리케이션 세션까지 끝낼지는 배포 정책에 달려 있습니다.

Recap

액세스 토큰은 API가, 리프레시 토큰은 Token Endpoint가, ID 토큰은 로그인 결과를 받은 Client가 읽습니다. 서로 모양이 비슷해도 바꿔 쓸 수 없습니다. 토큰을 설계하거나 검증할 때는 클레임 목록보다 수신자를 먼저 확인해야 합니다.




5. OIDC 로그인은 무엇을 검증해야 하는가

OpenID Connect는 OAuth 2.0 위에 인증 계층을 더합니다. OAuth가 API 권한 위임을 다룬다면, OIDC는 Client가 사용자의 인증 결과를 확인하는 방법을 표준화합니다.

OIDC에서는 인증 결과를 신뢰하는 Client를 Relying Party(RP), 사용자를 인증하고 클레임을 제공하는 서버를 OpenID Provider(OP)라고 부릅니다. Google, Microsoft Entra ID, Okta 같은 서비스가 OP가 될 수 있고, 우리 애플리케이션이 RP가 됩니다.

OAuth만으로는 "이 사용자가 누구인가"를 Client에 전달하는 표준 계약이 없습니다. 그 빈자리를 ID 토큰과 OIDC 검증 규칙이 채웁니다.

OIDC 요청에는 openid scope가 들어가고, 토큰 응답이 성공하면 ID 토큰이 함께 옵니다. ID 토큰은 보통 서명된 JWT이며 이렇게 말합니다.

이 Issuer가 이 Subject를 이 시점에 인증했고, 이 결과는 이 Client를 위해 발급했습니다.

문장에서 주어와 목적어를 빼면 안 됩니다. 서명이 유효하다는 사실만으로 우리 애플리케이션을 위한 토큰이라는 뜻은 아닙니다.

ID 토큰에서 확인할 것

Claim확인할 내용이유
iss기대한 Issuer와 정확히 같은가다른 공급자가 발급한 토큰을 거절합니다.
aud우리 client_id가 포함되고 다른 audience도 신뢰할 수 있는가다른 애플리케이션용 토큰을 거절합니다.
azp사용하는 확장과 공급자 계약에 맞는가authorized party가 필요한 배포에서 잘못된 Client를 거절합니다.
exp, iat유효 기간과 발급 시점이 정책에 맞는가만료되거나 지나치게 오래된 토큰을 거절합니다.
nonce요청 때 보낸 값과 같은가인증 응답을 요청 흐름에 묶고 재생을 줄입니다.
서명과 alg신뢰한 키와 허용한 알고리즘인가위조 또는 변조된 토큰을 거절합니다.

aud가 여러 값이면 우리 client_id가 포함되어야 하고, 함께 온 다른 audience도 신뢰할 수 있어야 합니다. 그렇다고 audience가 여러 개라는 이유만으로 azp가 반드시 있어야 하는 것은 아닙니다. OIDC Core에서 azp는 선택 사항이며 주로 확장이 사용하는 클레임입니다. 확장이나 공급자 계약에서 azp를 정의했다면 그 규칙에 맞춰 검증하고, 그렇지 않다면 임의의 규칙을 덧붙이지 않습니다.

nonce는 OIDC 인증 요청과 ID 토큰을 연결합니다. OAuth의 state가 콜백 전체를 브라우저에서 시작한 요청에 묶는다면, nonceID 토큰 자체를 그 인증 요청에 묶습니다. 둘은 닮았지만 검증하는 대상이 다릅니다.

사용자 키는 isssub로 만듭니다

sub는 한 Issuer 안에서 사용자를 안정적으로 가리키는 식별자입니다. Issuer가 달라지면 같은 sub가 다른 사람일 수 있으므로 일반적인 사용자 키는 (iss, sub) 쌍입니다.

이메일은 로그인 화면에 보여주거나 계정 연결의 단서로 쓸 수 있지만 기본 키로 삼기에는 불안정합니다. 주소는 바뀌고 재할당될 수 있으며, 공급자마다 정규화 규칙도 다릅니다. OIDC Core도 email의 유일성에 의존하지 말라고 명시합니다. 이메일을 이용해 기존 계정과 연결한다면 email_verified, 테넌트 정책, 명시적인 재인증 절차를 함께 설계해야 합니다.

UserInfo는 프로필을 가져오는 별도 API입니다

ID 토큰에 화면에 필요한 프로필 클레임이 모두 들어오지 않을 수 있습니다. OIDC의 UserInfo Endpoint는 액세스 토큰을 받아 이름, 이메일, 프로필 이미지처럼 허용된 사용자 클레임을 돌려주는 보호된 API(Protected Resource)입니다. ID 토큰을 대신 검증하는 곳도 아니고, ID 토큰을 보내는 곳도 아닙니다.

UserInfo 응답을 ID 토큰과 함께 쓴다면 두 응답의 sub가 정확히 같은지 확인해야 합니다. 같지 않으면 다른 사용자의 프로필이 섞인 토큰 치환 공격일 수 있으므로 응답을 사용하지 않습니다.

Discovery와 JWKS는 설정과 키 교체를 맡습니다

OIDC 제공자는 보통 다음 주소에 메타데이터를 공개합니다.

https://idp.example.com/.well-known/openid-configuration

여기에는 Authorization Endpoint, Token Endpoint, jwks_uri, 지원 알고리즘 같은 정보가 들어 있습니다. Client는 미리 신뢰하기로 한 Issuer를 고정한 뒤 그 Issuer의 메타데이터를 읽어야 합니다. 로그인 요청이나 콜백에서 넘어온 Issuer를 그대로 따라가면 공격자가 검증에 쓸 서버와 키까지 고르게 됩니다.

JWKS(JSON Web Key Set)는 ID 토큰 서명을 검증할 공개 키 모음입니다. JWT 헤더의 kid로 키를 고르고, 찾지 못하면 키 교체 가능성을 고려해 JWKS를 다시 가져옵니다. 매 요청마다 조회하지 않도록 캐시하되, 영구 캐시하지는 않습니다. 검증된 OIDC SDK나 JOSE 라이브러리가 이 동작을 이미 제공하므로 직접 구현할 이유는 드뭅니다.

아래 코드는 검증 항목을 보여주기 위한 축약 예시입니다. 실제 로그인에서는 공급자 SDK가 state, PKCE, nonce, 메타데이터 캐시까지 함께 관리하도록 두는 편이 낫습니다.

import { createRemoteJWKSet, jwtVerify } from "jose";

const issuer = "https://idp.example.com";
const clientId = "payroll-web";
const trustedAudiences = new Set([clientId]);

async function verifyIdToken(
idToken: string,
expectedNonce: string,
expectedAuthorizedParty?: string,
) {
const metadata = await fetch(
`${issuer}/.well-known/openid-configuration`,
).then((response) => response.json());

if (metadata.issuer !== issuer) {
throw new Error("Unexpected OIDC issuer");
}

const jwks = createRemoteJWKSet(new URL(metadata.jwks_uri));
const { payload } = await jwtVerify(idToken, jwks, {
issuer,
audience: clientId,
algorithms: ["RS256"],
});

const audiences = Array.isArray(payload.aud)
? payload.aud
: payload.aud === undefined
? []
: [payload.aud];

if (audiences.some((audience) => !trustedAudiences.has(audience))) {
throw new Error("Unexpected ID token audience");
}

// azp를 사용하는 확장이나 공급자 계약이 있을 때만 기대값을 전달한다.
if (
expectedAuthorizedParty !== undefined &&
payload.azp !== expectedAuthorizedParty
) {
throw new Error("Unexpected authorized party");
}

if (payload.nonce !== expectedNonce) {
throw new Error("Unexpected nonce");
}

if (typeof payload.sub !== "string") {
throw new Error("Missing subject");
}

return `${payload.iss}:${payload.sub}`;
}

알고리즘은 공급자가 공개한 메타데이터와 Client 등록 설정에 맞춰 허용 목록으로 둡니다. 예시의 RS256을 모든 공급자에 그대로 복사하면 안 됩니다.

JWT는 프로토콜이 아니라 포맷입니다

흔히 보는 header.payload.signature 형태는 서명된 JWT의 Compact JWS 표현입니다. header와 payload는 Base64URL로 인코딩됐을 뿐 암호화되지 않았습니다. 토큰을 가진 쪽은 내용을 쉽게 읽을 수 있으므로 비밀 정보를 함부로 넣어서는 안 됩니다.

eyJhbGciOi... . eyJpc3MiOi... . SflKxwRJS...
└─ JOSE Header └─ Claims └─ Signature
알고리즘·kid iss·sub·aud 앞의 두 부분을 검증

서명은 내용을 숨기지 않습니다. 서명을 검증하면 발급 뒤 내용이 바뀌지 않았고 신뢰한 키의 소유자가 서명했다는 사실을 확인할 수 있습니다. 그래서 JWT는 "파싱한 뒤 믿는" 형식이 아니라 검증에 성공한 뒤 사용하는 형식입니다.

모든 토큰이 JWT인 것도 아닙니다. OIDC ID 토큰은 JWT지만 OAuth 액세스 토큰의 형식은 배포 정책에 달려 있습니다. Client가 액세스 토큰을 불투명한 문자열로 취급해야 하는 이유입니다.

Recap

OIDC는 OAuth 위에 사용자 인증 결과를 전달하는 규칙을 더합니다. ID 토큰은 Client를 위한 문서이며 iss, aud, 시간, nonce, 서명을 검증해야 합니다. 사용하는 확장이 azp를 정의했다면 그 계약도 함께 확인합니다. 사용자 식별자는 이메일이 아니라 (iss, sub)를 기준으로 삼습니다.




6. 브라우저에 토큰을 두면 무엇이 달라지는가

브라우저는 서버와 다른 실행 환경입니다. 배포한 JavaScript를 사용자가 직접 내려받아 실행하며, 한 페이지에서 제품 코드뿐 아니라 분석 도구와 채팅 위젯, 브라우저 확장 프로그램까지 함께 움직입니다. 여기에 XSS가 생기면 악성 스크립트도 애플리케이션과 같은 오리진 권한을 얻습니다.

토큰 저장소 논쟁은 보통 localStorageHttpOnly 쿠키 중 무엇이 안전한지로 시작합니다. 먼저 두 공격을 구분해야 합니다.

  • 토큰 탈취: 자격 증명을 읽어 공격자 서버로 가져갑니다. 사용자의 브라우저를 떠나서도 재생할 수 있습니다.
  • 클라이언트 하이재킹: 자격 증명은 읽지 못해도 사용자의 세션을 이용해 정상 애플리케이션이 할 수 있는 요청을 보냅니다.

HttpOnly 쿠키는 첫 번째를 어렵게 하지만 두 번째는 막지 못합니다. XSS 코드도 같은 오리진에서 BFF를 호출할 수 있고 브라우저는 쿠키를 붙입니다. 그래도 공격자가 세션을 브라우저 밖으로 가져가 장기간 재생하기는 어려워집니다. 서버가 열어둔 엔드포인트와 권한 검사의 범위도 벗어날 수 없습니다.

이미 있는 토큰을 숨겨도 새 토큰을 받을 수 있습니다

RFC 10017 §5.1.3은 저장된 토큰을 읽지 않고 새 토큰을 받는 공격도 설명합니다. 인가 서버의 로그인 세션이 살아 있고 별도의 사용자 조작 없이 인가가 진행될 수 있다면 XSS 코드는 저장된 토큰을 찾지 않아도 됩니다. iframe이나 새 창에서 별도의 Authorization Code Flow를 시작하고, 같은 오리진 콜백에서 코드를 읽어 공격자가 제어하는 새 토큰을 얻을 수 있습니다.

이 공격에는 짧은 액세스 토큰 수명이나 리프레시 토큰 회전만으로 대응하기 어렵습니다. 공격자가 정상 애플리케이션과 별개인 토큰을 새로 발급받기 때문입니다. DPoP도 공격자가 자기 키로 새 토큰을 발급받으면 이 상황을 해결하지 못합니다.

Web Worker나 Service Worker에 기존 토큰을 격리하는 기법은 여전히 유용합니다. 다만 막는 것은 기존 토큰의 직접 유출입니다. 브라우저 전용 Client가 새 인가 흐름을 시작할 수 있다는 구조까지 바꾸지는 못합니다.

저장 위치는 피해의 범위를 바꿉니다

저장 위치스크립트의 직접 접근남는 성질
localStorage쉽습니다.탭을 닫거나 브라우저를 다시 열어도 남습니다.
sessionStorage쉽습니다.탭 단위이며 탭을 닫으면 사라집니다.
메모리가능합니다.새로고침하면 사라져 노출 시간이 짧습니다.
Worker 격리더 어렵습니다.메시지 API 악용과 새 인가 흐름은 남습니다.
HttpOnly 쿠키직접 읽을 수 없습니다.브라우저 안에서 인증 요청을 보내는 것은 가능합니다.

따라서 저장 위치가 무의미한 것은 아닙니다. 자격 증명이 얼마나 오래 남는지, 브라우저 밖으로 가져가 재생할 수 있는지, 사용자가 창을 닫은 뒤에도 공격이 이어질지가 달라집니다. 다만 XSS를 해결한 것으로 오해하면 안 됩니다.

CORS도 같은 오리진에서 실행되는 XSS를 막아주지 않습니다. 정상 프론트엔드의 오리진을 허용한 API는 그 오리진에서 실행되는 악성 코드의 요청도 함께 허용합니다. CORS는 서로 다른 오리진 사이의 경계이지, 같은 페이지 안에서 정상 코드와 악성 코드를 구분하는 장치가 아닙니다.

따라서 가장 먼저 할 일은 악성 코드가 실행되지 않게 막는 것입니다. 컨텍스트에 맞는 출력 인코딩, 안전한 DOM API, 의존성과 서드파티 스크립트 축소, CSP 같은 방어가 아키텍처 선택보다 앞에 옵니다. 이 내용은 웹 보안, 신뢰 경계부터 CSP까지에서 따로 다룹니다.

Recap

브라우저에서 XSS가 실행되면 토큰을 읽는 것보다 더 많은 일을 할 수 있습니다. 저장소 격리는 기존 토큰 유출과 피해 지속 시간을 줄이지만 새 인가 흐름이나 클라이언트 하이재킹까지 없애지는 못합니다. 브라우저에 어떤 자격 증명을 둘지는 "안전한가"보다 "침해 뒤 무엇을 어디까지 가져갈 수 있는가"로 비교해야 합니다.




7. 세션, BFF, SPA 중 무엇을 고를 것인가

RFC 10017은 OAuth를 사용하는 브라우저 애플리케이션을 BFF, 토큰 중개 백엔드(Token-Mediating Backend), 브라우저 OAuth Client 세 가지로 나눕니다. 여기에 OAuth를 쓰지 않는 서버 세션까지 더하면 실무의 선택지는 네 가지입니다.

자격 증명은 어디에 머무는가

먼저 브라우저에 무엇을 남기고 백엔드가 어디까지 맡는지 비교해 보겠습니다.

구조브라우저가 가진 자격 증명백엔드가 맡는 일
OIDC 로그인 + 서버 세션세션 쿠키로컬 세션과 도메인 인가를 관리합니다.
BFF세션 쿠키OAuth Client가 되어 토큰을 보관하고 API를 대신 호출합니다.
Token-Mediating Backend세션 쿠키와 액세스 토큰코드 교환과 리프레시 토큰을 맡습니다.
브라우저 OAuth Client액세스 토큰, 경우에 따라 리프레시 토큰토큰을 보관할 서버가 없습니다.

어떤 위험이 남고, 언제 어울리는가

같은 네 구조를 이번에는 남는 공격 경로와 적용하기 좋은 상황으로 비교합니다.

구조주로 남는 공격 경로어울리는 경우
OIDC 로그인 + 서버 세션CSRF, 세션 탈취한 제품 경계 안의 프론트엔드와 API
BFF세션을 이용한 XSS 요청, CSRF민감한 제3자 API를 대신 호출하는 서비스
Token-Mediating Backend브라우저 액세스 토큰 탈취브라우저의 API 직접 호출이 꼭 필요한 경우
브라우저 OAuth Client토큰 탈취와 새 인가 흐름백엔드가 없고 위험을 감수할 수 있는 경우

먼저 OAuth가 필요한지 묻습니다

프론트엔드와 API가 같은 서비스이고 자체 데이터만 다룬다면 OIDC로 로그인 결과만 받은 뒤 서버 세션을 만들 수 있습니다. 브라우저와 우리 API 사이에는 OAuth 액세스 토큰을 둘 필요가 없습니다. 구조가 단순해지고 토큰 발급·갱신·폐기 정책도 줄어듭니다.

외부 IdP를 썼다고 해서 우리 API까지 OAuth로 보호해야 하는 것은 아닙니다. OIDC는 로그인에 쓰고, 이후 요청은 세션으로 처리할 수 있습니다.

BFF는 OAuth Client를 서버로 옮깁니다

BFF(Backend for Frontend)는 서버에서 Authorization Code Flow를 마치고 액세스 토큰과 리프레시 토큰을 보관합니다. 브라우저는 HttpOnly 세션 쿠키로 BFF를 호출하고, BFF가 알맞은 액세스 토큰을 붙여 Resource Server로 전달합니다.

BFF는 구현할 때 리버스 프록시처럼 동작할 수 있고 API Gateway 뒤에 놓일 수도 있습니다. 이름을 가르는 기준은 제품 구성이 아니라 책임입니다. 프론트엔드를 대신해 OAuth Client가 되고, 토큰을 세션에 묶어 관리하며, Resource Server 호출에 토큰을 붙이는 컴포넌트가 BFF입니다.

이 구조에서는 브라우저가 액세스 토큰을 직접 읽지 못하고, 별도의 공개 Client로 새 인가 흐름을 시작하기도 어렵습니다. 그래도 XSS는 사용자의 로그인된 세션으로 BFF에 요청할 수 있습니다. BFF의 엔드포인트를 업무 단위로 좁히고 서버 측 인가와 요청 빈도 제한(rate limiting), 이상 탐지를 두는 이유입니다.

쿠키를 쓰므로 CSRF도 다시 고려해야 합니다. SameSite만 믿기보다 배포 도메인에 맞춰 Origin 검사, 커스텀 헤더를 요구하는 CORS 정책, 프레임워크의 anti-forgery 기능을 적용합니다.

Token-Mediating Backend는 절충안입니다

Token-Mediating Backend도 서버가 비밀값을 지킬 수 있는 confidential client가 되어 코드 교환과 리프레시 토큰을 관리합니다. 차이는 액세스 토큰을 브라우저에 넘겨 Resource Server를 직접 호출하게 한다는 점입니다.

API 응답을 모두 프록시하기 어려운 환경에서는 유용하지만 액세스 토큰 탈취 위험은 남습니다. 서버는 리프레시 토큰과 새 토큰 발급을 지킬 수 있으나, 브라우저가 받은 액세스 토큰까지 지켜주지는 못합니다. RFC 10017도 가능하다면 완전한 BFF를 먼저 검토하라고 권고합니다.

브라우저 OAuth Client는 가장 단순하고 노출이 큽니다

정적 호스팅만 있는 SPA는 Authorization Code + PKCE를 직접 수행하고 토큰도 브라우저에서 관리합니다. 서버 컴포넌트가 없어 배포는 단순하지만, 앞 절의 XSS 공격이 그대로 적용됩니다.

업무 시스템이나 개인정보처럼 민감한 정보를 다루는 서비스에는 권장하기 어렵습니다. 어쩔 수 없이 쓴다면 액세스 토큰의 수명과 scope, audience를 줄이고 refresh token rotation, sender constraint, 강한 CSP와 의존성 통제를 함께 적용해야 합니다. 그래도 BFF처럼 토큰을 브라우저 밖으로 옮기는 경계가 생기는 것은 아닙니다.

구조를 고르는 순서

Recap

아키텍처의 차이는 토큰 이름이 아니라 OAuth Client와 자격 증명이 놓이는 위치에서 나옵니다. 같은 제품 경계라면 OIDC와 서버 세션으로 충분할 수 있습니다. OAuth가 필요하고 데이터가 민감하다면 BFF를 먼저 검토합니다. Token-Mediating Backend나 브라우저 Client를 고르려면 브라우저에 토큰을 노출해야 하는 분명한 이유가 있어야 합니다.




8. SSO와 Identity Federation은 어디에 놓이는가

SSO(Single Sign-On)는 사용자가 한 번 로그인한 뒤 여러 애플리케이션에서 다시 자격 증명을 입력하지 않는 경험입니다. OIDC는 그 경험을 구현하는 데 자주 쓰이는 프로토콜이지만 둘은 같은 말이 아닙니다.

여러 애플리케이션이 같은 Identity Provider(IdP)를 신뢰한다고 해보겠습니다. 사용자가 첫 번째 앱에서 로그인하면 IdP 세션이 생깁니다. 두 번째 앱도 OIDC Authorization Request를 보내지만 IdP 세션이 살아 있으므로 비밀번호를 다시 묻지 않고 새 ID 토큰을 발급할 수 있습니다. 각 앱은 그 결과로 자기 세션을 따로 만듭니다.

이처럼 각 애플리케이션이 같은 IdP의 인증 결과를 신뢰하는 구조를 Identity Federation이라고 합니다. 애플리케이션이 비밀번호 검증과 MFA를 직접 구현하지 않고 신뢰한 IdP의 인증 결과를 받아들이는 방식입니다. SAML도 이 역할을 할 수 있으므로 Federation이 곧 OIDC라는 뜻은 아닙니다.

OIDC와 SAML은 같은 문제를 다른 형식으로 풉니다

프로토콜결과를 전달하는 형식자주 만나는 환경
OIDCJSON과 JWT 기반 ID 토큰웹·모바일 애플리케이션과 API 생태계
SAML 2.0XML 기반 Assertion기업용 SaaS와 기존 엔터프라이즈 IdP 연동

둘 다 외부 Identity Provider의 인증 결과를 애플리케이션이 신뢰하게 만드는 Federation 프로토콜입니다. OIDC가 더 새롭다는 이유만으로 SAML 연동을 억지로 바꿀 필요는 없습니다. 반대로 신규 웹 애플리케이션에서 JSON, REST, 모바일 흐름과 자연스럽게 맞물려야 한다면 OIDC가 다루기 편합니다.

로그아웃과 계정 비활성화는 로그인보다 복잡합니다. IdP 세션, 각 애플리케이션 세션, 이미 발급된 액세스 토큰과 리프레시 토큰은 수명이 서로 다릅니다. IdP 계정을 막았다고 모든 애플리케이션 세션이 즉시 끝난다고 가정해서는 안 됩니다. 짧은 로컬 세션, 주기적 재인증, 토큰 폐기, OIDC logout 규격, 공급자의 이벤트나 프로비저닝 체계를 필요한 종료 속도에 맞춰 조합해야 합니다.

IdP가 사용자를 인증해도 애플리케이션 권한까지 정해주지는 않습니다. HR 시스템의 테넌트, 관리자 역할, 급여 열람 범위는 여전히 우리 서비스가 판단합니다. 외부 그룹 클레임을 이용하더라도 어떤 그룹이 어떤 권한을 뜻하는지는 우리 쪽 정책입니다.

Recap

OIDC는 여러 서비스가 인증 결과를 주고받는 표준이고, SSO는 그 위에서 얻는 사용자 경험입니다. IdP 세션과 각 애플리케이션 세션은 별개이므로 로그인, 로그아웃, 계정 차단의 전파 시간을 따로 설계해야 합니다.




9. 실제 서비스에서는 어떻게 결정할 것인가

다음 조건의 B2B HR 서비스를 생각해보겠습니다.

  • 고객사는 Google Workspace 또는 Microsoft Entra ID로 로그인합니다.
  • 브라우저와 우리 API는 같은 제품 경계에 있습니다.
  • 우리 API는 급여와 인사 데이터를 다룹니다.
  • 지금은 Google이나 Microsoft의 업무 API를 대신 호출하지 않습니다.

이 요구사항만 놓고 보면 답은 OIDC 로그인과 서버 세션입니다.

1. 로그인은 OIDC에 맡깁니다

서버가 Authorization Code Flow를 시작하고 검증된 OIDC SDK에 state, PKCE, nonce 처리를 맡깁니다. 콜백에서는 ID 토큰의 Issuer, audience, 유효 시간, nonce, 서명을 검증합니다. 사용하는 확장이 azp를 정의했다면 그 계약도 함께 확인합니다.

사용자 레코드는 (iss, sub)로 찾습니다. 이메일 도메인과 email_verified는 고객사 연결 정책에 참고할 수 있지만, 이메일 자체를 영구 식별자로 두지 않습니다.

2. 로그인 뒤에는 우리 세션을 만듭니다

OIDC 검증이 끝나면 서버가 사용자, 테넌트, 계정 상태를 확인하고 짧은 수명의 세션을 만듭니다. 브라우저에는 HttpOnly, Secure, 적절한 SameSite 속성을 둔 세션 쿠키만 보냅니다.

우리 API를 위한 OAuth 액세스 토큰은 만들지 않아도 됩니다. 외부 IdP의 ID 토큰이나 액세스 토큰을 우리 세션처럼 오래 들고 있을 이유도 없습니다.

3. 인가는 우리 도메인 규칙으로 판단합니다

IdP는 "이 사람이 누구인지"를 알려줍니다. "이 회사의 8월 급여를 볼 수 있는지"는 알려주지 않습니다. 요청마다 세션에서 userIdtenantId를 찾고, 우리 권한 모델로 리소스와 작업을 검사합니다.

4. 제3자 API 위임이 생기면 BFF를 검토합니다

나중에 서비스가 Google Calendar나 Microsoft Graph를 사용자 대신 호출해야 한다면 그때 OAuth 권한 위임이 필요합니다. 같은 서버가 BFF 역할을 맡아 공급자의 액세스 토큰과 리프레시 토큰을 서버 세션에 묶어 보관하고 필요한 외부 API를 대신 호출할 수 있습니다.

미래에 필요할지 모른다는 이유만으로 처음부터 브라우저에 토큰을 내보내지는 않습니다. 실제로 호출할 Resource Server와 위임할 권한이 정해졌을 때 구조를 확장하는 편이 설계와 운영 모두 단순합니다.

5. 공격 경로와 종료 조건을 적습니다

아키텍처를 정했다면 공격 이름을 나열하는 데서 끝내지 않고, 침해가 발생했을 때 어느 컴포넌트에서 어떻게 접근을 끊을지도 정합니다.

  • 로그인 콜백은 정확한 Redirect URI, state, PKCE, nonce, Issuer 대조로 코드 주입과 mix-up을 막습니다.
  • 브라우저는 출력 인코딩과 CSP, 서드파티 스크립트 통제로 XSS 가능성을 줄입니다. 쿠키 기반 요청에는 CSRF 방어를 별도로 둡니다.
  • 세션은 로그인할 때 새 ID로 교체해 세션 고정(session fixation)을 막고 유휴 만료와 최대 수명, 강제 종료 경로를 둡니다.
  • API는 요청마다 테넌트와 리소스 권한을 다시 검사합니다. 화면에서 버튼을 숨긴 것은 인가가 아닙니다.
  • 사용자의 퇴사나 계정 차단이 IdP, 우리 세션, 이미 발급된 토큰에 전파되기까지 허용할 시간을 정합니다.

구현 전에 적어볼 질문

  • 로그인 공급자는 누구이며, 신뢰할 Issuer를 어떻게 고정하는가?
  • 사용자 기본 키를 (iss, sub)로 저장하는가?
  • 토큰과 세션의 수명, 유휴 만료, 강제 로그아웃 기준은 무엇인가?
  • 브라우저에 꼭 있어야 하는 자격 증명은 무엇인가?
  • 각 액세스 토큰의 Resource Server, scope, audience는 어디까지인가?
  • 쿠키를 쓴다면 CSRF를, 브라우저 토큰을 쓴다면 탈취와 새 토큰 획득을 어떻게 줄이는가?
  • IdP 계정이 비활성화된 뒤 우리 세션이 끝나기까지 허용할 시간은 얼마인가?
  • 프론트엔드가 아니라 서버에서 반드시 다시 검사할 인가 규칙은 무엇인가?

제품이 요구하는 보안 수준에 따라 답은 달라집니다. 그래도 먼저 물어야 할 것은 같습니다. 프로토콜을 고르기 전에 신뢰 경계와 자격 증명의 위치를 정합니다.

Recap

민감한 B2B 데이터라도 외부 업무 API를 호출하지 않는다면 OIDC 로그인과 서버 세션이 더 단순한 답일 수 있습니다. 제3자 API 위임이 생길 때 BFF를 추가하고 브라우저에는 꼭 필요한 자격 증명만 둡니다. 마지막으로 로그인, 세션, 도메인 인가, 계정 차단 단계마다 어디서 접근을 끊을지 적어야 설계가 완성됩니다.




남는 것

OAuth와 OIDC를 처음 볼 때는 토큰이 모두 비슷해 보입니다. JWT 안의 클레임부터 읽기 시작하면 더 복잡해집니다. 반대로 수신자를 따라가면 역할이 또렷해집니다.

  • Access Token은 Resource Server가 읽습니다.
  • Refresh Token은 Authorization Server가 읽습니다.
  • ID Token은 OIDC Client가 읽습니다.
  • Session Cookie는 우리 서버가 읽습니다.

그다음에는 위치를 봅니다. 브라우저가 토큰을 가지면 직접 API를 호출할 수 있지만, XSS가 생겼을 때 토큰과 인가 흐름까지 공격에 노출됩니다. BFF가 토큰을 가지면 운영할 서버와 CSRF 대응이 늘지만 토큰을 브라우저 밖에 둘 수 있습니다. 같은 서비스 경계 안에서는 OAuth 없이 OIDC 로그인과 서버 세션만으로 충분할 수 있습니다.

처음에 본 이 블로그의 비공개 페이지도 그래서 GitHub 액세스 토큰을 세션으로 쓰지 않습니다. 그 토큰은 GitHub API가 읽을 물건입니다. GitHub 계정을 확인하는 짧은 일을 마치면, 이후 요청에는 이 서비스가 이해하는 세션 쿠키를 씁니다.

긴 문자열의 이름을 외우는 것보다 한 가지 질문이 오래 남습니다.

이 토큰은 누구를 위한 것인가요?


References

OAuth

  1. RFC 6749: The OAuth 2.0 Authorization Framework
  2. RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage
  3. RFC 7009: OAuth 2.0 Token Revocation
  4. RFC 7636: Proof Key for Code Exchange by OAuth Public Clients
  5. RFC 7662: OAuth 2.0 Token Introspection
  6. RFC 8414: OAuth 2.0 Authorization Server Metadata
  7. RFC 9207: OAuth 2.0 Authorization Server Issuer Identification
  8. RFC 9700: Best Current Practice for OAuth 2.0 Security
  9. RFC 10017: OAuth 2.0 for Browser-Based Applications
  10. The OAuth 2.1 Authorization Framework (2026년 8월 현재 IETF 초안)

OpenID Connect와 JWT

  1. OpenID Connect Core 1.0 incorporating errata set 2
  2. OpenID Connect Discovery 1.0 incorporating errata set 2
  3. RFC 7519: JSON Web Token
  4. RFC 8725: JSON Web Token Best Current Practices
  5. RFC 9449: OAuth 2.0 Demonstrating Proof of Possession

Identity Federation

  1. OASIS: SAML V2.0 Technical Overview
좋은 사람들과 재미있는 일을 하며 열정적이고 즐겁게 살고 싶은 개발자