跳到主要内容

为你的 Passport.js 应用添加认证 (Authentication)

本指南将向你展示如何将 Logto 集成到你的应用中,使用 Passport.js 和 OIDC 策略。

提示:
  • 在本指南中,我们假设你已经在项目中设置了带有会话的 Express。如果没有,请访问 Express.js 网站 开始使用。

先决条件

安装

通过你喜欢的包管理器安装 Logto SDK:

npm i passport passport-openidconnect

集成

使用 OIDC 策略初始化 Passport.js

passport.ts
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 中间件:

your-app-entry.ts
import initPassport from './passport';

// ... other code
initPassport();
// ... other code
app.use(passport.authenticate('session'));
// ... other code

配置重定向 URI

在我们深入细节之前,下面是终端用户体验的快速概览。登录流程可以简化为如下:

  1. 你的应用调用登录方法。
  2. 用户被重定向到 Logto 登录页面。对于原生应用,会打开系统浏览器。
  3. 用户完成登录后被重定向回你的应用(配置为重定向 URI)。

关于基于重定向的登录

  1. 此认证 (Authentication) 过程遵循 OpenID Connect (OIDC) 协议,Logto 强制执行严格的安全措施以保护用户登录。
  2. 如果你有多个应用程序,可以使用相同的身份提供商 (IdP)(日志 (Logto))。一旦用户登录到一个应用程序,当用户访问另一个应用程序时,Logto 将自动完成登录过程。

要了解有关基于重定向的登录的原理和好处的更多信息,请参阅 Logto 登录体验解释


备注:

在以下代码片段中,我们假设你的应用程序运行在 http://localhost:3000/

配置重定向 URI

切换到 Logto Console 的应用详情页面。添加一个重定向 URI http://localhost:3000/callback

Logto Console 中的重定向 URI

就像登录一样,用户应该被重定向到 Logto 以注销共享会话。完成后,最好将用户重定向回你的网站。例如,添加 http://localhost:3000/ 作为注销后重定向 URI 部分。

然后点击“保存”以保存更改。

实现登录和登出

我们现在将为认证过程创建特定的路由:

your-app-entry.ts
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}`);
});
});

然后添加到主页

your-app-entry.ts
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>`);
}
});

检查点:测试你的应用程序

现在,你可以测试你的应用程序:

  1. 运行你的应用程序,你将看到登录按钮。
  2. 点击登录按钮,SDK 将初始化登录过程并将你重定向到 Logto 登录页面。
  3. 登录后,你将被重定向回你的应用程序,并看到登出按钮。
  4. 点击登出按钮以清除令牌存储并登出。

权限 (Scopes) 和声明 (Claims)

Logto 使用 OIDC 权限和声明约定 来定义从 ID 令牌和 OIDC 用户信息端点检索用户信息的权限和声明。“权限”和“声明”都是 OAuth 2.0 和 OpenID Connect (OIDC) 规范中的术语。

简而言之,当你请求一个权限时,你将获得用户信息中的相应声明。例如,如果你请求 `email` 权限,你将获得用户的 `email` 和 `email_verified` 数据。

默认情况下,Logto SDK 总是会请求三个权限:`openid`、`profile` 和 `offline_access`,并且无法移除这些默认权限。但你可以在配置 Logto 时添加更多权限:

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 nameType描述需要 userinfo 吗?
substring用户的唯一标识符

profile

Claim nameType描述需要 userinfo 吗?
namestring用户的全名
usernamestring用户名
picturestring终端用户头像的 URL。该 URL 必须指向图片文件(如 PNG、JPEG 或 GIF),而不是包含图片的网页。注意,该 URL 应专门指向适合在描述终端用户时展示的头像,而不是终端用户拍摄的任意照片。
created_atnumber终端用户的创建时间。该时间以自 Unix 纪元(1970-01-01T00:00:00Z)以来的毫秒数表示。
updated_atnumber终端用户信息最后更新时间。该时间以自 Unix 纪元(1970-01-01T00:00:00Z)以来的毫秒数表示。

其他 标准声明 (Claims) 包括 family_namegiven_namemiddle_namenicknamepreferred_usernameprofilewebsitegenderbirthdatezoneinfolocale 也会包含在 profile 权限 (Scope) 中,无需请求 userinfo 端点。与上表声明 (Claims) 不同的是,这些声明 (Claims) 只有在值不为空时才会返回,而上表中的声明 (Claims) 如果值为空则会返回 null

备注:

与标准声明 (Claims) 不同,created_atupdated_at 声明 (Claims) 使用的是毫秒而不是秒。

email

Claim nameType描述需要 userinfo 吗?
emailstring用户的电子邮件地址
email_verifiedboolean电子邮件地址是否已验证

phone

Claim nameType描述需要 userinfo 吗?
phone_numberstring用户的电话号码
phone_number_verifiedboolean电话号码是否已验证

address

关于 address 声明 (Claim) 的详细信息,请参阅 OpenID Connect Core 1.0

custom_data

Claim nameType描述需要 userinfo 吗?
custom_dataobject用户的自定义数据

identities

Claim nameType描述需要 userinfo 吗?
identitiesobject用户关联的身份信息
sso_identitiesarray用户关联的 SSO 身份信息

roles

Claim nameType描述需要 userinfo 吗?
rolesstring[]用户的角色 (Roles)

urn:logto:scope:organizations

Claim nameType描述需要 userinfo 吗?
organizationsstring[]用户所属的组织 (Organizations) ID 列表
organization_dataobject[]用户所属的组织 (Organizations) 数据
备注:

这些组织 (Organizations) 声明 (Claims) 也可以通过 userinfo 端点获取,当使用 不透明令牌 (Opaque token) 时也是如此。然而,不透明令牌 (Opaque tokens) 不能作为组织令牌 (Organization tokens) 用于访问组织专属资源。详情请参见 不透明令牌 (Opaque token) 与组织 (Organizations)

urn:logto:scope:organization_roles

Claim nameType描述需要 userinfo 吗?
organization_rolesstring[]用户所属组织 (Organizations) 的角色 (Roles),格式为 <organization_id>:<role_name>

考虑到性能和数据大小,如果“需要 userinfo 吗?”为“是”,则该声明 (Claim) 不会出现在 ID 令牌 (ID token) 中,而会在 userinfo 端点 响应中返回。

延伸阅读

终端用户流程:认证 (Authentication) 流程、账户流程和组织流程 配置连接器 (Connectors) 授权 (Authorization)