微博热搜API的免费调用与应用场景解析
确保OAuth 2.0访问令牌安全,使用持有者凭证证明
在OAuth中,一个有效的访问令牌能够授权请求者访问资源和执行相关操作。这表明访问令牌具有很大的权限,如果被恶意用户获取,可能会带来严重风险。
传统的无记名令牌模式意味着任何持有令牌的人都能够访问资源。而OAuth 2.0的新扩展规范“持有者凭证证明(DPoP)”定义了一种将访问令牌与发起请求的OAuth客户端相绑定的标准方法,从而增强了访问令牌的安全性。
DPoP的核心机制是利用公钥/私钥对创建签名的DPoP凭证,授权服务器和资源服务器通过这个凭证来验证请求及其客户端的真实性。这样一来,令牌就被限制只能由特定的发送者使用,即使令牌被盗,攻击者也难以利用它。请继续阅读,深入了解DPoP解决的问题和工作原理:通过持有凭证提升访问令牌的安全性。
了解如何使用DPoP保护您的OAuth 2.0访问令牌。掌握DPoP中的持有凭证证明。
DPoP的主要应用场景是公共客户端,但这一规范也提升了所有OAuth客户端类型的令牌安全性。公共客户端指的是在用户浏览器中运行认证代码的应用,例如单页应用(SPA)和移动应用。
由于架构特点,公共客户端在验证和授权方面天生就较为脆弱,安全性较低。它们无法使用客户端秘密,这些秘密是那些能够通过“后端通道”与授权服务器通信的应用程序类型所采用的,这种网络连接对用户、网络嗅探攻击者和好奇的开发人员都是不可见的。
如果没有适当的保护措施,SPA可能会存储令牌,这些令牌可能被最终用户访问,存在注入攻击的风险。DPoP增加了额外的保护层,使得令牌即使被盗也不易被滥用。
在本文章中,您将尝试使用 DPoP,并使用 OAuth 承载令牌与 DPoP 令牌比较,逐步迁移公共客户端应用程序。我们将以现有的 OAuth 2.0 授权代码流程为基础。需要复习一下吗?请查看这篇文章:验证和授权如何在 SPA 中运行
单页应用程序等公共客户端的身份验证和授权可能比较复杂!
在本篇文章中,我们将通过代码交换证明密钥扩展来介绍授权代码流程,以便更好地了解其工作原理以及如何处理从流程中获取的授权令牌。
备注
这个代码项目特别适合那些有网络开发经验、了解如何调试网络请求和响应,并且熟悉OAuth和OpenID Connect(OIDC)的开发人员。
虽然本文以Angular为例,但您也可以参考其他您偏好的单页应用(SPA)框架中的示例项目来学习相关的概念和网络调用。对于使用React或Vue 的示例,您需要做一些小的调整,我会在文章中指出这些调整的地方,但不会提供具体的代码或详细说明。
如果您正按照Angular的指南逐步操作,本文假设您已经具备了一定的Angular知识。如果您是Angular的初学者,建议您从Angular团队提供的教程开始,先构建您的第一个Angular应用。
实践项目需要本地网络开发工具。
先决条件
您需要以下工具
- Node.jsv18 或更高版本
- 具有良好调试功能的网络浏览器,如 Chrome 浏览器
- 您还在寻找心仪的集成开发环境(IDE)吗?我偏好VS Code和WebStorm,主要是因为它们都内嵌了终端窗口。
- 终端窗口(如果您使用的集成开发环境没有内置终端)
- 如果您希望使用版本控制系统来追踪代码变更,Git和可选的GitHub账号(用于源代码的控制管理)是您需要准备的。
- 显示 HTTP 请求和响应的 HTTP 客户端,如Http 客户端 VS 代码扩展或curl
获取起始的Angular、React或Vue项目
您将基于一个启动项目进行操作。这里的说明是针对Angular示例项目的。如果您使用的是React或Vue,需要将GitHub仓库的链接替换为您所使用的示例项目的URL。
打开终端窗口并运行以下命令,在okta-client-dpop-project
目录下获取项目的本地副本并安装依赖项。请随时fork这个repo,这样你就可以跟踪自己的改动。
git clone https://github.com/oktadev/okta-angular-dpop-example.git okta-client-dpop-project
cd okta-client-dpop-project
npm ci
在您最喜欢的集成开发环境中打开该项目。该项目包括 Okta 的客户端身份验证 SDK、一个登录按钮、一个通过调用 OIDC/userinfo
端点显示用户信息的配置文件路由,以及一个调用Okta 的用户 API 的路由。这两个 HTTP 请求都需要访问令牌,因此我们将跟踪这两个调用的请求和响应。
React 和 Vue 项目说明
React和Vue项目需要做一些改动。更改配置文件组件,调用
oktaAuth.token.getUserInfo()
并显示 JSON 输出。添加对Okta用户APIhttps://{yourOktaDomain}/api/v1/users
的调用。稍后您将替换域名。您可能需要创建一个新的用户组件(和路由)来匹配 Angular 示例。使用React和Vue 的 SDK 参考文档。
您需要设置一个身份验证配置来为项目提供服务。现在就开始吧。
在应用程序中添加 OAuth 2.0 和 OpenID Connect (OIDC)
在本项目中,您将使用 Okta 安全地处理身份验证和授权。Okta API 具有内置的 DPoP 支持 – 多么安全和方便!我们将通过调用 Okta 的 API 在客户端应用程序中尝试使用 DPoP。
React 和 Vue 项目说明
替换两个重定向 URI,使其与应用程序的端口和回调路由相匹配。您可以在项目的 README 文件中找到这两个 URI。按照 README 中的说明将发行方和客户端 ID 添加到应用程序中。发行人使用
https://{yourOktaDomain}
格式。注意这与启动代码不同。
开始之前,您需要注册一个免费的Okta开发者账号。请先安装Okta的命令行工具(CLI),然后通过执行okta register
命令来创建一个新的账户。如果您已有账户,可以直接使用okta login
来登录。登录后,执行okta application create
命令。您可以选择使用默认的应用程序名称,或者根据实际情况进行修改。在选择应用类型时,请选取“单页面应用(Single Page App)”,然后按回车继续。
请设置回调(redirect URI)为http://localhost:4200/login/callback
,并把注销回调(logout redirect URI)设为http://localhost:4200
。关于Okta CLI,它具备哪些功能?
Okta CLI能够在您的Okta组织内创建一个OIDC单页面应用程序。它会添加您指定的回调地址,并默认给予Everyone组访问权限。同时,它还会将http://localhost:4200
添加到可信的来源列表中。完成这些操作后,您将看到如下的输出结果:
Okta application configuration:
Issuer: https://dev-133337.okta.com/oauth2/default
Client ID: 0oab8eb55Kb9jdMIr5d6
请注意 “签发人
"
和 “客户端 ID
“。在即将进行的身份验证配置中,您将需要这些值。
只需在 Okta 管理控制台中进行一次手动更改。在您的 Okta 应用程序中添加刷新令牌授权类型。打开一个浏览器标签,登录到您的Okta 开发者账户。导航到应用程序>应用程序,找到您创建的 Okta 应用程序。选择名称以编辑应用程序。找到 “常规设置“部分,然后按 “编辑“按钮添加授权类型。激活刷新令牌复选框,然后按保存。
打开 Okta 管理控制台。您可以继续在那里进行更改。
我已经添加了Okta Angular和Okta Auth JS库,以便将我们的 Angular 应用程序与 Okta 身份验证连接起来。
在集成开发环境中,打开src/app/app.config.ts
,找到OktaAuthModule.forRoot()
配置。将{yourOktaDomain}
和{yourClientID}
替换为 Okta CLI 中的值。
为调用Okta API配置OAuth的权限范围(Scopes)
我们调用的是 Okta API,因此必须添加所需的 OAuth 作用域。
在Okta管理控制台中,导航到您的Okta应用程序中的 “Okta API作用域“选项卡。找到okta.apps.read
和okta.users.read.self
并分别点击✔️ Grant按钮。
打开 src/app/users/users.component.ts
,找到列出用户的调用: https://{yourOktaDomain}/api/v1/users
。我们在这里采用了快捷方式,例如在本演示项目中直接在组件中调用 API。在具有生产质量的 Angular 应用程序中,请确保按照最佳实践构建应用程序,以便添加自动化测试并快速排除故障。
将{yourOktaDomain}
替换为您的 Okta 域名。
React 和 Vue 项目说明
将两个作用域添加到应用程序的 OIDC 配置中。搜索 “作用域 “并将数组更改为
scopes: ['openid', 'profile', 'email', 'offline_access', 'okta.users.read.self', 'okta.apps.read'],
替换您在上一节中添加的 Okta 用户 API 调用中的
{yourOktaDomain}
。
运行程序启动应用程序:
npm start
打开浏览器,查看您的应用程序。同时打开浏览器的开发者工具,显示控制台和网络请求的调试信息。因为我使用的是Chrome浏览器,所以会打开DevTools。在控制台和网络标签页中,开启日志保存功能。在控制台标签页中,通过打开设置(齿轮图标)菜单,就能找到保存日志的选项。
接下来,我们要确保您能够登录系统,调用/userinfo端点查看用户信息,以及调用Okta用户API。您将通过授权码流程,被重定向到Okta进行身份验证。
一旦您向身份提供者证明了身份,授权服务器就会将您重定向回应用程序。重定向的URI会包含授权码。Okta的SDK(OIDC客户端库)会用这个授权码去/token端点换取访问令牌。
登录成功后,Angular应用会展示“Profile”和“Users”这两个路由。访问这些路由会触发对/userinfo和Users API的调用。如果您能够顺利访问这些路由,并且没有遇到任何HTTP请求错误,那么您就可以继续后续操作了!
核查OAuth 2.0的Bearer令牌并手动索取资源
登录完成后,您将获得OAuth 2.0的访问令牌和OIDC的ID令牌。Okta会将这些令牌保存在浏览器的存储空间里。在DevTools中,切换到“应用程序”标签来查看浏览器存储的数据。Okta Auth JS默认情况下会将令牌保存在本地存储中,当然,您也可以根据自己应用的需求进行相应的配置。在本地存储中展开对应的应用程序项,然后展开名为“okta-token-storage”的键,您就能看到存储的令牌和令牌的元数据。其中,“tokenType”属性表示的是携带者(Bearer)。
让我们看看应用程序中的 API 调用。导航到两个路径。在 “网络“选项卡中,你会看到初始的/token
、/userinfo
和 Users API 请求。
让我们检查一下用户 API 请求。
请求包含授权
标头,其中包含令牌方案和访问令牌。您可以看到格式为Bearer <access_token>
。
持有令牌的实体可以合法请求资源。让我们尝试在另一个客户端中使用令牌,并模仿攻击者在成功捕获令牌后可能采取的行动。
备注
访问令牌很快就会过期。如果接下来的步骤时间过长,可能会出现
401 未授权
。如果出现这种情况,请使用较新的访问令牌重复上述步骤,在配置文件和用户路由之间导航,以触发对 API 的调用。它会提示 OIDC 客户端(Okta Auth JS SDK)更新过期令牌。
从浏览器中复制令牌,并仔细检查是否捕获了整个令牌。打开 HTTP 客户端并运行以下 HTTP 请求,替换{yourOktaDomain}
和{yourAccessToken}
:
GET https://{yourOktaDomain}/api/v1/users HTTP/1.1
Authorization: Bearer {yourAccessToken}
如果使用 curl,请添加 verbose 标志,以查看请求和响应头信息:
curl -v --header "Authorization: Bearer {yourAccessToken}" https://{yourOktaDomain}/api/v1/users
即使 HTTP 客户端与授权服务器发送令牌的客户端(示例应用程序)不是同一个客户端,调用也会成功。
让我们用相同的访问令牌调用另一个端点,即 Okta 应用程序端点。运行以下 HTTP 请求,替换{yourOktaDomain}
和{yourAccessToken}
:
GET https://alisa.oktapreview.com/api/v1/apps HTTP/1.1
Authorization: Bearer {yourAccessToken}
即便您通过不同的客户端发起调用,只要Okta应用程序具备okta.apps.read
权限,并且OIDC配置中的范围(Scope)设置正确,像您之前在调用用户API时看到的那样,调用也会成功。您可能会觉得这有很多限制,确实。当您对顶层Okta组织中的资源(例如Okta应用程序列表)进行API请求时,Okta会施加许多保护措施。
这个例子展示了为管理员等具有特权的用户颁发的令牌是多么的强大,同时也是脆弱的。任何持有这些令牌的人都能够发起相同的请求,哪怕他们是攻击者。
回到应用程序,您需要先退出登录以清除已验证的会话和令牌。我们正在进行一些更改,所以需要您重新登录。
使用安全编码技术保护您的网络应用程序
所有网络应用都需采用安全的编码技术来防范攻击、漏洞和恶意利用。由于公共客户端将令牌存储在用户设备上,因此需要采取周全的安全措施。
请阅读本系列文章的四个部分,深入了解有关SPA(单页应用)网络安全和Angular安全实践的信息:确保您的SPA免受安全威胁。
掌握网络安全的基本概念,并学习如何将这些基础应用于保护您的单页应用。
无论应用程序使用的是匿名令牌还是DPoP,都必须遵循安全的编码实践。DPoP虽不能阻止攻击者盗取令牌,但可以限制令牌的使用范围。
DPoP利用非对称加密技术来验证令牌的所有权,因此必须防止密钥泄露或被未经授权的使用。一旦攻击者获取了私钥,他们就能生成有效的凭证。
让我们将应用程序升级至DPoP,并再次尝试执行这些HTTP请求。
将单页应用(SPA)升级以采用DPoP。
在浏览器中打开 Okta 管理控制台,导航到应用程序>应用程序。找到此项目的 Okta 应用程序。在 “常规“选项卡中找到 “常规设置“部分并按 “编辑“。选中需要在令牌请求中使用 DPoP 标头的 “拥有证明“复选框。按保存。退出 Okta 管理控制台。
如果在不修改任何代码的情况下再次尝试登录,就会在网络选项卡中看到/token
请求出错:
HTTP/1.1 400 Bad Request
{
"error": "invalid_dpop_proof",
"error_description": "The DPoP proof JWT header is missing."
}
向受 DPoP 保护的资源提出的所有 HTTP 请求(包括/token
请求)都需要证明。我们必须在 OIDC 配置中启用 DPoP。
作为 OIDC 配置的一部分,Okta Auth JS SDK 有一个用于 DPoP 的配置属性。在集成开发环境中,打开src/app/app.config.ts
,找到OktaAuthModule.forRoot()
配置。添加dpop: true
属性。您的 OIDC 配置将如下所示:
{
issuer: ...,
clientId: ...,
redirectUri: ...,
scopes: ['openid', 'profile', 'offline_access', 'okta.users.read.self', 'okta.apps.read'],
dpop: true
}
应用程序重建并在浏览器中重新加载后,确保已打开调试工具,然后登录。
跟踪需要 DPoP nonce 的令牌请求
登录时,你会看到对/token
端点的初始调用失败。
查看调用的请求头,你会发现一个名为DPoP的头部,里面包含了一个JWT格式的DPoP凭证,这意味着我们可以对其内容进行解码和检查。你可以使用一些可靠的在线工具(例如JWT.io的调试器),或者在本地对JWT的头部和载荷部分进行Base64解码。在JWT格式中,从开始到第一个”.”字符之间的内容是头部,两个”.”字符之间的内容是载荷。
头部包含了令牌类型、dpop+jwt、加密算法以及与这个凭证相关的加密密钥信息。载荷则包含了最基本的HTTP信息和其他属性,这些都是为了防御针对令牌的攻击手段。
{
"alg": "RS256",
"typ": "dpop+jwt",
"jwk": { /* Key information in JSON Web Key format */ }
}
{
"htm":"POST",
"htu":"https://{yourOktaDomain}/oauth2/v1/token",
"iat":1724685617,
"jti": "e84a...283bbf",
}
为什么最初调用/token
会失败?这是因为 Okta 需要额外的握手来提高安全性。/token
调用需要一个 DPoP nonce,Okta 在 DPoP 证明中提供了这个 nonce。在响应第一次/token
调用时,Okta 会返回标准的 DPoP nonce 错误和DPoP-Nonce
响应头,其中包含客户端纳入证明的 nonce。
HTTP/1.1 400 Bad Request
DPoP-Nonce: "SVD....ubNc"
{
"error": "use_dpop_nonce",
"error_description": "Authorization server requires nonce in DPoP proof."
}
Okta 的 Auth JS SDK 内置支持DPoP-Nonce
错误。请看 DPoP 证明令牌在成功的/token
请求中的有效载荷。有效载荷包括第一次调用返回的 nonce。
{
"htm":"POST",
"htu":"https://{yourOktaDomain}/oauth2/v1/token",
"iat":1724685617,
"jti": "e852...28396",
"nonce":"SVD....ubNc"
}
令牌请求成功,我们现在有了 DPoP 访问令牌。
使用 DPoP 标头请求资源
在应用程序中,由于 SDK 支持 DPoP 资源请求,因此导航查看个人资料会成功。当导航到调用 Okta 用户 API 的 “用户 “路由时,您会看到一个错误。
HTTP 响应包括呼叫出错的原因。
HTTP/1.1 400 Bad Request
WWW-Authenticate: Bearer authorization_uri="http://{yourOktaDomain}/oauth2/v1/authorize", realm="http://{yourOktaDomain}", scope="okta.users.read.self", error="invalid_request", error_description="The resource request requires a DPoP proof.", resource="/api/v1/users"
目前调用用户API的代码使用的是带有Bearer方案的授权头来添加访问令牌,但这种做法不适用于DPoP。我们需要在HTTP请求中加入DPoP凭证,并更改方案。
请在集成开发环境中打开认证拦截器。相关代码位于src/app/auth.interceptor.ts
文件中。
React 和 Vue 项目说明
找到添加到请求用户中的代码,并在项目中加入 Angular 说明,以添加 DPoP 证明头和
DPoP
方案。
拦截器会进行检查,以确保只将访问令牌添加到允许的来源。拦截器代码修改如下:
export const authInterceptor: HttpInterceptorFn = (req, next, oktaAuth = inject(OKTA_AUTH)) => {
let request = req;
const allowedOrigins = ['/api'];
if (!allowedOrigins.find(origin => req.url.includes(origin))) {
return next(request);
}
};
我们需要证明和授权头。我们将使用 Okta Auth JS 生成这两个文件。SDK 方法需要我们打算调用的 HTTP 方法和 URI。URI 不应包含查询参数或片段。
SDK 方法会返回一个对象,其中包含与标头及其值相匹配的属性,因此我们可以使用传播运算符来填充 DPoP 所需的标头。
更改拦截器,使其与下面的代码一致。
import { DPoPHeaders } from '@okta/okta-auth-js';
import { defer, map, switchMap } from 'rxjs';
export const authInterceptor: HttpInterceptorFn = (req, next, oktaAuth = inject(OKTA_AUTH)) => {
// allowed origin check
const url = new URL(req.url);
return defer(() => oktaAuth.getDPoPAuthorizationHeaders({url: `${url.origin}${url.pathname}`, method: req.method})).pipe(
map((dpop: DPoPHeaders) => req.clone({
setHeaders: { ...dpop }
})),
switchMap((request) => next(request))
);
};
现在,只要您登录并调用用户API,就能获取使用DPoP的Okta组织中的用户列表。
手动申请受 DPoP 保护的资源
之前,我们模拟了访问令牌被盗的情况,并用它来请求其他资源。您使用JWT令牌调用了Okta Apps API,查看了您的Okta组织中包含的所有应用程序列表。如果我们在需要DPoP的应用程序接口上再试一次,结果会如何呢?
打开DevTools中的网络标签页,找到对/users的调用。这个HTTP调用需要DPoP证明和访问令牌。尝试发起HTTP请求:
curl -v --header "Authorization: DPoP {yourAccessToken}" --header "DPoP: {yourDPoPProof}" https://{yourOktaDomain}/api/v1/apps
应用程序接口拒绝了您的请求!返回错误信息,说明 DPoP 证明无效:
HTTP/1.1 400 Bad Request
WWW-Authenticate: DPoP algs="RS256 RS384 RS512 ES256 ES384 ES512", authorization_uri="http://{yourOktaDomain}/oauth2/v1/authorize", realm="http://{yourOktaDomain}", scope="okta.apps.read", error="invalid_dpop_proof", error_description="'htu' claim in the DPoP proof JWT is invalid."
如果攻击者成功捕获了证明和令牌,他们可能只能发出相同的请求。证明会限制对 HTTP 方法和 URI 的调用,使其他 HTTP 请求无效。
提出同样的要求如何?
curl -v --header "Authorization: DPoP {yourAccessToken}" --header "DPoP: {yourDPoPProof}" https://{yourOktaDomain}/api/v1/users
应用程序接口拒绝了您的请求!您仍然会收到一个错误,说明 DPoP 证明无效:
HTTP/1.1 400 Bad Request
WWW-Authenticate: DPoP algs="RS256 RS384 RS512 ES256 ES384 ES512", authorization_uri="http://{yourOktaDomain}/oauth2/v1/authorize", realm="http://{yourOktaDomain}", scope="okta.users.read.self", error="invalid_dpop_proof", error_description="The DPoP proof JWT has already been used.", resource="/api/v1/users"
证明还有另外两种保护机制:JWT 唯一标识符(jit
)和签发时间(iat
)。当资源服务器执行jit
声明时,它会跟踪以前的调用,以防止证明重复使用。因此,攻击者无法重放他们窃取的证明和访问令牌。DPoP 规范并不要求执行 JWT ID。另一种保护机制是证明签发时间戳,即iat
claim。资源服务器会检查证明的签发时间,如果超过资源服务器确定的某个阈值,服务器就会拒绝请求。
在浏览器应用程序中存储加密密钥
我们必须确保在单页应用(SPA)中安全地保管密钥集,防止攻击者窃取。一旦攻击者获得密钥集,他们就能冒充你,发起受DPoP保护的请求。
幸运的是,Okta SDK采用了多种技术来降低密钥集被劫持的风险,无需您进行额外的编码工作。
本地存储和会话存储的安全性不足,因此我们将转而使用IndexedDB进行存储。IndexedDB通常用于存储大量数据,但它内置了一些安全机制,能够有效保护密钥集。SubtleCrypto API能够生成不可导出的密钥,避免浏览器代码将私钥转换成可携带的格式。IndexedDB将密钥以CryptoKeyPairs对象的形式存储,查询结果返回的是对该对象的引用,而非原始密钥。这样,IndexedDB既能保护敏感的私钥,又能通过WebCrypto API进行签名验证。
您可以按照以下步骤检查钥匙:
- 导航至 DevTools 中的应用程序选项卡
- 在 “存储 “侧导航下展开IndexedDB
- 展开OktaAuthJs>DPoPKeys
缺点是 IndexedDB API 比其他浏览器存储 API 更难使用。由于 IndexedDB 数据是持久的,因此我们必须手动清理键值。如果用户明确注销,SDK 会处理清理工作,但我们不能保证用户总是会注销。
我们可以在签到前清理钥匙。
打开src/app/app.component.ts
,找到signIn()
方法。
React 和 Vue 项目说明
找到项目调用
signInWithRedirect()
方法的代码,并按照针对 Angular 项目的说明进行操作。
在signIn()
方法的第一步中添加清除密钥的调用:
public async signIn() : Promise<void> {
await this.oktaAuth.clearDPoPStorage(true);
await this.oktaAuth.signInWithRedirect();
}
请使用最新的主流浏览器来安全地处理令牌
在JavaScript应用中生成和保存加密密钥需要一个支持高级功能的浏览器。当前主流的现代浏览器都已经支持DPoP所需的API。如果你的应用服务的用户使用的浏览器版本较旧或存在较多问题,请确保检查这些浏览器是否具备所需功能。
Auth JS SDK提供了一个方法来检查浏览器是否支持DPoP,即authClient.features.isDPoPSupported()
。你可以在应用的启动或初始化阶段加入这项检查。
请记住,即便你不采用DPoP,现代浏览器也内置了更多安全特性。为了安全起见,请及时更新浏览器,并尽可能使用安全性更高的浏览器。
原文链接:https://developer.okta.com/blog/2024/09/10/angular-dpop-jwt