跳至主要內容

為你的 Auth.js (Next Auth) 應用程式新增驗證 (Authentication)

本指南將向你展示如何將 Logto 整合到你的 Next.js 應用程式中,使用 Auth.js,之前稱為 Next Auth。

提示:
  • 在本指南中,我們假設你已在 Next.js 專案中設定 Next Auth。如果尚未設定,請參閱 Next Auth 文件 以開始使用。

先決條件​

安裝​

透過你喜愛的套件管理器安裝 Auth.js:

npm i next-auth@beta

詳情請參閱 Auth.js 文件。

整合​

設定 Auth.js 提供者​

提示:

你可以在管理控制台的應用程式詳細資訊頁面找到並複製「App Secret」:

App Secret

修改你的 Auth.js API 路由配置,將 Logto 添加為 OIDC 提供者:

設定環境變數:

AUTH_LOGTO_ISSUER=https://xxxx.logto.app/oidc
AUTH_LOGTO_ID=your-logto-app-id
AUTH_LOGTO_SECRET=your-logto-app-secret
./app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/auth';
export const { GET, POST } = handlers;
./auth.ts
import NextAuth from 'next-auth';
import Logto from 'next-auth/providers/logto';

export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [Logto],
});

然後你也可以添加一個可選的 Middleware 來保持 session 活躍:

./middleware.ts
export { auth as middleware } from '@/auth';

你可以在 Auth.js 文件 中找到更多詳細資訊。

配置登入重定向 URI​

在深入細節之前,以下是終端使用者體驗的快速概覽。登入流程可簡化如下:

  1. 你的應用程式呼叫登入方法。
  2. 使用者被重定向至 Logto 登入頁面。對於原生應用程式,系統瀏覽器會被開啟。
  3. 使用者登入後被重定向回你的應用程式(配置為重定向 URI)。

關於基於重導的登入​

  1. 此驗證流程遵循 OpenID Connect (OIDC) 協議,Logto 強制執行嚴格的安全措施以保護使用者登入。
  2. 如果你有多個應用程式,可以使用相同的身分提供者 (IdP, Identity provider)(Logto)。一旦使用者登入其中一個應用程式,Logto 將在使用者訪問另一個應用程式時自動完成登入流程。

欲了解更多關於基於重導登入的原理和優勢,請參閱 Logto 登入體驗解析。


備註:

在以下的程式碼片段中,我們假設你的應用程式運行在 http://localhost:3000/。

讓我們切換到 Logto Console 的應用程式詳細資訊頁面。新增一個重定向 URI http://localhost:3000/api/auth/callback/logto,然後點擊「儲存變更」。

Logto Console 中的重定向 URI

實作登入與登出​

實作登入與登出按鈕​

app/components/sign-in.tsx
import { signIn } from '@/auth';

export default function SignIn() {
return (
<form
action={async () => {
'use server';
await signIn('logto');
}}
>
<button type="submit">Sign In</button>
</form>
);
}
app/components/sign-out.tsx
import { signOut } from '@/auth';

export function SignOut() {
return (
<form
action={async () => {
'use server';
await signOut();
}}
>
<button type="submit">Sign Out</button>
</form>
);
}

在頁面中顯示登入與登出按鈕​

app/page.tsx
import SignIn from './components/sign-in';
import SignOut from './components/sign-out';
import { auth } from '@/auth';

export default function Home() {
const session = await auth();

return <div>{session?.user ? <SignOut /> : <SignIn />}</div>;
}

以上是一個簡單的範例,你可以查看 Auth.js 文件 以獲取更多詳細資訊。

檢查點​

現在,你可以測試你的應用程式以查看驗證是否如預期運作。

取得使用者資訊​

顯示使用者資訊​

當使用者登入後,auth() 的回傳值將是一個包含使用者資訊的物件。你可以在應用程式中顯示這些資訊:

app/page.tsx
import { auth } from '@/auth';

export default async function Home() {
const session = await auth();

return (
<main>
{session?.user && (
<div>
<h2>宣告 (Claims):</h2>
<table>
<thead>
<tr>
<th>名稱</th>
<th>值</th>
</tr>
</thead>
<tbody>
{Object.entries(session.user).map(([key, value]) => (
<tr key={key}>
<td>{key}</td>
<td>{String(value)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</main>
);
}

請求額外的宣告 (Claims)​

你可能會發現從 auth() 返回的物件中缺少一些使用者資訊。這是因為 OAuth 2.0 和 OpenID Connect (OIDC) 的設計遵循最小權限原則 (PoLP, Principle of Least Privilege),而 Logto 是基於這些標準構建的。

預設情況下,僅返回有限的宣告 (Claims)。如果你需要更多資訊,可以請求額外的權限範圍 (Scopes) 以存取更多宣告。

資訊:

「宣告 (Claim)」是對主體所做的斷言;「權限範圍 (Scope)」是一組宣告。在目前的情況下,宣告是關於使用者的一部分資訊。

以下是權限範圍與宣告關係的非規範性範例:

提示:

「sub」宣告表示「主體 (Subject)」,即使用者的唯一識別符(例如使用者 ID)。

Logto SDK 將始終請求三個權限範圍:openid、profile 和 offline_access。

要請求額外的權限範圍 (Scopes),你可以配置 Logto 提供者的參數:

./auth.ts
import NextAuth from 'next-auth';
import Logto from 'next-auth/providers/logto';

export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
Logto({
// ...
authorization: {
params: {
scope: 'openid offline_access profile email',
},
},
// ...
}),
],
});

需要網路請求的宣告 (Claims)​

為了避免 ID 權杖 (ID token) 膨脹,某些宣告 (Claims) 需要透過網路請求來獲取。例如,即使在權限範圍 (Scopes) 中請求了 custom_data 宣告 (Claim),它也不會包含在使用者物件中。要存取這些宣告 (Claims),你需要進行網路請求來獲取使用者資訊。

獲取存取權杖 (Access token)​

更新 NextAuth 配置,以便我們可以獲取存取權杖 (Access token):

./auth.ts
export const { handlers, signIn, signOut, auth } = NextAuth({
// ...
callbacks: {
async jwt({ token, account }) {
if (account) {
token.accessToken = account.access_token;
}
return token;
},
async session({ session, token }) {
// 將存取權杖 (Access token) 注入到 session 物件中
session.accessToken = token.accessToken;
return session;
},
},
});

獲取使用者資訊​

現在使用存取權杖 (Access token) 存取 OIDC 使用者資訊端點:

./app/page.tsx
// ...

export default async function Home() {
const session = await auth();
// 將 URL 替換為你的 Logto 端點,應以 `/oidc/me` 結尾
const response = await fetch('https://xxx.logto.app/oidc/me', {
headers: {
Authorization: `Bearer ${session?.accessToken}`,
},
});
const user = await response.json();
console.log(user);

// ...
}

以上是一個簡單的範例。記得處理錯誤情況。

存取權杖 (Access token) 重新整理​

存取權杖 (Access token) 的有效期很短。預設情況下,Next.js 只會在建立 session 時獲取一次。要實現自動存取權杖 (Access token) 重新整理,請參閱 重新整理權杖 (Refresh token) 輪換。

權限範圍 (Scopes) 和宣告 (Claims)​

Logto 使用 OIDC 權限範圍 (Scopes) 和宣告 (Claims) 慣例 來定義從 ID 權杖 (ID token) 和 OIDC 使用者資訊端點 (userinfo endpoint) 獲取使用者資訊的權限範圍和宣告。無論是「權限範圍 (Scope)」還是「宣告 (Claim)」,都是 OAuth 2.0 和 OpenID Connect (OIDC) 規範中的術語。

以下是支援的權限範圍 (Scopes) 及其對應的宣告 (Claims):

openid

宣告名稱類型描述需要使用者資訊嗎?
substring使用者的唯一識別符否

profile

宣告名稱類型描述需要使用者資訊嗎?
namestring使用者的全名否
usernamestring使用者的用戶名否
picturestring使用者個人資料圖片的 URL。此 URL 必須指向圖像文件(例如 PNG、JPEG 或 GIF 圖像文件),而不是包含圖像的網頁。請注意,此 URL 應特別參考適合在描述使用者時顯示的個人資料照片,而不是使用者拍攝的任意照片。否
created_atnumber使用者創建的時間。時間以自 Unix epoch(1970-01-01T00:00:00Z)以來的毫秒數表示。否
updated_atnumber使用者資訊最後更新的時間。時間以自 Unix epoch(1970-01-01T00:00:00Z)以來的毫秒數表示。否

其他 標準宣告 包括 family_name、given_name、middle_name、nickname、preferred_username、profile、website、gender、birthdate、zoneinfo 和 locale 也將包含在 profile 權限範圍中,無需請求使用者資訊端點。與上述宣告的不同之處在於,這些宣告僅在其值不為空時返回,而上述宣告在值為空時將返回 null。

備註:

與標準宣告不同,created_at 和 updated_at 宣告使用毫秒而非秒。

email

宣告名稱類型描述需要使用者資訊嗎?
emailstring使用者的電子郵件地址否
email_verifiedboolean電子郵件地址是否已驗證否

phone

宣告名稱類型描述需要使用者資訊嗎?
phone_numberstring使用者的電話號碼否
phone_number_verifiedboolean電話號碼是否已驗證否

address

請參閱 OpenID Connect Core 1.0 以獲取地址宣告的詳細資訊。

custom_data

宣告名稱類型描述需要使用者資訊嗎?
custom_dataobject使用者的自訂資料是

identities

宣告名稱類型描述需要使用者資訊嗎?
identitiesobject使用者的連結身分是
sso_identitiesarray使用者的連結 SSO 身分是

roles

宣告名稱類型描述需要使用者資訊嗎?
rolesstring[]使用者的角色否

urn:logto:scope:organizations

宣告名稱類型描述需要使用者資訊嗎?
organizationsstring[]使用者所屬的組織 ID否
organization_dataobject[]使用者所屬的組織資料是

urn:logto:scope:organization_roles

宣告名稱類型描述需要使用者資訊嗎?
organization_rolesstring[]使用者所屬的組織角色,格式為 <organization_id>:<role_name>否

考慮到效能和資料大小,如果「需要使用者資訊嗎?」為「是」,則表示該宣告不會顯示在 ID 權杖中,而會在 使用者資訊端點 回應中返回。

API 資源​

我們建議先閱讀 🔐 角色型存取控制 (RBAC, Role-Based Access Control),以瞭解 Logto RBAC 的基本概念以及如何正確設定 API 資源。

配置 Logto 提供者​

一旦你設定了 API 資源,就可以在應用程式中配置 Logto 時新增它們:

./auth.ts
import NextAuth from 'next-auth';
import Logto from 'next-auth/providers/logto';

export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
Logto({
// ...
authorization: {
params: {
scope: 'openid offline_access profile email',
resource: 'https://shopping.your-app.com/api',
},
},
// ...
}),
],
});

每個 API 資源都有其自身的權限(權限範圍)。

例如,https://shopping.your-app.com/api 資源具有 shopping:read 和 shopping:write 權限,而 https://store.your-app.com/api 資源具有 store:read 和 store:write 權限。

要請求這些權限,你可以在應用程式中配置 Logto 時新增它們:

./auth.ts
import NextAuth from 'next-auth';
import Logto from 'next-auth/providers/logto';

export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
Logto({
// ...
authorization: {
params: {
scope: 'openid offline_access profile email shopping:read shopping:write',
resource: 'https://shopping.your-app.com/api',
},
},
// ...
}),
],
});

你可能會注意到權限範圍是獨立於 API 資源定義的。這是因為 OAuth 2.0 的資源標示符 (Resource Indicators) 指定請求的最終權限範圍將是所有目標服務中所有權限範圍的笛卡兒積。

因此,在上述情況中,權限範圍可以從 Logto 的定義中簡化,兩個 API 資源都可以擁有 read 和 write 權限範圍而不需要前綴。然後,在 Logto 配置中:

./auth.ts
import NextAuth from 'next-auth';
import Logto from 'next-auth/providers/logto';

export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
Logto({
// ...
authorization: {
params: {
scope: 'openid offline_access profile read write',
resource: 'https://shopping.your-app.com/api',
},
},
// ...
}),
],
});

對於每個 API 資源,它將請求 read 和 write 權限範圍。

備註:

請求未在 API 資源中定義的權限範圍是可以的。例如,即使 API 資源中沒有可用的 email 權限範圍,你也可以請求 email 權限範圍。不可用的權限範圍將被安全地忽略。

成功登入後,Logto 將根據使用者的角色向 API 資源發出適當的權限範圍。

取得 API 資源的存取權杖​

Auth.js 只會在沒有 resource 參數的情況下獲取一次存取權杖 (Access token)。我們需要自行實作存取權杖的獲取。

獲取重新整理權杖 (Refresh token)​

更新 Logto 提供者配置,新增 "prompt" 參數並設為 consent,確保包含 offline_access 權限範圍 (Scope):

./auth.ts
import NextAuth from 'next-auth';

export const { handlers, signIn, signOut, auth } = NextAuth({
// ...
authorization: {
params: {
prompt: 'consent',
scope: 'openid offline_access shopping:read shopping:write',
resource: 'https://shopping.your-app.com/api',
// ...
},
},
// ...
});

然後新增一個回呼函式將 refresh_token 儲存到 session:

./auth.ts
export const { handlers, signIn, signOut, auth } = NextAuth({
// ...
callbacks: {
async jwt({ token, account }) {
if (account) {
// ...
token.refreshToken = account.refresh_token;
}
return token;
},
async session({ session, token }) {
// ...
session.refreshToken = token.refreshToken;
return session;
},
},
});

獲取存取權杖 (Access token)​

有了 refresh_token,我們可以從 Logto 的 OIDC 權杖端點獲取存取權杖。

./app/page.tsx
// ...

export default async function Home() {
const session = await auth();

if (session?.refreshToken) {
// 將應用程式 ID 和密鑰替換為你自己的,可以在 "Integration" 部分查看。
const basicAuth = Buffer.from('<logto-app-id>:<logto-app-secret>').toString('base64');

// 將 URL 替換為你的 Logto 端點,應以 `/oidc/token` 結尾
const response = await fetch('https://xxx.logto.app/oidc/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${basicAuth}`,
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: session.refreshToken,
resource: 'https://shopping.your-app.com/api',
}).toString(),
});

const data = await response.json();
console.log(data.access_token);
}

// ...
}

取得組織權杖​

如果你對組織 (Organization) 不熟悉,請閱讀 🏢 組織(多租戶,Multi-tenancy) 以開始了解。

在配置 Logto client 時,你需要新增 urn:logto:scope:organizations 權限範圍 (scope):

./auth.ts
import NextAuth from 'next-auth';
import Logto from 'next-auth/providers/logto';

export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
Logto({
// ...
authorization: {
params: {
scope: 'openid offline_access urn:logto:scope:organizations',
},
},
// ...
}),
],
});

使用者登入後,你可以為使用者獲取組織權杖 (organization token):

類似於 API 資源的存取權杖 (Access token),我們可以使用重新整理權杖 (Refresh token) 來獲取組織存取權杖 (Organization access token)。

./app/page.tsx
// ...

export default async function Home() {
const session = await auth();

if (session?.refreshToken) {
// 將應用程式 ID 和密鑰替換為你自己的,可以檢查 "Integration" 部分。
const basicAuth = Buffer.from('<logto-app-id>:<logto-app-secret>').toString('base64');

// 將 URL 替換為你的 Logto 端點,應以 `/oidc/token` 結尾
const response = await fetch('https://xxx.logto.app/oidc/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${basicAuth}`,
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: session.refreshToken,
resource: 'urn:logto:scope:organizations',
organization_id: 'organization-id',
}).toString(),
});

const data = await response.json();
console.log(data.access_token);
}

// ...
}

延伸閱讀​

終端使用者流程:驗證流程、帳號流程與組織流程 (End-user flows: authentication flows, account flows, and organization flows) 設定連接器 (Configure connectors) 授權 (Authorization) 從 NextAuth.js v4 遷移 Logto 整合到 v5