[译]OAuth2.0服务器
背景
前言
我第一次接触 OAuth 是在 2010 年,当时我正在构建一个 API,我知道我希望第三方开发人员能够在其基础上构建应用程序。当时,OAuth 看起来令人生畏。OAuth 1 的实现只有少数几个,而 OAuth 2.0 仍是一个草稿。一天晚上,我决定坐下来,拿着精酿啤酒和最新草案的纸质副本,从头到尾阅读它,直到我理解它。
在仔细阅读了长达 44 页的规范后,我学到了以下几点:阅读规范并不是了解 OAuth 工作原理的最佳方式,而且 OAuth 2.0 远没有我最初想象的那么复杂。我开始撰写一份简化的规范概述,当我第一次学习这个规范时,我希望它就存在。我将其作为一篇名为“OAuth 2.0 简化版”的博客文章发布在我的网站上。现在,这篇文章每年的阅读量达到数十万次。很明显,人们知道 OAuth 2.0 是保护其 API 的正确选择,并且正在寻找资源来帮助理解它。
我一直想将这篇博文扩展为更全面的 OAuth 服务器指南,2016 年,我与 Okta 取得了联系,我们在oauth.com上发布了这个新 OAuth 指南的第一个版本。2017 年,我们合作出版了这本书的印刷版,并于 2018 年和 2020 年出版了修订版。
我希望这本书能让 OAuth 2.0 更加平易近人,并为您在继续使用 Web 技术时提供坚实的知识基础。
背景
在 OAuth 出现之前,向第三方应用授予您帐户访问权限的常见模式是简单地向其提供您的密码并允许其以您的身份行事。我们经常在 Twitter 应用中看到这种情况,这些应用会要求您提供 Twitter 密码,以便向您提供一些帐户统计数据,或者要求能够从您的帐户发送推文以换取一些有价值的东西。
这种应用程序获取用户密码的模式显然存在许多问题。由于应用程序需要以用户身份登录服务,因此这些应用程序通常会以纯文本形式存储用户的密码,这使它们成为密码窃取的目标。一旦应用程序获得了用户的密码,它就可以完全访问用户的帐户,包括访问更改用户密码等功能!另一个问题是,在向应用程序提供密码后,您能够撤销该访问权限的唯一方法是更改密码,而用户通常不愿意这样做。
自然,许多服务很快意识到了这种模式的问题和局限性,并寻求快速解决。许多服务实现了类似于 OAuth 1.0 的东西。Flickr 的 API 使用了所谓的“FlickrAuth”,它使用了“frobs”和“tokens”。谷歌创建了“AuthSub”。Facebook 选择向每个应用程序发布一个秘密,并要求应用程序使用该秘密的 md5 哈希对每个请求进行签名。雅虎创建了“BBAuth”(基于浏览器的身份验证)。结果产生了各种各样的解决方案,彼此完全不兼容,并且经常无法解决某些安全问题。
2006 年 11 月左右,Twitter 首席架构师布莱恩·库克 (Blaine Cook) 正在寻找一种更好的 Twitter API 身份验证方法,这种方法不需要用户向第三方应用程序透露他们的 Twitter 密码。
我们想要类似 Flickr Auth / Google AuthSub / Yahoo! BBAuth 的东西,但以开放标准的形式发布,具有通用的服务器和客户端库等。
– 布莱恩·库克,2007 年 4 月 5 日
2007 年,一群致力于 OpenID 开发的人员聚在一起,创建了一个邮件列表,旨在制定 API 访问控制标准提案,该提案可供任何系统使用,无论其是否使用 OpenID。最初的这个小组包括 Blaine Cook、Kellen Elliott-McCrea、Larry Halff、Tara Hunt、Ian McKeller、Chris Messina 和其他一些人。
在接下来的几个月里,来自 Google 和 AOL 的几位人士也参与其中,希望能够支持这项工作。到 2007 年 8 月,OAuth 1 规范的初稿发布,同时发布的还有针对 Twitter 私下发布的 OAuth API 原型的几个 API 客户端实现。Eran Hammer 加入了该项目,最终担任社区主席和规范编辑。到年底,社区发布了 7 份更新的草案,OAuth Core 1.0 规范在互联网身份研讨会上被宣布为最终版本。
在接下来的几年里,OAuth 规范的工作转移到 IETF 工作组,该工作组开始着手发布 OAuth 1.1。2009 年 11 月,编辑提议放弃 1.1 修订版的工作,转而专注于差异更大的 2.0 版本。
OAuth 2.0 规范最初是为了简化和澄清 OAuth 1 中的许多困难或令人困惑的方面。
虽然已有多家公司实施了 OAuth 1 API(即 Twitter,以及后来的 Flickr),但仍有一些用例(例如移动应用程序)无法在 OAuth 1 中安全地实施。OAuth 2.0 的目标是吸取从 OAuth 1 的首次实施中学到的知识,并针对新兴的移动应用程序用例对其进行更新,同时简化 API 消费者容易混淆的方面。
OAuth 2.0 规范的制定工作始于 IETF 工作组,Eran Hammer 和其他几位成员被任命为该规范的编辑。虽然这项工作一开始进展顺利,但很快发现,工作组成员对该规范的目标截然不同。
OAuth 2.0 框架开发过程中的争议源于 Web 和企业界之间无法弥合的冲突。随着规范的制定工作不断进行,Web 社区中的大多数贡献者都离开了,去实现他们的产品,只留下企业界来完成规范。
2010 年 7 月,草案 10 发布,直到当年 12 月才发布新的草案。草案 10 仍然得到了网络社区的少数人的贡献,因此规范进展顺利。结果是,决定实施 OAuth 2.0 API 的大多数服务都在阅读草案 10。当时的大多数实施(Facebook、Salesforce、Windows Live、Google、Foursquare 等)都在做大致相同的事情。在发布 API 后,他们很少回头更新 OAuth 2 的较新草案。
在该标准的接下来 22 次修订中,Web 和企业贡献者继续在基本问题上存在分歧。解决分歧并继续取得进展的唯一方法是将冲突问题提出来,并将它们放入自己的草案中,从而在规范中留下所谓的“可扩展”漏洞。在最终草案中,核心内容被分成了多个单独的文档,以至于核心文档从“协议”更名为“框架”,并添加了免责声明:“此规范可能会产生大量不具互操作性的实现。”
2012 年,OAuth 2.0 标准的主要编辑 Eran Hammer 决定不再为该标准做出贡献,于是正式退出了工作组。这自然引起了人们对该标准进展的极大关注,他在博客文章和俄勒冈州波特兰市的最后一次会议上很好地阐述了这一点。他在博客文章的结尾写道:“我希望有人能利用 2.0 制作一份 10 页的简介,这对绝大多数网络提供商都有用。”
如今,如果有人想为他们的 Web 服务实施 OAuth 2.0,他们需要综合来自许多不同 RFC 和草案的信息。该标准本身不需要令牌类型,也不需要任何特定的授权类型。这意味着实施者必须决定他们将支持哪种类型。该标准甚至没有提供任何有关令牌字符串大小的指导,而这最终成为每个实施者在开始时首先要问的问题之一。实施者还必须阅读文档中的安全指南和注意事项,并了解他们被迫做出的决定的含义。
有趣的是,大多数为其 API 实施 OAuth 2.0 的 Web 服务都做出了许多相同的决定,因此现有的大多数 OAuth 2.0 API 看起来非常相似。本书是构建 OAuth 2.0 API 的指南,其中包含基于大多数实时实施的具体建议。
1. 准备工作
在本书的第一部分中,我们将介绍构建与现有 OAuth 2.0 API 对话的应用时需要了解的事项。无论您是构建 Web 应用还是移动应用,在开始时都需要记住一些事项。
每个 OAuth 2.0 服务都要求您首先注册一个新应用程序,这通常还要求您首先以开发人员的身份注册该服务。
1.1 创建应用程序
注册过程通常包括在服务的网站上创建开发者帐户,然后输入有关应用程序的基本信息,例如名称,网站,徽标等。注册应用程序后,您将获得一个client_id
(client_secret
在某些情况下是),您将在您的应用与服务交互时使用它。
创建应用程序时最重要的事情之一是注册应用程序将使用的一个或多个重定向 URL。重定向 URL 是 OAuth 2.0 服务在用户授权应用程序后将用户返回到的位置。注册这些 URL 至关重要,否则很容易创建可窃取用户数据的恶意应用程序。本书后面将更详细地介绍这一点。
1.2 重定向 URL 和状态
OAuth 2.0 API 只会将用户重定向到之前在服务中注册的 URL,以防止重定向攻击(攻击者可以拦截授权代码或访问令牌)。某些服务可能允许您注册多个重定向 URL,当您的 Web 应用可能在多个不同的子域上运行时,这会有所帮助。
为了确保安全,重定向 URL 必须是 https 端点,以防止授权代码在授权过程中被拦截。如果您的重定向 URL 不是 https,那么攻击者可能能够拦截授权代码并使用它来劫持会话。唯一的例外是运行在环回接口上的应用(例如本机桌面应用)或进行本地开发时。但是,即使规范允许此例外,您遇到的某些 OAuth 服务可能仍然需要 https 重定向 URL。
OAuth 服务应寻找与重定向 URL 完全匹配的 URL。这意味着的重定向 URLhttps://example.com/auth
将不匹配https://example.com/auth?destination=account
。最佳做法是避免在重定向 URL 中使用查询字符串参数,而只包含路径。
有些应用程序可能会有多个地方需要启动 OAuth 流程,例如主页上的登录链接以及查看某些公共项目时的登录链接。对于这些应用程序,您可能想尝试注册多个重定向 URL,或者您可能认为需要能够根据请求更改重定向 URL。相反,OAuth 2.0 为此提供了一种机制,即“state”参数。
“state”参数可用于对应用程序状态进行编码,但如果您未在请求中包含PKCE参数,则它还必须包含一定数量的随机数据。state 参数是一个对 OAuth 2.0 服务不透明的字符串,因此在初始授权请求期间传入的任何状态值都将在用户授权应用程序后返回。例如,您可以在 JWT 之类的东西中编码重定向 URL,并在用户重定向回您的应用程序后对其进行解析,以便在用户登录后将用户带回适当的位置。
请注意,除非您使用像 JWT 这样的签名或加密方法来编码状态参数,否则当它到达您的重定向 URL 时,您应该将其视为不受信任/未经验证的数据,因为任何人都可以轻易在重定向回您的应用程序时修改该参数。
2. 访问 OAuth 服务器中的数据
在本章中,我们将介绍如何在现有的 OAuth 2.0 服务器上访问您的数据。在本示例中,我们将使用 GitHub API,并构建一个简单的应用程序,该应用程序将列出登录用户创建的所有存储库。
2.1 创建应用程序
在开始之前,我们需要在 GitHub 上创建一个应用程序以获取客户端 ID 和客户端密钥。
在 GitHub.com 的“设置”页面中,单击侧边栏中的“开发人员设置”链接。您将进入 https://github.com/settings/developers。从那里,单击“新建 OAuth 应用”,您将看到一个简短的表单,如下所示。
填写所需信息,包括回调 URL。如果您在本地开发应用程序,则必须使用本地地址作为回调 URL。由于 GitHub 只允许每个应用程序注册一个回调 URL,因此创建两个应用程序很有用,一个用于开发,另一个用于生产。
在 GitHub 上注册新的 OAuth 应用程序
填写完此表格后,您将进入一个页面,您可以在该页面上看到颁发给您的应用程序的客户端 ID 和密钥,如下所示。
客户端 ID 被视为公开信息,用于构建授权 URL,或可包含在网页的 JavaScript 源代码中。客户端机密必须保密。请勿将其提交到您的 git 存储库或将其包含在任何 JavaScript 文件中!
GitHub 应用程序已创建
2.2 设置环境
此示例代码是用 PHP 编写的,无需外部包,也无需框架。希望这能让您轻松将其翻译成其他语言(如果需要)。要遵循此示例代码,您可以将所有内容放在一个 PHP 文件中。
创建一个新文件夹并在该文件夹中创建一个名为 的空文件index.php
。从命令行,php -S localhost:8000
从该文件夹内部运行,您将能够在浏览器中访问http://localhost:8000来运行您的代码。以下示例中的所有代码都应添加到此index.php
文件中。
为了让事情变得简单,我们来定义一个方法,apiRequest()
它是 cURL 的一个简单包装器。此函数将包含GitHub API 所需的Accept
和User-Agent
标头,并自动解码 JSON 响应。如果我们在会话中有一个访问令牌,它还会将正确的 OAuth 标头与访问令牌一起发送,以便发出经过身份验证的请求。
function apiRequest($url, $post=FALSE, $headers=array()) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
if($post)
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post));
$headers = [
'Accept: application/vnd.github.v3+json, application/json',
'User-Agent: https://example-app.com/'
];
if(isset($_SESSION['access_token']))
$headers[] = 'Authorization: Bearer '.$_SESSION['access_token'];
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$response = curl_exec($ch);
return json_decode($response, true);
}
现在让我们设置 OAuth 流程所需的一些变量。
// Fill these out with the values from GitHub
$githubClientID = '';
$githubClientSecret = '';
// This is the URL we'll send the user to first
// to get their authorization
$authorizeURL = 'https://github.com/login/oauth/authorize';
// This is the endpoint we'll request an access token from
$tokenURL = 'https://github.com/login/oauth/access_token';
// This is the GitHub base URL for API requests
$apiURLBase = 'https://api.github.com/';
// The URL for this script, used as the redirect URL
$baseURL = 'https://' . $_SERVER['SERVER_NAME']
. $_SERVER['PHP_SELF'];
// Start a session so we have a place to
// store things between redirects
session_start();
首先,让我们设置“已登录”和“已注销”视图。这将显示一条简单消息,表明用户是已登录还是已注销。
// If there is an access token in the session
// the user is already logged in
if(!isset($_GET['action'])) {
if(!empty($_SESSION['access_token'])) {
echo '<h3>Logged In</h3>';
echo '<p><a href="?action=repos">View Repos</a></p>';
echo '<p><a href="?action=logout">Log Out</a></p>';
} else {
echo '<h3>Not logged in</h3>';
echo '<p><a href="?action=login">Log In</a></p>';
}
die();
}
注销视图包含指向我们的登录 URL 的链接,该链接启动 OAuth 进程。
2.3 授权请求
现在我们已经设置了必要的变量,让我们开始 OAuth 流程。
我们让人们做的第一件事就是使用?action=login
查询字符串访问此页面以启动该过程。
请注意,我们在此请求中要求的范围包括user
和public_repo
。这意味着该应用程序将能够读取用户个人资料信息以及访问公共存储库。
// Start the login process by sending the user
// to GitHub's authorization page
if(isset($_GET['action']) && $_GET['action'] == 'login') {
unset($_SESSION['access_token']);
// Generate a random hash and store in the session
$_SESSION['state'] = bin2hex(random_bytes(16));
$params = array(
'response_type' => 'code',
'client_id' => $githubClientID,
'redirect_uri' => $baseURL,
'scope' => 'user public_repo',
'state' => $_SESSION['state']
);
// Redirect the user to GitHub's authorization page
header('Location: '.$authorizeURL.'?'.http_build_query($params));
die();
}
生成一个“state”参数来保护客户端免受 CSRF 攻击非常重要。这是客户端生成并存储在会话中的随机字符串。GitHub 将使用查询字符串中的状态将用户重定向回此处,因此我们可以在将授权代码交换为访问令牌之前验证它是否与会话中存储的状态相匹配。
我们创建授权 URL,然后将用户发送到那里。该 URL 包含我们的公共客户端 ID、我们之前在 GitHub 注册的重定向 URL、我们请求的范围以及“state”参数。
GitHub 的授权请求
此时,用户将看到 GitHub 的 OAuth 授权提示,如上图所示。
当用户批准请求时,他们将被重定向回我们的页面,其中包含code
请求state
中的参数。下一步是用授权码交换访问令牌。
2.4 获取访问令牌
当用户重定向回我们的应用时,查询字符串中会有一个code
参数。该参数与我们在初始授权请求中设置的参数相同,用于让我们的应用在继续之前检查它是否匹配。这有助于我们的应用避免被诱骗向 GitHub 发送攻击者的授权代码,并防止 CSRF 攻击。
// When GitHub redirects the user back here,
// there will be a "code" and "state" parameter in the query string
if(isset($_GET['code'])) {
// Verify the state matches our stored state
if(!isset($_GET['state'])
|| $_SESSION['state'] != $_GET['state']) {
header('Location: ' . $baseURL . '?error=invalid_state');
die();
}
// Exchange the auth code for an access token
$token = apiRequest($tokenURL, array(
'grant_type' => 'authorization_code',
'client_id' => $githubClientID,
'client_secret' => $githubClientSecret,
'redirect_uri' => $baseURL,
'code' => $_GET['code']
));
$_SESSION['access_token'] = $token['access_token'];
header('Location: ' . $baseURL);
die();
}
这里我们向 GitHub 的令牌端点发送请求,以将授权码换成访问令牌。该请求包含我们的公共客户端 ID 以及私有客户端密钥。我们还发送与之前相同的重定向 URL 以及授权码。
如果一切顺利,GitHub 会生成一个访问令牌并在响应中返回。我们将访问令牌存储在会话中并重定向到主页,然后用户登录。
GitHub 的响应如下所示。
{
"access_token": "e2f8c8e136c73b1e909bb1021b3b4c29",
"token_type": "Bearer",
"scope": "public_repo,user"
}
我们的代码已提取访问令牌并将其保存在会话中。下次您访问该页面时,它会识别出已经有一个访问令牌,并显示我们之前创建的登录视图。
注意:为简单起见,我们在此示例中未包含任何错误处理代码。实际上,您将检查 GitHub 返回的错误并向用户显示适当的消息。
2.5 发出 API 请求
现在我们的应用已为用户提供了 GitHub 访问令牌,我们可以使用它发出 API 请求。让我们在应用中添加一个新部分,该部分将在用户单击我们之前创建的“查看存储库”链接时运行。
还记得apiRequest
我们之前设置的函数吗?这就是访问令牌包含在 HTTP 请求中的地方。此代码发出的请求将在 HTTP 标头中包含访问令牌Authorization
,如下例所示。
GET /user/repos?sort=created&direction=desc HTTP/1.1
Host: api.github.com
Accept: application/vnd.github.v3+json
User-Agent: https://example-app.com/
Authorization: Bearer e2f8c8e136c73b1e909bb1021b3b4c29
以下代码将获取访问令牌并在请求中使用它来列出用户的存储库。然后它将输出存储库列表并链接到每个存储库。
if(isset($_GET['action']) && $_GET['action'] == 'repos') {
// Find all repos created by the authenticated user
$repos = apiRequest($apiURLBase.'user/repos?'.http_build_query([
'sort' => 'created', 'direction' => 'desc'
]));
echo '<ul>';
foreach($repos as $repo)
echo '<li><a href="' . $repo['html_url'] . '">'
. $repo['name'] . '</a></li>';
echo '</ul>';
}
就这样!现在您可以使用访问令牌向 GitHub 上的任何 API 端点发出 API 请求!您可以在https://developer.github.com/v3/上查看 GitHub API 的完整文档。
下载示例代码
您可以从 GitHub 的 https://github.com/aaronpk/sample-oauth2-client下载本示例中使用的完整示例代码。
3. 使用 Google 登录
尽管 OAuth 是一种授权协议而非身份验证协议,但它通常被用作身份验证工作流的基础。许多常见 OAuth API 的典型用途是在登录第三方应用程序时识别计算机上的用户。
身份验证和授权经常被混淆,但如果从应用程序的角度来思考,就会更容易理解。对用户进行身份验证的应用程序只是验证用户是谁。对用户进行授权的应用程序正在尝试获取访问权限或修改属于用户的内容。
OAuth 被设计为一种授权协议,因此每个 OAuth 流程的最终结果是应用程序获得访问令牌,以便能够访问或修改用户帐户的某些内容。访问令牌本身并未说明用户是谁。
不同的服务提供多种方式让应用找出用户的身份。一种简单的方法是让 API 提供一个“用户信息”端点,当使用访问令牌进行 API 调用时,该端点将返回经过身份验证的用户的姓名和其他个人资料信息。虽然这不是 OAuth 标准的一部分,但这是许多服务采用的常见方法。一种更高级、更标准化的方法是使用 OpenID Connect,这是 OAuth 2.0 扩展。OpenID Connect 的详细介绍请参见。
本章将介绍如何使用简化的 OpenID Connect 工作流程和 Google API 来识别登录到您的应用程序的用户。
3.1 创建应用程序
在开始之前,我们需要在 Google API 控制台中创建一个应用程序以获取客户端 ID 和客户端密钥,并注册重定向 URL。
访问 https://console.developers.google.com/ 并创建一个新项目。您还需要为该项目创建 OAuth 2.0 凭据,因为 Google 不会自动执行此操作。在侧边栏中,单击凭据选项卡,然后单击创建凭据并从下拉列表中选择OAuth 客户端 ID 。
在 Google API 控制台上为你的应用创建凭据
Google Console 将提示您输入一些有关您的应用程序的信息,例如产品名称、主页和徽标。在下一页上,选择Web 应用程序类型,然后输入我们接下来要构建的脚本所在的重定向 URL。然后您将收到客户端 ID 和密钥。
3.2 设置环境
此示例代码是用 PHP 编写的,无需外部包,也无需框架。希望这能让您轻松将其翻译成其他语言(如果需要)。要遵循此示例代码,您可以将所有内容放在一个 PHP 文件中。
创建一个新文件夹并在该文件夹中创建一个名为 的空文件index.php
。从命令行,php -S localhost:8000
从该文件夹内部运行,您将能够在浏览器中访问 http://localhost:8000 来运行您的代码。以下示例中的所有代码都应添加到此index.php
文件中。
让我们设置 OAuth 流程所需的一些变量,添加我们在创建应用程序时从 Google 获得的客户端 ID 和密钥。
// Fill these out with the values you got from Google
$googleClientID = '';
$googleClientSecret = '';
// This is the URL we'll send the user to first
// to get their authorization
$authorizeURL = 'https://accounts.google.com/o/oauth2/v2/auth';
// This is Google's OpenID Connect token endpoint
$tokenURL = 'https://www.googleapis.com/oauth2/v4/token';
// The URL for this script, used as the redirect URL
$baseURL = 'https://' . $_SERVER['SERVER_NAME']
. $_SERVER['PHP_SELF'];
// Start a session so we have a place
// to store things between redirects
session_start();
定义好这些变量并启动会话后,让我们设置登录和注销页面。我们将展示一个非常简单的页面,该页面仅指示用户是否已登录,并包含登录或注销的链接。
// If there is a user ID in the session
// the user is already logged in
if(!isset($_GET['action'])) {
if(!empty($_SESSION['user_id'])) {
echo '<h3>Logged In</h3>';
echo '<p>User ID: '.$_SESSION['user_id'].'</p>';
echo '<p>Email: '.$_SESSION['email'].'</p>';
echo '<p><a href="?action=logout">Log Out</a></p>';
// Fetch user info from Google's userinfo endpoint
echo '<h3>User Info</h3>';
echo '<pre>';
$ch = curl_init('https://www.googleapis.com/oauth2/v3/userinfo');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer '.$_SESSION['access_token']
]);
curl_exec($ch);
echo '</pre>';
} else {
echo '<h3>Not logged in</h3>';
echo '<p><a href="?action=login">Log In</a></p>';
}
die();
}
注销视图包含指向我们的登录 URL 的链接,该链接启动流程。
3.3 授权请求
现在我们已经设置了必要的变量,让我们开始 OAuth 流程。
我们让人们做的第一件事就是使用?action=login
查询字符串访问此页面以启动该过程。
请注意,此请求中的范围现在是 OpenID Connect 范围“openid email”,表明我们不是请求访问用户的 Google 数据,只是想知道他们是谁。
还要注意,我们使用该response_type=code
参数来表明我们希望 Google 返回一个授权码,以便我们稍后交换该授权码id_token
。
// Start the login process by sending the user
// to Google's authorization page
if(isset($_GET['action']) && $_GET['action'] == 'login') {
unset($_SESSION['user_id']);
// Generate a random string and store in the session
$_SESSION['state'] = bin2hex(random_bytes(16));
$params = array(
'response_type' => 'code',
'client_id' => $googleClientID,
'redirect_uri' => $baseURL,
'scope' => 'openid email',
'state' => $_SESSION['state']
);
// Redirect the user to Google's authorization page
header('Location: '.$authorizeURL.'?'.http_build_query($params));
die();
}
生成“state”参数来保护客户端免受 CSRF 攻击非常重要。这是客户端生成并存储在会话中的随机字符串。我们的应用将验证来自 Google 的重定向中的 state 参数是否与流程开始时创建的参数相匹配。
我们创建一个授权 URL,然后将用户发送到那里。该 URL 包含我们的公共客户端 ID、我们之前在 Google 注册的重定向 URL、我们请求的范围以及“state”参数。
Google 的授权请求
如果用户已登录 Google,他们将看到如上所示的帐户选择器屏幕,要求他们选择现有帐户或使用其他帐户。请注意,此屏幕看起来不像典型的 OAuth 屏幕,因为用户不会向应用程序授予任何权限,它只是试图识别他们。
当用户选择一个帐户时,他们将被重定向回我们的页面,请求中包含code
和state
参数。下一步是在 Google API 上用授权码交换访问令牌。
3.4 获取 ID 令牌
当用户重定向回我们的应用时,查询字符串中会有一个code
参数。该参数与我们在初始授权请求中设置的参数相同,用于让我们的应用在继续之前检查它是否匹配。这有助于保护我们的应用免受 CSRF 攻击。
// When Google redirects the user back here, there will
// be a "code" and "state" parameter in the query string
if(isset($_GET['code'])) {
// Verify the state matches our stored state
if(!isset($_GET['state']) || $_SESSION['state'] != $_GET['state']) {
header('Location: ' . $baseURL . '?error=invalid_state');
die();
}
// Exchange the authorization code for an access token
$ch = curl_init($tokenURL);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'grant_type' => 'authorization_code',
'client_id' => $googleClientID,
'client_secret' => $googleClientSecret,
'redirect_uri' => $baseURL,
'code' => $_GET['code']
]));
$response = json_decode(curl_exec($ch), true);
// ... fill in from the code in the next section
}
这段代码首先检查 Google 返回的“状态”是否与我们在会话中存储的状态匹配。
我们向 Google 的令牌端点建立一个 POST 请求,其中包含我们应用程序的客户端 ID 和密钥,以及 Google 在查询字符串中发回给我们的授权代码。
Google 将验证我们的请求,然后使用访问令牌和 ID 令牌进行响应。响应将如下所示。
{
"access_token": "ya29.Glins-oLtuljNVfthQU2bpJVJPTu",
"token_type": "Bearer",
"expires_in": 3600,
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImFmZmM2MjkwN
2E0NDYxODJhZGMxZmE0ZTgxZmRiYTYzMTBkY2U2M2YifQ.eyJhenAi
OiIyNzIxOTYwNjkxNzMtZm81ZWI0MXQzbmR1cTZ1ZXRkc2pkdWdzZX
V0ZnBtc3QuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQi
OiIyNzIxOTYwNjkxNzMtZm81ZWI0MXQzbmR1cTZ1ZXRkc2pkdWdzZX
V0ZnBtc3QuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIi
OiIxMTc4NDc5MTI4NzU5MTM5MDU0OTMiLCJlbWFpbCI6ImFhcm9uLn
BhcmVja2lAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUs
ImF0X2hhc2giOiJpRVljNDBUR0luUkhoVEJidWRncEpRIiwiZXhwIj
oxNTI0NTk5MDU2LCJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2ds
ZS5jb20iLCJpYXQiOjE1MjQ1OTU0NTZ9.ho2czp_1JWsglJ9jN8gCg
WfxDi2gY4X5-QcT56RUGkgh5BJaaWdlrRhhN_eNuJyN3HRPhvVA_KJ
Vy1tMltTVd2OQ6VkxgBNfBsThG_zLPZriw7a1lANblarwxLZID4fXD
YG-O8U-gw4xb-NIsOzx6xsxRBdfKKniavuEg56Sd3eKYyqrMA0DWnI
agqLiKE6kpZkaGImIpLcIxJPF0-yeJTMt_p1NoJF7uguHHLYr6752h
qppnBpMjFL2YMDVeg3jl1y5DeSKNPh6cZ8H2p4Xb2UIrJguGbQHVIJ
vtm_AspRjrmaTUQKrzXDRCfDROSUU-h7XKIWRrEd2-W9UkV5oCg"
}
访问令牌应被视为不透明字符串。除了能够使用它发出 API 请求外,它对您的应用没有任何重要意义。
ID 令牌具有特定的结构,您的应用可以对其进行解析以找出已登录的用户数据。ID 令牌是 JWT,在 OpenID Connect 中有更详细的说明。您可以将 Google 的 JWT 粘贴到 example-app.com/base64等网站以快速显示内容,或者您可以对两者之间的中间部分进行 base64 解码.
以查看用户数据,我们接下来会这样做。
3.5 验证用户信息
通常,在信任 ID 令牌中的任何信息之前,验证该令牌至关重要。这是因为在其他 OpenID Connect 流程中,您的应用将通过不受信任的渠道(例如浏览器重定向)获取 ID 令牌。
在这种情况下,您使用客户端密钥通过与 Google 的 HTTPS 连接获取了 ID 令牌,以验证请求,因此您可以确信您获得的 ID 令牌确实来自服务而不是攻击者。考虑到这一点,我知道乍一看似乎不安全,可以解码 ID 令牌而不进行验证。甚至 Google 也这么说。https://developers.google.com/identity/protocols/OpenIDConnect#obtainuserinfo。
看一下上面的 JWT。它由三部分组成,每部分都用句点分隔。我们可以用句点分割字符串,并取中间部分。中间部分是一个 base64 编码的 JSON 字符串,其中包含有关用户的数据。下面是 JWT 中数据的示例。
{
"azp": "272196069173.apps.googleusercontent.com",
"aud": "272196069173.apps.googleusercontent.com",
"sub": "110248495921238986420",
"hd": "okta.com",
"email": "[email protected]",
"email_verified": true,
"at_hash": "0bzSP5g7IfV3HXoLwYS3Lg",
"exp": 1524601669,
"iss": "https://accounts.google.com",
"iat": 1524598069
}
在这个演示中,我们真正关心的是两个属性sub
和email
。sub
(subject) 属性包含登录用户的唯一用户标识符。我们将提取该标识符并将其存储在会话中,这将向我们的应用程序表明用户已登录。
我们还将 ID 令牌和访问令牌存储在会话中,以便稍后使用它们,以展示获取用户信息的另一种方式。
// ... continuing from the previous code sample, insert this
// Split the JWT string into three parts
$jwt = explode('.', $data['id_token']);
// Extract the middle part, base64 decode, then json_decode it
$userinfo = json_decode(base64_decode($jwt[1]), true);
$_SESSION['user_id'] = $userinfo['sub'];
$_SESSION['email'] = $userinfo['email'];
// While we're at it, let's store the access token and id token
// so we can use them later
$_SESSION['access_token'] = $data['access_token'];
$_SESSION['id_token'] = $data['id_token'];
header('Location: ' . $baseURL);
die();
}
现在您将被重定向回应用程序的主页,我们将使用我们在开始时创建的代码向您显示用户 ID 和电子邮件。
echo '<p>User ID: '.$_SESSION['user_id'].'</p>';
echo '<p>Email: '.$_SESSION['email'].'</p>';
使用 ID 令牌检索用户信息
Google 提供了一个额外的 API 端点,称为 tokeninfo 端点,您可以使用它来查找 ID 令牌详细信息,而不必自己解析。不建议将其用于生产应用程序,因为它需要额外的 HTTP 往返,但对于测试和故障排除很有用。
Google 的 tokeninfo 端点位于https://www.googleapis.com/oauth2/v3/tokeninfo
,如其 OpenID Connect 发现文档 中所述https://accounts.google.com/.well-known/openid-configuration
。要查找我们收到的 ID 令牌的信息,请使用查询字符串中的 ID 令牌向 tokeninfo 端点发出 GET 请求。
https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=eyJ...
响应将是一个 JSON 对象,其中包含与 JWT 本身中包含的类似属性列表。
{
"azp": "272196069173.apps.googleusercontent.com",
"aud": "272196069173.apps.googleusercontent.com",
"sub": "110248495921238986420",
"hd": "okta.com",
"email": "[email protected]",
"email_verified": "true",
"at_hash": "NUuq_yggZYi_2-13hJSOXw",
"exp": "1524681857",
"iss": "https://accounts.google.com",
"iat": "1524678257",
"alg": "RS256",
"kid": "affc62907a446182adc1fa4e81fdba6310dce63f"
}
使用访问令牌检索用户信息
如前所述,许多 OAuth 2.0 服务还提供端点来检索登录用户的用户信息。这是 OpenID Connect 标准的一部分,该端点将成为服务的 OpenID Connect 发现文档的一部分。
Google 的用户信息端点是https://www.googleapis.com/oauth2/v3/userinfo
。在这种情况下,您使用访问令牌而不是 ID 令牌来查找用户信息。向该端点发出 GET 请求,并在 HTTPAuthorization
标头中传递访问令牌,就像您在发出 OAuth 2.0 API 请求时通常所做的那样。
GET /oauth2/v3/userinfo
Host: www.googleapis.com
Authorization: Bearer ya29.Gl-oBRPLiI9IrSRA70...
响应将是一个 JSON 对象,其中包含有关用户的多个属性。响应将始终包含密钥sub
,即用户的唯一标识符。Google 还会返回用户的个人资料信息,例如姓名(名字和姓氏)、个人资料照片网址、性别、语言环境、个人资料网址和电子邮件。服务器还可以添加自己的声明,例如 Googlehd
在使用 G Suite 帐户时显示帐户的“托管域”。
{
"sub": "110248495921238986420",
"name": "Aaron Parecki",
"given_name": "Aaron",
"family_name": "Parecki",
"picture": "https://lh4.googleusercontent.com/-kw-iMgD_j34/AAAAAAAAAAI/AAAAAAAAAAc/P1YY91tzesU/photo.jpg",
"email": "[email protected]",
"email_verified": true,
"locale": "en",
"hd": "okta.com"
}
下载示例代码
您可以从 GitHub 的https://github.com/aaronpk/sample-oauth2-client下载本示例中使用的完整示例代码。
您已经了解了用户登录后获取用户个人资料信息的三种不同方法。那么您应该在何时使用哪一种呢?
对于性能敏感的应用程序,您可能在每次请求时都读取 ID 令牌或使用它们来维护会话,因此您绝对应该在本地验证 ID 令牌,而不是发出网络请求。Google API 文档提供了有关离线验证 ID 令牌的详细信息的良好指南。
如果您所做的只是在用户登录后尝试查找用户的姓名和电子邮件,那么从 ID 令牌中提取数据并将其存储在应用程序会话中是最简单、最直接的选择。
4. 服务器端应用程序
服务器端应用是处理 OAuth 服务器时最常见的应用类型。这些应用在 Web 服务器上运行,应用程序的源代码不向公众开放,因此它们可以保持客户端机密的机密性。
下图展示了一个典型示例,其中用户与浏览器交互,而浏览器又与客户端通信。客户端和 API 服务器之间有单独的安全通信通道。用户的浏览器永远不会直接向 API 服务器发出请求,一切都先通过客户端。
应用程序的服务器与 API 进行通信
服务器端应用使用authorization_code
授权类型。在此流程中,用户授权应用程序后,应用程序将收到一个“授权码”,然后可以将其兑换为访问令牌。
4.1 授权码授予
授权码是客户端用来交换访问令牌的临时代码。授权码本身是从授权服务器获取的,用户可以通过授权服务器查看客户端请求的信息,并批准或拒绝该请求。
与其他授权类型相比,授权代码流程具有一些优势。当用户授权应用程序时,他们会使用 URL 中的临时代码重定向回应用程序。应用程序用该代码交换访问令牌。当应用程序请求访问令牌时,可以使用客户端密钥对该请求进行身份验证,从而降低攻击者拦截授权代码并自行使用的风险。这也意味着访问令牌对用户或其浏览器永远不可见,因此这是将令牌传回应用程序的最安全方式,从而降低了令牌泄露给其他人的风险。
Web 流程的第一步是向用户请求授权。这是通过创建授权请求链接供用户点击来实现的。
授权URL通常采用如下格式:
https://authorization-server.com/oauth/authorize?client_id=a17c21ed
&response_type=code
&state=5ca75bd30
&redirect_uri=https%3A%2F%2Fexample-app.com%2Fauth
&scope=photos
确切的 URL 端点将由您所连接的服务指定,但参数名称始终相同。
请注意,您很可能需要先在服务上注册您的重定向 URL,然后它才会被接受。这也意味着您无法根据请求更改重定向 URL。相反,您可以使用参数state
来自定义请求。有关更多信息,请参阅下文。
用户访问授权页面后,服务会向用户显示请求的说明,包括应用程序名称、范围等。(请参阅“批准请求”的示例屏幕截图。)如果用户单击“批准”,服务器将重定向回应用程序,其中包含“code”和您在查询字符串参数中提供的相同“state”参数。请务必注意,这不是访问令牌。您可以使用授权代码执行的唯一操作是发出请求以获取访问令牌。
OAuth 安全
直到 2019 年,OAuth 2.0 规范仅建议对移动和 JavaScript 应用使用PKCE扩展。最新的 OAuth Security BCP 现在也建议对服务器端应用使用 PKCE,因为它也提供了一些额外的好处。常见的 OAuth 服务可能需要一段时间才能适应这一新建议,但如果您从头开始构建服务器,则绝对应该为所有类型的客户端支持 PKCE。
授权请求参数
以下参数用于发出授权请求。您应该使用以下参数构建查询字符串,并将其附加到从其文档中获取的应用程序授权端点。
response_type=code
:response_type
设置为code
表示您想要授权码作为响应。client_id
:这client_id
是您的应用的标识符。首次向服务注册您的应用时,您将收到一个 client_id。redirect_uri
(选修的):根据 API 的不同,这redirect_uri
可能是可选的,但强烈建议使用。这是您希望用户在授权完成后重定向到的 URL。这必须与您之前在服务中注册的重定向 URL 相匹配。scope
(选修的):包含一个或多个范围值(以空格分隔)以请求其他级别的访问权限。这些值取决于特定的服务。state
:该state
参数有两个功能。当用户重定向回您的应用时,您作为 state 包含的任何值也将包含在重定向中。这使您的应用有机会在用户被定向到授权服务器并再次返回之间保留数据,例如使用 state 参数作为会话密钥。这可用于指示授权完成后应用中要执行的操作,例如,指示授权后要重定向到应用的哪个页面。如果 state 参数包含每个请求的随机值,则它还可用作 CSRF 保护机制。当用户重定向回您的应用时,请仔细检查 state 值是否与您最初设置的值相匹配。
PKCE Verifier:如果服务支持 Web 服务器应用的 PKCE,请在此处也包含 PKCE 质询和质询方法。单页应用和移动应用中的完整示例对此进行了描述。
将所有这些查询字符串参数组合到授权 URL 中,并将用户的浏览器定向到该 URL。通常,应用会将这些参数放入登录按钮中,或者将此 URL 作为 HTTP 重定向从应用自己的登录 URL 发送。
用户批准请求
用户进入服务并看到请求后,可以选择允许或拒绝该请求。如果用户允许该请求,系统会将用户重定向回查询字符串中指定的重定向 URL 以及授权代码。然后,应用需要用此授权代码交换访问令牌。
将授权码换成访问令牌
要将授权码换成访问令牌,应用需要向服务的令牌端点发出 POST 请求。该请求将具有以下参数。
grant_type
(必需的):该grant_type
参数必须设置为“authorization_code”。code
(必需的):此参数是从授权服务器接收的授权码,该授权码将位于此请求中的查询字符串参数“code”中。redirect_uri
(可能必需):如果重定向 URL 包含在初始授权请求中,则它也必须包含在令牌请求中,并且必须完全相同。有些服务支持注册多个重定向 URL,有些则要求在每个请求中指定重定向 URL。请查看服务文档以了解具体信息。客户端身份验证(必需):服务将要求客户端在请求访问令牌时验证自身身份。通常,服务支持通过 HTTP Basic Auth 使用客户端的
client_id
和进行客户端身份验证client_secret
。但是,某些服务支持通过接受client_id
和client_secret
作为 POST 正文参数进行身份验证。查看服务的文档以了解服务的期望,因为 OAuth 2.0 规范将此决定权留给服务。更高级的 OAuth 服务器可能还需要其他形式的客户端身份验证,例如 mTLS 或private_key_jwt
。请参阅服务自己的文档以了解这些示例。PKCE 验证器:如果服务支持 Web 服务器应用的 PKCE,则客户端在交换授权码时也需要包含后续 PKCE 参数。同样,请参阅单页应用和移动应用,了解使用 PKCE 扩展的完整示例。
4.2 示例流程
以下分步示例说明了如何使用 PKCE 的授权码流。
高层概述如下:
- 使用应用程序的客户端 ID、重定向 URL、状态和 PKCE 代码质询参数创建登录链接
- 用户看到授权提示并批准请求
- 用户通过授权码重定向回应用服务器
- 应用程序用授权码交换访问令牌
应用发起授权请求
应用程序通过制作包含客户端 ID、范围、状态和 PKCE 代码验证器的 URL 来启动流程。应用程序可以将其放入标签中<a href="">
。
<a href="https://authorization-server.com/oauth/authorize
?response_type=code&client_id=mRkZGFjM&state=5ca75bd30
&scope=photos
&code_challenge_method=S256
&code_challenge=hKpKupTM391pE10xfQiorMxXarRKAHRhTfH_xkGf7U4">
Connect Your Account</a>
请注意,这不是您的应用程序正在进行的 HTTP 调用,而是用户将点击以将其浏览器重定向到 OAuth 服务器的 URL。
用户批准请求
当用户被引导至授权服务器时,他们会看到下图所示的授权请求。如果用户批准该请求,他们将被重定向回应用程序,同时获得授权代码和状态参数。
授权请求示例
该服务将用户重定向回应用程序
该服务发送重定向标头,将用户浏览器重定向回发出请求的应用程序。重定向将在 URL 中包含“code”和原始“state”。
https://example-app.com/cb?code=Yzk5ZDczMzRlNDEwY&state=5ca75bd30
(这实际上将作为 HTTP 响应从授权服务器发送回用户的浏览器,而不是您的应用程序。实际的 HTTP 响应未显示在这里,因为它对于您在应用程序中编写的代码并不重要。)
应用程序用授权码交换访问令牌
最后,应用程序通过向授权服务器的令牌端点发出 HTTPS POST 请求,使用授权码来获取访问令牌。
POST /oauth/token HTTP/1.1
Host: authorization-server.com
code=Yzk5ZDczMzRlNDEwY
&grant_type=code
&redirect_uri=https://example-app.com/cb
&client_id=mRkZGFjM
&client_secret=ZGVmMjMz
&code_verifier=Th7UHJdLswIYQxwSg29DbK1a_d9o41uNMTRmuH0PM8zyoMAQ
授权服务器验证请求并使用访问令牌进行响应,如果访问令牌即将过期,则使用可选的刷新令牌进行响应。
回复:
{
"access_token": "AYjcyMzY3ZDhiNmJkNTY",
"refresh_token": "RjY2NjM5NzA2OWJjuE7c",
"token_type": "Bearer",
"expires": 3600
}
4.3 可能的错误
在某些情况下,您可能会在授权期间收到错误响应。
通过在查询字符串中使用附加参数重定向回提供的重定向 URL 来指示错误。始终会有一个错误参数,并且重定向还可能包含error_description
和error_uri
。
例如,
https://example-app.com/cb?error=invalid_scope
尽管服务器会返回error_description
密钥,但错误描述并不旨在显示给用户。相反,您应该向用户显示您自己的错误消息。这让您可以告诉用户采取适当的措施来纠正问题,并且如果您正在构建多语言网站,还可以让您有机会本地化错误消息。
重定向 URL 无效
如果提供的重定向 URL 无效,授权服务器将不会重定向到该 URL。相反,它可能会向用户显示一条描述问题的消息。
未被认可client_id
如果无法识别客户端 ID,授权服务器将不会重定向用户。相反,它可能会显示一条描述问题的消息。
用户拒绝请求
如果用户拒绝授权请求,服务器将把用户重定向回error=access_denied
查询字符串中的重定向 URL,并且不会出现任何代码。此时由应用程序决定向用户显示什么。
参数无效
如果一个或多个参数无效(例如缺少必需值或参数response_type
错误),则服务器将重定向到重定向 URL 并包含描述问题的查询字符串参数。错误参数的其他可能值包括:
invalid_request
:请求缺少必需参数、包含无效参数值或格式错误。
unauthorized_client
:客户端无权使用此方法请求授权码。
unsupported_response_type
:授权服务器不支持使用此方法获取授权码。
invalid_scope
:请求的范围无效、未知或格式错误。
server_error
:授权服务器遇到意外情况,导致其无法满足请求。
temporarily_unavailable
:由于服务器暂时过载或维护,授权服务器目前无法处理该请求。
此外,服务器可能包含参数error_description
和error_uri
有关错误的附加信息。
4.4 用户体验考虑因素
为了确保授权代码授予的安全性,授权页面必须显示在用户熟悉的网络浏览器中,并且不得嵌入在 iframe 弹出窗口或移动应用中的嵌入式浏览器中。如果它可以嵌入到另一个网站,用户将无法验证它是合法服务还是钓鱼尝试。
如果应用想要使用授权代码授权但无法保护其密钥(即原生移动应用或单页 JavaScript 应用),则在请求将授权代码交换为访问令牌时不需要客户端密钥,并且还必须使用 PKCE。但是,某些服务仍然不支持 PKCE,因此可能无法从单页应用本身执行授权流程,而客户端 JavaScript 代码可能需要有一个配套的服务器端组件来执行 OAuth 流程。
5. 单页应用程序
单页应用(也称为基于浏览器的应用)在从网页加载 JavaScript 和 HTML 源代码后完全在浏览器中运行。由于整个源代码都可供浏览器使用,因此它们无法维护客户端密钥的机密性,因此这些应用不使用密钥。由于它们不能使用客户端密钥,因此最好的选择是使用 PKCE 扩展来保护重定向中的授权代码。这类似于无法使用客户端密钥的移动应用的解决方案。
弃用通知
单页应用的一个常见历史模式是使用隐式流程在重定向中接收访问令牌,而无需中间授权代码交换步骤。如隐式流程所述,这存在许多安全问题,不应再使用。有关更多详细信息,请参阅 https://oauth.net/2/browser-based-apps/。
下图展示了一个示例,用户与浏览器交互,浏览器直接向服务发出 API 请求。首先从客户端下载 Javascript 和 HTML 源代码后,浏览器会直接向服务发出 API 请求。在这种情况下,应用的服务器永远不会向服务发出 API 请求,因为一切都直接在浏览器中处理。
用户的浏览器直接与 API 服务器通信
5.1 授权
授权码是客户端用来交换访问令牌的临时代码。授权码本身是从授权服务器获取的,用户可以通过授权服务器查看客户端请求的信息,并批准或拒绝该请求。
Web 流程的第一步是向用户请求授权。这是通过创建授权请求链接供用户点击来实现的。
授权URL通常采用如下格式:
https://authorization-server.com/oauth/authorize
?client_id=a17c21ed
&response_type=code
&state=5ca75bd30
&redirect_uri=https%3A%2F%2Fexample-app.com%2Fauth
用户访问授权页面后,服务会向用户显示请求的说明,包括应用程序名称、范围等。如果用户点击“批准”,服务器将重定向回网站,并在 URL 查询字符串中包含授权码和状态值。
授权授予参数
以下参数用于提出授权请求。
response_type
:response_type
设置为code
表示您想要授权码作为响应。client_id
:这client_id
是您的应用的标识符。首次向服务注册您的应用时,您将收到一个 client_id。redirect_uri
(选修的):在规范中是redirect_uri
可选的,但有些服务需要它。这是您希望用户在授权完成后重定向到的 URL。这必须与您之前在服务中注册的重定向 URL 相匹配。scope
(选修的):包含一个或多个范围值以请求其他级别的访问权限。这些值取决于特定的服务。state
(受到推崇的):该state
参数有两个功能。当用户重定向回您的应用时,您作为 state 包含的任何值也将包含在重定向中。这使您的应用有机会在用户被定向到授权服务器并再次返回之间保留数据,例如使用 state 参数作为会话密钥。这可用于指示授权完成后在应用中执行什么操作,例如,指示授权后要重定向到应用的哪个页面。这也可以用作 CSRF 保护机制。
请注意,不使用客户端机密意味着使用状态参数对于单页应用程序来说更为重要。
5.2 示例流程
以下分步示例说明如何使用授权授予类型进行单页应用程序。
应用发起授权请求
应用通过制作包含 ID 以及可选的范围和状态的 URL 来启动流程。应用可以将其放入标签中<a href="">
。
<a href="https://authorization-server.com/authorize?response_type=code
&client_id=mRkZGFjM&state=TY2OTZhZGFk">Connect Your Account</a>
用户批准请求
当被引导至身份验证服务器时,用户会看到授权请求。
授权请求示例
用户进入服务并看到请求后,可以选择允许或拒绝该请求。如果用户允许该请求,系统会将用户重定向回查询字符串中指定的重定向 URL 以及授权代码。然后,应用需要用此授权代码交换访问令牌。
https://example-app.com/cb?code=Yzk5ZDczMzRlNDEwY&state=TY2OTZhZGFk
如果您在初始授权网址中包含“state”参数,服务会在用户授权您的应用后将其返回给您。您的应用应将该状态与它在初始请求中创建的状态进行比较。这有助于确保您仅交换您请求的授权代码,从而防止攻击者使用任意或被盗的授权代码重定向到您的回调网址。
将授权码换成访问令牌
要将授权码换成访问令牌,应用需要向服务的令牌端点发出 POST 请求。该请求将具有以下参数。
grant_type
(必需的):该grant_type
参数必须设置为“authorization_code
”。code
(必需的):此参数是从授权服务器接收的授权码,该授权码将位于此请求中的查询字符串参数“code”中。redirect_uri
(可能必需):如果重定向 URL 包含在初始授权请求中,则它也必须包含在令牌请求中,并且必须完全相同。有些服务支持注册多个重定向 URL,有些则要求在每个请求中指定重定向 URL。请查看服务文档以了解具体信息。client_id
(必填):尽管此流程中未使用客户端密钥,但请求需要发送客户端 ID 来识别发出请求的应用程序。这意味着客户端必须将客户端 ID 作为 POST 正文参数包含在内,而不是像在包含客户端密钥时那样使用 HTTP 基本身份验证。
POST /oauth/token HTTP/1.1
Host: authorization-endpoint.com
grant_type=code
&code=Yzk5ZDczMzRlNDEwY
&redirect_uri=https://example-app.com/cb
&client_id=mRkZGFjM
隐式流
一些服务对单页应用使用替代的隐式流,而不是允许应用使用没有秘密的授权码流。
隐式流程绕过了代码交换步骤,而是立即将访问令牌在查询字符串片段中返回给客户端。
实际上,只有非常有限的情况下才需要这样做。几个主要的实现(Keycloak、德国电信、Smart Health IT)已选择完全避免使用隐式流程,而改用授权码流程。
为了使单页应用能够使用授权代码流程,它必须能够向授权服务器发出 POST 请求。这意味着,如果授权服务器位于不同的域中,则服务器将需要支持适当的 CORS 标头。如果不支持 CORS 标头,则服务可能会改用隐式流程。
无论如何,无论是隐式流程还是没有秘密的授权码流程,服务器都必须要求注册重定向 URL,以维护流程的安全性。
安全注意事项
不使用客户端密钥的授权代码授权的唯一安全方法是使用“state”参数并将重定向 URL 限制为受信任的客户端。由于不使用密钥,因此除了使用已注册的重定向 URL 之外,没有其他方法可以验证客户端的身份。这就是为什么您需要使用 OAuth 2.0 服务预先注册您的重定向 URL。
5.3 单页应用的隐式流程
一些服务历来对单页应用程序使用替代的隐式流,而不是当前建议的使用带有 PKCE 的授权码。
隐式流程绕过了代码交换步骤,而是立即将访问令牌在查询字符串片段中返回给客户端。
这种方法存在许多问题,以至于许多提供商选择完全避免实施隐式流程。
OAuth 2.0 中的隐式流程是在 10 多年前创建的,当时浏览器的工作方式与今天大不相同。创建隐式流程的主要原因是浏览器的一个旧限制。以前,JavaScript 只能向加载页面的同一域发出请求。但是,标准 OAuth 授权码流程要求向 OAuth 服务器的令牌端点发出 POST 请求,该端点通常与应用程序位于不同的域上。这意味着以前无法从 JavaScript 使用此流程。隐式流程通过避免该 POST 请求来解决此限制,而是在重定向中立即返回访问令牌。
如今,跨域资源共享 (CORS) 已被浏览器广泛采用,因此不再需要这种妥协。CORS 为 JavaScript 提供了一种向不同域上的服务器发出请求的方法,只要目标允许。这为在 JavaScript 中使用授权码流提供了可能性。
值得注意的是,与授权码流程相比,隐式流程一直被视为一种折衷方案。例如,规范没有提供在隐式流程中返回刷新令牌的机制,因为这样做太不安全了。该规范还建议通过隐式流程颁发的访问令牌的生命周期较短且范围有限。
无论如何,无论是隐式流程还是带有 PKCE 的授权码流程,服务器都必须要求注册重定向 URL,以维护流程的安全性。
5.4 单页应用程序的安全注意事项
对于基于浏览器的应用,由于攻击面增加且网站中移动部件数量增加,因此始终存在跨站点脚本 (XSS) 攻击等风险。此外,浏览器目前没有可用于存储访问令牌或刷新令牌等内容的安全存储机制。因此,与其他平台相比,浏览器在 OAuth 部署中始终被视为具有更高的风险,并且授权服务器通常会针对令牌生命周期制定特殊策略来降低该风险。
刷新令牌
从历史上看,在隐式流程中,从来没有任何机制可以将刷新令牌返回给 JavaScript 应用程序。这在当时是有道理的,因为众所周知,隐式流程不太安全,而且如果没有客户端密钥,刷新令牌可以无限期地用于获取新的访问令牌,因此这比泄露的访问令牌风险更大。刷新令牌也几乎不需要,因为 JavaScript 应用程序只会在用户主动使用浏览器时运行,因此他们可以重定向到授权服务器以获取新的访问令牌(如果需要)。
随着过去几年 JavaScript 应用采用 PKCE 的发展,现在也有可能向 JavaScript 应用颁发刷新令牌。这最终成为授权服务器的政策决策,即是否颁发刷新令牌,取决于授权服务器愿意承受的风险级别。
此外,浏览器 API 的增加意味着ServiceWorkers
基于浏览器的应用现在有可能在用户未主动使用浏览器时运行代码,例如响应后台同步事件。这意味着浏览器应用中的刷新令牌现在有更多潜在用途。
如果授权服务器希望允许 JavaScript 应用使用刷新令牌,则它们还必须遵循 OAuth 工作组最近通过的两份文档“ OAuth 2.0 安全最佳实践”和“基于浏览器的应用的 OAuth 2.0 ”中概述的最佳实践。具体而言,刷新令牌必须仅对一次使用有效,并且授权服务器必须在每次响应刷新令牌授权而发出新的访问令牌时发出新的刷新令牌。这为授权服务器提供了一种检测刷新令牌是否已被攻击者复制和使用的方法,因为在应用的正常运行中,刷新令牌只会使用一次。
刷新令牌还必须具有设定的最大有效期,或者如果在一定时间内未使用,则将过期。这也是另一种有助于减轻刷新令牌被盗风险的方法。
存储令牌
基于浏览器的应用程序需要在授权流程中临时存储一些信息,然后永久存储生成的访问令牌和刷新令牌。这在浏览器环境中带来了一些挑战,因为目前浏览器中没有通用的安全存储机制。
一般来说,浏览器的LocalStorage
API 是存储这些数据的最佳位置,因为它提供了最简单的 API 来存储和检索数据,并且几乎是您在浏览器中可以获得的最安全的 API。缺点是页面上的任何脚本,即使来自不同的域(例如您的分析或广告网络),也能够访问LocalStorage
您的应用程序。这意味着您存储的任何内容LocalStorage
都可能被页面上的第三方脚本看到。
由于第三方脚本存在数据泄露的风险,因此为您的应用配置良好的内容安全策略非常重要,这样您就可以更加确信任意脚本无法在应用程序中运行。 OWASP 提供了有关配置内容安全策略的优秀文档,网址为https://owasp.org/www-project-cheat-sheets/cheatsheets/Content_Security_Policy_Cheat_Sheet.html
选择替代架构
由于在纯 JavaScript 环境中执行 OAuth 流程存在固有风险,以及在 JavaScript 应用中存储令牌存在风险,因此建议考虑使用替代架构,其中 OAuth 流程由动态后端组件在 JavaScript 代码之外处理。这是一种相对常见的架构模式,其中应用程序由动态后端(如 .NET 或 Java 应用)提供服务,但它使用单页应用框架(如 React 或 Angular)作为其 UI。如果您的应用属于这种架构模式,那么最好的选择是将所有 OAuth 流程移至服务器组件,并将访问令牌和刷新令牌完全置于浏览器之外。请注意,在这种情况下,由于您的应用具有动态后端,因此它也被视为机密客户端,可以使用客户端机密来进一步保护 OAuth 交换。
“基于浏览器的应用程序的 OAuth 2.0 ”中对此模式进行了更详细的描述。
6. 移动和本机应用程序
与单页应用一样,移动应用也无法维护客户端密钥的机密性。因此,移动应用还必须使用不需要客户端密钥的 OAuth 流程。当前的最佳做法是使用带有 PKCE 的授权流程,并启动外部浏览器,以确保本机应用无法修改浏览器窗口或检查内容。
许多网站提供移动 SDK 来为您处理授权流程。对于这些服务,您最好直接使用它们的 SDK,因为它们可能已使用非标准附加功能增强了其 API。Google 提供了一个名为 AppAuth 的开源库,它处理下文所述流程的实现细节。它旨在能够与实现该规范的任何 OAuth 2.0 服务器配合使用。如果服务不提供自己的抽象,而您必须直接使用其 OAuth 2.0 端点,本节将介绍如何使用带有 PKCE 的授权代码流程与 API 进行交互。
6.1 授权
创建一个“登录”按钮,该按钮将在应用程序内打开一个安全的 Web 浏览器(在 iOS 上ASWebAuthenticationSession
为,在 Android 上为“自定义选项卡”)。您将使用与服务器端应用程序SFSafariViewController
中所述的授权请求相同的参数,包括 PKCE 参数。
生成的重定向将包含临时授权代码,应用程序将从其本机代码中将其交换为访问令牌。
在此示例中,我们将介绍一个获得访问虚构 API 的授权的简单 iPhone 应用程序。
发起授权请求
要开始授权流程,应用应该有一个“登录”按钮。该链接应构建为服务授权端点的完整 URL。
客户端首先创建所谓的 PKCE“代码验证器”。这是一个使用字符A-Z
、、a-z
和0-9
标点符号-._~
(连字符、句点、下划线和波浪号)的加密随机字符串,长度在 43 到 128 个字符之间。
应用程序生成代码验证器后,会使用它来创建代码质询。代码质询是代码验证器的 SHA256 哈希的 Base64-URL 编码字符串。此哈希值在授权请求中发送,因此原始随机字符串永远不会暴露给应用程序外部的任何东西。
授权请求参数用于创建授权URL,例如:
https://authorization-server.com/authorize
?client_id=eKNjzFFjH9A1ysYd
&response_type=code
&redirect_uri=com.example.app://auth
&state=1234zyx
&scope=photos
&code_challenge=hKpKupTM381pE10yfQiorMxXarRKAHRhTfH_xkGf7U4
&code_challenge_method=S256
在这种情况下,请注意重定向 URL 的自定义方案。iOS 和 Android 都为应用提供了注册可用作重定向 URL 的自定义 URL 方案的功能。在平台的文档中,这有时也称为“深度链接”。这两个平台还允许应用注册自身,以便在访问匹配的 URL 模式时启动(iOS 上的“通用链接”和 Android 上的“应用链接”)。这两种方法在使用应用时提供的体验大致相同,但如果用户未安装应用,则“通用/应用链接”方法在访问 URL 时提供更好的后备行为。“通用链接”和“应用链接”方法通常被认为更现代,可能是您今后应该使用的方法。
当用户点击“登录”按钮时,应用应在安全的应用内浏览器(ASWebAuthenticationSession
iOS 上;或 Android 上的“自定义选项卡”)中打开授权 URL。WebView
在应用内使用嵌入式窗口被认为极其危险,因为这无法保证用户正在查看服务自己的网站,因此很容易成为网络钓鱼攻击的源头。嵌入式 Web 视图还会提供更差的用户体验,因为它不共享系统 cookie,并且用户始终必须输入其凭据。通过使用与系统浏览器共享 cookie 的平台安全浏览器 API,您可以获得这样的优势:用户可能已经登录到服务,无需每次都输入其凭据。
用户批准请求
当被引导至身份验证服务器时,用户会看到如下所示的授权请求。
右上角的“完成”按钮可折叠视图并让用户返回到应用程序。
该服务将用户重定向回应用程序
当用户完成登录后,服务将重定向回您应用的重定向 URL,这将导致安全浏览器 API 将生成的 URL 发送到您的应用。Location
重定向的标头将如下所示,它将传递给您的应用。
com.example.app://auth://auth?state=1234zyx
&code=lS0KgilpRsT07qT_iMOg9bBSaWqODC1g061nSLsa8gV2GYtyynB6A
然后,您的应用程序应该从 URL 中解析出状态值和授权码,验证状态是否与其设置的值匹配,然后将授权码交换为访问令牌。
将授权码换成访问令牌
要将授权码换成访问令牌,应用会向服务的令牌端点发出 POST 请求。此操作来自应用的本机代码,而不是浏览器,因为浏览器存储了 PKCE code_verifier。该请求将包含以下参数。
grant_type
(必需的):该grant_type
参数必须设置为“authorization_code
”。code
(必需的):此参数是从授权服务器接收的授权码,该授权码将位于此请求中的查询字符串参数“code”中。redirect_uri
(可能必需):如果重定向 URL 包含在初始授权请求中,则它也必须包含在令牌请求中,并且必须完全相同。有些服务支持注册多个重定向 URL,有些则要求在每个请求中指定重定向 URL。请查看服务文档以了解具体信息。code_verifier
(必需的):由于客户端code_challenge
在初始请求中包含了一个参数,因此它现在必须通过在 POST 请求中发送该参数来证明它拥有用于生成哈希的密钥。这是用于计算先前在参数中发送的哈希的明文字符串code_challenge
。client_id
(必填):尽管此流程中未使用客户端密钥,但请求需要发送客户端 ID 来识别发出请求的应用程序。这意味着客户端必须将客户端 ID 作为 POST 正文参数包含在内,而不是像在包含客户端密钥时那样使用 HTTP 基本身份验证。
POST /oauth/token HTTP/1.1
Host: authorization-endpoint.com
grant_type=code
&code=Yzk5ZDczMzRlNDEwY
&redirect_uri=com.example.app://auth
&client_id=eKNjzFFjH9A1ysYd
&code_verifier=Th7UHJdLswIYQxwSg29DbK1a_d9o41uNMTRmuH0PM8zyoMAQ
6.2 安全注意事项
始终使用安全的嵌入式浏览器 API,或启动本机浏览器
至关重要的是,应用程序应使用平台上适当的浏览器 API,而不是使用嵌入式 Web 视图。在 iOS 上,这要么是 ,要么ASWebAuthenticationSession
是SFSafariViewController
,而在 Android 上,这称为“自定义选项卡”。
使用嵌入式 Web 视图有很多缺点,它会导致用户更容易遭受网络钓鱼攻击,因为用户无法验证他们正在查看的网页的来源。对于攻击者来说,创建一个与授权网页一模一样的网页并将其嵌入到他们自己的恶意应用中是轻而易举的事,这样他们就能够窃取用户名和密码。
从用户体验方面来看,使用嵌入式 Web 视图也有缺点,即 Web 视图不共享系统 Cookie,因此用户每次都必须输入其凭据。相反,如果用户已在浏览器中登录授权服务器,则使用适当的安全浏览器 API 将为用户提供机会,让他们可以绕过在应用中输入凭据。
7. 发出经过身份验证的请求
无论您使用哪种授予类型或是否使用客户端机密,您现在都拥有可与 API 一起使用的 OAuth 2.0 Bearer 令牌。
Authorization
访问令牌在以文本为前缀的HTTP 标头中发送给服务Bearer
。从历史上看,某些服务允许在帖子正文参数或甚至 GET 查询字符串中发送令牌,但这些方法存在缺点,大多数现代实现仅使用 HTTP 标头方法。
在 HTTP 标头中传递访问令牌时,您应该发出如下请求:
POST /resource/1/update HTTP/1.1
Authorization: Bearer RsT5OjbzRn430zqMLgV3Ia"
Host: api.authorization-server.com
description=Hello+World
访问令牌不旨在供您的应用程序解析或理解。您的应用程序唯一应该做的事情是使用它来发出 API 请求。某些服务将使用结构化令牌(如 JWT)作为其访问令牌,如自编码访问令牌中所述,但在这种情况下,客户端无需担心解码令牌。
事实上,尝试解码访问令牌是危险的,因为服务器无法保证访问令牌始终保持相同的格式。下次您从服务获取访问令牌时,它很有可能是不同的格式。需要记住的是,访问令牌对客户端是不透明的,只能用于发出 API 请求,而不能自行解释。
如果您想要确定访问令牌是否已过期,您可以存储首次获取访问令牌时返回的过期时间,或者尝试发出请求,如果当前访问令牌已过期,则获取新的访问令牌。实际上,两者没有太大区别。虽然预先刷新访问令牌可以节省 HTTP 请求,但您仍需要处理 API 调用在您预计令牌过期之前报告令牌过期的情况,因为访问令牌过期的原因可能有很多,而不仅仅是其预期的过期时间。
有关使用刷新令牌获取新访问令牌的更多详细信息,请参阅下文。
如果您想了解有关登录用户的更多信息,您应该阅读特定服务的 API 文档以了解他们的建议。例如,Google 的 API 使用 OpenID Connect 提供一个 userinfo 端点,该端点可以返回有关给定访问令牌的用户的信息,或者您可以从 ID 令牌中获取用户的信息。我们在使用Google 登录中介绍了 userinfo 端点工作流程的完整示例。
7.1 刷新令牌
当您最初收到访问令牌时,它可能包含一个刷新令牌以及一个过期时间,如下例所示。
{
"access_token": "AYjcyMzY3ZDhiNmJkNTY",
"refresh_token": "RjY2NjM5NzA2OWJjuE7c",
"token_type": "bearer",
"expires": 3600
}
刷新令牌的存在意味着访问令牌将过期,您将能够在无需用户交互的情况下获取新的令牌。
“expires_in”值是访问令牌的有效期(秒数)。访问令牌的有效期由您使用的服务决定,也可能取决于应用程序或组织自己的策略。您可以使用此时间戳预先刷新访问令牌,而不是等待带有过期令牌的请求失败。有些人喜欢在当前访问令牌过期前不久获取新的访问令牌,以避免 API 调用失败的 HTTP 请求。虽然这是一个非常好的优化,但它并不能阻止您仍然需要处理如果访问令牌在预期时间之前过期则 API 调用失败的情况。访问令牌可能因多种原因而过期,例如用户撤销应用程序,或者当用户更改密码时授权服务器使所有令牌过期。
如果您发出 API 请求并且令牌已过期,您将收到一条响应,表明此情况。您可以检查此特定错误消息,然后刷新令牌并再次尝试请求。
如果您使用的是基于 JSON 的 API,则它可能会返回包含invalid_token
错误的 JSON 错误响应。无论如何,WWW-Authenticate
标头也会包含invalid_token
错误代码。
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token"
error_description="The access token expired"
Content-type: application/json
{
"error": "invalid_token",
"error_description": "The access token expired"
}
当您的应用程序识别出此特定错误时,它可以使用先前收到的刷新令牌向令牌端点发出请求,并取回可用于重试原始请求的新访问令牌。
要使用刷新令牌,请使用 向服务的令牌端点发出 POST 请求grant_type=refresh_token
,并包含刷新令牌以及客户端凭据(如果需要)。
POST /oauth/token HTTP/1.1
Host: authorization-server.com
grant_type=refresh_token
&refresh_token=xxxxxxxxxxx
&client_id=xxxxxxxxxx
&client_secret=xxxxxxxxxx
响应将是一个新的访问令牌,并且可选地是一个新的刷新令牌,就像您在交换授权码以获取访问令牌时收到的一样。
{
"access_token": "BWjcyMzY3ZDhiNmJkNTY",
"refresh_token": "Srq2NjM5NzA2OWJjuE7c",
"token_type": "Bearer",
"expires": 3600
}
如果您没有取回新的刷新令牌,则意味着当新的访问令牌过期时,您现有的刷新令牌将继续有效。
最安全的选择是授权服务器在每次使用刷新令牌时都发出一个新的刷新令牌。这是最新的安全最佳当前实践中的建议,它使授权服务器能够检测刷新令牌是否被盗。这对于没有客户端密钥的客户端尤其重要,因为刷新令牌成为获取新访问令牌所需的唯一东西。
当刷新令牌在每次使用后发生变化时,如果授权服务器检测到刷新令牌被使用了两次,则意味着它可能已被复制并被攻击者使用,并且授权服务器可以立即撤销与其关联的所有访问令牌和刷新令牌。
请记住,用户可以随时撤销申请,因此您的应用程序需要能够处理使用刷新令牌也失败的情况。此时,您将需要再次提示用户进行授权,从头开始新的 OAuth 流程。
您可能会注意到,“expires_in”属性指的是访问令牌,而不是刷新令牌。刷新令牌的到期时间是故意不传达给客户端的。这是因为即使客户端能够知道刷新令牌何时到期,它也无法采取任何可操作的步骤。刷新令牌也可能在其预期的使用寿命之前到期,原因也有很多。
如果刷新令牌因任何原因过期,那么应用程序可以采取的唯一操作就是要求用户重新登录,从头开始新的 OAuth 流程,这将向应用程序发出新的访问令牌和刷新令牌。这就是为什么应用程序是否知道刷新令牌的预期寿命并不重要,因为无论其过期原因如何,结果总是相同的。
8. 客户注册
8.1 注册新应用程序
当开发人员访问您的网站时,他们需要一种创建新应用程序并获取凭据的方法。通常,您需要让他们创建一个开发人员帐户,或者代表他们的组织创建一个帐户,然后他们才能创建应用程序。
虽然 OAuth 2.0 规范并不要求您在授予凭据之前特别收集任何应用程序信息,但大多数服务在发出和之前都会收集有关应用程序的基本信息,例如应用程序名称和图标client_id
。client_secret
但是,出于安全目的,要求开发人员为应用程序注册一个或多个重定向 URL 非常重要。重定向 URL中对此进行了更详细的说明。
通常,服务会收集有关应用程序的信息,例如:
- 应用名称
- 应用程序的图标
- 应用程序主页的 URL
- 应用程序的简短描述
- 应用程序隐私政策的链接
- 重定向 URL 列表
下面是 GitHub 注册应用程序的界面。其中收集了应用程序名称、主页 URL、回调 URL 和可选的描述。
在 GitHub 上创建新应用程序
最好向开发人员说明您从他们那里收集的信息是否会显示给最终用户,或者是否仅供内部使用。
Foursquare 的应用程序注册页面会要求提供类似的信息,但他们还会要求提供简短的标语和隐私政策 URL。这些信息会在授权提示中显示给用户。
在 Foursquare 上创建新应用程序
由于使用旧式隐式授予类型的安全考虑,某些服务(例如 Instagram)默认为新应用程序禁用此授予类型,并要求开发人员在应用程序的设置中明确启用它,如下所示。
在 Instagram 上创建新应用程序
Instagram 提供了一条说明,指示开发人员不要使用可能让应用程序看起来像是来自 Instagram 的单词来命名他们的应用程序。这也是包含 API 使用条款链接的好地方。
您的服务还可以让开发人员选择他们正在创建的应用程序类型(公开或机密),或者选择可能更贴近开发人员的应用平台描述(Web 应用程序、移动应用程序、SPA 等)。您的服务应仅向机密应用程序发布客户端机密,并禁止对这些应用程序使用隐式授权。
在 Okta 中创建新应用程序
如上图所示,Okta 允许开发人员在收集有关应用程序的信息之前选择应用程序所针对的平台(原生、单页应用程序、Web 或服务)。根据开发人员在此处选择的值,这将决定为应用程序启用哪些授权类型,以及是否为应用程序颁发客户端密钥。
8.2 客户端 ID 和密钥
此时,您已构建了应用程序注册屏幕,您可以让开发人员注册该应用程序了。当开发人员注册应用程序时,您需要生成客户端 ID 和可选的密钥。在生成这些字符串时,需要考虑安全性和美观性方面的一些重要事项。
8.3 客户端 ID
是client_id
应用程序的公共标识符。尽管它是公开的,但最好不要让第三方猜到,因此许多实现都使用 32 个字符的十六进制字符串之类的东西。如果客户端 ID 是可以猜到的,那么针对任意应用程序的网络钓鱼攻击就会稍微容易一些。它在授权服务器处理的所有客户端中也必须是唯一的。
以下是支持 OAuth 2.0 的服务中的一些客户端 ID 示例:
- 四方:
ZYDPLLBWSK3MVQJSIYHB1OR2JXCY0X2C5UJ2QAR2MAAIT5Q
- Github:
6779ef20e75817b79602
- 谷歌:
292085223830.apps.googleusercontent.com
- Instagram:
f2a1ed52710d4533bde25be6da03b6e3
- SoundCloud:
269d98e4922fb3895e9ae2108cbb5064
- Windows 直播:
00000000400ECB04
- 奥克塔:
0oa2hl2inow5Uqc6c357
如果开发人员正在创建“公共”应用程序(移动或单页应用程序),那么您根本不应该client_secret
向该应用程序发出。这是确保开发人员不会意外将其包含在其应用程序中的唯一方法。如果它不存在,它就不会泄露!
因此,在开发人员开始时询问他们正在创建什么类型的应用程序通常是一个好主意。您可以向他们提供以下选项,并且只为“Web 服务器”或“服务”应用程序发布密钥。
- Web 服务器应用程序
- 单页应用程序
- 移动或本机应用程序
- 服务应用程序
当然,没有什么可以阻止开发人员选择错误的选项,但是通过主动询问开发人员哪种应用程序将使用凭据,可以帮助减少泄露机密的可能性。
8.4 客户端机密
这client_secret
是只有应用程序和授权服务器知道的秘密。它是必不可少的应用程序自己的密码。它必须足够随机,不可猜测,这意味着您应该避免使用常见的 UUID 库,这些库通常会考虑生成它的服务器的时间戳或 MAC 地址。生成安全秘密的一个好方法是使用加密安全库生成 256 位值,然后将其转换为十六进制表示。
在 PHP 中,您可以使用random_bytes
函数并转换为十六进制字符串:
bin2hex(random_bytes(32));
在 Ruby 中,您可以使用 SecureRandom 库来生成十六进制字符串:
require 'securerandom'
SecureRandom.hex(32)
至关重要的是,开发人员永远不要client_secret
在公共(移动或基于浏览器)客户端中包含他们的密钥。为了帮助开发人员避免意外地这样做,最好使客户端密钥在视觉上与 ID 不同。这样,当开发人员复制并粘贴 ID 和密钥时,很容易识别哪个是哪个。通常使用较长的密钥字符串是一种很好的指示方式,或者在密钥前加上“secret”或“private”。
8.5 存储和显示客户端 ID 和密钥
对于每个注册的应用程序,您需要存储公共client_id
和私有client_secret
信息。由于这些信息本质上相当于用户名和密码,因此您不应以纯文本形式存储机密,而应仅存储加密或散列版本,以帮助降低机密泄露的可能性。
当您发布客户端 ID 和密钥时,您需要将它们显示给开发人员。大多数服务都为开发人员提供了一种检索现有应用程序密钥的方法,尽管有些服务只会显示一次密钥,并要求开发人员自己立即存储它。如果您只显示一次密钥,则可以存储它的哈希版本,以避免存储纯文本密钥。
如果您以稍后可向开发人员显示的方式存储机密,则在透露机密时应采取额外的预防措施。保护机密的常用方法是在开发人员查看机密之前插入“重新授权”提示。
GitHub 在进行敏感更改时要求确认密码
该服务要求开发人员确认密码,然后才会透露秘密。当您尝试查看或更新敏感信息时,这种情况在亚马逊或 GitHub 网站上很常见。
Dropbox 隐藏秘密,直到用户点击它
此外,在应用程序详细信息页面上隐藏秘密直到开发人员点击“显示”是防止意外泄露秘密的好方法。
删除应用程序和撤销机密
开发人员需要一种方法来删除(或至少停用)他们的应用程序。为开发人员提供一种方法来撤销并为其应用程序生成新的客户端密钥也是一个好主意。
删除应用程序
当开发者删除应用程序时,服务应该告知开发者删除应用程序的后果。例如,GitHub 告诉开发者所有访问令牌都将被撤销,以及将有多少用户受到影响。
GitHub 要求确认删除应用程序
删除应用程序应立即撤销颁发给该应用程序的所有访问令牌和其他凭证,例如待处理的授权码和刷新令牌。
撤销机密
服务应为开发人员提供重置客户端密钥的方法。如果密钥意外泄露,开发人员需要一种方法来确保可以撤销旧密钥。撤销密钥不一定会使用户的访问令牌失效,因为如果开发人员想使所有用户令牌也失效,他们随时可以删除应用程序。
GitHub 要求确认重置应用程序的机密
重置密钥应使所有现有访问令牌保持活动状态。但是,这意味着任何使用旧密钥的已部署应用程序将无法使用旧密钥刷新访问令牌。已部署的应用程序需要先更新其密钥,然后才能使用刷新令牌。
9. 授权
授权界面是用户授权应用访问其帐户时看到的屏幕。以下部分介绍如何构建授权屏幕、界面中应包含哪些组件以及如何以最佳方式向最终用户呈现界面。
实施 OAuth 服务器时,您将使开发者社区能够构建利用您的平台的应用程序,从而允许应用程序访问并可能修改私人用户内容,或代表用户采取行动。因此,您需要确保向用户提供尽可能多的信息以保护他们的帐户,并确保他们了解应用程序对其帐户的操作。
9.1 授权请求
客户端会将用户浏览器引导至授权服务器以开始 OAuth 流程。客户端可以使用授权代码授予类型或隐式授予。除了参数指定的授予类型外response_type
,请求还会包含许多其他参数来指示请求的具体内容。
服务器端应用程序描述了客户端如何为您的服务构建授权 URL。授权服务器第一次看到用户就是这个授权请求,用户将被定向到带有客户端设置的查询参数的服务器。此时,授权服务器将需要验证请求并提供授权界面,允许用户批准或拒绝请求。
请求参数
以下参数用于开始授权请求。例如,如果授权服务器 URL 为,https://authorization-server.com/auth
则客户端将制作如下 URL 并将用户的浏览器定向到该 URL:
https://authorization-server.com/auth?response_type=code
&client_id=29352735982374239857
&redirect_uri=https://example-app.com/callback
&scope=create+delete
&state=xcoivjuywkdkhvusuye3kch
响应类型
response_type
将设置为code
,表示应用程序期望在成功时收到授权码。
client_id
这client_id
是该应用程序的公共标识符。
redirect_uri
(选修的)
规范redirect_uri
不要求提供 URL,但您的服务应该要求提供 URL。此 URL 必须与开发人员在创建应用程序时注册的 URL 之一匹配,如果不匹配,授权服务器应该拒绝该请求。
scope
(选修的)
请求可能具有一个或多个范围值,表示应用程序请求的额外访问权限。授权服务器需要向用户显示请求的范围。
state
(受到推崇的)
state
应用程序使用该参数来存储特定于请求的数据和/或防止 CSRF 攻击。授权服务器必须将未修改的状态值返回给应用程序。
如果授权服务器支持 PKCE 扩展(在PKCE中描述),则code_challenge
和code_challenge_method
参数也将存在。授权服务器必须在颁发授权码和颁发访问令牌之间记住这些参数。
验证授权请求
授权服务器必须首先验证client_id
请求中的是否对应于有效的应用程序。
如果您的服务器允许应用程序注册多个重定向 URL,则验证重定向 URL 需要两个步骤。如果请求包含参数redirect_uri
,则服务器必须确认它是此应用程序的有效重定向 URL。如果redirect_uri
请求中没有参数,并且只注册了一个 URL,则服务器将使用之前注册的重定向 URL。否则,如果请求中没有重定向 URL,并且没有注册重定向 URL,则会出现错误。
如果client_id
无效,服务器应该立即拒绝请求并向用户显示错误,而不是将用户重定向回应用程序。
重定向 URL 无效
如果授权服务器检测到重定向 URL 存在问题,则需要通知用户该问题,而不是重定向用户。重定向 URL 无效的原因有很多,包括:
- 缺少重定向 URL 参数
- 重定向 URL 参数无效,例如,它是一个无法解析为 URL 的字符串
- 重定向 URL 与应用程序已注册的重定向 URL 之一不匹配
在这些情况下,授权服务器应向用户显示错误,告知他们问题所在。服务器不得将用户重定向回应用程序。这可以避免所谓的“开放重定向器攻击”。服务器应仅在重定向 URL 已注册的情况下将用户重定向到重定向 URL。
其他错误
所有其他错误都应通过将用户重定向到带有查询字符串中的错误代码的重定向 URL 来处理。有关如何响应错误的详细信息,请参阅授权响应部分。
如果请求缺少response_type
参数,或者该参数的值是code
或之外的任何值token
,则服务器可能会返回invalid_request
错误。
由于授权服务器可能要求客户端指定它们是公开的还是机密的,因此它可以拒绝不允许的授权请求。例如,如果客户端指定它们是机密客户端,则服务器可以拒绝使用令牌授予类型的请求。当因这个原因拒绝时,请使用错误代码unauthorized_client
。
如果存在无法识别的范围值,授权服务器应拒绝请求。在这种情况下,服务器可以使用错误代码重定向到回调 URL invalid_scope
。
授权服务器需要存储此请求的“状态”值(和 PKCE 值),以便将其包含在授权响应中。服务器不得修改或假设状态值包含的内容,因为这纯粹是为了客户端的利益。
9.2 要求用户登录
用户点击应用程序的“登录”或“连接”按钮后首先看到的是授权服务器 UI。授权服务器将决定是否要求用户每次访问授权屏幕时都登录,或让用户在一段时间内保持登录状态。如果授权服务器在请求之间记住用户,那么它可能仍需要在用户下次访问时请求用户的许可以授权应用程序。
通常,Twitter 或 Facebook 等网站都希望用户大多数时间都处于登录状态,因此它们提供了一种授权屏幕方法,让用户无需每次登录即可获得简化的体验。但是,根据您服务以及第三方应用程序的安全要求,可能需要要求或允许开发人员选择在用户每次访问授权屏幕时都要求用户登录。
在 Google 的 API 中,应用程序可以添加prompt=login
到授权请求中,这会导致授权服务器强制用户再次登录,然后才会显示授权提示。
无论如何,如果用户已退出或尚未在您的服务上拥有帐户,您都需要在此屏幕上为他们提供一种登录或创建帐户的方式。
Twitter 授权屏幕的未登录视图
您可以以任何您希望的方式对用户进行身份验证,因为 OAuth 2.0 规范中没有对此进行指定。大多数服务使用传统的用户名/密码登录来对用户进行身份验证,但这绝不是解决问题的唯一方法。在企业环境中,一种常见的技术是使用 SAML 来利用组织中现有的身份验证机制,同时避免创建另一个用户名/密码数据库。
这也是授权服务器要求用户进行多因素身份验证的机会。在使用用户的主要用户名和密码进行身份验证后,授权服务器可以要求第二个因素,例如 WebAuthn 或 USB 安全密钥。这种模式的好处是应用程序不需要知道是否正在使用或需要多因素身份验证,因为这完全发生在用户和授权服务器之间,而应用程序看不到。
一旦用户通过授权服务器进行身份验证,它就可以继续处理授权请求并将用户重定向回应用程序。有时,服务器会认为成功登录也意味着用户授权了应用程序。在这种情况下,带有登录提示的授权屏幕需要包含描述通过登录,用户正在批准此授权请求的事实的文本。这将导致以下用户流程。
登录与未登录的用户流程
如果授权服务器需要通过 SAML 或其他内部系统对用户进行身份验证,则用户流程将如下所示
单独身份验证服务器的用户流程
在此流程中,用户登录后将被引导回授权服务器,在那里他们会看到授权请求,就像已经登录一样。
授权接口
OAuth 授权屏幕示例
授权界面是用户收到第三方应用的授权请求时看到的屏幕。这通常也称为“同意屏幕”或“权限提示”。由于用户被要求授予第三方应用某种级别的访问权限,因此您需要确保用户拥有所需的所有信息,以便做出有关授权该应用的明智决定。
通常,只有当用户登录第三方应用程序而非第一方应用程序时才需要这样做。例如,登录 Gmail 时,您不会指望 Google 会询问您是否允许 Gmail 知道您的帐户信息,因为应用程序 (Gmail) 和 OAuth 服务器都是同一家公司的产品的一部分。但是,如果您登录的是将从您的 Gmail 帐户发送电子邮件的第三方邮件列表应用程序,那么作为用户,您有必要了解此第三方应用程序将被授予哪些访问权限以及它将对您的帐户执行哪些操作。
授权接口通常具有以下组件:
网站名称和标志
用户应该能够轻松识别服务,因为他们需要知道他们授予了哪些服务的访问权限。但是,您在主页上标识网站的方式应该与授权界面保持一致。通常,这是通过在屏幕的一致位置显示应用程序名称和徽标,和/或在整个网站上使用一致的配色方案来实现的。
用户识别
如果用户已经登录,您应该向用户表明这一点。这可能类似于在屏幕的顶角显示他们的姓名和照片,就像在网站的其他部分一样。
重要的是,用户知道他们当前登录了哪个帐户,以防他们管理多个帐户,这样他们就不会错误地授权不同的用户帐户。
应用详细信息
授权界面应清楚地标识发出请求的应用程序。除了开发人员提供的应用程序名称外,通常最好也显示网站和应用程序的徽标。这是开发人员注册应用程序时收集的信息。我们在客户端注册中详细讨论了这一点。
请求的范围
授权请求中提供的范围值应清楚地显示给用户。范围值通常是代表特定访问权限的短字符串,因此应向用户显示更易于阅读的版本。
例如,如果某项服务将范围定义为“私有”,表示对私有配置文件数据的读取访问权限,则授权服务器应说明类似“此应用程序将能够查看您的私有配置文件数据”的内容。如果范围明确允许写入访问权限,则也应在描述中指明,例如“此应用程序将能够编辑您的配置文件数据”。
如果不存在范围,但您的服务仍授予对用户帐户的一些基本访问权限,则应包含一条消息,描述应用将获得哪些访问权限。如果省略范围意味着应用获得的唯一内容是用户身份识别,则可以包含一条消息,内容为“此应用希望您登录”或“此应用希望了解您的基本个人资料信息”。
有关如何在服务中有效使用范围的更多信息,请参阅范围。
请求或有效寿命
授权服务器必须决定授权的有效期、访问令牌的持续时间以及刷新令牌的持续时间。
大多数服务不会自动使授权过期,而是要求用户定期查看并撤销他们不再想使用的应用的访问权限。但是,有些服务默认提供有限的令牌有效期,要么允许应用请求更长的持续时间,要么强制用户在授权过期后重新授权应用。
无论您对授权的有效期做出何种决定,您都应该向用户明确说明应用能够代表用户执行操作的时间长度。这可以简单到说一句“此应用将能够访问您的帐户,直到您撤销访问权限”或“此应用将能够访问您的帐户一周”。有关令牌有效期的更多信息,请参阅访问令牌有效期。
允许否认
最后,授权服务器应该为用户提供两个按钮,以允许或拒绝请求。如果用户未登录,您应该提供登录提示,而不是“允许”按钮。
如果用户批准请求,授权服务器将创建一个临时授权代码并将用户重定向回应用程序。如果用户点击“拒绝”,服务器将重定向回应用程序,URL 中带有错误代码。下一节将详细介绍如何处理此响应。
9.3 授权响应
一旦用户完成登录并批准请求,授权服务器就准备将用户重定向回应用程序。
授权码响应
如果请求有效并且用户授予授权请求,则授权服务器将生成授权码并将用户重定向回应用程序,并将授权码和应用程序的“状态”值添加到重定向 URL。
生成授权码
授权码必须在发出后不久过期。OAuth 2.0 规范建议最大有效期为 10 分钟,但实际上,大多数服务将有效期设置得更短,大约为 30-60 秒。授权码本身可以是任意长度,但代码的长度应记录在案。
由于授权码是短暂且一次性使用的,因此您可以将它们实现为自编码令牌。使用这种技术,您可以避免将授权码存储在数据库中,而是将所有必要的信息编码到授权码本身中。您可以使用服务器端环境的内置加密库,也可以使用 JSON Web 签名 (JWS) 等标准。但是,由于此授权码仅供授权服务器使用,因此将它们实现为存储在授权端点和令牌端点可访问的服务器端缓存中的短字符串通常更简单。
无论如何,需要与授权码关联的信息如下。
client_id
– 请求此代码的客户端 ID(或其他客户端标识符)redirect_uri
– 使用的重定向 URL。需要存储此 URL,因为访问令牌请求必须包含相同的重定向 URL,以便在颁发访问令牌时进行验证。- 用户信息– 识别此授权码所属用户的某种方式,例如用户 ID。
- 到期日期– 代码需要包含到期日期,以便其只能持续很短的时间。
- 唯一 ID – 代码需要某种自己的唯一 ID,以便能够检查代码是否曾经被使用过。数据库 ID 或随机字符串就足够了。
- PKCE:
code_challenge
和code_challenge_method
– 当支持 PKCE 时,应用程序提供的这两个值需要存储起来,以便稍后在发出访问令牌时进行验证。
生成授权代码后(通过创建 JWS 编码的字符串或生成随机字符串并将相关信息存储在数据库中),您需要将用户重定向到指定的应用程序重定向 URL。要添加到重定向 URL 的查询字符串中的参数如下:
code
此参数包含客户端稍后将交换访问令牌的授权码。
state
如果初始请求包含状态参数,则响应还必须包含请求中的确切值。客户端将使用此值将此响应与初始请求关联起来。
例如,授权服务器通过发送以下 HTTP 响应来重定向用户。
HTTP/1.1 302 Found``Location: https://example-app.com/redirect?code=g0ZGZmNjVmOWI&state=dkZmYxMzE2
隐式授权类型响应
通过隐式授予(response_type=token
),授权服务器会立即生成访问令牌,并使用令牌和片段中的其他访问令牌属性重定向到回调 URL。
例如,授权服务器通过发送以下 HTTP 响应(为了显示目的而添加额外的换行符)来重定向用户。
HTTP/1.1 302 Found
Location: https://example-app.com/redirect#access_token=MyMzFjNTk2NTk4ZTYyZGI3
&state=dkZmYxMzE2
&token_type=Bearer
&expires_in=86400
您可以看到,这比发布临时的一次性授权码要危险得多。由于攻击者从 HTTP 重定向窃取数据的方法比拦截 HTTPS 请求的方法多得多,因此与授权码流程相比,使用此选项的风险要大得多。
从授权服务器的角度来看,在创建访问令牌并发送 HTTP 重定向时,它无法知道重定向是否成功以及正确的应用程序是否已收到访问令牌。这就像将访问令牌抛向空中,然后祈祷应用程序能接住它。这与授权码方法相反,在授权码方法中,尽管授权服务器无法保证授权码没有被盗,但它至少可以通过要求客户端密钥或 PKCE 代码验证器来防止被盗的授权码被利用。这提供了更高级别的安全性,因为授权服务器现在可以更有信心不会将访问令牌泄露给攻击者。
由于这些原因以及基于浏览器的应用程序的 OAuth 2.0中更多文档中所述的内容,建议不再使用隐式流程。
错误响应
有两种不同的错误需要处理。第一种错误是开发人员在创建授权请求时做错了什么。另一种错误是用户拒绝请求(点击“拒绝”按钮)。
如果请求的语法有问题,例如redirect_uri
或client_id
无效,那么重要的是不要重定向用户,而应该直接显示错误消息。这是为了避免让您的授权服务器被用作开放重定向器。
如果redirect_uri
和client_id
都有效,但仍然存在其他问题,则可以将用户重定向回带有查询字符串错误的重定向 URI。
当重定向回应用程序以指示错误时,服务器会将以下参数添加到重定向 URL:
error
以下列表中的一个 ASCII 错误代码:
invalid_request
– 请求缺少参数、包含无效参数、多次包含某个参数或其他无效因素。access_denied
– 用户或授权服务器拒绝了请求unauthorized_client
– 不允许客户端使用此方法请求授权码,例如,如果机密客户端尝试使用隐式授予类型。unsupported_response_type
– 服务器不支持使用此方法获取授权码,例如,如果授权服务器从未实现隐式授予类型。invalid_scope
– 请求的范围无效或未知。server_error
– 服务器可以使用此错误代码进行重定向,而不是向用户显示 500 内部服务器错误页面。temporarily_unavailable
– 如果服务器正在维护或不可用,则可以返回此错误代码,而不是响应 503 服务不可用状态代码。
error_description
授权服务器可以选择性地包含错误的人性化描述。此参数旨在帮助开发人员了解错误,并不旨在显示给最终用户。此参数的有效字符是除双引号和反斜杠之外的 ASCII 字符集,具体而言是十六进制代码 20-21、23-5B 和 5D-7E。
error_uri
服务器还可以返回一个包含错误信息的可读网页 URL。这旨在帮助开发人员获取有关错误的更多信息,并不旨在显示给最终用户。
state
如果请求包含状态参数,错误响应也必须包含请求中的准确值。客户端可以使用此值将此响应与初始请求关联起来。
例如,如果用户拒绝授权请求,服务器将构建以下 URL 并发送如下所示的 HTTP 重定向响应(URL 中的换行符仅用于说明目的)。
HTTP/1.1 302 Found
Location: https://example-app.com/redirect?error=access_denied
&error_description=The+user+denied+the+request
&error_uri=https%3A%2F%2Foauth2server.com%2Ferror%2Faccess_denied
&state=wxyz1234
9.4 安全注意事项
以下是构建授权服务器时应考虑的一些已知问题。
除了此处列出的注意事项之外,OAuth 2.0 线程模型和安全注意事项RFC 以及OAuth 2.0 安全最佳当前实践中还有更多信息。
网络钓鱼攻击
针对 OAuth 服务器的一种潜在攻击是网络钓鱼攻击。攻击者会制作一个与服务授权页面完全相同的网页,该页面通常包含用户名和密码字段。然后,攻击者可以通过各种手段诱骗用户访问该页面。除非用户可以检查浏览器的地址栏,否则该页面可能看起来与真正的授权页面完全相同,并且用户可能会输入其用户名和密码。
攻击者诱骗用户访问假冒服务器的一种方法是将此钓鱼页面嵌入到本机应用程序的嵌入式 Web 视图中。由于嵌入式 Web 视图不显示地址栏,因此用户无法直观地确认他们是否在访问合法网站。不幸的是,这在移动应用程序中很常见,并且通常是因为开发人员希望通过在整个登录过程中让用户留在应用程序中来提供更好的用户体验。一些 OAuth 提供商鼓励第三方应用程序打开 Web 浏览器或启动提供商的本机应用程序,而不是允许他们在 Web 视图中嵌入授权页面。
对策
确保授权服务器通过 https 提供服务以避免 DNS 欺骗。
授权服务器应该教育开发人员网络钓鱼攻击的风险,并采取措施防止页面嵌入到本机应用程序或 iframe 中。
应该向用户普及网络钓鱼攻击的危险,并教会他们最佳做法,例如只访问他们信任的应用程序,并定期检查他们授权的应用程序列表,以撤销对不再使用的应用程序的访问权限。
在允许其他用户使用应用程序之前,服务可能希望验证第三方应用程序。Instagram 和 Dropbox 等服务目前都这样做,在最初创建应用程序时,该应用程序只能由开发人员或其他白名单用户帐户使用。在应用程序提交审批和审核后,该服务的整个用户群就可以使用它。这让服务有机会检查应用程序如何与服务交互。
点击劫持
在点击劫持攻击中,攻击者会创建一个恶意网站,并在攻击者网页上方的透明 iframe 中加载授权服务器 URL。攻击者的网页堆叠在 iframe 下方,并有一些看似无害的按钮或链接,这些按钮或链接被小心地放置在授权服务器的确认按钮正下方。当用户点击误导性的可见按钮时,他们实际上是在点击授权页面上的不可见按钮,从而授予攻击者应用程序的访问权限。这允许攻击者在用户不知情的情况下诱骗用户授予访问权限。
对策
确保授权 URL 始终直接加载到本机浏览器中,而不是嵌入到 iframe 中,即可防止此类攻击。较新的浏览器允许授权服务器设置 HTTP 标头,X-Frame-Options
而较旧的浏览器可以使用常见的 Javascript“框架破坏”技术。
重定向 URL 操作
攻击者可以使用属于已知良好应用程序的客户端 ID 构建授权 URL,但将重定向 URL 设置为攻击者控制的 URL。如果授权服务器不验证重定向 URL,并且攻击者使用“令牌”响应类型,则用户将使用 URL 中的访问令牌返回到攻击者的应用程序。如果客户端是公共客户端,并且攻击者截获了授权代码,那么攻击者还可以用该代码交换访问令牌。
另一种类似的攻击是,攻击者可以欺骗用户的 DNS,并且注册的重定向不是 https URL。这将允许攻击者伪装成有效的重定向 URL,并以此方式窃取访问令牌。
“开放重定向”攻击是指授权服务器不需要重定向 URL 的精确匹配,而是允许攻击者构建重定向到攻击者网站的 URL。无论这最终是否被用来窃取授权代码或访问令牌,这也是一种危险,因为它可以用于发起其他不相关的攻击。有关开放重定向攻击的更多详细信息,请访问https://oauth.net/advisories/2014-1-covert-redirect/。
对策
授权服务器必须要求应用程序注册一个或多个重定向 URL,并且仅重定向到与先前注册的 URL 完全匹配的 URL。
授权服务器还应要求所有重定向 URL 均为 https。由于这有时会成为开发过程中的负担,因此也可以在应用程序“正在开发”且只能由开发人员访问时允许非 https 重定向 URL,然后在应用程序发布并可供其他用户使用之前要求将重定向 URL 更改为 https URL。
10. 范围
范围是一种限制应用访问用户数据的方式。与授予应用对用户帐户的完整访问权限相比,让应用能够请求更有限的权限范围来代表用户执行操作通常更为有用。
有些应用仅使用 OAuth 来识别用户,因此它们只需要访问用户 ID 和基本个人资料信息。其他应用可能需要知道更敏感的信息,例如用户的生日,或者可能需要能够代表用户发布内容或修改个人资料数据。如果用户确切知道应用程序可以对他们的帐户做什么和不能做什么,他们会更愿意授权应用程序。范围是一种控制访问权限并帮助用户识别他们授予应用程序的权限的方法。
请务必记住,范围与 API 的内部权限系统不同。范围是一种在用户可以执行的操作范围内限制应用程序可以执行的操作的方式。例如,如果您在“客户”组中有一个用户,并且应用程序正在请求“管理员”范围,则 OAuth 服务器不会创建具有“管理员”范围的访问令牌,因为该用户自己无权使用该范围。
范围应该被视为应用程序向使用该应用程序的用户请求许可。
10.1 定义范围
范围是一种让应用程序请求有限访问用户数据的机制。
定义服务范围的挑战在于不要沉迷于定义太多范围。用户需要能够理解他们授予应用程序的访问权限级别,这将以某种列表的形式呈现给用户。当呈现给用户时,他们需要真正了解发生了什么,而不是被信息淹没。如果你为用户设置了过于复杂的界面,他们只会点击“确定”直到应用程序正常运行,并忽略任何警告。
读取与写入
在定义服务范围时,读取和写入访问权限是一个很好的起点。通常,对用户私人个人资料信息的读取访问权限与想要更新个人资料信息的应用的访问控制是分开的。
需要能够代表用户创建内容的应用程序(例如,将推文发布到用户时间线的第三方 Twitter 应用程序)需要与只需要读取用户公开数据的应用程序不同的访问级别。
限制对敏感信息的访问
通常,服务会包含用户帐户的各个方面,这些方面具有不同的安全级别。例如,GitHub有一个单独的范围,允许应用程序访问私有存储库。默认情况下,应用程序无权访问私有存储库,除非它们请求该范围,因此用户可以放心,只有他们选择的应用程序才能访问属于他们组织的私有存储库。
GitHub 提供了一个单独的范围,允许应用程序删除存储库,因此用户可以放心,随机应用程序无法随意删除他们的存储库。
Dropbox为应用程序提供了一种限制自己只能编辑单个文件夹中文件的方法。这样,用户可以试用使用 Dropbox 作为存储或同步机制的应用程序,而不必担心应用程序可能能够读取他们的所有文件。
按功能选择性地启用访问
范围的一个绝佳用途是根据需要的功能选择性地启用对用户帐户的访问权限。例如,Google 为其各种服务(如 Google Drive、Gmail、YouTube 等)提供了一组范围。这意味着需要访问 YouTube API 的应用程序不一定也能访问用户的 Gmail 帐户。
Google 的 API 是有效使用范围的绝佳示例。如需查看 Google OAuth API 支持的范围的完整列表,请访问其 OAuth 2.0 Playground,网址为https://developers.google.com/oauthplayground/
限制对收费资源的访问
如果您的服务提供了可能导致用户产生费用的 API,则范围是防止应用程序滥用此功能的好方法。
让我们以一个使用许可内容提供高级功能的服务为例,在本例中,该服务提供了一个 API,用于汇总给定区域的人口统计数据。用户在使用服务时会产生费用,费用取决于查询区域的大小。登录使用完全不同 API 部分的应用程序的用户将希望确保此应用程序无法使用人口统计 API,因为这会导致该用户产生费用。在这种情况下,服务应该定义一个特殊范围,例如“人口统计”。人口统计 API 应该只响应包含此范围的令牌的 API 请求。
在此示例中,人口统计 API 可以使用令牌自检端点来查找对此令牌有效的范围列表。如果响应的范围列表中不包含“人口统计”,则端点将使用 HTTP 403 响应拒绝该请求。
10.2 用户界面
用户授权应用时看到的界面需要清楚地显示应用正在请求的范围列表。用户可能不知道服务提供的所有范围,因此最好使此文本尽可能清晰明了,避免使用行话和缩写。
如果请求授予应用程序对用户帐户的完全访问权限,或访问其帐户的大部分内容(例如可以执行除更改密码之外的所有操作),则该服务应该明确说明。
例如,Dropbox 授权 UI 上的第一句话是“示例 OAuth 应用程序想要访问 Dropbox 中的文件和文件夹”,其中包含“了解更多”链接,该链接链接到帮助页面,准确描述应用程序将拥有的访问权限。
Flickr 授权界面显示了用户登录时授予应用的三项权限,并清楚地显示了应用不会拥有的权限*。*显示这些权限的好处是,用户可以放心,他们授权的应用不会执行潜在的破坏性操作。
GitHub 在提供用户授予范围的详细信息方面做得非常出色。请求的每个范围在页面上都有一个部分,其中包含名称、图标、简短说明(突出显示这是只读还是读写)以及用于查看更详细说明的下拉菜单。
Google 为其所有服务(包括 Gmail API、Google Drive、Youtube 等)提供单一授权端点。其授权界面以列表形式显示每个范围,并包含一个“信息”图标,您可以单击该图标以获取有关特定范围的更多信息。
单击信息图标将显示一个覆盖层,详细描述此范围允许的内容。
您可以看到,有多种方式可以向用户提供有关 OAuth 授权范围的信息,并且各种服务采用的方法截然不同。在决定要包含何种程度的有关范围的详细信息时,请务必考虑应用程序的隐私和安全要求。
10.3 复选框
虽然这似乎是一个未被充分利用的功能,但 OAuth 2.0 规范明确允许授权服务器授予范围小于应用程序请求的访问令牌。这为一些有趣的可能性留下了空间。
在开始开发 OAuth 2.0 规范之前,Twitter 已部署了 OAuth 1,Twitter 应用生态系统正在快速发展。创建 Twitter 应用时,您可以选择您的应用是否需要读写权限或仅需要读取用户帐户的权限。这种机制导致了 OAuth 2.0 范围概念的发展。但是,这种实现相当有限,因为应用要么请求写权限,要么不请求,如果用户不想授予应用写权限,他们可能会直接拒绝请求。
很快,Twitter 应用程序就形成了一种常见的反模式,即只使用写权限发布推文来宣传该应用程序。其中最臭名昭著的事件之一是 2010 年,应用程序“Twifficiency”声称可以“根据您的 Twitter 活动计算您的 Twitter 效率”,但该应用程序却失控了。您可以使用您的 Twitter 帐户登录该应用程序,它会抓取您过去的推文并进行分析。然而,它还会自动发送推文“我的 Twifficiency 分数是 __%。您的分数是多少?”并附上网站链接。许多人甚至不知道该应用程序在做这件事,或者他们已授予该应用程序在其帐户上发帖的权限。这导致该应用程序迅速走红,因为任何使用该应用程序的人的关注者都会在他们的时间线上看到它。
很多人对此感到不满,并在 Twitter 上大声抱怨。当时在雅虎工作的开发人员 Ben Ward 更进一步,创建了一个可以解决这个问题的潜在用户界面模型,并写了一篇简短的博客文章来解释它。https: //benward.uk/blog/tumblr-968515729
在帖子中,沃德描述了一个用户界面,该界面允许应用程序请求特定权限,用户可以选择授予或不授予每个权限。这将允许用户登录应用程序,但最初不授予其向其帐户发帖的能力。后来,如果用户确实想允许应用程序发帖,应用程序可以提供一种机制,在 Twitter 上重新授权用户。几个月后,沃德被 Twitter 聘用。
这篇文章在参与 OAuth 规范开发的几个人中引起了一些讨论,这些讨论是在 Google Buzz 主题中提出的,现在该主题仅存在于 archive.org 上:http://web.archive.org/web/20100823114908/http ://www.google.com/buzz/tantek/5YHAAmztLcD/t-Look-BenWard-schools-Twitter-on-OAuth 。
时至今日,Twitter 仍未提供这种精细授权。不过,其他服务已开始实施类似功能,让用户在授权流程中拥有更多控制权,而不是让它看起来像一个“单击“确定”继续”的对话框。
Facebook 通过为初始屏幕提供一个简单的 UI 来支持这一想法的变体,但允许用户单击来编辑将授予应用程序的范围,如下所示。
如果您点击“编辑您提供的信息”,您将进入一个界面,其中列出了应用程序请求的每个范围,您可以根据需要取消选中它们。在下面的屏幕截图中,我选择不允许应用程序查看我的好友列表。
此列表中仅显示应用程序请求的范围。这为用户提供了更好的体验,因为他们能够保持控制并更好地了解应用程序如何使用他们的帐户。
FitBit
FitBit 跟踪用户健康的许多方面,例如步数、心率、饮食、睡眠质量、体重等。FitBit API 为第三方应用程序提供所有这些数据的访问权限。由于许多第三方应用程序只会读取或写入某些类型的数据(例如只需要写入体重条目的 wifi 秤),因此 FitBit 提供了精细范围,以便用户可以仅授予对其个人资料某些部分的访问权限。
FitBit 的授权屏幕(如下所示)允许用户有选择地授予或拒绝对应用程序请求的每个特定范围的访问。
GitHub 在 2013 年的一篇博客文章中描述了他们允许用户编辑范围的计划,但是截至 2018 年,还没有任何后续行动。
让用户能够选择授予哪些范围是让人们更愿意使用第三方应用程序的好方法。每个范围旁边的复选框就足够了,或者您可以将控件移动到单独的屏幕,如 Facebook。您需要确保在发送访问令牌响应时,如果与应用程序请求的范围不同,它包含授予的范围列表。有关更多详细信息,请参阅访问令牌响应。
11. 重定向 URI
重定向 URL 是 OAuth 流程的关键部分。用户成功授权应用程序后,授权服务器会将用户重定向回应用程序。由于重定向 URL 将包含敏感信息,因此服务不会将用户重定向到任意位置至关重要。
确保用户仅被重定向到适当位置的最佳方法是要求开发人员在创建应用程序时注册一个或多个重定向 URL。在这些部分中,我们将介绍如何处理移动应用程序的重定向 URL、如何验证重定向 URL 以及如何处理错误。
11.1 重定向 URL 注册
为了避免用户遭受开放重定向器攻击,您必须要求开发人员为应用程序注册一个或多个重定向 URL。授权服务器绝不能重定向到任何其他位置。注册新应用程序包括创建注册表单,以允许开发人员为其应用程序注册重定向 URL。
如果攻击者可以在用户到达授权服务器之前操纵重定向 URL,他们就可能导致服务器将用户重定向到恶意服务器,该服务器会将授权代码发送给攻击者。这是攻击者试图拦截 OAuth 交换并窃取访问令牌的一种方式。如果授权端点不限制它将重定向到的 URL,那么它就被视为“开放重定向器”,并且可以与其他东西结合使用来发起与 OAuth 甚至不一定相关的攻击。
有效的重定向 URL
当您构建表单以允许开发人员注册重定向 URL 时,您应该对他们输入的 URL 进行一些基本验证。
注册的重定向 URL 可以包含查询字符串参数,但不得包含任何片段。如果开发者尝试注册包含片段的重定向 URL,注册服务器应拒绝该请求。
请注意,对于原生应用和移动应用,平台可能允许开发者注册 URL 方案,然后可以在重定向 URL 中使用。这意味着授权服务器应允许注册任意 URL 方案,以支持注册原生应用的重定向 URL。有关更多信息,myapp://
请参阅移动和原生应用。
根据请求进行定制
开发人员经常会认为他们需要能够在每个授权请求中使用不同的重定向 URL,并会尝试更改每个请求的查询字符串参数。这不是重定向 URL 的预期用途,授权服务器不应允许这种做法。服务器应拒绝任何重定向 URL 与注册 URL 不完全匹配的授权请求。
如果客户端希望在重定向 URL 中包含特定于请求的数据,则可以使用“state”参数来存储用户重定向后将包含的数据。它可以将数据编码在 state 参数本身中,也可以使用 state 参数作为会话 ID 来将状态存储在服务器上。
11.2 本机应用程序的重定向 URL
原生应用是安装在设备上的客户端,例如桌面应用或原生移动应用。在支持与安全性和用户体验相关的原生应用时,需要注意一些事项。
授权端点通常会将用户重定向回客户端已注册的重定向 URL。根据平台的不同,原生应用可以声明 URL 模式,也可以注册用于启动应用程序的自定义 URL 方案。例如,iOS 应用程序可以注册自定义协议(例如),myapp://
然后使用 redirect_uri myapp://callback
。
应用程序声明的 https URL 重定向
某些平台(Android 和 iOS 9 及更高版本的 iOS)允许应用覆盖特定的 URL 模式,以启动本机应用而不是 Web 浏览器。例如,应用程序可以注册https://app.example.com/auth
,每当 Web 浏览器尝试重定向到该 URL 时,操作系统就会启动本机应用。
如果操作系统支持声明 URL,则应使用此方法。如果操作系统进行某种程度的验证,确认开发人员可以控制此 Web URL,则操作系统可以保证本机应用程序的身份。如果操作系统不支持此功能,则应用程序必须改用自定义 URL 方案。
自定义 URL 方案
大多数移动和桌面操作系统允许应用程序注册自定义 URL 方案,当从系统浏览器访问具有该方案的 URL 时,该方案将启动该应用程序。
使用此方法,本机应用程序会正常启动 OAuth 流程,通过使用标准授权代码参数启动系统浏览器。唯一的区别是重定向 URL 将是具有应用程序自定义方案的 URL。
当授权服务器发送Location
旨在将用户重定向到的标头时myapp://callback#token=....
,手机将启动应用程序,并且应用程序将能够恢复授权过程,从 URL 解析访问令牌并将其存储在内部。
自定义 URL 方案命名空间
由于没有集中注册 URL 方案的方法,应用程序必须尽力选择不会相互冲突的 URL 方案。
您的服务可以通过要求 URL 方案遵循特定模式来提供帮助,并且只允许开发人员注册与该模式匹配的自定义方案。
例如,Facebook 根据应用的客户端 ID 为每个应用生成一个 URL 方案。例如,fb00000000://
其中的数字对应于应用的客户端 ID。这提供了一种生成全局唯一 URL 方案的相当可靠的方法,因为其他应用不太可能使用具有此模式的 URL 方案。
应用程序的另一个选项是使用反向域名模式,该模式由应用程序发布者控制,从而产生例如 URL 方案com.example.myapp
。如果您愿意,服务也可以强制执行此操作。
11.3 重定向 URL 验证
有三种情况您需要验证重定向 URL。
- 当开发人员在创建应用程序时注册重定向 URL 时
- 在授权请求中(授权码和隐式授予类型)
- 当应用程序用授权码交换访问令牌时
重定向 URL 注册
如创建应用程序中所述,该服务应允许开发人员在创建应用程序时注册一个或多个重定向 URL。重定向 URL 的唯一限制是它不能包含片段组件。该服务必须允许开发人员使用自定义 URL 方案注册重定向 URL,以便支持某些平台上的本机应用程序。
授权请求
当应用程序启动 OAuth 流程时,它会将用户引导至您服务的授权端点。请求的 URL 中将包含多个参数,包括重定向 URL。
此时,授权服务器必须验证重定向 URL,以确保请求中的 URL 与应用程序的已注册 URL 之一匹配。请求还将包含一个client_id
参数,因此服务应根据该参数查找重定向 URL。攻击者完全有可能使用一个应用程序的客户端 ID 和攻击者的重定向 URL 来制作授权请求,这就是需要注册的原因。
服务应查找 URL 的完全匹配,避免仅匹配特定 URL 的一部分。(如果客户端需要自定义每个请求,则可以使用 state 参数。)简单的字符串匹配就足够了,因为重定向 URL 无法根据请求进行自定义。服务器需要做的就是检查请求中的重定向 URL 是否与开发人员在注册其应用程序时输入的重定向 URL 之一匹配。
如果重定向 URL 不是已注册的重定向 URL 之一,则服务器必须立即显示错误以表明这一点,并且不重定向用户。这可以避免您的授权服务器被用作开放重定向器。
授予访问令牌
令牌端点将收到一个请求,要求用授权码交换访问令牌。此请求将包含重定向 URL 以及授权码。作为一项额外的安全措施,服务器应验证此请求中的重定向 URL 是否与此授权码的初始授权请求中包含的重定向 URL 完全匹配。如果重定向 URL 不匹配,服务器将拒绝该请求并显示错误。
12. 访问令牌
访问令牌是应用程序用来代表用户发出 API 请求的东西。访问令牌代表特定应用程序访问用户数据特定部分的授权。
访问令牌不必采用任何特定格式,但对于不同的选项有不同的考虑,本章后面将对此进行讨论。就客户端应用程序而言,访问令牌是一个不透明的字符串,它将获取字符串的任何内容并将其用于 HTTP 请求。资源服务器需要了解访问令牌的含义以及如何验证它,但应用程序永远不会关心了解访问令牌的含义。
访问令牌在传输和存储过程中必须保密。唯一可以看到访问令牌的各方是应用程序本身、授权服务器和资源服务器。应用程序应确保同一设备上的其他应用程序无法访问访问令牌的存储。访问令牌只能通过 HTTPS 连接使用,因为通过非加密通道传递访问令牌会使第三方很容易拦截。
令牌端点是应用发出请求以获取用户访问令牌的地方。本节介绍如何验证令牌请求以及如何返回适当的响应和错误。
12.1 授权码请求
当应用程序用授权码交换访问令牌时,将使用授权码授予。用户通过重定向 URL 返回到应用程序后,应用程序将从 URL 获取授权码并使用它来请求访问令牌。此请求将发送到令牌端点。
请求参数
访问令牌请求将包含以下参数。
grant_type
(必需的)
该grant_type
参数必须设置为“authorization_code”。
code
(必需的)
此参数是客户端先前从授权服务器收到的授权码。
redirect_uri
(可能必需)
如果初始授权请求中包含重定向 URI,则服务也必须在令牌请求中要求它。令牌请求中的重定向 URI 必须与生成授权代码时使用的重定向 URI 完全匹配。否则,服务必须拒绝该请求。
code_verifier
(PKCE 支持所需)
如果客户端code_challenge
在初始授权请求中包含了参数,则它现在必须通过在 POST 请求中发送该参数来证明它拥有用于生成哈希的密钥。这是用于计算先前在参数中发送的哈希的纯文本字符串code_challenge
。
client_id
(如果没有其他客户端身份验证则需要)
如果客户端通过 HTTP Basic Auth 或其他方法进行身份验证,则此参数不是必需的。否则,此参数是必需的。
如果客户端被授予了客户端密钥,则服务器必须对客户端进行身份验证。对客户端进行身份验证的一种方法是接受此请求中的另一个参数。client_secret
或者,授权服务器可以使用 HTTP Basic Auth。从技术上讲,规范允许授权服务器支持任何形式的客户端身份验证,并提到公钥/私钥对作为一种选择。实际上,大多数消费者服务器都支持使用此处提到的一种或两种方法对客户端进行身份验证的更简单方法。有关对客户端进行身份验证的更高级方法,请参阅 RFC 7523,其中定义了一种使用签名的 JWT 作为客户端身份验证的方法。
验证授权码授予
在检查所有必需参数并对客户端进行身份验证(如果客户端已颁发凭证)后,授权服务器可以继续验证请求的其他部分。
然后,服务器检查授权码是否有效,并且未过期。然后,服务必须验证请求中提供的授权码是否已发给所标识的客户端。最后,服务必须确保存在的重定向 URI 参数与用于请求授权码的重定向 URI 相匹配。
对于 PKCE 支持,授权服务器应计算此令牌请求中显示的 SHA256 哈希值code_verifier
,并将其与code_challenge
授权请求中显示的 进行比较。如果它们匹配,授权服务器可以确信发出此令牌请求的客户端与发出原始授权请求的客户端是同一个。
如果一切检查无误,服务就可以生成访问令牌并做出响应。
例子
以下示例显示了机密客户端的授权授予请求。
POST /oauth/token HTTP/1.1
Host: authorization-server.com
grant_type=authorization_code
&code=xxxxxxxxxxx
&redirect_uri=https://example-app.com/redirect
&code_verifier=Th7UHJdLswIYQxwSg29DbK1a_d9o41uNMTRmuH0PM8zyoMAQ
&client_id=xxxxxxxxxx
&client_secret=xxxxxxxxxx
有关生成访问令牌或响应错误时返回的参数的详细信息,请参阅访问令牌响应。
安全注意事项
防止重放攻击
如果授权码被多次使用,授权服务器必须拒绝后续请求。如果授权码存储在数据库中,这很容易实现,因为只需将其标记为已使用即可。
如果您正在实施自编码授权代码(如我们的示例代码所示),则需要跟踪在令牌的整个生命周期内使用过的令牌。实现此目的的一种方法是将代码缓存在缓存中,以供代码的整个生命周期使用。这样,在验证代码时,我们可以首先通过检查缓存中的代码来检查它们是否已被使用。一旦代码达到其到期日期,它将不再位于缓存中,但我们无论如何都可以根据到期日期拒绝它。
如果代码被多次使用,则应将其视为攻击。如果可能,服务应撤销之前使用此授权代码颁发的访问令牌。
12.2 密码授予
当应用程序用用户名和密码交换访问令牌时,将使用密码授权。这正是 OAuth 最初要防止的事情,因此您绝不应该允许第三方应用程序使用此授权。
支持密码授权非常有限,因为无法向此流程添加多因素授权,并且检测暴力攻击的选项更加有限。实践中不应使用此流程。
最新的OAuth 2.0 安全最佳当前实践规范实际上建议完全不要使用密码授予,并且它在 OAuth 2.1 更新中被删除。
请求参数
访问令牌请求将包含以下参数。
grant_type
(必需)- 该grant_type
参数必须设置为“密码”。username
(必填)– 用户的用户名。password
(必填)—— 用户的密码。scope
(可选) – 应用程序请求的范围。- 客户端身份验证(如果客户端被授予机密则需要)
如果客户端已获得密钥,则客户端必须验证此请求。通常,服务将允许附加请求参数client_id
和client_secret
,或接受 HTTP Basic Auth 标头中的客户端 ID 和密钥。
以下是该服务将收到的密码授予示例。
POST /oauth/token HTTP/1.1
Host: authorization-server.com
grant_type=password
&username=[email protected]
&password=1234luggage
&client_id=xxxxxxxxxx
&client_secret=xxxxxxxxxx
有关生成访问令牌或响应错误时返回的参数的详细信息,请参阅访问令牌响应。
12.3 客户端凭证
当应用程序请求访问令牌来访问其自己的资源(而不是代表用户)时,将使用客户端凭据授予。
请求参数
grant_type
(必需的)
该grant_type
参数必须设置为client_credentials
。
scope
(选修的)
您的服务可以支持客户端凭据授予的不同范围。实际上,支持此功能的服务并不多。
客户端身份验证(必需)
客户端需要针对此请求进行身份验证。通常,服务将允许其他请求参数client_id
和client_secret
,或接受 HTTP Basic auth 标头中的客户端 ID 和机密。
例子
以下是该服务将收到的授权码授予示例。
POST /token HTTP/1.1
Host: authorization-server.com
grant_type=client_credentials
&client_id=xxxxxxxxxx
&client_secret=xxxxxxxxxx
有关生成访问令牌或响应错误时返回的参数的详细信息,请参阅访问令牌响应。
12.4 访问令牌响应
成功响应
如果访问令牌请求有效,授权服务器需要生成访问令牌(和可选的刷新令牌)并将其返回给客户端,通常还会返回一些有关授权的附加属性。
带有访问令牌的响应应包含以下属性:
access_token
(必需)授权服务器颁发的访问令牌字符串。token_type
(必需)此令牌的类型,通常只是字符串“Bearer”。expires_in
(推荐)如果访问令牌过期,服务器应该回复授予访问令牌的时间长度。refresh_token
(可选)如果访问令牌即将过期,则返回刷新令牌很有用,应用程序可以使用它来获取另一个访问令牌。但是,使用隐式授权颁发的令牌不能颁发刷新令牌。scope
(可选)如果用户授予的范围与应用请求的范围相同,则此参数为可选。如果授予的范围与请求的范围不同(例如,如果用户修改了范围),则此参数为必需。
当使用访问令牌进行响应时,服务器还必须包含额外的Cache-Control: no-store
HTTP 标头,以确保客户端不会缓存此请求。
例如,成功的令牌响应可能如下所示:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
{
"access_token":"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3",
"token_type":"Bearer",
"expires_in":3600,
"refresh_token":"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk",
"scope":"create"
}
访问令牌
OAuth 2.0 Bearer 令牌的格式实际上是在单独的规范RFC 6750中描述的。规范要求的令牌没有定义结构,因此您可以生成字符串并按照自己的意愿实现令牌。Bearer 令牌中的有效字符是字母数字和以下标点符号:
-._~+/
Bearer Tokens 的一个简单实现是生成一个随机字符串并将其与相关的用户和范围信息一起存储在数据库中,或者更高级的系统可以使用自编码令牌,其中令牌字符串本身包含所有必要的信息。
响应失败
如果访问令牌请求无效,例如重定向 URL 与授权期间使用的 URL 不匹配,则服务器需要返回错误响应。
错误响应将返回 HTTP 400 状态代码(除非另有说明),并带有error
和error_description
参数。error
参数将始终是下面列出的值之一。
invalid_request
– 请求缺少参数,因此服务器无法继续处理请求。如果请求包含不受支持的参数或重复参数,也可能会返回此信息。invalid_client
– 客户端身份验证失败,例如请求包含无效的客户端 ID 或密钥。在这种情况下,发送 HTTP 401 响应。invalid_grant
– 授权代码(或密码授予类型的用户密码)无效或已过期。如果授权授予中给出的重定向 URL 与此访问令牌请求中提供的 URL 不匹配,您也会返回此错误。invalid_scope
– 对于包含范围(密码或 client_credentials 授予)的访问令牌请求,此错误表示请求中的范围值无效。unauthorized_client
– 此客户端无权使用请求的授权类型。例如,如果您限制哪些应用程序可以使用隐式授权,则其他应用程序将返回此错误。unsupported_grant_type
– 如果请求的授权类型授权服务器无法识别,请使用此代码。请注意,未知的授权类型也使用此特定错误代码,而不是使用invalid_request
上述代码。
返回错误响应时有两个可选参数,error_description
和error_uri
。这些参数旨在向开发人员提供有关错误的更多信息,而不是向最终用户显示。但是,请记住,许多开发人员会将此错误文本直接传递给最终用户,无论您如何警告他们,因此最好确保它至少对最终用户也有帮助。
该error_description
参数只能包含 ASCII 字符,并且最多应为一两句话,描述错误的情况。这里error_uri
是链接到 API 文档以获取有关如何纠正遇到的特定错误的信息的好地方。
整个错误响应以 JSON 字符串形式返回,类似于成功响应。以下是错误响应的示例。
HTTP/1.1 400 Bad Request
Content-Type: application/json
Cache-Control: no-store
{
"error": "invalid_request",
"error_description": "Request was missing the 'redirect_uri' parameter.",
"error_uri": "See the full API docs at https://authorization-server.com/docs/access_token"
}
12.5 自编码访问令牌
自编码令牌提供了一种避免将令牌存储在数据库中的方法,即在令牌字符串本身中编码所有必要信息。这样做的主要好处是,API 服务器无需对每个 API 请求进行数据库查找即可验证访问令牌,从而使 API 更易于扩展。
OAuth 2.0 Bearer Tokens 的好处是,应用程序不需要知道您决定如何在服务中实现访问令牌。这意味着您可以在以后更改实现,而不会影响客户端。
如果您已经拥有一个可水平扩展的分布式数据库系统,那么使用自编码令牌可能不会给您带来任何好处。事实上,如果您已经解决了分布式数据库问题,那么使用自编码令牌只会带来新的问题,因为使自编码令牌无效会成为额外的障碍。
有许多方法可以自行编码令牌。您选择的实际方法仅对您的实现很重要,因为令牌信息不会暴露给外部开发人员。
实现自编码令牌的最常见方法是使用 JWS 规范,创建要包含在令牌中的所有数据的 JSON 序列化表示,并使用只有授权服务器知道的私钥对生成的字符串进行签名。
RFC 9068根据许多大型 OAuth 提供商的实际部署经验,定义了使用 JWT 作为访问令牌的标准方法。此规范定义了在包含有关身份验证、授权和身份的声明时使用的数据结构。有关更多详细信息,请参阅https://oauth.net/2/jwt-access-tokens/。
JWT 访问令牌编码
以下代码以 PHP 编写,并使用Firebase PHP-JWT库来编码和验证令牌。您需要包含该库才能运行示例代码
实际上,授权服务器将拥有一个用于签署令牌的私钥,而资源服务器将从授权服务器元数据中获取公钥以用于验证令牌。在此示例中,我们每次都会生成一个新的私钥,并使用相同的脚本验证令牌。实际上,您需要将私钥存储在某个地方,以便使用相同的密钥一致地签署令牌。
<?php
use \Firebase\JWT\JWT;
# Generate a private key to sign the token.
# The public key would need to be published at the authorization
# server if a separate resource server needs to validate the JWT
$private_key = openssl_pkey_new([
'digest_alg' => 'sha256',
'private_key_bits' => 1024,
'private_key_type' => OPENSSL_KEYTYPE_RSA
]);
# Set the user ID of the user this token is for
$user_id = "1000";
# Set the client ID of the app that is generating this token
$client_id = 'https://example-app.com';
# Provide the list of scopes this token is valid for
$scope = 'read write';
$token_data = array(
# Issuer (the authorization server identifier)
'iss' => 'https://' . $_SERVER['PHP_SELF'],
# Expires At
'exp' => time()+7200, // Valid for 2 hours
# Audience (The identifier of the resource server)
'aud' => 'api://default',
# Subject (The user ID)
'sub' => $user_id,
# Client ID
'client_id' => $client_id,
# Issued At
'iat' => time(),
# Identifier of this token
'jti' => microtime(true).'.'.bin2hex(random_bytes(10)),
# The list of OAuth scopes this token includes
'scope' => $scope
);
$token_string = JWT::encode($token_data, $private_key, 'RS256');
这将产生如下字符串:
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodH``RwczovL2F1dGhvcml6YXRpb24tc2VydmVyLmNvbS8iLCJleHAiO``jE2MzczNDQ1NzIsImF1ZCI6ImFwaTovL2RlZmF1bHQiLCJzdWIi``OiIxMDAwIiwiY2xpZW50X2lkIjoiaHR0cHM6Ly9leGFtcGxlLWF``wcC5jb20iLCJpYXQiOjE2MzczMzczNzIsImp0aSI6IjE2MzczMz``czNzIuMjA1MS42MjBmNWEzZGMwZWJhYTA5NzMxMiIsInNjb3BlI``joicmVhZCB3cml0ZSJ9.SKDO_Gu96WeHkR_Tv0d8gFQN1SEdpN8``S_h0IJQyl_5syvpIRA5wno0VDFi34k5jbnaY5WHn6Y912IOmg6t``MO91KlYOU1MNdVhHUoPoNUzYtl_nNab7Ywe29kxgrekm-67ZInD``I8RHbSkL7Z_N9eZz_J8c3EolcsoIf-Dd5n9y_Y
此 token 由三部分组成,以句点分隔。第一部分描述使用的签名方法。第二部分包含 token 数据。第三部分是签名。
例如,此令牌的第一个组件是此 JSON 对象:
{
"typ":"JWT",
"alg":"RS256"
}
第二个组件包含 API 端点处理请求所需的实际数据,例如用户标识和范围访问。
{
"iss": "https://authorization-server.com/",
"exp": 1637344572,
"aud": "api://default",
"sub": "1000",
"client_id": "https://example-app.com",
"iat": 1637337372,
"jti": "1637337372.2051.620f5a3dc0ebaa097312",
"scope": "read write"
}
然后对这两个组件进行 base64 编码,JWT 库计算两个字符串的 RS256 签名,然后用句点将所有三个部分连接起来。
解码
可以使用相同的 JWT 库来验证访问令牌。该库将同时解码和验证签名,如果签名无效或令牌的到期日期已过,则会引发异常。
您需要与签署令牌的私钥相对应的公钥。通常您可以从授权服务器的元数据文档中获取此公钥,但在此示例中,我们将从先前生成的私钥中派生公钥。
注意:任何人都可以通过对令牌字符串的中间部分进行 base64 解码来读取令牌信息。因此,重要的是不要在令牌中存储私人信息或您不想让用户或开发人员看到的信息。如果您想隐藏令牌信息,可以使用 JSON Web 加密规范来加密令牌中的数据。
$public_key = openssl_pkey_get_details($private_key)['key'];
try {
# Note: You must provide the list of supported algorithms in order to prevent
# an attacker from bypassing the signature verification. See:
# https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
$token = JWT::decode($token_string, $jwt_key, ['RS256']);
$error = false;
} catch(\Firebase\JWT\ExpiredException $e) {
$token = false;
$error = 'expired';
$error_description = 'The token has expired';
} catch(\Firebase\JWT\SignatureInvalidException $e) {
$token = false;
$error = 'invalid';
$error_description = 'The token provided was malformed';
} catch(Exception $e) {
$token = false;
$error = 'unauthorized';
$error_description = $e->getMessage();
}
if($error) {
header('HTTP/1.1 401 Unauthorized');
echo json_encode(array(
'error'=>$error,
'error_description'=>$error_description
));
die();
} else {
// Now $token has all the data that we encoded in it originally
print_r($token);
}
此时,服务已获得所需的所有信息,例如用户 ID、范围等,无需进行数据库查找。接下来,它可以检查以确保访问令牌未过期,可以验证范围是否足以执行请求的操作,然后可以处理请求。
使无效
由于无需进行数据库查找即可验证令牌,因此在令牌过期之前无法使其失效。您需要采取其他步骤来使自编码的令牌失效,例如临时存储已撤销令牌的列表,这是令牌中声明的一种用途。有关更多信息,jti
请参阅刷新访问令牌。
12.6 访问令牌有效期
当您的服务发出访问令牌时,您需要决定令牌的有效期。遗憾的是,没有适用于所有服务的一揽子解决方案。不同的选项会带来各种权衡,因此您应该选择最适合您应用程序需求的选项(或选项组合)。
短期访问令牌和长期刷新令牌
授予令牌的常用方法是结合使用访问令牌和刷新令牌,以实现最大的安全性和灵活性。OAuth 2.0 规范推荐使用此选项,并且一些较大的实现也采用了这种方法。
通常,使用此方法的服务将颁发有效期从几个小时到几周不等的访问令牌。当服务颁发访问令牌时,它还会生成一个永不过期的刷新令牌,并在响应中返回该令牌。(请注意,不能使用隐式授权颁发刷新令牌。)
当访问令牌过期时,应用程序可以使用刷新令牌来获取新的访问令牌。它可以在后台执行此操作,无需用户参与,因此对用户来说这是一个无缝的过程。
这种方法的主要好处是服务可以使用自编码访问令牌,无需查询数据库即可验证。但是,这意味着无法直接使这些令牌过期,因此,这些令牌的过期时间较短,因此应用程序必须不断刷新它们,让服务有机会在必要时撤销应用程序的访问权限。
从第三方开发者的角度来看,处理刷新令牌通常令人沮丧。开发者强烈倾向于使用不会过期的访问令牌,因为这样处理的代码要少得多。为了帮助缓解这些问题,服务通常会将令牌刷新逻辑构建到其 SDK 中,以便该过程对开发者透明。
总之,在以下情况下使用短期访问令牌和长寿命刷新令牌:
短期访问令牌且无刷新令牌
如果您想确保用户知道哪些应用程序正在访问他们的帐户,服务可以发出相对短暂的访问令牌,而无需刷新令牌。访问令牌的有效期可能从当前应用程序会话持续到几周。当访问令牌过期时,应用程序将被迫让用户再次登录,这样您作为服务就知道用户不断参与重新授权应用程序。
通常,如果第三方应用程序意外或恶意泄露访问令牌,则服务会使用此选项,因为此类服务存在很高的损害风险。通过要求用户不断重新授权应用程序,服务可以确保在攻击者从服务中窃取访问令牌时,潜在损害受到限制。
通过不发放刷新令牌,这使得应用程序无法在用户不在屏幕前的情况下持续使用访问令牌。需要访问权限才能持续同步数据的应用程序将无法在此方法下这样做。
从用户的角度来看,这是最有可能让人沮丧的选项,因为它看起来就像用户必须不断地重新授权应用程序。
总之,在以下情况下,请使用无刷新令牌的短期访问令牌:
- 您希望最大程度地防范访问令牌泄露的风险
- 您希望强制用户了解他们授予的第三方访问权限
- 你不希望第三方应用离线访问用户的数据
永不过期的访问令牌
对于开发者来说,永不过期的访问令牌是最简单的方法。如果您选择此选项,则必须考虑您要做出的权衡。
如果您希望能够任意撤销自编码令牌,则使用自编码令牌是不切实际的。因此,您需要将这些令牌存储在某种数据库中,以便可以根据需要删除它们或将它们标记为无效。
请注意,即使服务打算发布用于正常用途的永不过期的访问令牌,您仍然需要提供一种在特殊情况下使其过期的机制,例如,如果用户明确想要撤销应用程序的访问权限,或者如果用户帐户被删除。
对于测试自己的应用程序的开发人员来说,永不过期的访问令牌要容易得多。您甚至可以为开发人员预先生成一个或多个永不过期的访问令牌,并在应用程序详细信息屏幕上向他们显示。这样,他们可以立即开始使用令牌发出 API 请求,而不必担心设置 OAuth 流程以开始测试您的 API。
总之,在以下情况下请使用无过期时间的访问令牌:
- 你有任意撤销访问令牌的机制
- 如果令牌泄露,你不会面临很大的风险
- 您希望为开发人员提供一个简单的身份验证机制
- 您希望第三方应用程序能够离线访问用户的数据
12.7 刷新访问令牌
本节介绍如何允许您的开发人员使用刷新令牌获取新的访问令牌。如果您的服务随访问令牌一起发出刷新令牌,则您需要实现此处描述的刷新授权类型。
请求参数
访问令牌请求将包含以下参数。
grant_type
(必需的):该grant_type
参数必须设置为“refresh_token”。refresh_token
(必需的):先前向客户端发出的刷新令牌。scope
(选修的)请求的范围不得包含原始访问令牌中未颁发的其他范围。通常,这不会包含在请求中,如果省略,服务应颁发与之前颁发的范围相同的访问令牌。客户端身份验证(如果客户端被授予机密则需要)通常,刷新令牌仅用于机密客户端。但是,由于可以使用没有客户端密钥的授权代码流程,因此刷新授权也可以由没有密钥的客户端使用。如果客户端被授予了密钥,则客户端必须验证此请求。通常,服务将允许其他请求参数
client_id
和client_secret
,或接受 HTTP Basic auth 标头中的客户端 ID 和密钥。如果客户端没有密钥,则此请求中将不存在客户端身份验证。
验证刷新令牌授予
在检查所有必需参数并对客户端进行身份验证(如果客户端获得了机密)之后,授权服务器可以继续验证请求的其他部分。
然后,服务器检查刷新令牌是否有效,并且未过期。如果刷新令牌已颁发给机密客户端,则服务必须确保请求中的刷新令牌已颁发给经过身份验证的客户端。
如果一切正常,服务可以生成访问令牌并做出响应。服务器可能会在响应中发出新的刷新令牌,但如果响应中不包含新的刷新令牌,则客户端会假定现有刷新令牌仍然有效。
例子
以下是该服务将收到的刷新授权示例。
POST /oauth/token HTTP/1.1
Host: authorization-server.com
grant_type=refresh_token
&refresh_token=xxxxxxxxxxx
&client_id=xxxxxxxxxx
&client_secret=xxxxxxxxxx
回复
对刷新令牌授予的响应与颁发访问令牌时相同。您可以选择在响应中颁发新的刷新令牌,或者如果您不包含新的刷新令牌,则客户端假定当前刷新令牌将继续有效。
13. 列出授权
一旦用户开始授权多个应用,让许多应用可以访问他们的帐户,就需要提供一种方式让用户管理有权访问的应用程序。这通常在帐户设置页面或帐户隐私页面中呈现给用户。
OAuth 2.0 规范中没有任何内容要求用户能够撤销访问权限,甚至没有建议如何做到这一点,所以我们将研究几个主要的 API 提供商,以获得有关如何实现这一点的灵感。
大多数提供商都有一个页面,其中列出了用户已授权访问其帐户的所有应用程序。通常会显示一些有关该应用程序的信息,以及旨在向用户提供有关此应用程序何时以及为何具有访问权限的信息。
谷歌
Google 在https://security.google.com/settings/security/permissions上提供了您在您的帐户上授权的应用程序列表。
您已授权访问您的 Google 帐户的应用程序列表
该列表显示申请图标、名称以及申请获准范围的摘要。单击其中一个可展开该部分以显示更多详细信息。
有权访问您的 Google 帐户的某个应用程序的详细信息
此视图提供了已授予范围的更详细列表,以及您授权申请的日期。
推特
Twitter 在https://twitter.com/settings/applications提供了您已授权的应用程序列表。
您已授权访问 Twitter 帐户的应用程序列表
Twitter 会显示授予的范围(只读、读/写、读/写/直接消息),以及该应用是否可以看到您的电子邮件地址。该列表包括您授权该应用的日期。这让用户可以轻松地撤销他们一段时间未使用的应用的凭据。
GitHub
GitHub 在https://github.com/settings/applications提供了您已授权的应用程序列表。
您已授权访问您的 GitHub 帐户的应用程序列表
GitHub 提供的列表包含应用程序上次使用情况的描述,以便让您了解如果应用程序在一段时间内未使用,是否可以安全地撤销其凭据。
单击某个应用程序可以提供有关该应用程序访问的更多详细信息。
有权访问你的 GitHub 帐户的一个应用程序的详细信息
在这里您可以看到该应用程序对您的帐户拥有的权限(范围)。
您可以在此处找到其他服务授权页面的链接。
撤销访问权限
所有这些服务都为用户提供了一种撤销特定应用程序对其帐户的访问权限的方法。下一节将更详细地介绍如何撤销访问权限。
13.1 撤销访问权限
您可能需要出于多种原因撤销应用程序对用户帐户的访问权限。
- 用户明确希望撤销应用程序的访问权限,例如,如果他们发现授权页面上列出了他们不再想使用的应用程序
- 开发人员希望撤销其应用程序的所有用户令牌
- 开发人员删除了他们的应用程序
- 作为服务提供商,您已确定某个应用程序已被入侵或属于恶意软件,并希望禁用它
根据您实现生成访问令牌的方式,撤销它们将以不同的方式进行。
令牌数据库
如果您将访问令牌存储在数据库中,那么撤销属于特定用户的所有令牌就相对容易了。您可以轻松编写查询来查找并删除属于该用户的令牌,例如在令牌表中查找他们的令牌user_id
。假设您的资源服务器通过在数据库中查找来验证访问令牌,那么下次撤销的客户端发出请求时,他们的令牌将无法验证。
自编码令牌
如果您拥有真正无状态的令牌验证机制,并且您的资源服务器正在验证令牌而不与其他系统共享信息,那么唯一的选择就是等待所有未到期的令牌过期,并通过阻止来自该客户端 ID 的任何刷新令牌请求来阻止应用程序为该用户生成新令牌。这是在使用自编码令牌时使用极短有效期令牌的主要原因。
如果您能够承受一定程度的状态性,则可以将令牌标识符的撤销列表推送到资源服务器,并且资源服务器可以在验证令牌时检查该列表。访问令牌可以包含唯一 ID(例如声明jti
),可用于跟踪各个令牌。如果您想撤销特定令牌,则需要将该令牌放入jti
资源服务器可以检查的某个列表中。当然,这意味着您的资源服务器不再进行纯粹的无状态检查,因此这可能不是适用于每种情况的选项。
您还需要使与访问令牌一起颁发的应用程序刷新令牌失效。撤销刷新令牌意味着下次应用程序尝试刷新访问令牌时,新访问令牌的请求将被拒绝。
14. 资源服务器
资源服务器是 OAuth 2.0 术语,指的是 API 服务器。应用程序获得访问令牌后,资源服务器会处理经过身份验证的请求。
大规模部署可能拥有多个资源服务器。例如,Google 的服务拥有数十个资源服务器,例如 Google Cloud 平台、Google Maps、Google Drive、Youtube、Google+ 等。这些资源服务器各自独立,但都共享同一个授权服务器。
Google 的一些 API
较小的部署通常只有一个资源服务器,并且通常作为与授权服务器相同的代码库或相同的部署的一部分构建。
14.1 验证访问令牌
资源服务器将通过Authorization
包含访问令牌的 HTTP 标头从应用程序获取请求。资源服务器需要能够验证访问令牌以确定是否处理请求,并找到关联的用户帐户等。
如果您使用自编码访问令牌,则可以完全在资源服务器中验证令牌,而无需与数据库或外部服务器交互。
如果您的令牌存储在数据库中,那么验证令牌只是在令牌表上进行数据库查找。
另一种选择是使用Token Introspection规范构建 API 来验证访问令牌。这是处理跨大量资源服务器验证访问令牌的好方法,因为这意味着您可以将访问令牌的所有逻辑封装在单个服务器中,并通过 API 将信息公开给系统的其他部分。令牌自检端点仅供内部使用,因此您需要使用一些内部授权来保护它,或者仅在系统防火墙内的服务器上启用它。
14.2 验证范围
资源服务器需要知道与访问令牌关联的范围列表。如果访问令牌中的范围不包含执行指定操作所需的范围,则服务器负责拒绝该请求。
OAuth 2.0 规范本身不定义任何范围,也没有范围的中央注册表。范围列表由服务自行决定。有关更多信息,请参阅范围。
14.3 令牌已过期
如果您的服务使用短期访问令牌和长寿命刷新令牌,那么您需要确保在应用程序使用过期令牌发出请求时返回正确的错误响应。
WWW-Authenticate
返回带有如下所述标头的HTTP 401 响应。如果您的 API 通常返回 JSON 响应,那么您也可以返回带有相同错误信息的 JSON 正文。
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token"
error_description="The access token expired"
Content-type: application/json
{
"error": "invalid_token",
"error_description": "The access token expired"
}
这将向客户端表明他们现有的访问令牌已过期,并且他们应该尝试使用刷新令牌获取新的访问令牌。
14.4 错误代码和未经授权的访问
如果访问令牌不允许访问所请求的资源,或者请求中没有访问令牌,则服务器必须使用 HTTP 401 响应进行回复并WWW-Authenticate
在响应中包含标头。
最小WWW-Authenticate
标头包含字符串Bearer
,表示需要承载令牌。标头还可以指示其他信息,例如“领域”和“范围”。 “领域”值用于传统的HTTP 身份验证。 “范围”值允许资源服务器指示访问资源所需的范围列表,因此应用程序可以在启动授权流程时向用户请求适当的范围。响应还应根据发生的错误类型包含适当的“错误”值。
invalid_request
(HTTP 400)– 请求缺少参数,或者格式错误。invalid_token
(HTTP 401) – 访问令牌已过期、已撤销、格式错误或因其他原因无效。客户端可以获取新的访问令牌并重试。insufficient_scope
(HTTP 403)– 访问令牌
例如:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example",
scope="delete",
error="insufficient_scope"
如果请求没有身份验证,则不需要错误代码或其他错误信息。
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example"
15. 适用于本机应用程序的 OAuth
本章介绍了支持原生应用的 OAuth 时需要注意的一些特殊事项。与基于浏览器的应用一样,原生应用不能使用客户端密钥,因为这需要开发人员在其应用程序的二进制分发中发送密钥。事实证明,反编译和提取密钥相对容易。因此,原生应用必须使用不需要预先注册客户端密钥的 OAuth 流程。
当前行业最佳实践是使用授权流程和 PKCE 扩展,从请求中省略客户端密钥,并使用外部用户代理来完成流程。外部用户代理通常是设备的本机浏览器(具有与本机应用程序不同的安全域),因此应用程序无法访问 cookie 存储或检查或修改浏览器内的页面内容。由于在这种情况下应用程序无法访问正在使用的浏览器内部,因此这为设备提供了在授权不同应用程序时保持用户登录的机会,这样他们就不必在每次授权新应用程序时都输入凭据。
近年来,iOS 和 Android 都在努力进一步改善 OAuth 对原生应用的用户体验,方法是提供可以从应用程序内部启动的原生用户代理,同时仍与启动它的应用程序隔离。结果是,用户不再需要离开应用程序即可启动共享系统 cookie 的原生浏览器。此功能最初是SFSafariViewController
在 iOS 9 中添加的,后来SFAuthenticationSession
在 iOS 11 和ASWebAuthenticationSession
iOS 12 中发展。
这些针对原生应用的建议以RFC 8252的形式发布,其中对这些概念进行了更详细的描述。
15.1 使用系统浏览器
过去,原生应用通常会将 OAuth 接口嵌入到应用内的 Web 视图中。这种方法存在多个问题,包括客户端应用可能会窃听用户在登录时输入的凭据,甚至会显示虚假的授权页面。移动操作系统的安全性通常以嵌入式 Web 视图不与系统的原生浏览器共享 Cookie 的方式实现,因此用户的体验会更差,因为他们每次都需要输入凭据。
完成授权流程更安全、更可靠的方法是启动系统浏览器。然而,在添加专用设备 API 之前,这种方法的缺点是用户会退出应用并启动浏览器,然后重定向回应用,这也不是理想的用户体验。
值得庆幸的是,移动平台一直在解决这个问题。现在 iOS 和 Android 上都有 API 可供应用启动系统浏览器,但仍然留在应用程序上下文中。该 API 不允许客户端应用窥视浏览器内部,从而获得使用外部浏览器的安全优势和始终留在应用程序内的用户体验优势。
强烈建议原生应用开发人员使用这些专用 API,但如果由于某种原因无法使用,则可以启动外部浏览器而不是嵌入式 Web 视图。
授权服务器应通过尝试检测授权 URL 是否在嵌入式 Web 视图中启动并拒绝请求来强制执行此行为。检测页面是否在嵌入式 Web 视图中被访问(而不是在系统浏览器中被访问)的具体技术将取决于平台,但通常涉及检查用户代理标头。
15.2 本机应用程序的重定向 URL
为了支持多种类型的本机应用程序,您的服务器将需要支持注册三种类型的重定向 URL,每种类型的 URL 都支持略有不同的用例。
HTTPS URL 匹配
iOS 和 Android 都允许应用注册 URL 模式,指示只要系统浏览器访问与注册模式匹配的 URL,就应启动应用。应用通常使用这种方式“深度链接”到原生应用,例如在浏览器中查看 Yelp URL 时,Yelp 应用会打开餐厅页面。
应用还可以使用此技术来注册 URL 模式,当授权服务器重定向回应用时,该模式将启动应用。如果平台提供此功能,则建议本机应用使用此方法,因为这可以提供应用所属的 URL 的最大完整性。如果平台不支持应用声明的 URL,这还可以提供合理的后备方案。
自定义 URL 方案
某些平台允许应用注册自定义 URL 方案,只要在浏览器或其他应用中打开采用该方案的 URL,该应用就会启动。支持采用自定义 URL 方案的重定向 URL 允许客户端启动外部浏览器以完成授权流程,然后在授权完成后重定向回应用。但是,这种方法不如 HTTPS URL 匹配方法安全,因为没有自定义 URL 方案的全局注册表来避免开发人员之间的冲突。
应用程序开发人员应选择一种可能在全球范围内唯一的 URL 方案,并且他们可以控制该方案。由于操作系统通常没有特定应用程序是否已声明 URL 方案的注册表,因此理论上两个应用程序可以独立选择相同的方案,例如myapp://
。如果您想帮助防止使用自定义方案的应用程序开发人员发生冲突,您应该建议(甚至强制)他们使用他们控制的域的反向域名模式的方案。至少,您可以要求重定向 URL 至少包含一个,.
以免与其他系统方案(例如mailto
或 )冲突ftp
。
例如,如果某个应用有一个名为 的对应网站photoprintr.example.org
,则可用作其 URL 方案的反向域名为org.example.photoprintr
。开发人员注册的重定向 URL 将以 开头org.example.photoprintr://
。通过强制执行此操作,您可以帮助鼓励开发人员选择不会与其他已安装应用程序冲突的明确 URL 方案。
使用自定义 URL 方案的应用将按正常方式启动授权请求(如授权请求中所述),但将提供具有其自定义 URL 方案的重定向 URL。授权服务器仍应验证此 URL 之前是否已注册为允许的重定向 URL,并且可以将其视为 Web 应用注册的任何其他重定向 URL。
当授权服务器将本机应用重定向到具有自定义方案的 URL 时,操作系统将启动该应用并使整个重定向 URL 可供原始应用访问。该应用可以像常规 OAuth 2.0 客户端一样提取授权代码。
环回 URL
本机应用程序可能用于支持无缝重定向的另一种技术是在环回接口的随机端口上打开新的 HTTP 服务器。这通常仅在桌面操作系统或命令行应用程序中执行,因为移动操作系统通常不向应用程序开发人员提供此功能。
这种方法适用于命令行应用和桌面 GUI 应用。应用将启动 HTTP 服务器,然后开始授权请求,将重定向 URL 设置为环回地址(例如)http://127.0.0.1:49152/redirect
并启动浏览器。当授权服务器将浏览器重定向回环回地址时,应用可以从请求中获取授权代码。
为了支持此用例,授权服务器必须支持注册以http://127.0.0.1:[port]/
和http://::1:[port]/
和开头的重定向 URL http://localhost:[port]/
。授权服务器应允许任意路径组件以及任意端口号。请注意,在这种情况下,可以使用 HTTP 方案而不是 HTTPS,因为请求永远不会离开设备。
登记
与服务器端应用一样,原生应用也必须向授权服务器注册其重定向 URL。这意味着,除了服务器端应用的传统 HTTPS URL 之外,授权服务器还需要允许符合上述所有模式的已注册重定向 URL。
当授权请求在授权服务器发起时,服务器将验证所有请求参数,包括给出的重定向 URL。授权应拒绝请求中无法识别的 URL,以帮助避免授权代码拦截攻击。
15.3 PKCE 扩展
由于本机平台上的重定向 URL 执行能力有限,因此还有另一种获得额外安全性的技术,称为代码交换证明密钥(简称 PKCE),发音为“pixie”。
此技术涉及本机应用程序创建一个初始随机密钥,并在将授权代码交换为访问令牌时再次使用该密钥。这样,如果另一个应用程序拦截了授权代码,则在没有原始密钥的情况下将无法使用。
请注意,PKCE 不会阻止应用程序模拟,它只会阻止授权码被启动流程的应用程序以外的其他应用程序使用。
有关详细信息,请参阅代码交换的证明密钥。
15.4 本机应用程序的服务器支持清单
总结本章,您的授权服务器应该支持以下内容,以便完全支持本机应用程序的安全授权。
- 允许客户端为其重定向 URL 注册自定义 URL 方案。
- 支持具有任意端口号的环回 IP 重定向 URL,以支持桌面应用程序。
- 不要假设原生应用可以保守秘密。要求所有应用声明它们是公开的还是机密的,并且只向机密应用发布客户端机密。
- 支持PKCE扩展,并要求公共客户端使用它。
- 尝试检测授权界面何时嵌入在本机应用程序的 Web 视图中(而不是在系统浏览器中启动),并拒绝这些请求。
16. 适用于无浏览器和输入受限设备的 OAuth
OAuth 2.0“设备流程”扩展可在具有互联网连接但没有浏览器或无法轻松输入文本的设备上启用 OAuth。如果您曾在 Apple TV 等设备上登录过 YouTube 帐户,那么您已经遇到过此工作流程。Google 参与了此扩展的开发,并且也是其在生产中的早期实施者。
此流程也出现在智能电视、媒体控制台、相框、打印机或硬件视频编码器等设备上。在此流程中,设备指示用户在智能手机或计算机等辅助设备上打开 URL 以完成授权。用户的两个设备之间不需要任何通信通道。
16.1 用户流程
当您开始在设备(例如此硬件视频编码器)上登录时,设备会与 Google 通信以获取设备代码,如下所示。
设备发出 API 请求以获取设备代码
接下来,我们看到设备会显示代码以及 URL。
设备显示设备代码和URL
登录您的 Google 帐户后,访问该网址将显示一个界面,提示您输入设备上显示的代码。
Google 提示用户输入代码
输入代码并单击“下一步”后,您将看到标准 OAuth 授权提示,其中描述了应用程序请求的范围,如下所示。
Google 显示应用程序请求的范围
一旦您允许请求,Google 就会显示一条消息,提示您返回到您的设备,如下所示。
Google 指示用户返回设备
几秒钟后,设备完成启动并且您已登录。
总体而言,这是一个相当轻松的体验。由于您可以使用任何想要打开 URL 的设备,因此您可以使用主计算机或手机,您可能已经登录了授权服务器。这也可以无需在设备上输入数据!无需在笨重的小键盘上输入密码或代码。
让我们了解一下设备需要什么才能实现这一点。
16.2 授权请求
首先,设备向授权服务器发出请求,请求设备代码,使用其客户端 ID 标识自己,并在需要时请求一个或多个范围。
POST /token HTTP/1.1
Host: authorization-server.com
Content-type: application/x-www-form-urlencoded
client_id=a17c21ed
授权服务器以 JSON 有效负载进行响应,其中包含设备代码、用户将输入的代码、用户应该访问的 URL 和轮询间隔。
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
{
"device_code": "NGU5OWFiNjQ5YmQwNGY3YTdmZTEyNzQ3YzQ1YSA",
"user_code": "BDWP-HQPK",
"verification_uri": "https://authorization-server.com/device",
"interval": 5,
"expires_in": 1800
}
该设备在显示屏上向用户显示verification_uri
和 ,引导用户在该 URL 处输入代码。user_code
16.3 令牌请求
当设备等待用户在自己的计算机或手机上完成授权流程时,设备同时开始轮询令牌端点以请求访问令牌。
设备以device_code
指定的速率使用 发出 POST 请求interval
。设备应继续请求访问令牌,直到authorization_pending
返回 以外的响应(用户同意或拒绝请求或设备代码过期)。
POST /token HTTP/1.1
Host: authorization-server.com
Content-type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:device_code&
client_id=a17c21ed&
device_code=NGU5OWFiNjQ5YmQwNGY3YTdmZTEyNzQ3YzQ1YSA
授权服务器将回复错误或访问令牌。除了 OAuth 2.0 核心中定义的错误代码之外,设备流规范还定义了两个额外的错误代码,authorization_pending
和slow_down
。
如果设备轮询过于频繁,授权服务器将返回slow_down
错误。
HTTP/1.1 400 Bad Request
Content-Type: application/json
Cache-Control: no-store
{
"error": "slow_down"
}
如果用户尚未允许或拒绝请求,授权服务器将返回authorization_pending
错误。
HTTP/1.1 400 Bad Request
Content-Type: application/json
Cache-Control: no-store
{
"error": "authorization_pending"
}
如果用户拒绝请求,授权服务器将返回access_denied
错误。
HTTP/1.1 400 Bad Request
Content-Type: application/json
Cache-Control: no-store
{
"error": "access_denied"
}
如果设备代码已过期,授权服务器将返回错误expired_token
。设备可以立即请求新的设备代码。
HTTP/1.1 400 Bad Request
Content-Type: application/json
Cache-Control: no-store
{
"error": "expired_token"
}
最后,如果用户允许请求,则授权服务器像平常一样发出访问令牌并返回标准访问令牌响应。
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
{
"access_token": "AYjcyMzY3ZDhiNmJkNTY",
"refresh_token": "RjY2NjM5NzA2OWJjuE7c",
"token_type": "Bearer",
"expires": 3600,
"scope": "create"
}
16.4 授权服务器要求
对于授权服务器来说,支持设备流并不需要做大量的额外工作。在向现有授权服务器添加对设备流的支持时,请注意以下几点。
设备代码请求
设备将向授权服务器发出请求,以获取流程所需的一组验证码。以下参数是请求的一部分。
client_id
– 必填,如客户端注册中所述客户端标识符。scope
– 可选,请求的范围如范围中所述。
验证客户端 ID 和范围后,授权服务器将返回包含验证 URL、设备代码和用户代码的响应。除了上面给出的示例之外,授权服务器还可以返回一些可选参数。
device_code
– 必填,授权服务器生成的验证码。user_code
– 必填,用户将在设备屏幕上输入的代码应相对较短。通常使用 6-8 个数字和字母。verification_uri
– 必填,授权服务器上用户应访问以开始授权的 URL。用户应在计算机或手机上手动输入此 URL。expires_in
– 可选,设备代码和用户代码的有效期(以秒为单位)。interval
– 可选,客户端在轮询令牌端点请求之间应等待的最短时间(以秒为单位)。
用户代码
在许多情况下,用户最接近的设备就是他们的手机。通常,这些界面比完整的计算机键盘更受限制,例如 iPhone 需要额外点击才能更改按键大小写或切换到数字输入。为了帮助减少数据输入错误并加快代码输入速度,用户代码的字符集应考虑到这些限制,例如仅使用大写字母。
设备代码应使用不区分大小写的 AZ 字符,不包含元音,以避免意外拼写单词。这会产生以 20 为基数的字符集BCDFGHJKLMNPQRSTVWXZ
。比较输入的代码时,最好忽略字符集中不存在的任何字符(例如标点符号)。遵循此准则且熵为 20^8 的示例代码为BDWP-HQPK
。授权服务器应不区分大小写地比较输入的字符串,忽略标点符号,因此应允许以下内容作为匹配项:bdwphqpk
。
验证网址
设备显示的验证 URL 应尽可能简短且容易记住。该 URL 可能会显示在非常小的屏幕上,用户必须在计算机或手机上手动输入。
请注意,服务器应返回包含 URL 方案的完整 URL,但某些设备可能会选择在显示 URL 时修剪该方案。因此,应将服务器配置为将 http 重定向到 https,并在普通域和带有 www 前缀的域上提供服务,以防用户输入错误或设备省略 URL 的该部分。
Google 的授权服务器是一个很好的例子,它提供了一个易于输入的短网址。代码请求的响应是,https://www.google.com/device
但设备只需显示google.com/device
,Google 就会进行相应的重定向。
非文本界面的优化
没有显示屏或没有文本显示屏的客户端显然无法向用户显示 URL。因此,可以使用一些其他方法将验证 URL 和用户代码传达给用户。
设备可能能够通过 NFC 或蓝牙,甚至通过显示二维码来广播验证 URL。在这些情况下,设备可以使用参数将用户代码作为验证 URL 的一部分user_code
。例如:
https://authorization-server.com/device?user_code=BDWP-HQPK
这样,当用户启动 URL 时,用户代码就可以预先填入验证界面。建议授权服务器仍然要求用户确认代码,而不是自动进行。
如果设备能够显示代码,即使它无法显示 URL,也可以通过提示用户确认验证界面上的代码是否与设备上显示的代码相匹配来获得额外的安全性。如果这不可行,那么授权服务器至少可以要求用户确认他们刚刚请求授权设备。
16.5 安全注意事项
用户代码暴力破解
由于用户代码是用户手动输入到尚不知道设备是否被授权的界面中的,因此应采取预防措施避免对用户代码进行暴力攻击的可能性。
通常,为了便于手动输入,会使用比授权代码熵小得多的短代码。因此,建议授权服务器对用于验证用户代码的端点进行速率限制。
速率限制应基于用户代码的熵,以使暴力攻击无法进行。例如,上面描述的 20 个字符集中的 8 个字符可提供大约 34 位熵。在选择可接受的速率限制时,您可以使用此公式来计算熵位数。log2(208) = 34.57
远程网络钓鱼
攻击者可能会在自己拥有的设备上启动设备流程,以诱骗用户授权攻击者的设备。例如,攻击者可能会发送一条短信,指示用户访问某个 URL 并输入用户代码。
为了降低这种风险,建议授权界面除了用户界面中描述的授权界面中包含的标准信息之外,还要向用户清楚地表明他们正在授权物理设备访问他们的帐户。
17. 使用 PKCE 保护应用程序
代码交换证明密钥(缩写为 PKCE,发音为“pixie”)是授权代码流程的扩展,用于防止 CSRF 和授权代码注入攻击。该技术涉及客户端首先在每个授权请求上创建一个密钥,然后在将授权代码交换为访问令牌时再次使用该密钥。这样,如果代码被拦截,它将毫无用处,因为令牌请求依赖于初始密钥。
PKCE 最初旨在保护移动应用中的授权码流,后来也被推荐用于单页应用。后来,人们认识到它能够防止授权码注入,这使得它适用于每种类型的 OAuth 客户端,甚至适用于在使用客户端密钥的 Web 服务器上运行的应用。由于它在移动应用和单页应用中的使用历史,人们有时会错误地认为 PKCE 是客户端密钥的替代品。然而,PKCE 并不能替代客户端密钥,即使客户端使用客户端密钥,也建议使用 PKCE,因为使用客户端密钥的应用仍然容易受到授权码注入攻击。
完整规范可参见RFC7636。我们将在下面介绍该协议的摘要。
17.1 授权请求
当本机应用程序开始授权请求时,客户端不会立即启动浏览器,而是首先创建所谓的“代码验证器A-Z
”。这是一个使用字符、、和标点符号(连字符、句点、下划线和波浪号)的加密随机字符串a-z
,长度在 43 到 128 个字符之间。0-9``-._~
应用程序生成代码验证器后,会使用它来派生代码质询。对于可以执行 SHA256 哈希的设备,代码质询是代码验证器的 SHA256 哈希的 Base64-URL 编码字符串。无法执行 SHA256 哈希的客户端可以使用纯代码验证器字符串作为质询,尽管这提供的安全优势较少,因此实际上只应在绝对必要时使用。
Base64-URL 编码是典型 Base64 编码方法的一个小变种。它以大多数编程语言中可用的 Base64 编码方法开始,但改用 URL 安全字符。您可以通过获取 Base64 编码字符串并对该字符串进行以下修改来实现 Base64-URL 编码方法:获取 Base64 编码字符串,将其更改为 ,并将其更改为+
,-
然后/
从末尾_
修剪尾部。=
PHP
function base64_urlencode($str) {
return rtrim(strtr(base64_encode($hash), '+/', '-_'), '=');
}
JavaScript
function base64_urlencode(str) {
return btoa(String.fromCharCode.apply(null,
new Uint8Array(str)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
现在客户端有了代码质询字符串,它包括该字符串和一个参数,该参数指示使用哪种方法生成质询(纯文本或 S256)以及授权请求的标准参数。这意味着完整的授权请求将包含以下参数。
- response_type=code – 表示你的服务器希望收到授权码
- client_id= – 您首次创建应用程序时收到的客户端 ID
- redirect_uri= – 表示授权完成后返回用户的 URL,例如 org.example.app://redirect
- state=1234zyx – 应用程序生成的随机字符串,稍后您将验证
- code_challenge=XXXXXXXXX – 如前所述生成的代码挑战
- code_challenge_method=S256 – 要么
plain
,要么S256
,取决于挑战是纯文本验证字符串还是字符串的 SHA256 哈希值。
授权服务器应识别code_challenge
请求中的参数,并将其与它生成的授权码关联起来。要么将其与授权码一起存储在数据库中,要么如果您使用的是自编码授权码,则可以将其包含在代码本身中。(有关详细信息,请参阅授权响应。)服务器正常返回授权码,并且不会在返回的数据中包含质询。
错误响应
授权服务器可以要求公共客户端必须使用 PKCE 扩展。这实际上是允许本机应用在不使用客户端密钥的情况下拥有安全授权流程的唯一方法,尤其是在没有基于 Web 的客户端可用的重定向 URI 安全性的情况下。由于授权服务器应该知道特定客户端 ID 对应于公共客户端,因此它可以拒绝不包含代码质询的公共客户端的授权请求。
如果授权服务器要求公共客户端使用 PKCE,并且授权请求缺少代码质询,则服务器应返回错误响应,或者error=invalid_request
应解释错误的性质。error_description``error_uri
17.2 授权码交换
然后,应用程序将用授权码交换访问令牌。除了授权码请求中定义的参数外,客户端也会发送该code_verifier
参数。一个完整的访问令牌请求将包含以下参数:
- grant_type=authorization_code – 表示此令牌请求的授予类型
- code – 客户端将发送在重定向中获得的授权码
- redirect_uri – 初始授权请求中使用的重定向 URL
- client_id – 应用程序的注册客户端 ID
- client_secret(可选)– 如果应用程序已发布机密,则为应用程序注册的客户端机密
- code_verifier – PKCE 请求的代码验证器,应用程序在授权请求之前最初生成。
由于code_challenge
和code_challenge_method
最初与授权码相关联,因此服务器应该已经知道使用哪种方法来验证code_verifier
。
如果方法是plain
,则授权服务器只需检查提供的内容code_verifier
是否与预期code_challenge
字符串匹配。如果方法是S256
,则授权服务器应获取提供的内容code_verifier
并使用相同的哈希方法对其进行转换,然后将其与存储的code_challenge
字符串进行比较。
如果验证器与预期值匹配,则服务器可以继续正常运行,发出访问令牌并做出适当响应。如果出现问题,则服务器会响应错误invalid_grant
。
PKCE 扩展不添加任何新的响应,因此即使授权服务器不支持 PKCE 扩展,客户端也可以始终使用 PKCE 扩展。
18. 令牌自检端点
当 OAuth 2.0 客户端向资源服务器发出请求时,资源服务器需要某种方法来验证访问令牌。OAuth 2.0 核心规范没有定义资源服务器应如何验证访问令牌的具体方法,只是提到它需要资源服务器和授权服务器之间的协调。在某些情况下,尤其是对于小型服务,两个端点都是同一系统的一部分,并且可以在内部(例如在数据库中)共享令牌信息。在两个端点位于不同服务器上的大型系统中,这导致了两个服务器之间通信的专有和非标准协议。
OAuth 2.0 Token Introspection 扩展定义了一个协议,该协议返回有关访问令牌的信息,旨在供资源服务器或其他内部服务器使用。
令牌自检的替代方法是使用授权服务器和资源服务器均可识别的结构化令牌格式。OAuth 2.0 访问令牌的 JWT 配置文件是最近的 RFC,它描述了使用 JWT 的访问令牌的标准化格式。这使资源服务器能够通过验证签名和解析结构化令牌本身内的声明来验证访问令牌,而无需网络调用。
18.1 自省端点
令牌自检端点需要能够返回有关令牌的信息,因此您很可能将其构建在令牌端点所在的同一位置。这两个端点需要共享一个数据库,或者如果您已经实现了自编码令牌,它们将需要共享密钥。
18.2 令牌信息请求
该请求将是一个 POST 请求,仅包含一个名为“token”的参数。预计此端点不会向开发人员公开。不应允许应用程序使用此端点,因为响应可能包含开发人员不应访问的特权信息。保护端点的一种方法是将其放在外部无法访问的内部服务器上,或者可以使用 HTTP 基本身份验证进行保护。
POST /token_info HTTP/1.1
Host: authorization-server.com
Authorization: Basic Y4NmE4MzFhZGFkNzU2YWRhN
token=c1MGYwNDJiYmYxNDFkZjVkOGI0MSAgLQ
18.3 令牌信息响应
Token Introspection Endpoint 应使用具有以下所列属性的 JSON 对象进行响应。只有“active”属性是必需的,其余属性是可选的。Introspection 规范中的某些属性专门用于 JWT 令牌,因此我们在此仅介绍基本属性。如果您有关于令牌的其他可能有用的信息,您还可以在响应中添加其他属性。
active
:必填。这是一个布尔值,表示所呈现的令牌当前是否处于活动状态。如果令牌已由此授权服务器颁发、未被用户撤销且未过期,则该值应为“true”。scope
:包含与此令牌关联的范围的空格分隔列表的 JSON 字符串。client_id
:颁发令牌的 OAuth 2.0 客户端的客户端标识符。username
:授权此令牌的用户的人类可读标识符。exp
:指示此令牌何时过期的 unix 时间戳(整数时间戳,自 1970 年 1 月 1 日 UTC 以来的秒数)。
响应示例
以下是自省端点返回的响应的示例。
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{
"active": true,
"scope": "read write email",
"client_id": "J8NFmU4tJVgDxKaJFmXTWvaHO",
"username": "aaronpk",
"exp": 1437275311
}
18.4 错误响应
如果自省端点可公开访问,则端点必须首先验证身份验证。如果身份验证无效,端点应使用 HTTP 401 状态代码和响应进行响应invalid_client
。
HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
{
"error": "invalid_client",
"error_description": "The client authentication was invalid"
}
任何其他错误都被视为“不活跃”令牌。
- 请求的令牌不存在或无效
- 令牌已过期
- 令牌已颁发给发出此请求的客户端以外的其他客户端
在任何这些情况下,它都不被视为错误响应,并且端点仅返回一个非活动标志。
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{
"active": false
}
18.5 安全注意事项
使用令牌自检端点意味着任何资源服务器都将依赖该端点来确定访问令牌当前是否处于活动状态。这意味着自检端点全权负责决定 API 请求是否会成功。因此,端点必须针对令牌的状态执行所有适用的检查,例如检查令牌是否已过期、验证签名等。
令牌钓鱼
如果自省端点保持打开状态且不受限制,攻击者就有机会轮询端点以寻找有效令牌。为了防止这种情况发生,服务器必须要求对使用该端点的客户端进行身份验证,或者仅通过防火墙等其他方式向内部服务器提供该端点。
请注意,资源服务器也是钓鱼攻击的潜在目标,应该采取速率限制等对策来防止这种情况。
缓存
出于性能原因,自省端点的消费者可能希望缓存端点的响应。因此,在决定缓存值时,考虑性能和安全性权衡非常重要。例如,较短的缓存过期时间将导致更高的安全性,因为资源服务器必须更频繁地查询自省端点,但这会导致端点负载增加。较长的过期时间会留下一个窗口,其中令牌可能实际上已过期或被撤销,但仍可以在资源服务器上使用,直至缓存时间的剩余时间。
缓解此问题的一种方法是让消费者永远不要缓存超过令牌过期时间的值,该值将在自省响应的“exp”参数中返回。
限制信息
自省端点不一定需要为同一令牌的所有查询返回相同的信息。例如,两个不同的资源服务器(如果它们在发出自省请求时进行身份验证)可能会获得令牌状态的不同视图。这可用于限制返回到特定资源服务器的令牌信息。这使得可以在多个资源服务器上使用令牌,而其他服务器永远不会知道该令牌可以在任何其他服务器上使用。
19. 创建文档
读到这里你可能已经注意到,OAuth 2.0 规范中有很多地方将决策权留给了实现。其中许多内容都没有明确规定,以便不同的实现能够根据自己的安全要求做出不同的决策。最终结果是大多数 OAuth 2.0 实现都无法互操作,尽管在实践中,许多实现都做出了相同的决策,并且非常相似。
由于实现方式有很多种差异,并且流程的某些部分(例如注册应用程序)必须手动进行,因此为您的服务构建良好的文档至关重要。
本部分涵盖了为了让开发人员能够使用您的 API,您需要记录的内容。其中一些内容可以内联记录在相应的界面中(例如开发人员用于客户端注册的界面),而有些内容更适合记录在 API 文档的“概述”部分中。
19.1 客户注册
开发人员如何注册新的客户端应用程序以获取客户端 ID 和可选的机密?
- 在网页上?提供注册页面的链接。
- 以编程方式?你的服务可能实现了动态客户端注册规范,或者拥有用于注册应用程序的专有 API
- 您是否为开发人员提供其他注册应用程序的机制?如果提供,您需要描述注册应用程序的其他方式。
您的服务至少应询问开发者他们的应用是机密客户端还是公共客户端,并提供注册重定向 URI 的方法。除此之外,您还应记录收集的有关应用的其他信息,并指明在授权请求期间向最终用户显示哪些信息。
- 应用名称
- 关于应用程序的网页
- 描述
- 徽标或其他图像
- 关于应用程序使用条款的网页
- 其他信息?
19.2 端点
在 OAuth 过程中,开发人员将使用两个主要端点。**授权端点是用户被引导开始授权流程的地方。应用程序获得授权码后,它将在令牌端点**将该代码交换为访问令牌。令牌端点还负责为其他授权类型颁发访问令牌。
您需要让开发人员知道他们将要使用的这两个端点的 URL。
19.3 客户端身份验证
当请求中需要客户端身份验证时(例如在授权码授权中),您的服务可以通过两种方式接受请求中的客户端 ID 和密钥。您的服务可以使用客户端 ID 作为用户名、密钥作为密码来接受 HTTP Basic Auth 标头中的身份验证,或者通过接受帖子正文中的字符串作为client_id
和 来接受身份验证client_secret
。是否要接受其中一种或两种方法取决于您的服务,因此您需要告诉开发人员您希望他们如何在请求中包含此身份验证。
此外,您的服务可能支持其他形式的客户端身份验证,例如公钥/私钥对。这在当前部署的 OAuth 2.0 实现中相对不常见,但规范保留了这种可能性。
对于发送给应用程序的客户端 ID 和机密的最大或最小长度没有要求,因此通常最好让开发人员知道这些字符串的预期长度,以便他们可以适当地存储它们。
19.4 字符串的大小
由于开发人员在开始编写代码之前可能不会看到授权码或访问令牌,因此您应该记录他们将遇到的字符串的最大大小。
- 客户端 ID
- 客户端机密
- 授权码
- 访问令牌
19.5 响应类型
您的服务支持哪些响应类型?通常,服务仅支持基于 Web 和本机应用程序的“代码”响应类型,但您应确保指出您的服务是否需要公共客户端的 PKCE。
19.6 重定向 URL 限制
您的服务可能会对开发人员可以使用的已注册重定向 URL施加限制。例如,服务通常会禁止开发人员使用非 TLS http 端点,或限制非生产应用程序使用这些端点。虽然支持自定义方案对于支持原生应用很重要,但某些服务也不允许这些方案。您应该记录对注册重定向 URL 的任何要求。
19.7 默认作用域
如果开发人员在授权请求期间未指定范围,则服务可能会为该请求假定一个默认范围。如果是这种情况,您应该记录默认范围是什么。
授权服务器可能会忽略开发人员请求的范围,或者在请求的范围之外添加其他范围。服务器还可能允许用户更改请求的范围。如果存在上述任何一种情况,服务应向开发人员明确指出,以便他们能够考虑到访问令牌可能具有与他们请求的范围不同的范围。
该服务还应记录已颁发授权代码的有效期,以便开发人员大致了解代码在颁发和使用之间可以持续多长时间。授权服务器还可以防止代码被多次使用,如果是这种情况,则应记录下来。
19.8 访问令牌响应
当您发出访问令牌时,访问令牌响应会列出一些可选参数。您应该记录您的服务支持哪些参数,以便开发人员知道会发生什么。
响应何时包含expires_in
参数?如果令牌过期,您的服务可能始终会包含该参数;或者,如果响应中没有此值,您的服务可以记录开发人员应预期的默认过期时间。
响应是否始终包含授予的访问令牌的范围?通常,在响应中返回此信息是个好主意,但如果授予的范围与请求的范围相匹配,许多服务都会忽略它。无论哪种方式,您都应该记录服务器对此参数的行为方式。
19.9 刷新令牌
对于 OAuth 2.0 API 的开发人员来说,最令人困惑或沮丧的方面之一是刷新令牌。如果有的话,一定要明确说明您的服务如何处理刷新令牌。
如果您的访问令牌过期,您可能需要支持刷新令牌,以便开发人员可以构建能够继续访问用户帐户的应用程序,而无需用户不断重新授权该应用程序。
您应该清楚地记录哪些受支持的授予类型在响应中包含刷新令牌,以及在什么情况下包含刷新令牌。
当您的服务在响应刷新令牌授予时发出新的访问令牌时,您的服务可能会同时发出新的刷新令牌,并使之前的令牌过期。这意味着刷新令牌会频繁轮换,这对您的应用程序来说可能是理想的。如果是这种情况,请确保开发人员知道这种情况会发生,这样他们就不会错误地认为他们获得的第一个刷新令牌将无限期地继续工作。
19.10 扩展授权类型
除了四种基本授予类型(授权码,密码,客户端凭证和隐式)之外,您的服务可能还支持其他授予类型。
某些授权类型已标准化为 OAuth 2.0 的扩展,例如设备流和SAML。某些服务还会实现自己的自定义授权类型,例如将旧版 API 迁移到 OAuth 2.0 时。记录您的服务支持的其他授权类型并提供有关如何使用它们的文档非常重要。
20. 术语参考
角色
OAuth 定义了四个角色:
- 资源所有者(用户)
- 资源服务器(API)
- 授权服务器(可以与API是同一台服务器)
- 客户端(应用程序)
用户
OAuth 2.0 规范将用户称为“资源所有者”。资源所有者是授予其帐户部分访问权限的人。在这种情况下,资源可以是数据(照片、文档、联系人)、服务(发布博客文章、转账)或任何其他需要访问限制的资源。任何想要代表用户行事的系统都必须首先获得他们的许可。
API
规范将您通常认为的主要 API 称为“资源服务器”。资源服务器是包含第三方应用程序正在访问的用户信息的服务器。资源服务器必须能够接受和验证访问令牌,并在用户允许的情况下授予请求。资源服务器不一定需要了解应用程序。
授权服务器
当应用程序请求访问其帐户时,用户将与授权服务器进行交互。授权服务器会显示 OAuth 提示,用户也会在此批准或拒绝应用程序的请求。授权服务器还负责在用户授权应用程序后授予访问令牌。
客户端
客户端是尝试代表用户行事或访问用户资源的应用。客户端需要先获得权限,然后才能访问用户的帐户。客户端将通过将用户引导至授权服务器或直接向授权服务器声明权限(无需用户交互)来获得权限。
保密客户
机密客户端是能够维护 机密性的客户端client_secret
。通常,这些客户端只是在开发人员控制的服务器上运行的应用程序,用户无法访问源代码。这些类型的应用程序通常称为“Web 应用程序”,因为它们通常在 Web 服务器上运行。
公共客户端
公共客户端无法保持 的机密性client_secret
,因此这些应用程序不使用密钥。移动应用程序和 JavaScript 应用程序都被视为公共客户端。由于运行 JavaScript 应用程序的任何人都可以轻松查看应用程序的源代码,因此密钥在那里很容易被看到。对于移动应用程序,可以反编译二进制文件以提取字符串。只要应用程序在用户控制的设备上运行,就应该将其视为公共客户端。
访问令牌
访问令牌是向 API 发出经过身份验证的请求时使用的字符串。该字符串本身对于使用它的应用程序没有任何意义,但表示用户已授权第三方应用程序访问其帐户。令牌具有相应的访问期限、范围以及服务器可能需要的其他信息。
刷新令牌
刷新令牌是一个字符串,用于在访问令牌过期时获取新的访问令牌。并非所有 API 都使用刷新令牌。
授权码
授权码是服务器端应用流程中使用的中间令牌,在服务器端应用中有更详细的描述。授权步骤后,授权码将返回给客户端,然后客户端将其交换为访问令牌。
21. OAuth 1 和 2 之间的区别
OAuth 2.0 是对 OAuth 1.0 的完全重写,仅共享总体目标和一般用户体验。OAuth 2.0 不向后兼容 OAuth 1.0 或 1.1,应被视为一种全新的协议。
OAuth 1.0 主要基于两个现有的专有协议:Flickr 的授权 API 和 Google 的 AuthSub。OAuth 1.0 的制定是基于当时实际实施经验的最佳解决方案。经过几年的发展,许多公司都在构建 OAuth 1 API,许多开发人员都在编写代码来使用这些 API,社区了解到该协议对人们的挑战所在。有几个特定领域被确定为需要改进,因为它们要么限制了 API 的功能,要么实施起来太困难。
OAuth 2.0 代表了众多公司和个人多年讨论的结果,其中包括雅虎、Facebook、Salesforce、微软、Twitter、德国电信、Intuit、Mozilla 和谷歌。
本节介绍 OAuth 1.0 和 2.0 之间的主要差异及其背后的动机。如果您熟悉 OAuth 1.0,那么这是一个很好的起点,可以帮助您快速了解 OAuth 2.0 中的主要变化。
术语和角色
OAuth 2.0 定义了四个角色(客户端、授权服务器、资源服务器和资源所有者),而 OAuth 1 则对这些角色使用了不同的术语。OAuth 2.0“客户端”称为“消费者”,“资源所有者”简称为“用户”,“资源服务器”称为“服务提供商”。OAuth 1 也没有明确区分资源服务器和授权服务器的角色。
术语“两条腿”和“三条腿”已被授予类型的概念所取代,例如客户端凭证授予类型和授权码授予类型。
21.1 身份验证和签名
大多数失败的 OAuth 1.0 实施尝试都是由于协议的加密要求而失败的。OAuth 1.0 签名的复杂性是任何习惯了用户名/密码身份验证的人的主要痛点。
开发人员过去只需使用用户名和密码就能快速编写 Twitter 脚本来执行有用的操作。随着 OAuth 1.0 的推出,这些开发人员被迫查找、安装和配置库,以便向 Twitter API 发出请求,因为它要求对每个请求进行加密签名。
随着 OAuth 2.0 Bearer 令牌的引入,再次可以通过 cURL 命令快速进行 API 调用。使用访问令牌代替用户名和密码。
例如,在 OAuth 之前,您可能已经在 API 文档中看到过这样的示例:
curl --user bob:pa55 https://api.example.com/profile
使用 OAuth 1 API,不再可能对这样的示例进行硬编码,因为请求必须使用应用程序的密钥进行签名。一些服务(例如 Twitter)开始在其开发者网站中提供“签名生成器”工具,以便您无需使用库即可从网站生成 curl 命令。例如,Twitter 上的工具会生成如下 curl 命令:
curl --get 'https://api.twitter.com/1.1/statuses/show.json' \
--data 'id=210462857140252672' \
--header 'Authorization: OAuth oauth_consumer_key="xRhHSKcKLl9VF7fbyP2eEw", oauth_nonce="33ec5af28add281c63db55d1839d90f1", oauth_signature="oBO19fJO8imCAMvRxmQJsA6idXk%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1471026075", oauth_token="12341234-ZgJYZOh5Z3ldYXH2sm5voEs0pPXOPv8vC0mFjMFtG", oauth_version="1.0"'
使用 OAuth 2.0 Bearer Tokens,请求中只需要令牌本身,因此示例再次变得非常简单:
curl https://api.example.com/profile -H "Authorization: Bearer XXXXXXXXXXX"
这在 API 的易用性和良好的安全实践之间提供了良好的平衡。
21.2 用户体验和替代令牌发行选项
OAuth 2.0 主要分为两个部分:获得用户的授权(最终结果是应用程序拥有该用户的访问令牌),以及使用访问令牌代表用户发出请求。获取访问令牌的方法称为流程。
OAuth 1.0 最初有 3 个流程,分别适用于基于 Web 的应用程序、桌面客户端以及移动或“受限”设备。然而,随着规范的发展,这三个流程被合并为一个流程,理论上可以支持所有三种客户端类型。实际上,该流程对于基于 Web 的应用程序运行良好,但在其他方面体验较差。
随着越来越多的网站开始使用 OAuth,尤其是 Twitter,开发人员意识到 OAuth 提供的单一流程非常有限,并且经常产生糟糕的用户体验。另一方面,Facebook Connect 提供了一套更丰富的流程,适用于 Web 应用程序、移动设备和游戏机。
OAuth 2.0 解决了这个问题,它重新定义了多个流程,称为“授权类型”,可以灵活地支持各种应用程序类型。此外,还有一种机制可以开发扩展来处理以前没有想到的用例。
服务器端应用使用带有客户端密钥的“授权码”授权类型,提示用户授权应用程序,并生成授权码并将其返回给应用。然后,应用的服务器将授权码交换为访问令牌。此流程的安全性是通过服务器端应用使用其密钥将授权码交换为访问令牌这一事实来获得的。
单页或移动应用使用相同的授权类型,但不使用客户端密钥。相反,安全性在于验证重定向 URL 以及可选的 PKCE 扩展。
OAuth 2.0 正式定义了“密码”授权类型,允许应用程序收集用户的用户名和密码并将其交换为访问令牌。虽然这是规范的一部分,但它仅供受信任的客户端使用,例如服务自己的第一方应用程序。第三方应用程序不应使用它,因为这将允许第三方应用程序访问用户的用户名和密码。
当应用程序访问其自己的资源时,将使用“客户端凭据”授权。此授权类型只是用client_id
和交换client_secret
访问令牌。
OAuth 2.0 还支持扩展授予类型,允许组织定义自己的自定义授予类型以支持其他客户端类型或在 OAuth 和现有系统之间提供桥梁。
其中一个扩展是设备流,用于在没有网络浏览器的设备上授权应用程序。
21.3 规模性能
随着大型提供商开始使用 OAuth 1.0,社区意识到该协议存在一些限制,难以扩展到大型系统。OAuth 1.0 需要跨不同步骤和跨不同服务器进行状态管理。它需要生成临时凭证,而这些凭证通常会被丢弃而不使用,并且通常需要颁发长期凭证,而这些凭证的安全性较低且更难管理。
此外,OAuth 1.0 要求受保护资源端点能够访问客户端凭据,以便验证请求。这打破了大多数大型提供商的典型架构,即使用集中式授权服务器颁发凭据,使用单独的服务器处理 API 调用。由于 OAuth 1.0 要求使用客户端凭据来验证签名,因此这种分离非常困难。
OAuth 2.0 解决了这个问题,即仅在应用程序获得用户授权时才使用客户端凭据。在授权步骤中使用凭据后,在进行 API 调用时仅使用生成的访问令牌。这意味着 API 服务器不需要知道客户端凭据,因为它们可以自行验证访问令牌。
21.4 Bearer Tokens
在 OAuth 1 中,访问令牌由两个部分组成:公共字符串和私有字符串。私有字符串用于签署请求,不会通过网络发送。
访问 OAuth 2.0 API 的最常见方式是使用“Bearer Token”。这是一个作为 API 请求身份验证的字符串,在 HTTP“Authorization”标头中发送。该字符串对于使用它的客户端来说毫无意义,并且长度可能各不相同。
Bearer 令牌是一种更简单的 API 请求方式,因为它们不需要对每个请求进行加密签名。缺点是所有 API 请求都必须通过 HTTPS 连接进行,因为请求包含明文令牌,如果被拦截,任何人都可以使用该令牌。优点是它不需要复杂的库来发出请求,并且客户端和服务器都更容易实现。
Bearer 令牌的缺点是,如果其他应用可以访问 Bearer 令牌,则没有什么可以阻止它们使用 Bearer 令牌。这是对 OAuth 2.0 的常见批评,尽管大多数提供商无论如何都只使用 Bearer 令牌。在正常情况下,当应用程序妥善保护其控制下的访问令牌时,这不是问题,尽管从技术上讲它不太安全。如果您的服务需要更安全的方法,您可以使用其他可能满足您的安全要求的访问令牌类型。
21.5 短期令牌与长期授权
OAuth 1.0 API 通常会颁发非常持久的访问令牌。这些令牌可以无限期地持续,或持续一年左右。虽然这对开发人员来说很方便,但在某些情况下,这对某些服务提供商来说是一种限制。
负责任的 API 提供商应允许用户查看他们已授权哪些第三方应用使用其帐户,并且应能够在需要时撤销应用。如果用户撤销某个应用,API 应尽快停止接受向该应用颁发的访问令牌。根据 API 的实施方式,这可能具有挑战性或需要在系统内部各部分之间建立额外的联系。
使用 OAuth 2.0,授权服务器可以颁发短期访问令牌和长期刷新令牌。这样一来,应用无需再次让用户参与即可获得新的访问令牌,同时还增加了服务器更轻松地撤销令牌的能力。此功能源自 Yahoo! 的 BBAuth 协议,后来又源自其 OAuth 1.0 会话扩展。
有关详细信息,请参阅刷新访问令牌。
21.6 角色分离
OAuth 2.0 的设计决策之一是明确将授权服务器的角色与 API 服务器分开。这意味着您可以将授权服务器构建为一个独立组件,该组件仅负责从用户那里获取授权并向客户端颁发令牌。这两个角色可以位于物理上独立的服务器上,甚至可以位于不同的域名上,从而允许系统的每个部分独立扩展。一些提供商有许多资源服务器,每个服务器都位于不同的子域上。
授权服务器需要了解应用的client_id
和client_secret
,但 API 服务器只需接受访问令牌。通过将授权服务器构建为独立组件,您可以避免与 API 服务器共享数据库,从而更容易独立于授权服务器扩展 API 服务器,因为它们不需要共享公共数据存储。
例如,Google 的 OAuth 2.0 实现使用“accounts.google.com”上的服务器发出授权请求,但向 Google+ API 发出请求时使用“www.gooogleapis.com”。
对服务提供商的好处是,这些系统的开发可以完全独立进行,由不同的团队在不同的时间线上进行。由于它们是完全独立的,因此可以独立扩展、升级或替换,而无需考虑系统的其他部分。
22. OpenID 连接
OAuth 2.0 框架明确不提供已授权应用程序的用户的任何信息。OAuth 2.0 是一个委托框架,允许第三方应用程序代表用户行事,而应用程序无需知道用户的身份。
OpenID Connect 采用 OAuth 2.0 框架,并在其上添加了一个身份层。它提供有关用户的信息,并允许客户端建立登录会话。虽然本章并非 OpenID Connect 的完整指南,但它旨在阐明 OAuth 2.0 和 OpenID Connect 之间的关系。
22.1 授权与身份验证
OAuth 2.0 被称为授权“框架”而不是“协议”,因为核心规范实际上为各种实现留出了很大的空间,让它们根据用例以不同的方式执行操作。具体来说,OAuth 2.0 不提供一种机制来说明用户是谁或他们如何进行身份验证,它只是说用户委托应用程序代表他们行事。OAuth 2.0 框架以访问令牌的形式提供这种委托,应用程序可以使用该令牌代表用户行事。访问令牌呈现给 API(“资源服务器”),它知道如何验证访问令牌是否处于活动状态。从应用程序的角度来看,它是一个不透明的字符串。
当您入住酒店时,您会获得一张钥匙卡,您可以使用它进入您指定的房间。您可以将钥匙卡视为访问令牌。钥匙卡不会说明您是谁,也不会说明您在前台是如何进行身份验证的,但您可以在入住期间使用该卡进入酒店房间。同样,OAuth 2.0 访问令牌不会表明用户是谁,它只是您可以用来访问数据的东西,并且它可能会在将来的某个时间点过期。
OAuth 2.0 的设计初衷是提供授权而不提供用户身份和身份验证,因为这些问题具有截然不同的安全考虑因素,不一定与授权协议的安全考虑因素重叠。分别处理身份验证和身份允许将 OAuth 2.0 框架用作构建身份验证协议的一部分。
22.2 构建身份验证框架
使用 OAuth 2.0 框架作为构建身份验证和身份协议的基础是完全有可能的。
要使用 OAuth 2.0 作为身份验证协议的基础,您至少需要做几件事。
- 定义端点以返回有关用户的属性
- 定义第三方应用程序可用于向用户请求身份信息的一个或多个范围
- 定义其他错误代码和必要的扩展参数,以适应您在处理身份验证和身份时遇到的情况,例如,何时根据会话超时重新提示用户输入凭据,或者如何允许用户在登录应用程序时选择新帐户
通常,当单个提供商尝试向 OAuth 2.0 添加内容以创建身份验证和身份协议时,这会产生另一个具有不同安全程度的雪花 API。OpenID Connect 利用从许多不同实现中获得的共享知识,并将其标准化为适合企业级实现的协议。
22.3 ID 令牌
OpenID Connect 的核心基于一个称为“ID 令牌”的概念。这是授权服务器将返回的一种新令牌类型,用于对用户的身份验证信息进行编码。与仅供资源服务器理解的访问令牌不同,ID 令牌旨在供 OAuth 客户端理解。当客户端发出 OpenID Connect 请求时,它可以请求 ID 令牌和访问令牌。
OpenID Connect 的 ID Tokens 采用 JWT(JSON Web Token)的形式,它是使用发行者的私钥签名的 JSON 有效负载,可以由应用程序解析和验证。
JWT 中定义了一些属性名,用于向应用程序提供信息。它们以简写名称表示,以使 JWT 的整体大小保持较小。这包括用户的唯一标识符(sub
,即“subject”的缩写)、颁发令牌的服务器的标识符(iss
)、请求此令牌的客户端的标识符(aud
,即“audience”的缩写),以及一些属性,例如令牌的有效期,以及多久前向用户显示主要身份验证提示。
{
"iss": "https://server.example.com",
"sub": "24400320",
"aud": "s6BhdRkqt3",
"nonce": "n-0S6_WzA2Mj",
"exp": 1311281970,
"iat": 1311280970,
"auth_time": 1311280969
}
标准化端点、名称和元数据有助于减少实施错误,并允许传递有关每个端点的安全考虑的共享知识。
22.4 概括
OpenID Connect 在 OAuth 2.0 框架之上提供用户身份和身份验证。您可以使用 OpenID Connect 建立登录会话,并使用 OAuth 访问受保护的资源。
您可以在同一流程中请求 ID 令牌和访问令牌,以便对用户进行身份验证以及获得访问受保护资源的授权。
OpenID Connect 由OpenID 基金会维护。 核心 OpenID Connect 规范以及许多扩展可在https://openid.net/connect/上完整阅读。
OpenID Connect 调试器是一项很棒的资源,可帮助您构建 OpenID Connect 请求并演示流程。此外,OAuth 2.0 Playground还提供了针对实时服务器的 OpenID Connect 流程演示。
在使用 Google 登录时,我们将介绍如何使用 OpenID Connect 构建示例应用程序。
23. IndieAuth
IndieAuth 是基于 OAuth 2.0 构建的去中心化身份协议,使用 URL 来识别用户和应用程序。它允许人们在登录和授权使用该身份的应用程序时使用他们控制的域作为身份。规范可在https://www.w3.org/TR/indieauth/找到。
所有用户 ID 都是 URL,应用也通过其 URL 而不是预先注册的客户端 ID 进行标识。这非常适合您不想要求开发人员在每个授权服务器上注册帐户的情况,例如编写在任意 WordPress 安装中验证用户身份的应用。
当应用程序只需要识别用户登录时,可以使用 IndieAuth 作为一种身份验证机制,或者应用程序可以使用它来获取针对用户网站的访问令牌。
例如,Micropub 客户端使用 IndieAuth获取访问令牌,然后使用该令牌在用户的网站上创建内容。
IndieAuth 建立在 OAuth 2.0 框架之上,如下所示:
- 指定识别用户的机制和格式(可解析的 URL)
- 指定根据配置文件 URL 发现授权和令牌端点的方法
- 指定客户端 ID 的格式(也可作为可解析的 URL)
- 所有客户端都是公共客户端,因为不使用客户端机密
- 客户端注册不是必需的,因为所有客户端都必须使用可解析的 URL 作为其客户端 ID
- 重定向 URI 注册是通过应用程序在其网站上公布其有效的重定向 URL 来完成的
更多信息和规范可在indieauth.net上找到。以下是两个工作流程的简要概述。
23.1 发现
在应用程序重定向到授权服务器之前,应用程序需要知道将用户定向到哪个授权服务器!这是因为每个用户都由一个 URL 标识,而用户的 URL 指示其授权服务器所在的位置。
应用首先需要提示用户输入其 URL,或通过其他方式获取其 URL。通常,应用会包含一个 URL 字段供用户输入其 URL。
应用程序将向用户的个人资料 URL 发出 HTTP GET 请求,查找带有 值的HTTPLink
标头或 HTML<link>
标记。如果客户端还尝试获取用户的访问令牌,它还将查找的值。rel``authorization_endpoint``rel``token_endpoint
例如,GET 请求https://aaronparecki.com/
可能会返回以下内容,显示为缩写的 HTTP 请求。
HTTP/2 200
content-type: text/html; charset=UTF-8
link: <https://aaronparecki.com/auth>; rel="authorization_endpoint"
link: <https://aaronparecki.com/token>; rel="token_endpoint"
link: <https://aaronparecki.com/micropub>; rel="micropub"
<!doctype html>
<meta charset="utf-8">
<title>Aaron Parecki</title>
<link rel="authorization_endpoint" href="/auth">
<link rel="token_endpoint" href="/token">
<link rel="micropub" href="/micropub">
请注意,端点 URL 可以是相对 URL 或绝对 URL,并且可以与用户的端点位于同一域或不同域。这允许用户使用任何组件的托管服务。
有关发现的更多详细信息,请访问 https://indieauth.spec.indieweb.org/#discovery-by-clients。
23.2 IndieAuth 登录工作流程
用户登录应用程序的基本流程如下。
- 用户在应用程序的登录表单中输入其个人 URL。
- **发现:**应用程序获取 URL 并找到用户的授权端点。
- **授权请求:**应用程序将用户的浏览器引导到发现的授权端点,作为标准 OAuth 2.0 授权授予以及用户在第一步输入的 URL。
- **身份验证/批准:**用户在其授权端点进行身份验证并批准登录请求。授权服务器生成授权码并重定向回应用程序的重定向 URL。
- **验证:**应用程序在授权端点检查代码,类似于用代码交换访问令牌,但由于这只是身份验证检查,因此不会返回访问令牌。授权端点会使用经过身份验证的用户的完整 URL 进行响应。
身份验证请求
当应用程序构建 URL 来验证用户身份时,请求与 OAuth 授权请求非常相似,只是不需要预先注册客户端,并且请求还将包含用户的个人资料 URL。URL 如下所示。
https://user.example.net/auth?
me=https://user.example.net/
&redirect_uri=https://example-app.com/redirect
&client_id=https://example-app.com/
&state=1234567890
&code_challenge=XXXXXXXXX
&code_challenge_method=S256
然后,授权服务器将要求用户登录,就像 OAuth 流程中通常发生的那样,然后询问用户是否要继续登录应用程序,如下所示。
如果用户同意,他们将被重定向回应用程序,查询字符串中包含授权码(和应用程序的状态值)。
然后,应用程序将获取授权码并使用授权端点进行验证,以确认登录用户的身份。应用程序使用 、 和 向授权端点发出 POST 请求code
,client_id
就像redirect_uri
典型的授权码交换一样。
POST /auth
Host: user.example.net
Content-type: application/x-www-form-urlencoded
code=xxxxxxxx
&client_id=https://example-app.com/
&redirect_uri=https://example-app.com/redirect
&code_verifier=XXXXXXXXX
响应将是一个简单的 JSON 对象,包含用户的完整个人资料 URL。
HTTP/1.1 200 OK
Content-Type: application/json
{
"me": "https://user.example.net/"
}
有关处理请求和响应的更多详细信息,请参阅https://indieauth.spec.indieweb.org/#redeeming-the-authorization-code 。
23.3 IndieAuth 授权工作流程
当应用程序尝试获取用户的访问令牌以修改或访问用户的数据时,将改用授权工作流。这类似于访问数据中描述的 OAuth 2.0 授权代码工作流,只不过无需预先注册客户端,因为改用 URL。
用户授权应用程序的基本流程如下:
- 用户在应用程序的登录表单中输入其个人 URL。
- **发现:**应用程序获取 URL 并找到用户的授权和令牌端点。
- **授权请求:**应用程序将用户的浏览器引导到发现的授权端点,作为标准 OAuth 2.0 授权授予和请求的范围,以及用户在第一步输入的 URL。
- **身份验证/批准:**用户在其授权端点进行身份验证,查看请求的范围并批准请求。授权服务器生成授权码并重定向回应用程序的重定向 URL。
- **令牌交换:**应用程序向令牌端点发出请求,以将授权代码交换为访问令牌。令牌端点将使用访问令牌和经过身份验证的用户的完整 URL 进行响应。
授权请求
当应用程序构建 URL 来验证用户身份时,请求与 OAuth 授权请求非常相似,只是不需要预先注册客户端,并且请求还将包含用户的个人资料 URL。URL 如下所示。
https://user.example.net/auth?
me=https://user.example.net/
&response_type=code
&redirect_uri=https://example-app.com/redirect
&client_id=https://example-app.com/
&state=1234567890
&code_challenge=XXXXXXXXXXXXXXXX
&code_challenge_method=S256
&scope=create+update
请注意,与上面的身份验证请求不同,此请求包含response_type=code
应用程序正在请求的请求范围列表。
授权服务器将要求用户登录,然后向他们提供授权提示。
不同的 IndieAuth 服务器可能会以不同的方式显示此提示,如我的网站的授权服务器和下面显示的 WordPress IndieAuth 插件的屏幕截图所示。
当用户批准请求时,服务器会使用查询字符串中的授权码将用户重定向回应用程序。
为了获取访问令牌,应用程序使用授权码和其他所需数据向用户的令牌端点(该端点在第一个发现步骤中发现)发出 POST 请求。
POST /token
Host: user.example.net
Content-type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=xxxxxxxx
&client_id=https://example-app.com/
&redirect_uri=https://example-app.com/redirect
&code_verifier=XXXXXXXXXXXXXX
令牌端点将为用户生成访问令牌,并使用普通的 OAuth 2.0 令牌响应以及授权该应用程序的用户的个人资料 URL 进行响应。
HTTP/1.1 200 OK
Content-Type: application/json
{
"me": "https://user.example.net/",
"token_type": "Bearer",
"access_token": "XXXXXX",
"scope": "create update"
}
24. OAuth 2.0 规范图
OAuth 2.0 核心框架 (RFC 6749) 定义了角色和基本功能,但许多实现细节尚未指定。自 RFC 发布以来,OAuth 工作组已在此框架的基础上发布了许多附加规范,以填补缺失的部分。查看该小组正在制定的完整规范列表可能会让人有些不知所措。本章阐述了各种规范之间的关系。
24.1 核心规范
OAuth 2.0 核心(RFC 6749)
https://datatracker.ietf.org/doc/html/rfc6749
RFC 6749 是核心 OAuth 2.0 框架。它描述了角色(资源所有者、客户端、授权服务器等,在术语参考中有更详细的描述)、几个授权流程和几个错误定义。重要的是要记住这是一个“框架”,因为在构建完整的实现时,有许多方面尚未指定,您需要填写这些方面。其中许多细节已记录为扩展规范。
承载令牌用法 (RFC 6750)
https://datatracker.ietf.org/doc/html/rfc6750
访问令牌的使用在 RFC 6750 中定义,尽管这里没有定义访问令牌的格式。此规范定义了“承载令牌”,这仅意味着它是一种令牌类型,任何拥有令牌的人都可以使用它,而无需其他信息。访问令牌所采用的特定格式(随机字符串、JWT 等)与 OAuth 客户端无关,因此不包含在此规范中。只有授权服务器和资源服务器需要协调访问令牌格式,因此这取决于特定实现或未来的规范。
PKCE:代码交换的证明密钥(RFC 7636)
https://datatracker.ietf.org/doc/html/rfc7636
PKCE 是授权码流程的扩展,它在流程的开始和完成之间添加了一个安全链接,以防止授权码被拦截后被使用。
PKCE 的工作原理是,应用程序每次启动授权码流程时都会先生成一个新密钥,然后在初始授权请求中发送该密钥的哈希值。然后需要原始密钥才能将授权码换成访问令牌,从而确保即使攻击者可以窃取授权码,他们也无法使用它。
在发布时,PKCE 被推荐用于移动应用程序,但事实证明它甚至对 JavaScript 应用程序也很有用,现在最新的安全最佳当前实践建议将其用于所有类型的应用程序,甚至是具有客户端机密的应用程序。
威胁模型和安全注意事项(RFC 6819)
https://datatracker.ietf.org/doc/html/rfc6819
威胁模型和安全注意事项文档旨在提供核心 RFC 中所述内容以外的其他指导。该文档的大部分内容是在主要提供商拥有实际实施经验后添加的。该文档描述了许多已知攻击,无论是理论上的攻击还是已在野外演示的攻击。它描述了每种攻击的对策。
每个实施 OAuth 2.0 服务器的人都应该阅读此文档,以避免陷入已经探索和解决的陷阱。
OAuth 2.0 安全最佳当前实践(安全 BCP)
https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics
OAuth 2.0 安全最佳当前实践描述了实施 OAuth 2.0 的客户端和服务器的安全要求和其他建议。这是围绕 OAuth 安全性的新最佳当前实践,旨在收集自 2013 年发布第一个安全注意事项 RFC 以来多年来从实际部署中获得的经验。
该规范仍处于草案形式,因此在最终确定为 RFC 之前可能还会经历一些修改。
该草案中的一些具体建议是弃用隐式流程和密码授予,并建议每次使用时都发出一个新的刷新令牌。
24.2 令牌
令牌撤销(RFC 7009)
https://datatracker.ietf.org/doc/html/rfc7009
令牌撤销描述了授权服务器上的一个新端点,客户端可以使用它来通知服务器不再需要访问令牌或刷新令牌。这用于在客户端中启用“注销”功能,允许授权服务器清理与该会话相关的任何令牌或其他数据。
令牌自检(RFC 7662)
https://datatracker.ietf.org/doc/html/rfc7662
Token Introspection 规范定义了资源服务器获取有关访问令牌的信息的机制。如果没有此规范,资源服务器必须采用定制的方式来检查访问令牌是否有效,并找出有关它们的用户数据等。这通常是通过自定义 API 端点实现的,或者因为资源服务器和授权服务器共享数据库或其他一些公共存储。
通过此规范,资源服务器可以通过 HTTP API 调用检查访问令牌的有效性并查找其他信息,从而更好地分离授权服务器和任何资源服务器之间的关注点。
OAuth 访问令牌的 JWT 配置文件(RFC 9068)
https://datatracker.ietf.org/doc/html/rfc9068
JWT 配置文件根据从多个大型部署中吸取的集体经验,为访问令牌定义了基于 JWT 的格式和词汇表。
24.3 移动设备和其他设备
制定这些规范是为了让更多种类的设备能够支持 OAuth。
适用于本机应用程序的 OAuth 2.0(RFC 8252)
https://datatracker.ietf.org/doc/html/rfc8252
此规范的目标受众是移动应用程序或运行在桌面设备的应用程序的实施者,其中应用程序和浏览器之间的交互并不像在纯浏览器环境中那样简单。
在本文档中,您将找到本机应用程序环境特有的安全建议。它描述了不允许第三方应用程序打开更容易受到网络钓鱼攻击的嵌入式 Web 视图等内容,以及如何执行此操作的特定于平台的建议。它还建议使用 PKCE 扩展。
基于浏览器的应用程序
datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps
基于浏览器的应用程序的 OAuth 2.0 描述了使用 OAuth 的 JavaScript 应用程序(通常称为单页应用程序)的安全要求和其他建议。
截至本文发布时,该文件仍处于草案阶段,尚未成为 RFC。在最终定稿之前,它可能会经历一些修改。它已被工作组采纳,这意味着人们广泛认识到这种指导的必要性,尽管其中的具体建议尚未完全达成一致。
本文档旨在补充本机应用最佳当前实践,解决基于浏览器的环境的具体问题。
它建议使用带有 PKCE 的授权码流程,而不是使用隐式流程,并禁止浏览器应用授予密码。它还为这些应用提供了几种不同的架构模式。
设备授权授予(RFC 8628)
https://datatracker.ietf.org/doc/html/rfc8628
设备授权许可是一项扩展,它使没有浏览器或输入功能有限的设备能够使用 OAuth。您通常会在没有网络浏览器的 Apple TV 或没有键盘的流媒体视频编码器等设备上看到此功能。
该流程的工作原理是让用户访问智能手机等辅助设备上的 URL,并输入设备上显示的代码。
无浏览器和输入受限设备的 OAuth中更详细地描述了设备流程。
24.4 身份验证和识别
这些规范用于向应用程序提供用户身份,而核心 OAuth 规范并未提供该身份。
OpenID 连接
由于 OAuth 框架仅描述了授权方法,并没有提供任何有关用户的详细信息,因此 OpenID Connect 通过描述身份验证和会话管理机制来填补这一空白。
我们在OpenID Connect中简要概述了 OpenID Connect 与 OAuth 2.0 的关系。
独立授权
https://indieauth.spec.indieweb.org/
IndieAuth 是基于 OAuth 2.0 构建的去中心化身份协议,使用 URL 来识别用户和应用程序。这样就无需事先注册客户端,因为所有客户端都具有内置的客户端 ID:应用程序的 URL。
我们在IndieAuth中简要概述了 IndieAuth 的身份验证和授权工作流程。
24.5 互操作性
为了支持创建可以与任何 OAuth 2.0 服务器协同工作的完全通用客户端,发现和客户端注册等内容需要标准化,因为它们超出了核心规范的范围。
授权服务器元数据(RFC 8414)
https://datatracker.ietf.org/doc/html/rfc8414
授权服务器元数据规范(也称为 Discovery)定义了一种格式,供客户端用于查找与特定 OAuth 服务器交互所需的信息。这包括查找授权端点和列出支持的范围等内容。
动态客户端注册(RFC 7591)
https://datatracker.ietf.org/doc/html/rfc7591
通常,开发人员会在服务上手动注册应用程序以获取客户端 ID 并提供将在授权界面上使用的应用程序信息。此规范提供了一种动态或以编程方式注册客户端的机制。此规范源自 OpenID Connect 动态客户端注册规范,并且仍然与 OpenID Connect 服务器兼容。
动态客户端管理 (RFC 7592)
https://datatracker.ietf.org/doc/html/rfc7592
如果需要更新客户端信息,此规范提供了一种以编程方式进行更新的机制。此规范扩展了动态注册 RFC 7591,但仍处于试验阶段。
24.6 高安全性
OAuth 有一些扩展,与基本配置文件相比,它们提供了更高级别的安全性。其中一些也是 OpenID Connect 中正在进行的金融级 API 工作的一部分。
推送授权请求 (RFC 9126)
https://datatracker.ietf.org/doc/html/rfc9126
推送授权请求是对 OAuth 流程的一个重大改变,通过将授权代码流的开始移至后端通道,减少对前端通道的依赖。
JWT 授权请求(RFC 9101)
https://datatracker.ietf.org/doc/html/rfc9101
JWT 授权请求描述了一种将授权请求参数编码并签名为 JWT 的方法,而不是使用纯查询字符串组件。这让授权服务器可以确保特定 OAuth 应用程序发起了授权请求,并且该请求未被伪造或篡改。
相互 TLS 绑定访问令牌 (RFC 8705)
https://datatracker.ietf.org/doc/html/rfc8705
相互 TLS 证书绑定访问令牌描述了一种使用 TLS 证书进行客户端身份验证以及颁发证书绑定访问令牌的方法。这是实施者提高持有者令牌安全性的一种方法。
24.7 实验规范
这些是一些新规范的早期草案,最终可能会成为 OAuth 2.0 的一部分。这些规范支持更多用例,或提供更好的安全性。这些都仍是早期草案,因此在您阅读本文时它们可能会发生重大变化,或者可能已完全被放弃。如果您有兴趣了解该领域的最新发展,请关注这些内容。
丰富的授权请求
https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar
丰富的授权请求描述了应用程序请求权限的方式,这种方式比当前 OAuth“范围”机制所能提供的权限更细粒度。例如,这可以用于授权特定的银行转账。
双向PoP
https://datatracker.ietf.org/doc/html/draft-fett-oauth-dpop
DPoP 描述了一种用于颁发与特定客户端绑定的访问令牌的 Mutual TLS 替代方案。此版本在应用层而非传输层中实现该功能。
24.8 企业
这些规范支持更高级的企业用例。
断言框架(RFC 7521)
https://datatracker.ietf.org/doc/html/rfc7521
此规范提供了一个使用 OAuth 2.0 断言的框架。它定义了一种新的客户端身份验证机制和一种新的授权授予类型。由于此规范也是一个框架,因此它仅适用于下文所述的特定断言类型之一。
客户端身份验证的 JWT 配置文件 (RFC 7523)
https://datatracker.ietf.org/doc/html/rfc7523
此规范描述了如何使用 JWT 代替客户端密钥进行客户端身份验证。此方法比使用共享客户端密钥更安全,因为私钥无需离开客户端,而是用于签署 JWT。
SAML 断言(RFC 7522)
https://datatracker.ietf.org/doc/html/rfc7522
此规范描述了当与客户端存在信任关系时如何使用 SAML 断言来请求访问令牌。例如,这可用于将旧版 SAML 工作流与新 OAuth 2.0 系统集成。
25. 工具和库
25.1 工具和库
OAuth 2.0 游乐场
https://www.oauth.com/playground/
OAuth 2.0 Playground 通过与真实的 OAuth 2.0 授权服务器交互引导您完成各种 OAuth 流程。
它有授权码流、PKCE、设备流的示例,以及 OpenID Connect 的简单示例。
25.2 示例 OAuth 客户端
https://example-app.com/client
这是一个示例 OAuth 客户端,您可以使用自己的 OAuth 服务器的授权端点和令牌端点对其进行配置,提供客户端 ID 和可选密钥,然后使用实时服务器逐步完成 OAuth 流程。该工具将在执行每个重定向或请求之前向您显示,以便您了解流程中的确切步骤。
25.3 OpenID 连接调试器
OpenID Connect 调试器允许您测试 OpenID Connect 请求并调试来自服务器的响应。您可以配置该工具以与任何 OpenID 服务器(例如 Google 的服务器)配合使用。
服务器和客户端库目录
oauth.net 网站包含支持 OAuth 2.0 的服务器、客户端和服务的目录。您可以找到从完整的 OAuth 2.0 服务器实现到促进流程每个步骤的库以及客户端库和代理服务等所有内容。
如果您有任何图书馆或服务可以贡献,您也可以将它们添加到目录中。
jwt.io
jwt.io
JWT.io是一款用于调试 JSON Web Tokens 的工具。它允许您粘贴 JWT,然后它会对其进行解码并显示各个组件。如果您向它提供用于签署 JWT 的密钥,它还可以验证签名。
25.4 有关 OAuth 的视频
oauth.net/videos上有大量关于各种 OAuth 主题的视频。您也可以通过网站底部的链接随意添加自己的视频!
26 附录
规范
- OAuth 2.0(RFC 6749)
- 承载令牌用法 (RFC 6750)
- OAuth 2.0 威胁模型和安全注意事项
- 代码交换的证明密钥 (RFC 7636)
- 适用于本机应用程序的 OAuth 2.0
- OAuth 2.0 设备流程
- OAuth 2.0 访问令牌的 JWT 配置文件
- OAuth 2.0 令牌撤销
- OpenID 连接
- 独立授权
- 所有 OAuth 工作组规范
供应商文档
社区资源