Next.js (App Router) アプリケーションへ認証機能の追加
- サンプルプロジェクトは、私たちの SDK リポジトリ で利用可能です。
- この例は、Next.js の App Router に基づいています。
- チュートリアルビデオは、私たちの YouTube チャンネル で視聴できます。
前提条件
- Logto Cloud アカウント、または セルフホスト Logto。
- Logto のトラディショナルアプリケーションが作成済みであること。
インストール
お好みのパッケージマネージャーを使用して Logto SDK をインストールします:
- npm
- pnpm
- yarn
npm i @logto/nextpnpm add @logto/nextyarn add @logto/next統合
設定の準備
Logto クライアント用の設定を準備します:
import { LogtoNextConfig } from '@logto/next';
export const logtoConfig: LogtoNextConfig = {
appId: '<your-application-id>',
appSecret: '<your-app-secret-copied-from-console>',
endpoint: '<your-logto-endpoint>', // 例: http://localhost:3001
baseUrl: '<your-nextjs-app-base-url>', // 例: http://localhost:3000
cookieSecret: 'complex_password_at_least_32_characters_long',
cookieSecure: process.env.NODE_ENV === 'production',
};
注意:
cookieSecret に環境変数(例: process.env.LOGTO_COOKIE_SECRET)を使用する場合、その値が 32 文字以上であることを確認してください。この要件を満たさない場合、Logto はビルドまたは実行時に次のエラーをスローします:
TypeError: Either sessionWrapper or encryptionKey must be provided for CookieStorage
このエラーを防ぐには、環境変数が正しく設定されているか、または 32 文字以上のフォールバック値を指定してください。
リダイレクト URI の設定
詳細に入る前に、エンドユーザー体験の概要を簡単にご紹介します。サインインプロセスは次のようにシンプルにまとめられます:
- アプリがサインインメソッドを呼び出します。
- ユーザーは Logto のサインインページにリダイレクトされます。ネイティブアプリの場合は、システムブラウザが開かれます。
- ユーザーがサインインし、アプリ(リダイレクト URI として設定)に戻されます。
リダイレクトベースのサインインについて
- この認証 (Authentication) プロセスは OpenID Connect (OIDC) プロトコルに従い、Logto はユーザーのサインインを保護するために厳格なセキュリティ対策を講じています。
- 複数のアプリがある場合、同じアイデンティティプロバイダー (Logto) を使用できます。ユーザーがあるアプリにサインインすると、Logto は別のアプリにアクセスした際に自動的にサインインプロセスを完了します。
リダイレクトベースのサインインの理論と利点について詳しく知るには、Logto サインイン体験の説明を参照してください。
以下のコードスニペットでは、あなたのアプリが http://localhost:3000/ で実行されていると仮定しています。
リダイレクト URI を設定する
Logto Console のアプリケーション詳細ページに移動します。リダイレクト URI http://localhost:3000/callback を追加します。
サインインと同様に、ユーザーは共有セッションからサインアウトするために Logto にリダイレクトされるべきです。完了したら、ユーザーをあなたのウェブサイトに戻すと良いでしょう。例えば、http://localhost:3000/ をサインアウト後のリダイレクト URI セクションとして追加します。
その後、「保存」をクリックして変更を保存します。
コールバックの処理
ユーザーがサインインした後、Logto は上記で設定したリダイレクト URI にユーザーをリダイレクトします。ただし、アプリケーションが正しく動作するためには、さらに処理が必要です。
サインインコールバックを処理するためのヘルパー関数 handleSignIn を提供しています:
import { handleSignIn } from '@logto/next/server-actions';
import { redirect } from 'next/navigation';
import { NextRequest } from 'next/server';
import { logtoConfig } from '../logto';
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
await handleSignIn(logtoConfig, searchParams);
redirect('/');
}
サインイン・サインアウトの実装
サインイン・サインアウトボタンの実装
Next.js App Router では、イベントはクライアントコンポーネントで処理されるため、まず SignIn と SignOut の 2 つのコンポーネントを作成する必要があります。
'use client';
type Props = {
onSignIn: () => Promise<void>;
};
const SignIn = ({ onSignIn }: Props) => {
return (
<button
onClick={() => {
onSignIn();
}}
>
サインイン
</button>
);
};
export default SignIn;
'use client';
type Props = {
onSignOut: () => Promise<void>;
};
const SignOut = ({ onSignOut }: Props) => {
return (
<button
onClick={() => {
onSignOut();
}}
>
サインアウト
</button>
);
};
export default SignOut;
これらのコンポーネントがクライアントコンポーネントであることを示すため、ファイルの先頭に 'use client' を追加するのを忘れないでください。
ホームページにボタンを追加
クライアントコンポーネント内でインラインの "use server" 注釈付きサーバーアクションを定義することはできません。サーバーコンポーネントから props を通じて渡す必要があります。
次に、ホームページにサインイン・サインアウトボタンを追加します。必要に応じて SDK のサーバーアクションを呼び出す必要があります。これを助けるために、getLogtoContext を使って認証 (Authentication) 状態を取得します。
import { getLogtoContext, signIn, signOut } from '@logto/next/server-actions';
import SignIn from './sign-in';
import SignOut from './sign-out';
import { logtoConfig } from './logto';
export default async function Home() {
const { isAuthenticated, claims } = await getLogtoContext(logtoConfig);
return (
<nav>
{isAuthenticated ? (
<p>
こんにちは、{claims?.sub} さん、
<SignOut
onSignOut={async () => {
'use server';
await signOut(logtoConfig);
}}
/>
</p>
) : (
<p>
<SignIn
onSignIn={async () => {
'use server';
await signIn(logtoConfig);
}}
/>
</p>
)}
</nav>
);
}
チェックポイント: アプリケーションをテストする
これで、アプリケーションをテストできます:
- アプリケーションを実行すると、サインインボタンが表示されます。
- サインインボタンをクリックすると、SDK がサインインプロセスを初期化し、Logto のサインインページにリダイレクトされます。
- サインインすると、アプリケーションに戻り、サインアウトボタンが表示されます。
- サインアウトボタンをクリックして、トークンストレージをクリアし、サインアウトします。
ユーザー情報の取得
ユーザー情報を表示する
ユーザーがサインインすると、getLogtoContext() の戻り値はユーザー情報を含むオブジェクトになります。この情報をアプリで表示できます:
import { getLogtoContext } from '@logto/next/server-actions';
import { logtoConfig } from './logto';
export default async function Home() {
const { claims } = await getLogtoContext(logtoConfig);
return (
<main>
{claims && (
<div>
<h2>クレーム (Claims):</h2>
<table>
<thead>
<tr>
<th>名前</th>
<th>値</th>
</tr>
</thead>
<tbody>
{Object.entries(claims).map(([key, value]) => (
<tr key={key}>
<td>{key}</td>
<td>{String(value)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</main>
);
}
API ルートハンドラーでユーザー情報を取得する
API ルートハンドラーでもユーザー情報を取得できます:
import { getLogtoContext } from '@logto/next/server-actions';
import { logtoConfig } from '../../logto';
export const dynamic = 'force-dynamic';
export async function GET() {
const { claims } = await getLogtoContext(logtoConfig);
return Response.json({ claims });
}
追加のクレーム (Claims) をリクエストする
getLogtoContext から返されるオブジェクトに一部のユーザー情報が欠けていることがあります。これは、OAuth
2.0 と OpenID Connect (OIDC) が最小特権の原則 (PoLP) に従うように設計されており、Logto
はこれらの標準に基づいて構築されているためです。
デフォルトでは、限られたクレーム (Claims) が返されます。より多くの情報が必要な場合は、追加のスコープ (Scopes) をリクエストして、より多くのクレーム (Claims) にアクセスできます。
「クレーム (Claim)」はサブジェクトについての主張であり、「スコープ (Scope)」はクレーム (Claims) のグループです。現在のケースでは、クレーム (Claim) はユーザーに関する情報の一部です。
スコープ (Scope) とクレーム (Claim) の関係の非規範的な例を示します:
「sub」クレーム (Claim) は「サブジェクト (Subject)」を意味し、ユーザーの一意の識別子(つまり、ユーザー ID)です。
Logto SDK は常に 3 つのスコープ (Scopes) をリクエストします:openid、profile、および offline_access。
追加のスコープをリクエストするには、Logto クライアントを初期化する際にパラメータを設定できます:
import { UserScope, LogtoNextConfig } from '@logto/next';
export const logtoConfig: LogtoNextConfig = {
scopes: [UserScope.Email, UserScope.Phone], // 必要に応じてスコープを追加
// ...他の設定
});
その後、コンテキストレスポンスで追加のクレーム (Claims) にアクセスできます:
export default async function Home() {
const { claims: { email } = {}, } = await getLogtoContext(logtoConfig);
return (
<div>
{email && <p>Email: {email}</p>}
</div>
);
};
export default Home;
ネットワークリクエストが必要なクレーム (Claims)
ID トークンの肥大化を防ぐために、一部のクレーム (Claims) は取得するためにネットワークリクエストが必要です。例えば、custom_data クレームはスコープで要求されてもユーザーオブジェクトに含まれません。これらのクレームにアクセスするには、 fetchUserInfo オプションを設定できます:
export default async function Home() {
const { userInfo } = await getLogtoContext(logtoConfig, { fetchUserInfo: true });
return (
<div>
{userInfo && <p>Email: {userInfo.email}</p>}
</div>
);
};
export default Home;
fetchUserInfo を設定することで、SDK はユーザーがサインインした後に userinfo エンドポイント にリクエストを送信してユーザー情報を取得し、リクエストが完了すると userInfo が利用可能になります。
スコープとクレーム (Claims)
Logto は OIDC の スコープ (Scope) とクレーム (Claim) の規約 を使用して、ID トークンおよび OIDC userinfo エンドポイント からユーザー情報を取得するためのスコープ (Scope) とクレーム (Claim) を定義しています。「スコープ (Scope)」と「クレーム (Claim)」は、OAuth 2.0 および OpenID Connect (OIDC) 仕様の用語です。
標準の OIDC クレーム (Claim) については、ID トークンへの含有はリクエストされたスコープ (Scope) によって厳密に決定されます。拡張クレーム (Claim)(例:custom_data や organizations)は、カスタム ID トークン 設定を通じて ID トークンに追加で表示するように構成できます。
こちらはサポートされているスコープと対応するクレーム (Claims) の一覧です:
標準 OIDC スコープ
openid(デフォルト)
| Claim name | Type | 説明 |
|---|---|---|
| sub | string | ユーザーの一意の識別子 |
profile(デフォルト)
| Claim name | Type | 説明 |
|---|---|---|
| name | string | ユーザーのフルネーム |
| username | string | ユーザー名 |
| picture | string | エンドユーザーのプロフィール画像の URL。この URL は画像ファイル(例:PNG、JPEG、GIF 画像ファイル)を指す必要があり、画像を含む Web ページではありません。この URL は、エンドユーザーを説明する際に表示するのに適したプロフィール写真を特に参照するべきであり、エンドユーザーが撮影した任意の写真ではありません。 |
| created_at | number | エンドユーザーが作成された時刻。Unix エポック(1970-01-01T00:00:00Z)からのミリ秒数で表されます。 |
| updated_at | number | エンドユーザー情報が最後に更新された時刻。Unix エポック(1970-01-01T00:00:00Z)からのミリ秒数で表されます。 |
その他の 標準クレーム (Standard Claims) には、family_name、given_name、middle_name、nickname、preferred_username、profile、website、gender、birthdate、zoneinfo、locale などがあり、これらも profile スコープに含まれます(userinfo エンドポイントをリクエストする必要はありません)。上記のクレームとの違いは、これらのクレームは値が空でない場合のみ返される点です。一方、上記のクレームは値が空の場合 null が返されます。
標準クレーム (Standard Claims) とは異なり、created_at および updated_at クレームは秒ではなくミリ秒を使用しています。
email
| Claim name | Type | 説明 |
|---|---|---|
string | ユーザーのメールアドレス | |
| email_verified | boolean | メールアドレスが認証済みかどうか |
phone
| Claim name | Type | 説明 |
|---|---|---|
| phone_number | string | ユーザーの電話番号 |
| phone_number_verified | boolean | 電話番号が認証済みかどうか |
address
アドレスクレームの詳細については OpenID Connect Core 1.0 を参照してください。
(デフォルト) と記載されたスコープは常に Logto SDK によってリクエストされます。標準 OIDC スコープ下のクレーム (Claims) は、対応するスコープがリクエストされた場合、常に ID トークン (ID token) に含まれます — 無効化できません。
拡張スコープ
以下のスコープは Logto によって拡張されており、userinfo エンドポイント を通じてクレーム (Claims) を返します。これらのクレームは Console > Custom JWT を通じて ID トークン (ID token) に直接含めるよう設定することもできます。詳細は カスタム ID トークン を参照してください。
custom_data
| Claim name | Type | 説明 | デフォルトで ID トークンに含まれるか |
|---|---|---|---|
| custom_data | object | ユーザーのカスタムデータ |
identities
| Claim name | Type | 説明 | デフォルトで ID トークンに含まれるか |
|---|---|---|---|
| identities | object | ユーザーのリンク済みアイデンティティ | |
| sso_identities | array | ユーザーのリンク済み SSO アイデンティティ |
roles
| Claim name | Type | 説明 | デフォルトで ID トークンに含まれるか |
|---|---|---|---|
| roles | string[] | ユーザーのロール | ✅ |
urn:logto:scope:organizations
| Claim name | Type | 説明 | デフォルトで ID トークンに含まれるか |
|---|---|---|---|
| organizations | string[] | ユーザーが所属する組織 ID | ✅ |
| organization_data | object[] | ユーザーが所属する組織データ |
これらの組織クレーム (Organization Claims) は、不透明トークン (Opaque token) を使用している場合でも userinfo エンドポイント経由で取得できます。ただし、不透明トークン (Opaque token) は組織トークン (Organization token) として組織固有リソースへのアクセスには使用できません。詳細は 不透明トークン (Opaque token) と組織 (Organizations) を参照してください。
urn:logto:scope:organization_roles
| Claim name | Type | 説明 | デフォルトで ID トークンに含まれるか |
|---|---|---|---|
| organization_roles | string[] | ユーザーが所属する組織ロール(<organization_id>:<role_name> 形式) | ✅ |
API リソース
まず 🔐 ロールベースのアクセス制御 (RBAC) を読むことをお勧めします。これにより、Logto の RBAC の基本概念と API リソースを適切に設定する方法を理解できます。
Logto クライアントの設定
API リソースを設定したら、アプリで Logto を設定する際にそれらを追加できます:
export const logtoConfig = {
// ...other configs
resources: ['https://shopping.your-app.com/api', 'https://store.your-app.com/api'], // API リソースを追加
};
各 API リソースには独自の権限 (スコープ) があります。
例えば、https://shopping.your-app.com/api リソースには shopping:read と shopping:write の権限があり、https://store.your-app.com/api リソースには store:read と store:write の権限があります。
これらの権限を要求するには、アプリで Logto を設定する際にそれらを追加できます:
export const logtoConfig = {
// ...other configs
scopes: ['shopping:read', 'shopping:write', 'store:read', 'store:write'],
resources: ['https://shopping.your-app.com/api', 'https://store.your-app.com/api'],
};
スコープが API リソースとは別に定義されていることに気付くかもしれません。これは、OAuth 2.0 のリソースインジケーター が、リクエストの最終的なスコープはすべてのターゲットサービスでのすべてのスコープの直積になると指定しているためです。
したがって、上記のケースでは、Logto での定義からスコープを簡略化できます。両方の API リソースは、プレフィックスなしで read と write スコープを持つことができます。その後、Logto の設定では:
export const logtoConfig = {
// ...other configs
scopes: ['read', 'write'],
resources: ['https://shopping.your-app.com/api', 'https://store.your-app.com/api'],
};
各 API リソースは、read と write の両方のスコープを要求します。
API リソースで定義されていないスコープを要求しても問題ありません。例えば、API リソースに email スコープが利用できなくても、email スコープを要求できます。利用できないスコープは安全に無視されます。
サインインが成功すると、Logto はユーザーのロールに応じて適切なスコープを API リソースに発行します。
API リソース用のアクセス トークンの取得
特定の API リソースのアクセス トークンを取得するには、getAccessToken メソッドを使用できます:
クライアントコンポーネント内でインラインの "use server" 注釈付きサーバーアクションを定義することはできません。サーバーコンポーネントから props を通じて渡す必要があります。
import { getAccessToken } from '@logto/next/server-actions';
import { logtoConfig } from './logto';
import GetAccessToken from './get-access-token';
export default async function Home() {
return (
<main>
<GetAccessToken
onGetAccessToken={async () => {
'use server';
return getAccessToken(logtoConfig, 'https://shopping.your-app.com/api');
}}
/>
</main>
);
}
'use client';
type Props = {
onGetAccessToken: () => Promise<string>;
};
const GetAccessToken = ({ onGetAccessToken }: Props) => {
return (
<button
onClick={async () => {
const token = await onGetAccessToken();
console.log(token);
}}
>
アクセス トークンを取得 (コンソールログを参照)
</button>
);
};
export default GetAccessToken;
このメソッドは、ユーザーが関連する権限を持っている場合に API リソースにアクセスするために使用できる JWT アクセス トークンを返します。現在キャッシュされているアクセス トークンが期限切れの場合、このメソッドは自動的にリフレッシュ トークンを使用して新しいアクセス トークンを取得しようとします。
サーバーコンポーネントでアクセス トークンを取得する必要がある場合は、getAccessTokenRSC 関数を利用できます:
import { getAccessTokenRSC } from '@logto/next/server-actions';
import { logtoConfig } from './logto';
export default async function Home() {
const accessToken = await getAccessTokenRSC(logtoConfig, 'https://shopping.your-app.com/api');
return (
<main>
<p>Access token: {accessToken}</p>
</main>
);
}
React Server Components では Cookie の書き込みができません(Next.js の制限)。getAccessTokenRSC はリフレッシュ トークンを使って期限切れトークンを更新できますが、新しいアクセス トークンはセッションクッキーに保存されません。つまり、キャッシュされたトークンが期限切れの場合、すべての RSC リクエストでトークンのリフレッシュが発生する可能性があります。
解決策:
- Server Actions でクライアントコンポーネントを利用する
クライアントコンポーネントから Server Actions 経由でgetAccessTokenを呼び出し、Cookie を更新します。 - 外部セッションストレージを利用する
Redis / KV ストレージでsessionWrapperを設定します。Cookie には固定セッション ID のみを保存し、トークンデータは外部ストレージに保存することで、RSC でもリフレッシュ済みトークンを永続化できます。詳細は 外部セッションストレージの利用 を参照してください。
組織トークンの取得
組織 (Organization) が初めての場合は、🏢 組織 (マルチテナンシー) を読んで始めてください。
Logto クライアントを設定する際に、UserScope.Organizations スコープを追加する必要があります:
import { UserScope } from '@logto/next';
export const logtoConfig = {
// ...other configs
scopes: [UserScope.Organizations],
};
ユーザーがサインインしたら、ユーザーのための組織トークンを取得できます:
クライアントコンポーネント内でインラインの "use server" 注釈付きサーバーアクションを定義することはできません。サーバーコンポーネントから props を通じて渡す必要があります。
import { getOrganizationToken } from '@logto/next/server-actions';
import { logtoConfig } from './logto';
import GetOrganizationToken from './get-organization-token';
export default async function Home() {
return (
<main>
<GetOrganizationToken
onGetOrganizationToken={async () => {
'use server';
return getOrganizationToken(logtoConfig, 'organization-id');
}}
/>
</main>
);
}
'use client';
type Props = {
onGetOrganizationToken: () => Promise<string>;
};
const GetOrganizationToken = ({ onGetOrganizationToken }: Props) => {
return (
<button
onClick={async () => {
const token = await onGetOrganizationToken();
console.log(token);
}}
>
組織トークンを取得 (コンソールログを参照)
</button>
);
};
export default GetOrganizationToken;
サーバーコンポーネントで組織トークンを取得する必要がある場合は、getOrganizationTokenRSC 関数を利用できます:
import { getOrganizationTokenRSC } from '@logto/next/server-actions';
import { logtoConfig } from './logto';
export default async function Home() {
const token = await getOrganizationTokenRSC(logtoConfig, 'organization-id');
return (
<main>
<p>Organization token: {token}</p>
</main>
);
}
getAccessTokenRSC と同様に、リフレッシュされた組織トークンは RSC には永続化されません。上記の解決策 を参照してください。
外部セッションストレージの利用
SDK はデフォルトで、暗号化されたセッションデータを Cookie に保存します。この方法は安全で、追加のインフラストラクチャを必要とせず、特に Vercel のようなサーバーレス環境で効果的に機能します。
ただし、セッションデータを外部に保存する必要がある場合もあります。たとえば、セッションデータが Cookie に収まりきらないほど大きくなった場合や、複数の組織セッションを同時に維持する必要がある場合などです。このようなケースでは、sessionWrapper オプションを使用して外部セッションストレージを実装できます:
import { MemorySessionWrapper } from './storage';
export const config = {
// ...
sessionWrapper: new MemorySessionWrapper(),
};
import { randomUUID } from 'node:crypto';
import { type SessionWrapper, type SessionData } from '@logto/next';
export class MemorySessionWrapper implements SessionWrapper {
private readonly storage = new Map<string, unknown>();
private currentSessionId?: string;
async wrap(data: unknown, _key: string): Promise<string> {
// 既存のセッション ID があれば再利用し、初回ユーザーのみ新規生成します。
// これは Cookie を更新できない環境(例:React Server Components)で重要です。
// Cookie 内のセッション ID は安定している必要があり、外部ストレージ内のデータのみ更新できます。
const sessionId = this.currentSessionId ?? randomUUID();
this.currentSessionId = sessionId;
this.storage.set(sessionId, data);
return sessionId;
}
async unwrap(value: string, _key: string): Promise<SessionData> {
if (!value) {
return {};
}
// wrap() で再利用できるようにセッション ID を保存します
this.currentSessionId = value;
const data = this.storage.get(value);
return data ?? {};
}
}
上記の実装はシンプルなインメモリストレージを使用しています。本番環境では、Redis やデータベースなど、より永続的なストレージソリューションの利用を検討してください。
未認証時の自動サインインリダイレクト
signIn ヘルパーはサインインセッションを確立するために Cookie を変更します。そのため、React Server Component (RSC) から直接呼び出すことはできません。RSC で未認証ユーザーを検出した際に自動でサインインを促すには、専用のルートハンドラーで signIn を呼び出し、そのルートへリダイレクトしてください。
import { signIn } from '@logto/next/server-actions';
import { logtoConfig } from '../../logto';
export async function GET() {
await signIn(logtoConfig);
}
import { getLogtoContext } from '@logto/next/server-actions';
import { redirect } from 'next/navigation';
import { logtoConfig } from '../logto';
export default async function ProtectedPage() {
const { isAuthenticated } = await getLogtoContext(logtoConfig);
if (!isAuthenticated) {
redirect('/sign-in');
}
return <div>Protected content</div>;
}