JavaScript 브라우저 SDK
@keenauth/js는 KeenAuth의 브라우저 핵심 패키지입니다. 단일 페이지 앱에서 사용자의 로그인을 처리하고, 토큰을 보관하며, API를 호출합니다. 런타임 종속성이 없으며, React 컴포넌트는 이 패키지를 기반으로 합니다. 이 패키지는 아직 npm에 게시되지 않았습니다.
리디렉션으로 로그인
SDK는 표준 OpenID Connect 공개 클라이언트입니다:
- PKCE(S256)와 DPoP 바인딩 토큰을 사용합니다.
- 콜백에서
state,nonce,iss를 확인합니다. - 환경의 키를 기준으로 ID 토큰의 서명과 클레임을 검증합니다.
import { KeenAuth } from "@keenauth/js";
const auth = new KeenAuth({
issuer: "https://acme.eu.keenauth.app", // your environment's issuer, exactly
clientId: "app_01j9zzzzzzzzzzzzzzzzzzzz",
redirectUri: "https://app.acme.example/callback",
postLogoutRedirectUri: "https://app.acme.example/",
});
// On your sign-in button:
await auth.signInRedirect({ appState: "/orders" });
// On https://app.acme.example/callback:
const { appState } = await auth.handleRedirectCallback();
handleRedirectCallback은 응답을 한 번만 허용합니다. 다음의 경우 KeenAuthError와 함께 실패합니다:
state또는iss가 일치하지 않는 경우;- 진행 중인 로그인이 10분보다 오래된 경우;
- ID 토큰이 유효하지 않은 경우.
서버가 되돌려 보낸 오류는 OAuth error 코드를 담은 KeenAuthOAuthError로 발생합니다.
API 호출하기
API용 액세스 토큰을 받으려면 API 식별자를 resource(RFC 8707)로 전달합니다. 이 값은 인가 요청, 코드 교환, 모든 갱신 요청에 전송되며, 토큰의 aud는 이 식별자가 됩니다:
const auth = new KeenAuth({
issuer: "https://acme.eu.keenauth.app",
clientId: "app_01j9zzzzzzzzzzzzzzzzzzzz",
redirectUri: "https://app.acme.example/callback",
resource: "https://api.acme.example", // your API's identifier
scope: "openid profile email offline_access messages:read",
});
- 값은 프래그먼트(
#)가 없는 절대 URI여야 합니다. 입력한 그대로 전송되므로, 등록된 API 식별자를 그대로 사용하십시오. 잘못된 값은 생성자에서 코드config_invalid의KeenAuthError를 던집니다. signInRedirect({ resource })는 한 번의 로그인에 대해 이 값을 재정의합니다.resource를 지정하지 않으면, 애플리케이션에 기본 API가 설정되어 있을 경우 서버가 그 API를 사용합니다. 그렇지 않으면 API 범위는 부여되지 않습니다.- 이 옵션은 배열도 받을 수 있으며, RFC 8707이 허용하는 대로 반복된
resource매개변수로 전송됩니다. KeenAuth 액세스 토큰은 정확히 하나의 audience를 가지므로, 서버는 둘 이상의 값을invalid_target(KeenAuthOAuthError)으로 거부합니다.
const res = await auth.authorizedFetch("https://api.acme.example/v1/orders");
// Or take the token yourself; it is refreshed when it is about to expire.
const token = await auth.getAccessToken();
authorizedFetch는 토큰에 바인딩된 새 증명과 함께 Authorization: DPoP를 전송합니다. 서버가 DPoP nonce를 요구하면, 그 nonce로 한 번 재시도합니다. 동시 호출은 하나의 갱신 요청을 공유하며, 모든 갱신은 갱신 토큰을 교체합니다. API에서 이 토큰을 검증하는 방법은 API에서 토큰 검증하기를 참조하십시오.
로그아웃
await auth.signOut();
signOut은 토큰을 삭제하고 갱신 토큰을 취소합니다. 그런 다음 브라우저를 환경의 로그아웃 엔드포인트로 보내며, 이 엔드포인트는 브라우저를 postLogoutRedirectUri로 되돌립니다.
토큰 보관 위치
- 액세스, 갱신, ID 토큰은 메모리에만 보관되며, localStorage, sessionStorage, IndexedDB, 쿠키 또는 URL에는 저장되지 않습니다. 페이지를 새로 고치면 리디렉션을 통해 사용자가 다시 로그인하며, SDK는 세션을 복원하지 않습니다.
- sessionStorage에는 진행 중인 로그인의 일회성 값만 보관됩니다.
- DPoP 키는 추출할 수 없는 WebCrypto 키이며, 기본적으로 메모리에 보관됩니다.
내장 로그인
같은 객체는 내장 컴포넌트에 프런트 API 클라이언트도 제공합니다:
- 로그인 단계용
transactions; - 계정의 프로필과
user_metadata, 이메일(코드 인증 포함), 패스키, 세션용me; - 패스키 절차용
webauthn.
이 호출은 환경의 쿠키와 CSRF 헤더를 사용합니다. 서버는 환경 자체의 로그인 출처에서 온 요청만 허용합니다.
리디렉션 없이 로그인
로그인 출처 밖의 페이지에서는 clientId로 트랜잭션을 시작합니다. 그러면 SDK는 쿠키 대신 자체 DPoP 키에 트랜잭션을 바인딩합니다:
let tx = await auth.transactions.create({ client_id: "app_01j9...", organization: "acme" });
tx = await auth.transactions.step("identifier", { identifier: "ada@acme.example" });
tx = await auth.transactions.step("password", { password });
if (tx.state === "completed") await auth.complete(tx); // { kind: "signed_in" }
- 애플리케이션은 자사(first-party) 애플리케이션이어야 하며, 페이지 출처는 그 애플리케이션의 허용된 웹 출처 중 하나여야 합니다.
- 모든 호출에는 DPoP 증명이 포함되며 쿠키는 포함되지 않습니다. 서버가 DPoP nonce를 요구하면, SDK는 그 nonce로 한 번 재시도합니다.
- SDK는 PKCE verifier,
state,nonce를 직접 생성하여 메모리에 보관합니다.scope와resource는 옵션에서 가져오며,create에organization과uiLocales를 전달할 수 있습니다. 여기서는resource값을 하나만 사용하십시오. complete는 응답을 확인하고, 같은 DPoP 키로 인가 코드를 교환하며, 토큰을 메모리에 보관합니다. 페이지는 이동하지 않으며, 코드는 사용자의 코드에 반환되지 않습니다.client_id포함 여부와 관계없이 모든step호출은 브라우저의 시간대를Idp-Zoneinfo헤더로 전송합니다. 단계가 로그인을 완료했고 사용자가 시간대를 선택하지 않은 경우, 호스티드 로그인 페이지와 마찬가지로 KeenAuth가 이를 프로필의zoneinfo에 저장합니다. 브라우저가 사용 가능한 시간대를 보고하지 않으면 헤더는 생략되고 단계는 그대로 유지됩니다.- 로그인에 사용자가 직접 확인해야 하는 페이지(동의, 법적 약관, 조직 선택)가 필요하면, 마지막 단계는 코드가
authorization_failed인KeenAuthProblemError로 실패합니다. 이런 사용자에게는signInRedirect를 사용하십시오.
자체 출처에서 계정 호출
사용자가 로그인하고 SDK가 갱신 토큰을 보유하면(기본 범위는 offline_access를 요청합니다), me 및 organizations 호출은 더 이상 로그인 호스트의 세션 쿠키를 필요로 하지 않습니다:
- SDK는
resource=<issuer>/fapi로 갱신하여 프런트 API용 단기 액세스 토큰을 받습니다. 이 토큰은 DPoP에 바인딩되며, 다른 토큰과 마찬가지로 메모리에만 보관됩니다. - 각 계정 호출은
Authorization: DPoP <token>과 해당 호출을 위한 DPoP 증명을 전송하며, 쿠키는 포함되지 않습니다. 서버가 DPoP nonce를 요구하면 SDK는 그 nonce로 한 번 재시도합니다. 토큰이 더 이상 유효하지 않으면 새 토큰을 받고 한 번 재시도합니다. - 갱신 토큰은 갱신할 때마다 교체되므로, SDK는 갱신을 하나씩 순서대로 실행합니다.
- 이 방식은 허용된 웹 출처에 페이지의 출처가 포함된 자사 애플리케이션에서만 동작합니다.
- 토큰이 없으면 SDK는 이전과 같이 세션 쿠키를 사용합니다.
서버 규칙은 프런트 API 액세스 토큰을 참조하십시오.
조직 전환
organizations.switch(orgId)는 페이지 이동 없이 로그인한 사용자를 다른 조직으로 옮깁니다. organizations.listMine()에서 얻은 조직의 org_… ID를 전달하십시오:
const { data } = await auth.organizations.listMine();
await auth.organizations.switch(data[1].id);
- 위의 계정 호출과 같은 조건이 필요합니다: 갱신 토큰,
redirectUri, 그리고 허용된 웹 출처에 페이지의 출처가 포함된 자사 애플리케이션. - SDK는 프런트 API 토큰과 함께 전환 요청을 보내고, 같은 키의 증명으로 코드를 교환하며,
org_id가 해당 조직인 새 토큰을 보관합니다. 이 모든 과정이 성공할 때까지 기존 토큰은 유지됩니다. - 조직이 사용자가 직접 확인해야 하는 페이지(MFA, SSO, 동의)를 요구하면, 이 호출은 코드가
authorization_failed인KeenAuthProblemError로 실패합니다. 이런 사용자에게는signInRedirect({ organization })를 사용하십시오.
현재 제한 사항
- 프로필 쓰기(
me.update,me.updateMetadata)는 마지막 프로필 읽기 또는 쓰기의ETag를 담은If-Match를 보냅니다. 그 사이에 프로필이 다른 곳에서 변경되었다면, 쓰기는 코드가precondition_failed인KeenAuthProblemError로 실패합니다. 다시 시도하기 전에me.get()을 다시 호출하십시오. - 전화번호는 아직 사용할 수 없습니다:
me.phones는undefined입니다.