為你的 Passport.js 應用程式新增驗證 (Authentication)
本指南將向你展示如何使用 Passport.js 和 OIDC 策略將 Logto 整合到你的應用程式中。
- 在本指南中,我們假設你已在專案中設置了帶有 session 的 Express。如果還沒有,請查看 Express.js 官網 以開始使用。
先決條件
- 一個 Logto Cloud 帳戶或 自託管 Logto。
- 一個已建立的 Logto 傳統應用程式。
- 一個已配置 session 的 express 專案。請參閱 Express.js 網站。
安裝
透過你喜愛的套件管理器安裝 Logto SDK:
- npm
- pnpm
- yarn
npm i passport passport-openidconnectpnpm add passport passport-openidconnectyarn add passport passport-openidconnect整合
使用 OIDC 策略初始化 Passport.js
import passport from 'passport';
import OpenIDConnectStrategy, { type Profile, type VerifyCallback } from 'passport-openidconnect';
const endpoint = '<your-logto-endpoint>';
const appId = '<your-application-id>';
const appSecret = '<your-application-secret>';
export default function initPassport() {
passport.use(
new OpenIDConnectStrategy(
{
issuer: `${endpoint}/oidc`,
authorizationURL: `${endpoint}/oidc/auth`,
tokenURL: `${endpoint}/oidc/token`,
userInfoURL: `${endpoint}/oidc/me`,
clientID: appId,
clientSecret: appSecret,
callbackURL: '/callback',
scope: ['profile', 'offline_access'],
},
(issuer: string, profile: Profile, callback: VerifyCallback) => {
callback(null, profile);
}
)
);
passport.serializeUser((user, callback) => {
callback(null, user);
});
passport.deserializeUser(function (user, callback) {
callback(null, user as Express.User);
});
}
此程式碼使用 OpenIDConnectStrategy 初始化 Passport。序列化和反序列化方法設置僅供示範。
確保在應用程式中初始化並附加 Passport 中介軟體:
import initPassport from './passport';
// ... other code
initPassport();
// ... other code
app.use(passport.authenticate('session'));
// ... other code
配置重定向 URI
在進入細節之前,這裡先快速說明一下終端使用者的體驗。登入流程可簡化如下:
- 你的應用程式呼叫登入方法。
- 使用者被重新導向至 Logto 登入頁面。對於原生應用程式,會開啟系統瀏覽器。
- 使用者登入後,會被重新導向回你的應用程式(設定為 redirect URI)。
關於基於重導的登入
- 此驗證流程遵循 OpenID Connect (OIDC) 協議,Logto 強制執行嚴格的安全措施以保護使用者登入。
- 如果你有多個應用程式,可以使用相同的身分提供者 (IdP, Identity provider)(Logto)。一旦使用者登入其中一個應用程式,Logto 將在使用者訪問另一個應用程式時自動完成登入流程。
欲了解更多關於基於重導登入的原理和優勢,請參閱 Logto 登入體驗解析。
在以下的程式碼片段中,我們假設你的應用程式運行在 http://localhost:3000/。
配置重定向 URI
切換到 Logto Console 的應用程式詳細資訊頁面。新增一個重定向 URI http://localhost:3000/callback。
就像登入一樣,使用者應被重定向到 Logto 以登出共享會話。完成後,將使用者重定向回你的網站會很不錯。例如,將 http://localhost:3000/ 新增為登出後重定向 URI 區段。
然後點擊「儲存」以保存更改。
實作登入與登出
我們現在將為驗證流程創建特定路由:
app.get('/sign-in', passport.authenticate('openidconnect'));
app.get(
'/callback',
passport.authenticate('openidconnect', {
successReturnToOrRedirect: '/',
})
);
app.get('/sign-out', (request, response, next) => {
request.logout((error) => {
if (error) {
next(error);
return;
}
response.redirect(`${endpoint}/oidc/session/end?client_id=${appId}`);
});
});
然後新增到首頁
app.get('/', (request: Request, response) => {
const { user } = request;
response.setHeader('content-type', 'text/html');
if (user) {
response.end(
`<h1>Hello Logto</h1><p>Signed in as ${JSON.stringify(
user
)}, <a href="/sign-out">Sign Out</a></p>`
);
} else {
response.end(`<h1>Hello Logto</h1><p><a href="/sign-in">Sign In</a></p>`);
}
});
檢查點:測試你的應用程式
現在,你可以測試你的應用程式:
- 執行你的應用程式,你會看到登入按鈕。
- 點擊登入按鈕,SDK 會初始化登入流程並將你重定向到 Logto 登入頁面。
- 登入後,你將被重定向回應用程式並看到登出按鈕。
- 點擊登出按鈕以清除權杖存儲並登出。
權限範圍 (Scopes) 和宣告 (Claims)
Logto 使用 OIDC 權限範圍 (Scopes) 和宣告 (Claims) 慣例 來定義從 ID 權杖 (ID token) 和 OIDC 使用者資訊端點 (userinfo endpoint) 獲取使用者資訊的權限範圍和宣告。無論是「權限範圍 (Scope)」還是「宣告 (Claim)」,都是 OAuth 2.0 和 OpenID Connect (OIDC) 規範中的術語。
簡而言之,當你請求一個權限範圍 (Scope) 時,你將獲得使用者資訊中的對應宣告 (Claims)。例如,如果你請求 `email` 權限範圍 (Scope),你將獲得使用者的 `email` 和 `email_verified` 資料。
預設情況下,Logto SDK 會始終請求三個權限範圍 (Scopes):`openid`、`profile` 和 `offline_access`,且無法移除這些預設的權限範圍 (Scopes)。但你可以在配置 Logto 時新增更多權限範圍 (Scopes):
export default function initPassport() {
passport.use(
new OpenIDConnectStrategy(
{
// ... other options
clientID: appId,
clientSecret: appSecret,
callbackURL: '/callback',
scope: ['openid', 'offline_access', 'profile', 'email'],
}
// ... other options
)
);
// ... other options
}
以下是支援的權限範圍 (Scopes) 及對應的宣告 (Claims) 清單:
openid
| Claim name | Type | Description | Needs userinfo? |
|---|---|---|---|
| sub | string | 使用者的唯一識別符 | No |
profile
| Claim name | Type | Description | Needs userinfo? |
|---|---|---|---|
| name | string | 使用者的全名 | No |
| username | string | 使用者的使用者名稱 | No |
| picture | string | 終端使用者大頭貼的 URL。此 URL 必須指向圖片檔案(例如 PNG、JPEG 或 GIF),而非包含圖片的網頁。請注意,此 URL 應明確指向適合描述終端使用者的個人照片,而非終端使用者隨意拍攝的照片。 | No |
| created_at | number | 終端使用者建立的時間。時間以自 Unix 紀元(1970-01-01T00:00:00Z)以來的毫秒數表示。 | No |
| updated_at | number | 終端使用者資訊最後更新的時間。時間以自 Unix 紀元(1970-01-01T00:00:00Z)以來的毫秒數表示。 | No |
其他 標準宣告 (Standard claims) 包含 family_name、given_name、middle_name、nickname、preferred_username、profile、website、gender、birthdate、zoneinfo 與 locale 也會包含在 profile 權限範圍內,無需額外請求 userinfo endpoint。與上表宣告不同的是,這些宣告僅在其值不為空時才會返回,而上表宣告若值為空則會返回 null。
與標準宣告不同,created_at 與 updated_at 宣告使用毫秒而非秒數。
email
| Claim name | Type | Description | Needs userinfo? |
|---|---|---|---|
string | 使用者的電子郵件地址 | No | |
| email_verified | boolean | 電子郵件地址是否已驗證 | No |
phone
| Claim name | Type | Description | Needs userinfo? |
|---|---|---|---|
| phone_number | string | 使用者的手機號碼 | No |
| phone_number_verified | boolean | 手機號碼是否已驗證 | No |
address
請參閱 OpenID Connect Core 1.0 以瞭解 address 宣告的詳細資訊。
custom_data
| Claim name | Type | Description | Needs userinfo? |
|---|---|---|---|
| custom_data | object | 使用者的自訂資料 | Yes |
identities
| Claim name | Type | Description | Needs userinfo? |
|---|---|---|---|
| identities | object | 使用者的連結身分 | Yes |
| sso_identities | array | 使用者的連結 SSO 身分 | Yes |
roles
| Claim name | Type | Description | Needs userinfo? |
|---|---|---|---|
| roles | string[] | 使用者的角色 (Roles) | No |
urn:logto:scope:organizations
| Claim name | Type | Description | Needs userinfo? |
|---|---|---|---|
| organizations | string[] | 使用者所屬的組織 (Organizations) ID | No |
| organization_data | object[] | 使用者所屬的組織 (Organizations) 資料 | Yes |
這些組織 (Organizations) 宣告也可透過 userinfo endpoint 取得(當使用 不透明權杖 (Opaque token) 時)。但不透明權杖 (Opaque tokens) 無法作為組織權杖 (Organization tokens) 存取組織專屬資源。詳情請見 不透明權杖 (Opaque token) 與組織 (Organizations)。
urn:logto:scope:organization_roles
| Claim name | Type | Description | Needs userinfo? |
|---|---|---|---|
| organization_roles | string[] | 使用者所屬的組織 (Organizations) 角色 (Roles),格式為 <organization_id>:<role_name> | No |
考量效能與資料大小,若 "Needs userinfo?" 為 "Yes",表示該宣告 (Claim) 不會出現在 ID 權杖 (ID token) 中,而會在 userinfo endpoint 回應中返回。