Quickstart: Web BFF#
この Quickstart では web_bff Client を Management API に登録し、Hosted Login の Email OTP で認証する Node BFF を作る。例では Production 形式の HTTPS Canonical Issuer を使う。
0. 前提#
- Dashboard (
https://dashboard.zeroword.smartcrab.ai) にサインインし、Workspace、Project、Test Environment を用意する。Workspace ID と Project ID は Dashboard で確認できる。 - Dashboard の Workspace メニュー「API キー」(
/w/<workspaceId>/api-keys) で、clients:writeとenvironments:readを持つ Management API key を発行する。secret は作成直後の画面にだけ表示される。同じ Workspace に固定された既存 key にapi_keys:writeがあれば、POST /v1/api-keysから同じ scope を持つ key も発行できる。API key は Management API を参照する。 - Environment ID は Dashboard で確認するか、
environments:readscope を持つ key でGET /v1/environments?project_id=<projectId>を呼び、対象 Environment のidを取得する。GET /v1/environments/{environmentId}のissuerをAUTH_ISSUERに使う。
1. Client を登録する#
Management API key には clients:write scope が必要である。AUTH_PLATFORM_API_KEY に key を設定し、<workspaceId> と <environmentId> を置き換えて実行する。
この例の http://localhost callback は開発用の例外である。localhost 以外の Web callback は HTTPS を登録する。
curl -sS -X POST 'https://api.zeroword.smartcrab.ai/v1/clients' \
-H "Authorization: Bearer ${AUTH_PLATFORM_API_KEY}" \
-H 'Idempotency-Key: quickstart-web-bff-create-01' \
-H 'Content-Type: application/json' \
--data '{
"environment_id": "<environmentId>",
"client_type": "web_bff",
"name": "My app Web BFF",
"redirect_uris": ["http://localhost:4100/auth/callback"],
"allowed_scopes": ["openid", "profile", "email", "offline_access"],
"token_endpoint_auth_method": "client_secret_basic"
}'
作成 response の id と client_secret を保存する。client_secret は作成 response に一度だけ含まれる。Management API では token_endpoint_auth_method が必須で、省略時の既定値はない。web_bff では client_secret_basic を指定する。認証 endpoint はこの方式と PKCE S256 を使う。redirect_uris は BFF の AUTH_REDIRECT_URI と byte-for-byte で一致させる。
同じ Idempotency-Key は同じ論理操作の再送にだけ使う。同じ key を異なる request body で使うと 409 idempotency_key_conflict になり、method や path/query が異なる操作にも別の key を発行する。
2. BFF を作る#
次の環境変数を設定する。AUTH_ISSUER は GET /v1/environments/{environmentId} の issuer field にある Canonical Issuer であり、Interaction Domain ではない。
export AUTH_ISSUER='https://id.zeroword.smartcrab.ai/e/<environmentId>'
export AUTH_CLIENT_ID='<clientId>'
export AUTH_CLIENT_SECRET='<clientSecret>'
export AUTH_REDIRECT_URI='http://localhost:4100/auth/callback'
export AUTH_COOKIE_SECRET="$(openssl rand -hex 32)"
server.ts は Node の標準 HTTP server と @auth-platform/server だけを使う。GET /me から未認証なら Hosted Login へ送り、callback 後に session user を JSON response と server log に出力する。
import { createServer } from "node:http";
import type { IncomingMessage, ServerResponse } from "node:http";
import { createServerAuth } from "@auth-platform/server";
const auth = createServerAuth({
issuer: process.env.AUTH_ISSUER!,
clientId: process.env.AUTH_CLIENT_ID!,
clientSecret: process.env.AUTH_CLIENT_SECRET!,
redirectUri: process.env.AUTH_REDIRECT_URI!,
cookieSecret: process.env.AUTH_COOKIE_SECRET!,
postLoginRedirect: "/me",
});
const authHandlers: Record<string, (request: Request) => Promise<Response>> = {
"/auth/login": auth.loginHandler,
"/auth/callback": auth.callbackHandler,
"/auth/logout": auth.logoutHandler,
};
const dispatch = async (incoming: IncomingMessage, outgoing: ServerResponse): Promise<void> => {
const requestHeaders = new Headers();
for (const [name, value] of Object.entries(incoming.headers)) {
if (value !== undefined) requestHeaders.set(name, Array.isArray(value) ? value.join(", ") : value);
}
const host = incoming.headers.host ?? "localhost:4100";
const request = new Request(new URL(incoming.url ?? "/", `http://${host}`), {
method: incoming.method ?? "GET",
headers: requestHeaders,
});
const path = new URL(request.url).pathname;
let response: Response;
const authHandler = authHandlers[path];
if (authHandler !== undefined) {
response = await authHandler(request);
} else if (path === "/me") {
const outcome = await auth.getSession(request);
if (outcome.status === "authenticated") {
console.log("session user:", outcome.session.user);
response = Response.json({ user: outcome.session.user });
if (outcome.setCookie !== undefined) response.headers.set("set-cookie", outcome.setCookie);
} else {
response = Response.redirect(new URL("/auth/login", request.url), 302);
}
} else {
response = new Response("Not found", { status: 404 });
}
outgoing.statusCode = response.status;
response.headers.forEach((value, name) => {
if (name.toLowerCase() !== "set-cookie") outgoing.setHeader(name, value);
});
const cookies = response.headers.getSetCookie();
if (cookies.length > 0) outgoing.setHeader("set-cookie", cookies);
outgoing.end(Buffer.from(await response.arrayBuffer()));
};
createServer((request, response) => {
void dispatch(request, response);
}).listen(4100, "localhost", () => console.log("BFF listening on http://localhost:4100"));
Node 22 以降の TypeScript strip-types 対応 runtime では次のように起動できる。
node --experimental-strip-types server.ts
3. Hosted Login で Email OTP を試す#
ブラウザーで http://localhost:4100/me を開く。BFF は AUTH_ISSUER の authorize endpoint へ redirect し、Hosted Login に進む。Email OTP を選んでメールアドレスと受信した 6 桁 code を入力すると、callback が code を token と交換し、__Host-auth-session cookie を設定する。最後に /me の JSON と BFF の標準出力に次のような user が現れる。
{
"user": {
"sub": "usr_<user-id>",
"email": "user@example.com",
"emailVerified": true
}
}
BFF session cookie は Secure、HttpOnly、SameSite=Lax、Path=/ で、Domain 属性を付けない。Client secret と cookie secret をブラウザーへ渡さない。