본문으로 건너뛰기
언어

@plott/webkit-auth-oauth0.4.0

@plott/webkit-auth 발급자 연동 — OAuth 2.0/OIDC 클라이언트(PKCE)와 Authorization Server 토큰 교환(RFC 8693)·인증 코드 그랜트·갱신·폐기

설치

pnpm
pnpm add @plott/webkit-auth-oauth@0.4.0

진입점(exports)

import타입파일
@plott/webkit-auth-oauth./dist/index.d.ts./dist/index.js
@plott/webkit-auth-oauth/bearer./dist/bearer.d.ts./dist/bearer.js

@plott/webkit-auth 의 AuthBackend 구현입니다. OAuth 2.0/OIDC 클라이언트(Authorization Code + PKCE)로 제공자(IdP)에 로그인하고, 발급자에 따라 토큰을 세션으로 만듭니다. 서버에서 받은 Bearer 토큰을 검증하는 /bearer 진입점도 있습니다.

모든 로그인 수단(브라우저 로그인, 인증 코드, 매직 링크, 비밀번호, 네이티브 소셜 토큰, WebView 핸드오프)은 «토큰 응답 → 세션» 하나로 모입니다. 업스트림 호출은 언제나 Authorization: Bearer <세션 액세스 토큰> 하나입니다. redirect_uri·state·트랜잭션 쿠키·동시 갱신 합치기는 코어(@plott/webkit-auth)가 하고, 이 패키지는 다시 만들지 않습니다.

진입점

import어디서주요 export
@plott/webkit-auth-oauth서버(createAuth 설정)oauth, authorizationServer, providerIssuer, oidcProvider, oauth2Provider, OAuthBackend, authMethodOf, 표준 상수(GRANT_TOKEN_EXCHANGE·TOKEN_TYPE_ACCESS·TOKEN_TYPE_ID), 타입 OAuthHandle
@plott/webkit-auth-oauth/bearer서버 전용(API 라우트·미들웨어·백엔드)createBearerVerifier, BearerError, 타입 BearerOptions·BearerPrincipal·BearerVerifier·BearerInput·BearerFailure

붙이기

서비스의 Authorization Server(AS)가 세션 토큰을 줍니다. 소셜 토큰·인증 코드·WebView 토큰을 모두 AS 토큰으로 바꿉니다.

ts
import { authorizationServer, oauth, type OAuthProvider } from "@plott/webkit-auth-oauth";
declare const issuer: string;declare const tokenEndpoint: string;declare const revocationEndpoint: string;declare const verificationEndpoint: string;declare const verificationGrantType: string;declare const clientId: string;declare const clientSecret: string;declare const providers: OAuthProvider[];
export const backend = oauth({  issuer: authorizationServer({ issuer, tokenEndpoint, revocationEndpoint, verificationEndpoint, verificationGrantType, clientId, clientSecret }),  providers,});

IdP 가 준 토큰을 그대로 세션 토큰으로 쓰려면(사내 SSO) 제공자가 발급자입니다.

ts
import { oauth, providerIssuer } from "@plott/webkit-auth-oauth";import { keycloak } from "@plott/webkit-auth-provider-keycloak";
declare const clientId: string;
export const backend = oauth({  issuer: providerIssuer("keycloak"),  providers: [keycloak({ issuer: "https://sso.example.com/realms/<realm>", clientId })],});

oauth() 는 발급자가 할 수 있는 기능만 싣습니다. 교환(exchange)은 AuthBackend 에, 인증 코드는 verification 포트(verificationEndpoint 가 있을 때), 비밀번호는 password 포트(passwordEndpoint 가 있을 때)에 실립니다. 앱은 defaultMethods(backend) 로 포트가 있는 수단만 켭니다. 없는 기능은 라우트(404)와 proxy 단계가 꺼집니다.

oauth(opts) 의 defaultExpiresIn(기본 300)은 토큰 응답에 expires_in 이 없을 때 쓰는 수명(초)입니다. 양의 유한한 숫자가 아니면 시작할 때 멈춥니다.

  • 교환(exchange): 우리 AS 가 발급자일 때만
  • 인증 코드 확인·발송(verification 포트의 verificationComplete·verificationStart): verificationEndpoint 가 있을 때
  • 비밀번호(password 포트의 passwordLogin): passwordEndpoint 가 있을 때(비밀번호 로그인은 인증 코드 그랜트로 마칩니다)

발급자

함수켜지는 기능
authorizationServer(opts)토큰 교환(RFC 8693: 네이티브 소셜 토큰·WebView 핸드오프), 인증 코드, 비밀번호 인증, 다중 주체(merge_token), 폐기. AS 쪽 계약은 AS 계약
providerIssuer(id, { principal? })그 제공자의 갱신·RP 로그아웃(end_session)

authorizationServer 옵션:

항목기본값뜻
id"as"세션 쿠키에 적는 발급자 ID
issuer(필수)AS 의 issuer 식별자. 메타데이터를 읽지 않고 아래 엔드포인트를 직접 씁니다
tokenEndpoint(필수)토큰 엔드포인트
revocationEndpoint없음리프레시 토큰 폐기. 없으면 로그아웃이 폐기를 건너뜁니다
verificationEndpoint없음인증 코드 발송(이메일·SMS·메신저·TOTP). 없으면 인증 코드 라우트가 꺼집니다
passwordEndpoint없음비밀번호 확인. 없으면 비밀번호 로그인 라우트가 꺼집니다
clientId(필수)
clientSecret없음없으면 퍼블릭 클라이언트: 토큰 엔드포인트(token_endpoint_auth_method=none)에 client_id 만 보내고, 발송·비밀번호 엔드포인트에는 본문의 client_id 로 자기를 밝힙니다. 있으면 clientAuth 방식으로 자기를 밝힙니다. AS 가 그 클라이언트에 허용한 그랜트만 쓸 수 있습니다
clientAuthbasic시크릿이 있을 때의 방식. basic 은 Authorization: Basic(RFC 6749 §2.3.1), post 는 본문의 client_id·client_secret
verificationGrantType없음인증 코드를 토큰으로 바꾸는 확장 그랜트 이름(AS 가 정한 값). verificationEndpoint 나 passwordEndpoint 를 쓰면 필수이고, 없으면 시작할 때 멈춥니다
clientIpHeader없음사용자 IP 를 AS 에 넘기는 헤더 이름(발송 횟수 제한용). 적으면 인증 코드 완료 그랜트도 같은 헤더로 보냅니다. 적지 않으면 IP 를 보내지 않습니다
principal표준 클레임 + principal_type 클레임을 주체 종류로클레임 → 화면 요약

제공자

정해진 IdP 는 @plott/webkit-auth-provider-* 패키지를 씁니다. 목록에 없는 IdP 는 직접 정의합니다.

ts
import { oauth2Provider, oidcProvider } from "@plott/webkit-auth-oauth";
declare const clientId: string;declare const clientSecret: string;
export const line = oidcProvider({ id: "line", issuer: "https://access.line.me", clientId, clientSecret, scope: "openid profile" });
OAuthProvider 항목뜻
id제공자 ID. 목록 안에서 겹치면 만들 때 오류
kindoidc(디스커버리 + id_token 검증) 또는 oauth2(엔드포인트를 직접 적는다 — oauth2Provider 가 정합니다)
issueroidc 의 issuer. /.well-known/openid-configuration 을 읽습니다. oidc 에서 비면 만들 때 오류
authorizationEndpoint·tokenEndpoint·endSessionEndpointoauth2 의 엔드포인트. oauth2 에서 앞 둘이 비면 만들 때 오류
clientId · scope
clientSecret문자열 또는 함수(Apple 처럼 주기적으로 새로 만드는 값 — 호출마다 부르니 안에서 캐시합니다). 없으면 퍼블릭 클라이언트: 시크릿 없이 client_id + PKCE
clientAuthbasic(시크릿이 있을 때 기본) · post(Kakao·Meta·Apple) · none(퍼블릭 클라이언트 — 시크릿이 있어도 보내지 않습니다). 시크릿이 없으면 언제나 none
responseModeform_post 면 IdP 가 콜백을 크로스사이트 POST 로 보냅니다(Apple 이 name·email 을 요청하면 강제). 코어가 같은 사이트 POST 로 한 번 더 보내 쿠키가 실리게 합니다
extraAuthParams · popupAuthParams인가 요청에 더 붙일 파라미터(팝업일 때는 popupAuthParams 도). state·PKCE·nonce·redirect_uri 는 덮어쓰지 못합니다
exchangeToken웹 로그인 뒤 AS 로 교환할 때 내미는 토큰. 기본: oidc 면 id_token, oauth2 면 access_token
nativeToken네이티브 SDK 토큰을 /exchange 로 받는가(받는다면 기본 토큰 종류). false 면 받지 않습니다. 우리 AS 가 교환할 때만 알립니다
embeddedWebView앱 내 WebView 에서 웹 로그인 화면을 막는 IdP(Google·Meta)면 blocked. 기본 allowed
principal이 제공자가 세션 토큰의 발급자일 때(providerIssuer) 클레임 → 화면 요약

백엔드 옵션

항목기본값뜻
issuer(필수)authorizationServer(...) 또는 providerIssuer(...)
providers(필수)제공자 목록
timeoutSeconds10디스커버리를 포함한 모든 IdP·AS 호출의 제한 시간(초). proxy 가 기다리는 호출이라 짧게 둡니다
allowInsecureRequestsfalsehttp IdP/AS 허용. 로컬 목 서버 전용

받은 Bearer 토큰 검증(/bearer)

내 서버가 Authorization: Bearer <JWT> 를 받아 «이 토큰이 우리 발급자가 이 API 용으로 준 것인가»를 확인합니다. 서버 전용입니다.

ts
import { createBearerVerifier } from "@plott/webkit-auth-oauth/bearer";
const bearer = createBearerVerifier({  issuer: "https://sso.example.com/realms/<realm>",  authorizedParty: "my-api",});
export async function GET(req: Request) {  const who = await bearer.verify(req);  if (!who) return new Response(null, { status: 401 });  return Response.json({ sub: who.sub });}

verify 는 토큰 문자열·Authorization 헤더 값·Headers·headers 를 가진 요청을 받아 주체 또는 null 을 돌려줍니다. verifyOrThrow 는 거절 때 BearerError(코어 AuthError, 상태 401, 코드 invalid_token)를 던지며, error.reason 에 이유 코드(missing·malformed·algorithm·type·invalid_signature·expired·not_yet_valid·issuer·audience·authorized_party·scope·email_unverified·email_domain·claims·jwks_unavailable)가 있고 토큰은 어디에도 없습니다.

옵션기본값 / 뜻
issuer(필수) 토큰 iss 와 글자 그대로 같아야 합니다(끝 슬래시 포함). 디스커버리 주소의 바탕이기도 합니다
authorizedParty받을 azp(토큰을 받아 간 클라이언트). 문자열 또는 목록
audience받을 aud. 하나라도 들어 있으면 통과. 문자열 또는 목록
match둘 다 주면 both(둘 다 맞아야 통과) · authorizedParty 만 있으면 azp · 아니면 aud. azp·aud·either(둘 중 하나)는 직접 적을 때만 씁니다(azp 는 audience 를 보지 않습니다). match 에 맞는 값이 없으면 만들 때 오류
rejectIdTokenstrue. 페이로드 typ: "ID" 이거나 nonce·at_hash 가 있는 토큰(ID 토큰)을 거절
requireAccessTokenTypefalse. 켜면 헤더 typ 가 at+jwt(RFC 9068)인 토큰만 받습니다
failureCacheMs5000. 디스커버리·JWKS 가 실패하면 이 시간 동안은 다시 받지 않고 바로 jwks_unavailable 로 거절. 0 이면 끔
algorithms["RS256"]. none·HS* 는 넣을 수 없습니다
allowedTypes["JWT", "at+jwt"]. 헤더 typ 가 없거나 목록에 없으면 거절(대소문자·application/ 접두 무시)
clockToleranceSec30. exp·nbf 시계 오차(초)
jwksUri디스커버리(<issuer>/.well-known/openid-configuration)의 jwks_uri. 한 번 받아 기억합니다
jwksCooldownMs30000. 모르는 kid 때문에 JWKS 를 다시 받는 최소 간격. 키 교체 직후 새 키로 서명한 토큰은 마지막 재요청 뒤 이 시간이 지나기 전까지 모르는 kid 로 거절될 수 있습니다. 키를 자주 돌리는 발급자면 줄이고, 가짜 kid 로 JWKS 서버를 두들기는 것을 막으려면 늘립니다
timeoutMs5000. 디스커버리·JWKS 요청 제한 시간
requiredScopes없음. scope(공백 구분) 또는 scp 에 모두 있어야 함
requireEmailVerifiedfalse(allowedEmailDomains 가 있으면 true). 켜면 email_verified === true 필요. 도메인 목록과 false 를 같이 주면 만들 때 오류
allowedEmailDomains없음. 주면 email_verified === true 이고, 이메일이 있고 @ 가 하나이며 도메인이 목록에 있어야 합니다(대소문자 무시)
mapPrincipal(claims)기본은 { sub, email, emailVerified, name, scopes, claims }. null·예외는 거절. Keycloak 롤은 keycloakPrincipal(provider-keycloak)
jwks테스트용으로 키를 직접 넣습니다({ keys } 또는 jose 의 키 조회 함수)

기본으로 지키는 것

기본막는 사고
PKCE S256 은 언제나, nonce 는 oidc 만코드 가로채기, id_token 재생
제공자의 추가 인가 파라미터가 state·PKCE·nonce·redirect_uri 를 덮어쓰지 못함설정 한 줄로 PKCE 가 plain 으로 내려가거나 redirect_uri 가 바뀌는 것
콜백의 redirect_uri 는 들어온 요청 URL 이 아니라 시작할 때 만든 값프록시 뒤 내부 주소 때문에 코드 교환이 어긋나는 것
제공자 정의를 시작할 때 검사(겹치는 id, 빠진 issuer·엔드포인트, 발급자 제공자 없음)첫 로그인에서야 터지는 설정 오류
설정과 IdP 응답이 어긋나 교환할 토큰이 없으면 AS 를 부르지 않고 exchange_failedAS 에 문자열 "undefined" 를 보내는 것
주체(sub)를 읽지 못한 토큰으로는 세션을 만들지 않음(exchange_failed, 502). 갱신 응답에 주체가 안 보이면 같은 세션의 주체 요약을 잇고, 합치기 대상의 주체는 빌리지 않음모든 사용자가 한 주체로 섞이는 것
expires_in 이 없는 응답의 수명은 300초죽은 토큰을 오래 싣는 것
갱신 응답에 없는 refresh_token·id_token·scope 는 이전 값을 씀회전하지 않는 발급자에서 리프레시 토큰을 잃는 것(RFC 6749 §6)
갱신 실패 중 invalid_grant 만 AuthRequiredError(세션의 끝). 5xx·연결 실패·클라이언트 설정 오류는 일반 오류발급자 장애로 사용자가 로그아웃되는 것
폐기(revoke) 실패는 경고만 남기고 던지지 않음로그아웃이 폐기 실패로 막히는 것
merge_token 은 같은 발급자의 세션일 때만다른 발급자의 토큰을 AS 에 내미는 것
토큰 엔드포인트 오류 본문의 error_code 가 우리 코드(AuthErrorCode)일 때만 그 코드로 옮김. 발급자 5xx·연결 실패는 502, 그 밖은 400AS 의 임의 문자열이 브라우저 오류 코드가 되는 것
시크릿이 없으면 clientAuth: "post" 여도 none빈 client_secret 을 거절하는 IdP(Kakao)에서 로그인이 실패하는 것
발송·완료 요청에 사용자 IP 헤더(clientIpHeader)를 실음모든 사용자가 Next 서버 IP 하나로 보여 한 사람이 모두를 잠그거나 제한이 무의미해지는 것
연결 실패·시간 초과는 502 exchange_failed일시 장애가 입력 오류(400)로 보여 사용자가 입력을 의심하는 것
발송 응답은 계약 필드(verificationId·expiresIn·maskedTarget·resendAfter)만, 오류는 코드(소문자 snake_case 64자)만 브라우저로AS 의 설명·내부 정보가 새는 것
verificationId 없는 200 은 502프록시 오류 화면을 성공으로 보고 코드 입력 단계로 넘어가는 것
발송 엔드포인트의 401·403 은 502(exchange_failed)이 클라이언트의 설정 오류(invalid_client)를 «로그인 필요»로 읽는 것
비밀번호 확인 실패는 아이디/비밀번호 어느 쪽인지 구분하지 않음(invalid_credentials). 429 는 too_many_attempts. 5xx·invalid_client 는 502계정 탐색, 설정 오류가 «비밀번호가 틀렸다»로 보여 사용자가 계속 다시 치는 것
비밀번호 확인 응답은 계약의 두 모양(verification_id+code / verificationId+channel)만 인정모르는 응답을 성공으로 처리하는 것
모든 IdP·AS 호출에 제한 시간(timeoutSeconds, 기본 10초)을 걸고, 디스커버리 실패는 기억하지 않아 다음 로그인이 다시 시도proxy 가 30초 기본값을 기다리는 것, 한 번의 IdP 장애가 영구 실패가 되는 것
Bearer 검증: 모든 오류는 거절(null/BearerError)검증 도중 예외가 «통과»로 새는 것
Bearer 검증: authorizedParty/audience 없이는 만들 수 없음발급자만 검사해 같은 realm 의 다른 클라이언트 토큰이 모두 통과하는 것
Bearer 검증: RS256 고정, none·HS* 설정 거부알고리즘을 토큰이 정하게 두어 none 이나 «공개 키를 HMAC 비밀로 쓴» 위조 토큰이 통과하는 것
Bearer 검증: 헤더 typ 확인, ID 토큰 거절ID 토큰이나 다른 용도의 JWT 를 액세스 토큰으로 받는 것
Bearer 검증: 발급자 글자 그대로 비교, 디스커버리 문서의 issuer 도 대조비슷한 주소의 다른 발급자·바꿔치기된 디스커버리
Bearer 검증: exp·sub 필수, nbf 확인만료 없는 토큰·주체 없는 토큰
Bearer 검증: 모르는 kid 에만 JWKS 재요청, 쿨다운키 교체 때 전부 거절되는 것과 가짜 kid 로 JWKS 서버를 두들기는 것
Bearer 검증: 디스커버리·JWKS 실패는 failureCacheMs 동안만 기억한 번의 장애가 프로세스가 죽을 때까지 모든 요청을 막는 것, 장애 중 토큰마다 요청하는 것
Bearer 검증: 토큰·원인을 로그·메시지에 싣지 않음(console 미사용)로그로 토큰이 새는 것
Bearer 검증: 숫자 옵션(clockToleranceSec 등)은 만들 때 검사NaN 이 JWKS 장애로 오진되어 원인을 못 찾는 것
Bearer 검증: 헤더는 Bearer + 공백 하나 + 토큰만(스킴 없는 JWT 는 Authorization 에서 거절), 16KB 상한Basic·빈 토큰·공백 변형·거대 입력

그 밖

의존성: openid-client, jose. peer: @plott/webkit-auth. 각 항목의 동작은 test/ 의 양성·음성 테스트가 기록합니다.