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 は応答を 1 回だけ受け付けます。次の場合には KeenAuthError で失敗します。
stateまたはissが一致しない場合。- 保留中のサインインが 10 分より古い場合。
- ID トークンが有効でない場合。
サーバーから返されたエラーは、OAuth の error コードを保持する KeenAuthOAuthError として送出されます。
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 })は、そのサインイン 1 回に限り値を上書きします。resourceを指定しない場合、アプリケーションに既定の API が設定されていればサーバーはそれを使用します。設定されていない場合、API のスコープは付与されません。- このオプションは配列も受け付けます。RFC 8707 が許容するとおり、繰り返しの
resourceパラメータとして送信されます。KeenAuth のアクセストークンは正確に 1 つのオーディエンスを持つため、サーバーは 2 つ以上の値を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 のノンスを要求した場合、そのノンスを付けて 1 回だけ再試行します。並行する呼び出しは 1 回のリフレッシュリクエストを共有し、リフレッシュのたびにリフレッシュトークンはローテーションされます。API でこれらのトークンを検証する方法は、API でのトークン検証を参照してください。
サインアウト
await auth.signOut();
signOut はトークンを破棄し、リフレッシュトークンを取り消します。その後、ブラウザを環境のログアウトエンドポイントへ遷移させ、postLogoutRedirectUri に戻します。
トークンの保管場所
- アクセス、リフレッシュ、ID の各トークンはメモリ上にのみ保持され、localStorage、sessionStorage、IndexedDB、Cookie、URL には保存されません。ページを再読み込みすると、ユーザーはリダイレクトによって再度サインインします。SDK はセッションを復元しません。
- sessionStorage には、進行中のサインインのワンタイム値のみを保持します。
- DPoP 鍵は抽出不可の WebCrypto 鍵であり、既定ではメモリ上に保持されます。
埋め込みサインイン
同じオブジェクトから、埋め込みコンポーネント向けのフロント API クライアントも利用できます。
- サインインの各ステップ用の
transactions。 - アカウントのプロフィールと
user_metadata、メール (コード認証を含む)、パスキー、セッション用のme。 - パスキーのセレモニー用の
webauthn。
これらの呼び出しは、環境の Cookie と CSRF ヘッダーを使用します。サーバーは、環境自身のログインオリジンからの呼び出しのみを受け付けます。
リダイレクトなしのサインイン
ログインオリジン以外のページでは、clientId を指定してトランザクションを開始します。すると SDK は、Cookie の代わりにそのトランザクションを 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 アプリケーションである必要があり、ページのオリジンはその許可済み Web オリジンのいずれかである必要があります。
- すべての呼び出しは DPoP 証明を伴い、Cookie は使用しません。サーバーが DPoP のノンスを要求した場合、SDK はそのノンスを付けて 1 回だけ再試行します。
- SDK は PKCE の verifier、
state、nonceを自身で生成し、メモリに保持します。scopeとresourceはオプションから取得されます。createにはorganizationとuiLocalesを渡せます。ここではresourceの値は 1 つだけ使用してください。 completeは応答を検証し、同じ DPoP 鍵で認可コードを交換し、トークンをメモリに保持します。ページは遷移せず、コードがアプリケーションのコードに返されることはありません。- すべての
step呼び出しは、client_idの有無にかかわらず、ブラウザのタイムゾーンをIdp-Zoneinfoヘッダーで送信します。ステップでサインインが完了し、ユーザーがタイムゾーンを選択していない場合、KeenAuth はホステッドのサインインページと同様に、それをプロフィールのzoneinfoに保存します。ブラウザが利用可能なタイムゾーンを報告しない場合、ヘッダーは省略され、ステップの動作は変わりません。 - サインインに、ユーザーが確認しなければならないページ (同意、法的規約、組織の選択) が必要な場合、最後のステップは
authorization_failedのコードを持つKeenAuthProblemErrorで失敗します。そのユーザーにはsignInRedirectを使用してください。
自身のオリジンからのアカウント呼び出し
ユーザーがサインインし、SDK がリフレッシュトークンを保持している場合 (既定のスコープは offline_access を要求します)、me と organizations の呼び出しはサインインホストのセッション Cookie を必要としなくなります。
- SDK は
resource=<issuer>/fapiを指定してリフレッシュし、フロント API 用の短命なアクセストークンを取得します。このトークンは DPoP に紐づき、他のトークンと同様にメモリ上にのみ保持されます。 - 各アカウント呼び出しは、
Authorization: DPoP <token>と、その呼び出し用の DPoP 証明を Cookie なしで送信します。サーバーが DPoP のノンスを要求した場合、SDK はそのノンスを付けて 1 回だけ再試行します。トークンが無効になっている場合は、新しいトークンを取得して 1 回だけ再試行します。 - リフレッシュトークンはリフレッシュのたびにローテーションされるため、SDK はリフレッシュを 1 つずつ順番に実行します。
- これは、許可済み Web オリジンにページのオリジンを含む first-party アプリケーションでのみ機能します。
- トークンがない場合、SDK は従来どおりセッション Cookie を使用します。
サーバー側の規則については、フロント API アクセストークンを参照してください。
組織の切り替え
organizations.switch(orgId) は、ページを遷移させずにサインイン中のユーザーを別の組織へ移します。organizations.listMine() から取得した組織の org_… ID を渡してください。
const { data } = await auth.organizations.listMine();
await auth.organizations.switch(data[1].id);
- 上記のアカウント呼び出しと同じ条件が必要です。リフレッシュトークン、
redirectUri、そしてページのオリジンを許可済み Web オリジンに含む first-party アプリケーションです。 - 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です。