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

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

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

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

## 始める前に

- KeenAuth の環境。その発行者は `https://your-environment.eu.keenauth.app` のような形式です。
- 環境内の Web アプリケーション ([Create an application](/docs/api/management/applications/create))。クライアント認証として `private_key_jwt` またはクライアントシークレットを使用します。`development` 環境では、リダイレクト URI `http://localhost/auth/callback` とログアウト後のリダイレクト URI `http://localhost/` を登録してください。ループバック URI はポートなしで登録され、`3000` などの任意のポートに一致します。
- 環境に登録された API ([Create an API](/docs/api/management/apis/create))。スコープは `messages:read` です。`https://api.example.com` などの `identifier` が、そのアクセストークンのオーディエンスになります。[Express クイックスタート](/docs/quickstarts/express)では、これらのトークンを受け入れる API を構築します。
- Node.js 22.18 以降。

## 1. インストール

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

```ts
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` にはシークレットが不要です。

```ts
/** 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` を処理します:

```ts
// 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` が返されます。

```ts
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 つの関数にまとめられています:

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

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

## 6. API を呼び出す

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

```tsx
  // 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` です:

```ts
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` を使用します:

```ts
  // 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 を削除し、リフレッシュトークンを失効させ、ブラウザを環境のログアウトエンドポイントに送ります。そこからアプリに戻されます。ブラウザが別のサイトからのリクエストとしてマークしたリクエストは拒否されます。

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

## 8. アプリを実行する

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

```sh
npm run dev
```

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

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

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

```ts
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);
```

```sh
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](/docs/sdk/nextjs#現在の制限) を参照してください。

## 次のステップ

- [Next.js SDK](/docs/sdk/nextjs): すべてのオプション、セッション Cookie、現在の制限。
- [Express API を保護する](/docs/quickstarts/express): このアプリが呼び出す API。
- [トークンの検証](/docs/sdk/token-verification): API のミドルウェアの下で動作する検証機能。
