实现一个简单的客户端 OIDC SDK
Logto 为不同的平台提供了各种 SDK。除了我们的官方 SDK,我们鼓励来自社区的开发人员创建他们自己的用户友好型 SDK。本文将指导你构建一个 OIDC 的基本客户端 SDK。
简介
Logto 为我们的开发人员和商业团队提供全面的客户身份和访问管理(CIAM)解决方案。我们提供各种不同平台和应用框架的现成 SDK。结合我们的 Logto 云服务,你可以在几分钟内轻松地建立一个高度安全的用户授权流程。 作为一个从开发者社区诞生的公司,Logto 拥抱并珍视我们的社区参与。除了那些官方开发的 Logto SDK,我们持续鼓励并热烈欢迎来自社区的开发人员通过创建更多元化和用户友好型的 SDK,满足各种平台和框架的独特需求。 在本文中,我们将简要演示如何逐步实现一个 OIDC 标准认证 SDK。
上下文
OpenID Connect(OIDC)流程是一种建立在 OAuth 2.0 框架之上的认证协议,它提供了身份验证和单一签-on 盾示的能力。它允许用户与应用程序进行身份验证,并得到进一步安全访问任何私有资源的授权。请参阅 OIDC 规范以获取更多详细信息。
工作流程
一个标准的授权代码流程包括以下步骤:
认证流程
- **用户发起登录请求:**一个匿名用户从一个公共入口来到你的应用程序。试图进行认证并可能进一步请求访问第三方应用或服务的受保护资源。
- **用户认证:**客户端应用生成一个认证 URI,并发送请求到授权服务器,该服务器将用户重定向到其登录页面。用户使用各种登录方法与登录页面进行交互,并由授权服务器进行认证。
- **处理登录回调:**在成功的认证之后,用户将会被重定向回你的应用程序,并附带获得的
authorization_code
. 这个authorization_code
包含了与认证状态和请求的授权数据相关的所有权限。 - **令牌交换:**使用从上面的重定向地址抽取的
authorization_code
请求进行令牌交换。回报是:id_token
:包含认证用户身份信息的数字签名 JWT。access_token
:一个不透明的access_token
,可以用来访问用户基础信息端点。refresh_token
:允许用户维持对access_token
的持续交换的凭据令牌。
授权流程
- **访问用户信息:**要访问更多的用户信息,应用程序可以使用从初始令牌交换流程中获得的不透明
access_token
向 UserInfo 端点发出额外的请求。这能够获取关于用户的更多详细信息,如他们的电子邮件地址或个人资料照片。 - **允许访问受保护的资源:**如果需要,应用程序可以使用
refresh_token
结合resource
和scope
参数,向令牌交换端点发出额外的请求,以获得用于用户访问目 标资源的专用access_token
。这个过程会导致一个 JWT 格式的access_token
的发行,它包含了访问受保护资源所需的所有授权信息。
实现
我们将在我们的 @logto/client JavaScript SDK 中遵循一些设计策略,来展示实现一个简单 SDK 的过程。请记住,详细的代码结构可能会根据你正在使用的客户端框架而有所不同。你可以自由地选择任何 Logto 官方 SDK 作为你自己的 SDK 项目的示例。
预览
构造器
构造器应该接受一个 logtoConfig 作为其输入。这提供了所有你需要通过这个 SDK 建立一个认证连接的必要配置。
根据您为 SDK 使用的平台或框架,您可能会将一个持久的本地存储实例传给构造器。这个存储实例将被用来存储所有的授权相关的令牌和秘密。
初始化用户认证
在生成一个认证请求 URL 前,完成几个准备步骤是非常必要的,以确保一个安全的过程。
从授权服务器获取 OIDC 配置
定义一个私有方法 getOidcConfigs
从授权服务器的发现端点获取 OIDC 配置。OIDC 配置响应包含了客户端可以用来与授权服务器交互的所有元数据信息,包括其端点位置和服务器的能力。 (请参阅 OAuth OAuth 认证服务器元数据规范 以获取更多详细信息。)
PKCE 生成器
一个 PKCE(Proof Key for Code Exchange) 验证流程对于所有的公共客户端授权代码交换流都是必要的。它可以降低 authorization_code 的截取攻击的风险。因此,所有的公共客户端应用(例如,本地应用和 SPA)的授权请求都需要一个 code_challenge
和 code_verifier
。
实现方式可能会因你使用的语言和框架而有所不同。请参考 code_challenge 和 code_verifier 规范获得详细的定义。
生成 state 参数
在授权流程中,state 参数 是一个随机生成的值,它被包含在客户端发送的授权请求中。它作为一个安全措施,来防止跨站请求伪造(CSRF)攻击。
存储中间会话信息
有几个参数需要被保留在存储中,用于用户在认证后被重定向回来后的验证。我们将实现一个方法来将这些中间参数设置到存储中。
登录
让我们将以上所有的内容封装起来,定义一个方法生成用户的登录 URL 并将用户重定向到授权服务器进行认证。
处理用户登录回调
在上一部分,我们创建了一个登录方法,这个方法生成了一个用于用户认证的 URL。这个 URL 包含了从客户端应用启动认证流程所需的所有必要参数。这个方法将用户重定向到授权服务器的登录页面的进行认证。在成功登录后,最终用户将被重定向回我们之前提供的 redirect_uri 的位置 。所有必要的参数都将附带在 redirect_uri 中,以完成后续的令牌交换流程。
抽取并验证回调 URL
这个验证步骤非常重要,以防止任何形式的伪造的授权回调攻击。在向授权服务器发送进一步的代码交换请求之前, 必须 仔细验证回调 URL。 首先,我们需要从应用的存储中检索之前存储的 signInSession 数据。
然后在向授权服务器发送令牌交换请求之前,验证回调 URL 的参数。
- 使用以前保存的
redirectUri
来验证callbackUri
是否与我们给授权服务器的一个。 - 使用以前保存的
state
来验证返回的 state 是否与我们给授权服务器的一个。 - 检查授权服务器是否返回了任何错误信息
- 检查返回的
authorization_code
的存在
发送代码交换请求
作为用户认证流程的最后一步,我们将使用返回的 authorization_code
发送令牌交换请求,并获取必需的授权令牌。有关请求参数定义的更多详细信息,请参阅 token exchange specification。
- code: 我们从回调 URI 中收到的
authorization_code
- clientId: 应用 ID
- redirectUri: 是在为用户生成登录 URL 时使用的同一值。
- codeVerifier: PKCE 代码验证器。类似于 redirectUri,授权服务器将比较这个值和之前我们发送的,确保进入的令牌交换请求的验证。
处理登录回调
将我们所有的东西封装起来,我们构建一个 signInCallback 处理方法:
结果,令牌交换请求将返回以下令牌:
id_token
:OIDC idToken,一个包含关于认证用户身份信息的 JSON Web Token(JWT)。id_token 也可以被用作用户认证状态的 single source of truth(SSOT)。access_token
:由授权服务器返回的默认授权代码。可以用于调用用户信息端点和检索已认证的用户信息。refresh_token
:(如果在授权请求中存在 offline_access 作用域):这个 refresh token 允许客户端应用获取一个新的 access token,无需要求用户重新认证,从而授予长期访问资源的权限。expires_in
:以秒为单位的时间,即 access token 在过期前有效的持续时间。
ID token 验证
验证并从 id_token
抽取声明是认证过程中的关键步骤,用于确保令牌的真实性和完整性。以下是验证 id token
的关键步骤:
- 签名验证:
id_token
由授权服务器使用其私钥进行数字签名。客户端应用需要使用授权服务器的公钥验证签名。这确保了令牌没有被篡改,并且的确是由合法的授权服务器签发的。 - 发行者验证:检查
id_token
中的 "iss"(发行者)声明是否与期望的值匹配,表示令牌是由正确的授权服务器颁发的。 - 观众验证:确保
id_token
中的 "aud"(观众)声明和客户端应用的客户端 ID 相匹配,确保令牌是针对客户端的 - 到期检查:验证
id_token
中的 "iat"(发行时间)声明是否已过当前时间,确保令牌仍然有效。由于有网络交易成本,我们需要在验证收到的令牌 iat 声称时设置一个发行时间容忍度。
返回的 id_token 是一个标准的 JSON Web Token (JWT)。根据你使用的框架,你可以找到各种便利的 JWT 令牌验证插件来帮助解码和验证令牌。对于这个例子,我们将在我们的 JavaScript SDK 中使用 jose 来方便令牌的验证和解码。
获取用户信息
在成功的用户认证之后,可以从授权服务器颁发的 OIDC id_token
中获取基本的用户信息。然而,由于性能考虑,JWT 令牌的内容是有限的。为了获取更详细的用户个人资料信息,符合 OIDC 的授权服务器提供了一个开箱即用的用户信息端点。通过请求特定的用户个人资料作用域,这个端点允许你通过获取附加的用户个人资料数据。
在调用令牌交换端点时,如果没有指明一个特定的 API 资源,授权服务器将默认地发出一个 opaque 类型的 access_token
。这个 access_token
只能被用来访问用户信息端点,允许检索到基本的用户个人资料数据。
获取受保护资源授权的 access token
在大多数情况下,客户端应用不仅需要用户认证,还需要用户授权访问某些受保护的资源或 API 端点。在这里,我们将使用在登录期间获得的 refresh_token
来获取特定于管理特定资源的 access_token(s)
。这样我们就可以获得对那些受保护的 API 的访问权限。
总结
我们已经提供了客户端应用的用户认证和授权过程的基本方法实现。根据你的特定场景,你可以组织和优化 SDK 的逻辑。请注意,由于不同的平台和框架,可能会存在变化。
对于更多的详细信息,请浏览 Logto 提供的 SDK 包。我们鼓励更多的用户参与到开发中来,并和我们进行讨论。你们的反馈和贡献对于我们提升和扩展 SDK 的能力有着极大的价值。
附录
保留的作用域
Logto 保留的作用域,你需要在初始 auth 请求中通过。这些作用域是 OIDC 保留的或者 Logto 保留的,用来完成一个成功的授权流程。
作用域名称 | 描述 |
---|---|
openid | 在成功认证后获得 id_token 所需的。 |
offline-access | 允许您的客户端应用在屏幕停止的情况下交换和续签 access_token 并保留 |
profile | 用于获取访问基本用户信息的权限 |
Logto 配置
属性名称 | 类型 | 是否必填 | 描述 | 默认值 |
---|---|---|---|---|
appId | string | 是 | 唯一的应用标识符。由授权服务器生成以识别客户端应用。 | |
appSecret | string | 应用程序的保密用于验证请求者的身份。机密客户端(如 Go web 或 Next.js web 应用)需要它,并且公共客户端(如原生应用或单页应用程序(SPA))可以选择它 | ||
endpoint | string | 是 | 授权服务器的根端点。此属性将被广泛用于生成授权请求的端点。 | |
scopes | string list | 包含了用户可能需要被授予以访问任何给定的受保护资源的所有必要资源的作用域。 | [reservedScopes] | |
resources | string list | 用户可能请求访问的所有受保护资源指示器 |