Next.js アプリにサインインを追加する

このクイックスタートでは、@keenauth/nextjs SDK を使って Next.js App Router アプリに KeenAuth のサインインを追加します。アプリは OAuth クライアントであり、すべてのトークンをサーバー側で保持します。ブラウザが受け取るのは、暗号化された HttpOnly のセッション Cookie だけです。サインインのルートを追加し、ページを保護し、ユーザーのアクセストークンで保護された API を呼び出し、アプリをオフラインでテストします。SDK がサポートするルーターは App Router のみです。SDK の詳細は Next.js SDK を参照してください。

このアプリには次のページとルートがあります:

Route What it does
/ 公開ホームページ: サインインのリンク、またはユーザー名とサインアウトボタン
/dashboard セッションが必要です。ユーザーと、API が返すメッセージを表示します
/api/messages セッションが必要です。ユーザーのアクセストークンで API を呼び出します
/auth/login, /auth/callback, /auth/logout SDK が処理します

始める前に

1. インストール

npm install @keenauth/nextjs next@16.4.2 react@19.3.1 react-dom@19.3.1

SDK はまだ公開されていません。公開されるまでは、この例では package.json 内の SDK のソースを参照します。

2. 設定

アプリの設定は環境変数からのみ読み込みます。Next.js が読み込む .env.local に記述し、そのファイルは決してコミットしないでください。

Variable Required Meaning
KEENAUTH_ISSUER yes 環境の発行者
KEENAUTH_CLIENT_ID yes Web アプリケーションの client_id
KEENAUTH_CLIENT_KEY one of the two private_key_jwt 用の秘密鍵 (JSON 形式の秘密 JWK)。推奨
KEENAUTH_CLIENT_SECRET one of the two クライアントシークレット
APP_BASE_URL yes アプリの公開 URL。SDK はここからリダイレクト URI とすべてのリダイレクトを組み立てます。http は localhost、127.0.0.1、[::1] の場合のみ使用できます
KEENAUTH_COOKIE_SECRET yes 32 文字以上。セッション Cookie を封印します。openssl rand -base64 32 で生成できます
API_URL yes アプリが呼び出す API。例: http://localhost:8080
KEENAUTH_API_AUDIENCE yes KeenAuth における、その API の識別子

loadConfig はこれらの値を読み込んで検証します。値が欠けている場合や、クライアント認証情報の両方または両方とも未設定の場合は処理を続行せず、そのエラーメッセージにシークレットが含まれることはありません。

export function loadConfig(env: Record<string, string | undefined>): AppConfig {
  const need = (name: string): string => {
    const value = env[name];
    if (!value) throw new Error(`${name} is required`);
    return value;
  };

3. SDK インスタンスを作成する

createAuth は SDK インスタンスを作成し、SDK は作成時にすべてのオプションを検証します。getApp は初回使用時にプロセスごとに 1 つのインスタンスを作成するため、next build にはシークレットが不要です。

/** Builds the SDK instance. The options are checked here, so a configuration error stops the app at start-up. */
export function createAuth(config: AppConfig, overrides: Partial<KeenAuthOptions> = {}): KeenAuth {
  return createKeenAuth({ ...config.auth, ...overrides });
}

let instance: { auth: KeenAuth; config: AppConfig } | undefined;

/** One instance per process, created on first use so that `next build` needs no secrets. */
export function getApp(): { auth: KeenAuth; config: AppConfig } {
  if (!instance) {
    const config = loadConfig(process.env);
    instance = { auth: createAuth(config), config };
  }
  return instance;
}

設定では resource に API の識別子を指定します (RFC 8707)。指定がないと、アクセストークンは API 向けのものにならず、API はそのトークンを拒否します。

4. サインインのルートを追加する

1 つのキャッチオールのルートハンドラーで /auth/login、/auth/callback、/auth/logout を処理します:

// Serves /auth/login, /auth/callback and /auth/logout.
export const GET = (request: Request) => getAuth().handlers.GET(request);
export const POST = (request: Request) => getAuth().handlers.POST(request);

サインインには PKCE 付きの認可コードフローを使用し、環境が対応している場合はプッシュ型認可リクエストを使用します。コールバックでは state、iss、ID トークンを検証します。アプリのパスではない returnTo は無視されます。

5. proxy でページを保護する

proxy.ts (Next.js 16 より前は middleware.ts) は、ページの描画前にセッションを更新し、選択したパスを保護します。セッションのないページリクエストは returnTo 付きで /auth/login にリダイレクトされ、/api/… へのリクエストには 401 が返されます。

export async function proxy(request: NextRequest) {
  const result = await getAuth().proxy(request, { protect: isProtected });
  if (result.response) return result.response;
  const response = NextResponse.next({ request: { headers: result.requestHeaders } });
  result.apply(response);
  return response;
}

セッションを必要とするパスは 1 つの関数にまとめられています:

export function isProtected(pathname: string): boolean {
  return pathname === "/dashboard" || pathname.startsWith("/dashboard/") || pathname.startsWith("/api/");
}

proxy のマッチャーはパスを見逃すことがあるため、ユーザーを必要とするすべてのページとルートハンドラーは、セッションを自分でも確認します。

6. API を呼び出す

Server Component では、Cookie ストアからセッションを読み取り、アクセストークンを取得します。トークンはサーバー側に留まり、ブラウザが受け取るのは HTML のみです。

  // The proxy has already sent users without a session to the sign-in; check again here, because a matcher can miss a path.
  const session = await auth.getSession(cookieStore);
  if (!session) return <a href="/auth/login?returnTo=/dashboard">Sign in</a>;

  // The access token stays on the server. The browser receives only the HTML below.
  const response = await fetchMessages(config.apiUrl, await auth.getAccessToken(cookieStore));

session.user には ID トークンのクレーム (subject、email、name など) が入ります。セッションにトークンが含まれることはありません。

実際の呼び出しは、Bearer トークン付きの単純な fetch です:

export async function fetchMessages(apiUrl: string, accessToken: string, fetchImpl: typeof fetch = fetch): Promise<Response> {
  return fetchImpl(new URL("/api/messages", apiUrl), {
    headers: { Authorization: `Bearer ${accessToken}` },
    cache: "no-store",
  });
}

ルートハンドラーは Cookie を設定できるため、有効期限が近いアクセストークンを更新する updateSession を使用します:

  // A route handler can set cookies, so updateSession also refreshes an access token that is about to expire.
  const { accessToken, apply } = await auth.updateSession(request);
  if (!accessToken) return new Response(null, { status: 401 });

アクセストークンを Client Component に渡さないでください。

7. サインアウトする

サインアウトは /auth/logout に POST するフォームです。SDK はセッション Cookie を削除し、リフレッシュトークンを失効させ、ブラウザを環境のログアウトエンドポイントに送ります。そこからアプリに戻されます。ブラウザが別のサイトからのリクエストとしてマークしたリクエストは拒否されます。

      <form action="/auth/logout" method="post">
        <button type="submit">Sign out</button>
      </form>

8. アプリを実行する

Express クイックスタートの API (またはトークンを受け入れる任意の API) を起動し、アプリを実行します:

npm run dev

http://localhost:3000 を開き、Sign in を選択して、KeenAuth でサインインし、ダッシュボードを開きます。

9. ネットワークなしでテストする

テストにはネットワークも KeenAuth アカウントも必要ありません。偽の fetch が認可サーバーの役割を担い (ディスカバリーとプッシュ型認可リクエスト)、Node が TypeScript のテストを直接実行します。

test("proxy: a page request without a session is redirected to sign-in with returnTo", async () => {
  const { auth } = app();
  const result = await auth.proxy(new Request(`${APP}/dashboard?tab=1`), { protect: isProtected });
  assert.equal(result.response?.status, 307);
npm test
npm run deps    # pinned versions, lockfile sources and licences

テストでは、設定が起動時に検証されること、セッションのないページがサインインにリダイレクトされ、API のパスが 401 を返すこと、サインインで PKCE S256 のプッシュ型リクエストが送られ封印された HttpOnly Cookie が設定されること、未知の state を持つコールバックが拒否されること、別のサイトからのサインアウトが拒否されること、そしてアクセストークンが Authorization ヘッダーで API に送られることを確認します。

実ブラウザでの完全なサインインには KeenAuth 環境が必要です。これはオフラインテストには含まれません。

プロキシまたはロードバランサーの背後で

APP_BASE_URL にアプリの公開 URL を設定してください。SDK はリダイレクトを組み立てるために Host や X-Forwarded-* を読み取ることは決してありません。複数のサーバーインスタンスがある場合、2 つのインスタンスが同時に同じリフレッシュトークンを更新することがあります。詳しくは Current limits を参照してください。

次のステップ