본문으로 건너뛰기
언어

@plott/webkit-auth-provider-keycloak0.4.0

@plott/webkit-auth-oauth 제공자 — Keycloak(OIDC SSO)과 realm 롤·그룹 클레임 매퍼

설치

pnpm
pnpm add @plott/webkit-auth-provider-keycloak@0.4.0

진입점(exports)

import타입파일
@plott/webkit-auth-provider-keycloak./dist/index.d.ts./dist/index.js
@plott/webkit-auth-provider-keycloak/browser./dist/browser/index.d.ts./dist/browser/index.js
@plott/webkit-auth-provider-keycloak/react./dist/react/index.d.ts./dist/react/index.js

@plott/webkit-auth-oauth 용 Keycloak(OIDC SSO) 정의와 클레임 매퍼입니다. 사내 SSO 처럼 Keycloak 토큰을 그대로 세션 토큰으로 쓸 때는 providerIssuer("keycloak") 와 함께 씁니다.

ts
import { oauth, providerIssuer } from "@plott/webkit-auth-oauth";import { keycloak } from "@plott/webkit-auth-provider-keycloak";
declare const clientId: string;declare const clientSecret: string;
export const backend = oauth({  issuer: providerIssuer("keycloak"),  providers: [keycloak({ issuer: "https://sso.example.com/realms/<realm>", clientId, clientSecret, kind: "staff" })],});
옵션기본값 / 뜻
issuerrealm issuer
clientSecret없으면 퍼블릭 클라이언트(Keycloak 클라이언트의 Client authentication 을 끈 경우 — PKCE 만으로 로그인)
kinduser. 세션 주체 종류(requireSession({ kind }) 로 거른다)
scopeopenid profile email
clientRolesfalse. 켜면 이 클라이언트의 롤(resource_access[clientId].roles)도 roles 에 더합니다
amrFallback[]. 토큰에 amr 이 없을 때 세션에 적을 값(예: ["sso"])
nativeTokenfalse. 네이티브 SDK 토큰을 받을 종류(id_token·access_token)
  • 회사 Keycloak 에서는 서비스마다 클라이언트를 따로 씁니다. clientId 는 서비스의 저장소 이름(조직 이름 제외)이고, 다른 서비스의 클라이언트를 빌려 쓰지 않습니다. 새 클라이언트는 인증 서버 담당자만 만듭니다. 리다이렉트 <origin>/auth/callback/keycloak 은 등록된 https://<host>/* 안에 이미 들어 있습니다.
  • 롤은 realm_access.roles, 그룹은 Keycloak 그룹 매퍼가 넣은 groups 클레임에서 읽습니다(keycloakClaims).
  • 로그아웃은 Keycloak 세션까지 끊습니다(RP-initiated logout, id_token_hint).
  • 그룹이 많으면 토큰이 커져 쿠키 조각 상한(약 11KB)을 넘을 수 있습니다. 그룹 매퍼 범위를 좁힙니다.

진입점

import어디서export
@plott/webkit-auth-provider-keycloak서버keycloak, keycloakClaims, keycloakPrincipal
@plott/webkit-auth-provider-keycloak/browser브라우저(서버에서 불러도 안전, 아무 일도 하지 않음)createKeycloakSpa, authorizationHeader, KeycloakConfigError
@plott/webkit-auth-provider-keycloak/react클라이언트 컴포넌트("use client")KeycloakProvider, useKeycloak, authorizationHeader

붙이기: 서버가 받은 토큰의 주체

ts
import { createBearerVerifier } from "@plott/webkit-auth-oauth/bearer";import { keycloakPrincipal } from "@plott/webkit-auth-provider-keycloak";
declare const issuer: string;declare const clientId: string;
export const bearer = createBearerVerifier({  issuer,  authorizedParty: clientId,  mapPrincipal: (claims) => keycloakPrincipal(claims, { clientId }),});

위 예의 authorizedParty: clientId 는 «내 서비스의 로그인으로 받은 토큰만» 받습니다. 다른 서비스가 사람의 토큰을 들고 내 서버를 부르게 하려면 audience 에 내 서비스를 가리키는 값을 적고(호출하는 쪽 클라이언트에 내 서비스의 클라이언트 스코프가 붙어 있어야 토큰의 aud 에 들어갑니다), 호출을 허용할 클라이언트를 가려야 하면 authorizedParty 에 목록을 줍니다. match 를 aud 로 두면 azp 는 보지 않습니다(옵션 표).

keycloakPrincipal 은 { sub, email, name, roles, realmRoles, clientRoles } 를 돌려줍니다.

roles 는 realm_access.roles 와 resource_access[clientId].roles 를 합친 것이고, 출처가 필요하면 realmRoles(realm 롤)·clientRoles(이 클라이언트 롤)를 따로 읽습니다. name 은 keycloakClaims 와 같이 name → preferred_username 순서입니다.

붙이기: 브라우저 SPA

keycloak-js(^26.2.4)를 앱이 설치합니다. 서버 번들에는 들어가지 않습니다(동적 import).

lib/spa.ts 에서 모듈 하나가 한 번 만듭니다. init 은 처음 읽을 때 시작합니다.

ts
import { createKeycloakSpa } from "@plott/webkit-auth-provider-keycloak/browser";
export const spa = createKeycloakSpa({ url: process.env.NEXT_PUBLIC_KC_URL!, realm: "r", clientId: "web" });

"use client" 파일(app/providers.tsx)에서 Provider 를 두르고, 컴포넌트에서 훅을 씁니다. KeycloakProvider 는 init 이 끝날 때까지 fallback 을 그리고, init 이 실패하면 error(노드 또는 ({ error, retry }) => 노드)를 그립니다. error 를 주지 않으면 fallback 이 남습니다.

tsx
import type { ReactNode } from "react";import { KeycloakProvider, useKeycloak } from "@plott/webkit-auth-provider-keycloak/react";import { createKeycloakSpa } from "@plott/webkit-auth-provider-keycloak/browser";
declare const spa: ReturnType<typeof createKeycloakSpa>;declare function Loading(): ReactNode;
export function Providers({ children }: { children: ReactNode }) {  return (    <KeycloakProvider spa={spa} fallback={<Loading />} error={({ retry }) => <button onClick={retry}>다시 시도</button>}>      {children}    </KeycloakProvider>  );}
export function Me() {  const { ready, authenticated, claims, error, retry, token, login, logout } = useKeycloak();  return <pre>{JSON.stringify({ ready, authenticated, claims, error, retry, token, login, logout })}</pre>;}

useKeycloak 은 KeycloakProvider 안에서만 쓸 수 있고, 밖에서 부르면 오류를 던집니다.

API 호출에는 토큰을 헤더로 붙입니다. authorizationHeader(spa) 는 필요하면 갱신한 Bearer <토큰> 이고, 로그인 전이면 undefined 입니다.

ts
import { authorizationHeader, type createKeycloakSpa } from "@plott/webkit-auth-provider-keycloak/browser";
declare const spa: ReturnType<typeof createKeycloakSpa>;
const auth = await authorizationHeader(spa);await fetch("/api/me", { headers: auth ? { authorization: auth } : {} });
옵션기본값 / 뜻
url · realm · clientId(필수) 비었거나 문자열 "undefined" 면 KeycloakConfigError
onLoadlogin-required. 조용히 확인만 하려면 check-sso(+ silentCheckSsoRedirectUri)
refreshSkewSec60. 만료 몇 초 전부터 새로 받나
refreshIntervalMs30000
refreshOnFocustrue. 창이 포커스를 되찾을 때 확인
onRefreshFaillogin(refresh token 이 정말 끝났을 때만 한 번 로그인 화면으로) · ignore · 함수. 오프라인이거나 refresh token 이 아직 유효하면 이동하지 않고 1초부터 30초까지 늘려 가며 다시 시도하고, online·화면 복귀에서도 바로 시도합니다
initOptionsinit 에 더 넘길 옵션(기본값을 덮어씁니다)
keycloak인스턴스 공급 함수(테스트용)

spa.onChange(cb) 는 로그인 상태나 토큰이 바뀔 때만 부릅니다. 토큰을 서버로 보내는 일이 있다면 이 콜백에서 하세요(갱신 주기마다 보내지 마세요).

기본으로 지키는 것

기본막는 사고
서버(window 없음)에서는 아무것도 하지 않는 핸들SSR 에서 keycloak-js 가 window 를 건드려 죽는 것
같은 (url, realm, clientId) 는 페이지에서 init 한 번, 읽을 때까지 시작하지 않음StrictMode·재마운트에서 init 이 두 번 불려 keycloak-js 가 예외를 던지는 것
설정이 비면 KeycloakConfigError환경 변수가 빠졌는데 로그인 화면으로 계속 되돌아가는 무한 리다이렉트
checkLoginIframe: false, pkceMethod: "S256"모바일 Chrome 에서 로그인 확인 iframe 이 멈추거나 반복되는 것
화면이 숨겨지면 갱신 타이머 정지, 다시 보일 때 한 번 갱신백그라운드 탭의 불필요한 갱신, 복귀 직후 만료된 토큰
갱신 동시 호출을 하나로 합침토큰 요청이 겹쳐 서버에 중복 요청이 가는 것
갱신 실패 시 로그인은 한 번만, 세션이 정말 끝났을 때만실패한 갱신이 타이머마다 로그인 이동을 반복하는 것, 와이파이 순단·절전 복귀에 입력 중이던 화면을 잃는 것
init 실패는 state.error + retry() (한 번에 하나)로드 시 네트워크 장애로 실패한 init 이 영원히 캐시되어 새로고침 말고는 복구되지 않는 것
같은 (url, realm, clientId) 에 다른 옵션을 주면 개발에서 던지고 운영에서 경고두 번째 createKeycloakSpa 의 옵션이 조용히 무시되는 것
모든 핸들을 놓아도 init 이 진행 중이면 등록을 유지인증 code 가 붙은 페이지에서 destroy → 재생성으로 init 이 두 번 돌아 code 를 이중 교환하는 것
토큰이 바뀐 때만 onChange토큰을 주기마다 서버로 보내던 것

그 밖

  • keycloak() 은 id: "keycloak", 스코프 openid profile email, embeddedWebView: "allowed", nativeToken: false 인 OIDC 제공자를 만듭니다. clientRoles: true 면 이 클라이언트의 클라이언트 롤(resource_access.<clientId>.roles)도 roles 에 더하고, 기본은 렐름 롤만입니다. id·extraAuthParams 도 받습니다.
  • resource_access 에서는 클라이언트 ID 와 같은 자기 키만 읽습니다(__proto__·constructor 같은 상속 키는 믿지 않습니다).
  • amr 클레임이 없으면 ["sso"] 입니다.
  • KeycloakSpa 의 ready(init 결과 Promise), token(), claims(), login(), logout(), retry(), getState(), destroy() 도 쓸 수 있습니다. destroy() 는 같은 설정을 쓰는 핸들이 모두 놓였을 때 타이머·이벤트 등록을 풉니다.
  • 서버(window 없음)에서 만든 핸들의 ready 는 false 로 바로 끝납니다. init 이 실패하면 retry() 전까지 ready 가 같은 거절을 돌려줍니다.

peer: @plott/webkit-auth, @plott/webkit-auth-oauth. 선택 peer: keycloak-js(/browser·/react), react(/react).