本文へ移動
cccskills
無料GitHub で公開日本語紹介

passkeys

パスキーの登録・ログイン・アカウントへの追加を、Auth0のサーバーAPIとブラウザーのWebAuthn処理を組み合わせて実装します。

原文原文の説明を見る

Passkey (WebAuthn) flows with @auth0/nextjs-auth0 v4: signup, sign-in, and My Account enrollment using the server-side auth0.passkey.* API inside App Router route handlers or server actions, with navigator.credentials and serializeCredential in the browser.

auth0/nextjs-auth0スター 2,312(GitHub 2,312 + cccskills 0)

実行スコア 未検証

インストール方法を見る

こんなときに便利

  • パスキー登録を実装したいとき
  • パスキーでログインさせたいとき
  • 既存アカウントに認証方法を追加したいとき
  • 登録時の確認コードを扱いたいとき

日本語での紹介

できること

@auth0/nextjs-auth0 v4のサーバー側auth0.passkey.* APIを使った、パスキーの登録、サインイン、ログイン中のアカウントへの追加を説明します。サーバーでチャレンジを作成し、ブラウザーでnavigator.credentialsによるWebAuthn処理を行い、serializeCredentialで結果を送れる形にしてから、サーバーでセッションを作る流れを扱います。メールや電話番号の確認コード、MFAが必要になった場合の処理上の要点も含みます。

こんなときに便利

Next.jsアプリにパスキー認証を加えたい開発者や、登録とログイン、既存アカウントへの追加を実装したい場合に向いています。認証処理をサーバー側のルートハンドラーやServer Actionで組み立てる構成が対象です。

使い方の例

  • 「パスキー登録をサーバーAPIとブラウザーに分けて実装して」
  • 「パスキーのサインイン処理で必要な手順を教えて」
  • 「ログイン中の利用者がパスキーを追加する方法を示して」

注意点

@auth0/nextjs-auth0 4.22.0以上が必要です。Auth0のカスタムドメインとConfidential Clientが必要で、アカウントへの追加にはMRRTポリシーも必要です。識別子確認を伴う登録はEarly Accessで、Auth0サポートによる有効化が必要です。

この紹介文は、公開されている SKILL.md をもとに AI(OpenAI Codex)が作成しました。正確な仕様は下の原文を確認してください。

含まれるファイル(1)

  • SKILL.md11.5 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

@auth0/nextjs-auth0 — Passkeys (v4)

Minimum version: 4.22.0 (passkey signup/login + My Account enrollment).

Covers the three passkey flows using the server-side auth0.passkey.* API inside your own route handlers / server actions. The token exchange and client secret stay on the server; only the WebAuthn call runs in the browser.

For tenant setup (custom domain, passkey grant, connection auth method) see the Auth0 skill hub (feature:passkeys).

Two layers — use the server API here

This SDK ships two ways to build passkeys:

  • Server methods (auth0.passkey.*) — you own the route handlers / server actions. Use when the task asks you to drive the ceremony yourself. This is what this skill covers.
  • Client one-call wrappers (passkey.signup() / passkey.login() from @auth0/nextjs-auth0/client) — shortcut that runs the whole flow in one call. Convenient, but gives you no control over individual steps.

The ceremony shape (all three flows)

Every flow follows: server challenge → browser WebAuthn → serialize → server exchange.

Signup

// server (route handler or server action)
import { auth0 } from "@/lib/auth0";

const { authSession, authnParamsPublicKey } = await auth0.passkey.register({
  email,
  name,
  // also accepts: username, phoneNumber, givenName, familyName, nickname,
  //               picture, userMetadata, connection, organization
}); // → PasskeyRegisterResponse
// Send authSession + authnParamsPublicKey to the browser
"use client";
import { serializeCredential } from "@auth0/nextjs-auth0/client";

// authnParamsPublicKey has base64url binary fields — decode to ArrayBuffers first.
// The SDK does NOT export a decoder, so write a small helper:
function b64urlToBuf(s: string): ArrayBuffer {
  const b = atob(s.replace(/-/g, '+').replace(/_/g, '/').padEnd(
    s.length + (4 - (s.length % 4)) % 4, '='));
  return Uint8Array.from(b, c => c.charCodeAt(0)).buffer;
}

const publicKey: PublicKeyCredentialCreationOptions = {
  ...authnParamsPublicKey,
  challenge: b64urlToBuf(authnParamsPublicKey.challenge),
  user: { ...authnParamsPublicKey.user, id: b64urlToBuf(authnParamsPublicKey.user.id) },
  excludeCredentials: authnParamsPublicKey.excludeCredentials?.map(c => ({
    ...c, id: b64urlToBuf(c.id),
  })),
};
const credential = await navigator.credentials.create({ publicKey });
if (!credential) throw new Error("No passkey was created"); // can be null
const authResponse = serializeCredential(credential as PublicKeyCredential);
// POST { authSession, authResponse } back to a server action / route
// server: exchange credential for a session
await auth0.passkey.getToken({ authSession, authResponse }); // → Promise<void>

Signup with identifier verification (OTP)

Note: Identifier verification on passkey signup is in Early Access. Please contact Auth0 Support to get it enabled for your tenant.

If the connection requires email or phone verification, register() returns verificationRequired (e.g. ["email"]). Collect the one-time codes before navigator.credentials.create, then pass them to getToken as verification:

The OTP screen and the passkey prompt run in the browser between the two server calls, so use two Server Actions called from a "use client" component. The browser holds authSession and the serialized authResponse between calls.

"use server";
// Both actions return plain objects: error properties thrown from a Server Action do not reach the browser.
// Action 1: challenge = { authSession, authnParamsPublicKey, verificationRequired? }
export async function startSignup(email: string, phoneNumber?: string) {
  try {
    // deliveryMethod ("text" | "voice") is the phone code channel: send it only with phoneNumber
    const challenge = await auth0.passkey.register({ email, ...(phoneNumber && { phoneNumber, deliveryMethod: "text" as const }) });
    return { ok: true as const, challenge };
  } catch (err) {
    if (err instanceof PasskeyRegisterError) return { ok: false as const, error: err.error }; // e.g. too_many_requests
    throw err;
  }
}

// Action 2
export async function completeSignup(authSession: string, authResponse: PasskeyAuthResponse, verification?: PasskeyVerificationCodes) {
  try {
    await auth0.passkey.getToken({ authSession, authResponse, verification });
    return { ok: true as const };
  } catch (err) {
    if (err instanceof MfaRequiredError) {
      // codes accepted, MFA needed. mfa_token is SDK-encrypted, safe to return; the browser
      // continues with the client `mfa` helper (getAuthenticators/challenge/verify), which creates the session
      return { ok: false as const, error: "mfa_required", mfaToken: err.mfa_token };
    }
    if (err instanceof PasskeyGetTokenError) {
      return { ok: false as const, error: err.error, retryable: err.isRetryable, authSession: err.authSession, verificationRequired: err.verificationRequired };
    }
    throw err;
  }
}

Client component order: startSignup, then show OTP inputs for verificationRequired, then navigator.credentials.create once, then completeSignup(authSession, authResponse, codes) with one code per identifier in verificationRequired (e.g. { email, phone }). If retryable, re-prompt for verificationRequired and call completeSignup again with the returned authSession (falls back to the original) + the SAME authResponse, sending codes only for identifiers still listed (no second biometric prompt). Otherwise a new code will not help: on too_many_requests wait and resubmit, on a terminal session restart from startSignup. On mfa_required, run mfa.getAuthenticators/challenge/verify({ mfaToken }) in the browser.

With the client wrapper, pass onVerificationRequired: async ({ verificationRequired, isRetry }) => codes to passkey.signup(), returning one code per listed identifier. It runs before the passkey prompt and again on a wrong code.

Sign-in

Same shape — replace register with challenge, and navigator.credentials.create with .get (decode challenge + each allowCredentials[].id):

// server
const { authSession, authnParamsPublicKey } = await auth0.passkey.challenge(); // → PasskeyChallengeResponse
// browser: decode + navigator.credentials.get + serializeCredential → authResponse
const publicKey: PublicKeyCredentialRequestOptions = {
  ...authnParamsPublicKey,
  challenge: b64urlToBuf(authnParamsPublicKey.challenge),
  allowCredentials: authnParamsPublicKey.allowCredentials?.map(c => ({
    ...c, id: b64urlToBuf(c.id),
  })),
};
const credential = await navigator.credentials.get({ publicKey });
if (!credential) throw new Error("No passkey was selected"); // can be null
const authResponse = serializeCredential(credential as PublicKeyCredential);
// POST { authSession, authResponse } back to server
// server
await auth0.passkey.getToken({ authSession, authResponse });

Enrollment (signed-in user — adds a passkey to the current account)

Uses the My Account API internally. Requires an MRRT policy on the tenant so the SDK can mint the create:me:authentication_methods token from the session refresh token.

// server
const { authenticationMethodId, authSession, authnParamsPublicKey } =
  await auth0.passkey.enrollmentChallenge(); // → PasskeyEnrollmentChallengeResponse
// Send authenticationMethodId + authSession + authnParamsPublicKey to the browser
// browser: same decode + navigator.credentials.create + null check + serializeCredential as signup
const authResponse = serializeCredential(credential as PublicKeyCredential);
// POST { authenticationMethodId, authSession, authResponse } back to server
// server
await auth0.passkey.enrollmentVerify({ authenticationMethodId, authSession, authResponse });
// → PasskeyEnrollmentVerifyResponse: the registered auth method { id, type: "passkey", ... }

API reference

MethodSignatureReturns
auth0.passkey.register(options?)PasskeyRegisterOptions? (incl. deliveryMethod?: "text" | "voice", phone code channel, send only with phoneNumber)PasskeyRegisterResponse: { authSession, authnParamsPublicKey, verificationRequired? }
auth0.passkey.challenge(options?)PasskeyChallengeOptions?PasskeyChallengeResponse — { authSession, authnParamsPublicKey }
auth0.passkey.getToken(options){ authSession, authResponse, connection?, organization?, scope?, audience?, verification? }Promise<void>: sets session
auth0.passkey.enrollmentChallenge(options?){ connection?, userIdentityId? }?PasskeyEnrollmentChallengeResponse — { authenticationMethodId, authSession, authnParamsPublicKey }
auth0.passkey.enrollmentVerify(options){ authenticationMethodId, authSession, authResponse }PasskeyEnrollmentVerifyResponse
serializeCredential(credential)PublicKeyCredentialPasskeyAuthResponse — JSON-safe, base64url-encoded

Pages Router / Middleware overloads: register(req, options?), challenge(req, options?), enrollmentChallenge(req, options?), enrollmentVerify(req, options) take req: NextRequest; getToken(req, res, options) needs both NextRequest and NextResponse (wrong arity throws TypeError).

Key gotchas

  • getToken credential field is authResponse, not credential.
  • No public decoder for authnParamsPublicKey — the SDK doesn't export one; write the tiny base64url helper shown above. Do not use @simplewebauthn — it double-encodes and breaks the token exchange.
  • Confidential client required — the server token exchange authenticates with the client secret.
  • Custom domain required: passkeys will not work on the default *.auth0.com domain. The custom domain becomes the passkey rpId, and the app must be served from that domain or a subdomain of it (also in local dev; plain localhost fails the WebAuthn ceremony).
  • Enrollment requires MRRT — without the Multi-Resource Refresh Token policy, enrollmentChallenge cannot mint the My Account access token.
  • Never spread a request body into register() / getToken() from a Server Action or route: the browser could set connection, organization, userMetadata, audience or scope. Pass only validated fields.
  • verification keys are email / phone, not phoneNumber. Other keys or non-string codes throw InvalidRequestError before any request.
  • Only invalid_grant with authSession is retryable (err.isRetryable, also is_retryable in the /auth/passkey/get-token error body). invalid_request (missing code) is NOT retryable: send a code for every identifier. Never branch on status or error_description. On retry reuse the same authResponse; never re-run navigator.credentials.create.
  • getToken can throw mfa_required: continue with the client mfa helper (mfa/SKILL.md) using the mfa_token. The user has no session yet, so mfa.verify() (route /auth/mfa/verify) creates it; a step-up MFA page or server auth0.mfa.verify() fails with no session.
  • Do not call the server register/challenge methods from a client component — they are server-only.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

コメント

ログインなしで書き込めます。使ってみた感想、質問、動かなかった環境などを気軽にどうぞ。

まだコメントはありません。最初のコメントを書いてみませんか。

送信するとすぐに公開されます。メールアドレスや電話番号などの個人情報は書かないでください。URL は 2 つまでです。

0 / 1000 文字

このフォームは reCAPTCHA で保護されており、Google のプライバシーポリシーと利用規約が適用されます。

同じリポジトリのスキル

mfa

無料概要あり

Auth0とNext.jsで多要素認証を実装し、認証の追加要求や要素の登録・確認、リダイレクトやポップアップによる再認証を扱います。

  • 重要な操作の前に追加認証したいとき
  • MFAの登録や検証画面を作りたいとき
  • 認証不足時にログインへ誘導したいとき
auth0/nextjs-auth02,3122026年10月26日 更新

nextjs-auth0

無料概要あり

Next.jsアプリにAuth0認証を導入し、ログイン設定からセッション取得、ページやAPIの保護までをApp Router・Pages Router別に案内します。

  • Next.jsにAuth0ログインを導入したいとき
  • 認証ルートや環境変数を設定したいとき
  • ページやAPIをログイン必須にしたいとき
auth0/nextjs-auth02,3122026年10月26日 更新

organizations

無料概要あり

顧客企業ごとにログイン先やメンバー権限を分ける、Auth0 Organizationsを使ったB2B向けマルチテナント認証の実装を支援します。

  • 顧客企業ごとに認証を分けたいとき
  • 組織IDをログイン時に渡したいとき
  • 招待リンクから参加を受け付けたいとき
auth0/nextjs-auth02,3122026年10月26日 更新

auth0 のスキルをすべて見る

このスキルの問題を報告する