OAuth 详解<5> 什么是OAuth 2.0 隐式流? 已经不被推荐了么?
OAuth 2.0 隐式流死了吗?
demo007x/oauth2-client: Oauth2 Client package for Golang (github.com) 欢迎star
您最近可能听说过一些关于 OAuth 2.0 隐式流程的讨论。OAuth 工作组发布了一些关于隐式流程和基于 JavaScript 的应用程序的新指南,特别指出不应再使用隐式流程。在本文中,我们将了解隐式流程发生了什么变化以及原因。
OAuth 2.0 中隐式的最佳实践正在改变
OAuth 2.0 中的隐式流创建于将近 10 年前,当时浏览器的工作方式与今天截然不同。创建隐式流的主要原因是浏览器中的旧限制。过去的情况是,JavaScript 只能向加载页面的同一服务器发出请求。但是,标准的 OAuth 授权代码流程要求向 OAuth 服务器的令牌端点发出 POST 请求,该端点通常与应用程序位于不同的域中。这意味着以前无法通过 JavaScript 使用此流程。隐式流程通过避免该 POST 请求来解决此限制,而是在重定向中立即返回访问令牌。
如今,跨源资源共享 (CORS) 已被浏览器普遍采用,不再需要这种妥协。CORS 为 JavaScript 提供了一种向不同域上的服务器发出请求的方法,只要目的地允许。这开启了在 JavaScript 中使用授权码流程的可能性。
值得注意的是,与授权码流程相比,隐式流程一直被视为一种妥协。例如,规范没有提供在隐式流中返回刷新令牌的机制,因为它被认为太不安全而不允许这样做。该规范还建议通过隐式流程发布的访问令牌的生命周期短,范围有限。
OAuth 授权代码流程更好
既然可以从浏览器使用授权代码流,我们还有一个关于 JavaScript 应用程序的问题需要处理。传统上,授权代码流程在为访问令牌交换授权代码时使用客户端密码,但没有办法在 JavaScript 应用程序中包含客户端密码并使其保持秘密。如果你要在源代码中包含一个秘密,任何使用该应用程序的人都可以在他们的浏览器中“查看源代码”并看到它。所以我们需要一个解决方案。
值得庆幸的是,这个问题已经解决了,因为同样的问题也适用于移动应用程序。本机应用程序也无法安全地使用客户端密码。OAuth 工作组在几年前通过对授权代码流程的 PKCE 扩展解决了这个问题。
PKCE 的授权代码流程添加了一个额外的步骤,它允许我们保护授权代码,这样即使它在重定向期间被盗,它本身也将毫无用处。您可以在我们的博客文章OAuth 2.0 for Native and Mobile Apps中阅读有关 PKCE 工作原理的更多信息。
现有应用程序的 OAuth 2.0 隐式流程
这里要记住的重要一点是,在隐式流中没有发现新的漏洞。如果您有一个使用隐式流程的现有应用程序,并不是说您的应用程序在发布此新指南后突然变得不安全。
也就是说,安全地实施隐式流程是 - 而且一直是 - 极具挑战性。如果您不厌其烦地彻底审查了您的源代码,确切地知道您在您的应用程序中使用了哪些第三方库,拥有强大的内容安全策略,并且对您构建安全的 JavaScript 应用程序的能力充满信心,那么您的应用程序可能没问题。
那么,您是否应该立即将所有应用程序切换为使用 PKCE 而不是隐式流?可能不会,这取决于你的风险承受能力。但在这一点上,我绝对不建议使用隐式流程创建新应用程序。
授权代码流是否使基于浏览器的应用程序完全安全?
不幸的是,没有完美的安全性。尤其是在浏览器中,应用程序总是有很多种可能受到攻击的方式。我们能做的最好的事情就是防止常见的攻击,并减少应用程序的整体攻击面。
具体来说,带有 PKCE 的授权代码流确实可以完全保护应用程序免受授权代码在传输回应用程序的过程中被盗的攻击。然而,一旦 JavaScript 应用程序获得了访问令牌,它仍然必须将它存储在某个地方才能使用它,并且无论应用程序使用隐式流还是 PKCE 来获取它,它存储访问令牌的方式都是相同的。您仍然需要确保您拥有良好的内容安全策略,并了解您在应用程序中使用的任何第三方库。
在 JavaScript 应用程序中安全实施 OAuth 的最佳方式是将令牌管理完全置于 JavaScript 之外。如果您正在构建一个由动态服务器提供服务的 JavaScript 应用程序,例如带有 Angular 前端的 Spring Boot 后端,或带有 React 前端的 ASP.NET 后端,那么您可以保留所有 OAuth 交换和后端内部的令牌管理,从不将其暴露给 JavaScript 前端,并避免在 JavaScript 中管理令牌的所有固有风险。
立即开始在 JavaScript 中使用 PKCE
那么您准备好开始在 JavaScript 中使用 PKCE 编写应用程序了吗?让我们来看看这到底意味着什么。
出于本演示的目的,我们假设您希望在纯 JavaScript 中实现它,而不需要额外的库。这将准确说明 PKCE 的工作原理,然后您应该能够将其转化为您选择的特定框架。
首先,注册一个免费的 Okta Developer 帐户。注册后,从页面顶部的菜单中选择应用程序,然后单击****添加应用程序。
从选项中选择单页应用程序,这将配置此应用程序以在令牌端点上启用 CORS 标头,并且不会创建客户端机密。
为您的应用程序命名,然后您需要更改两个设置。
更改登录重定向 URI以匹配基本 URI,因为我们将在一个 HTML 文件中构建单页应用程序。
还要确保选中Authorization Code复选框,并取消选中Implicit。
client_id
这将注册应用程序并在下一个屏幕上为您提供一个。记下这个值,因为我们稍后会再次需要它。
创建一个新文件夹,并在其中创建一个名为index.html
以下内容的 HTML 文件。在下面的配置块中填写您的客户端 ID。
<html>
<title>OAuth Authorization Code + PKCE in Vanilla JS</title>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<script>
// Configure your application and authorization server details
var config = {
client_id: "",
redirect_uri: "http://localhost:8080/",
authorization_endpoint: "",
token_endpoint: "",
requested_scopes: "openid"
};
</script>
接下来,我们需要为您的 OAuth 服务器找到授权端点和令牌端点。导航到顶部主菜单中的API ,然后选择****Authorization Servers。您可能在该列表中只有一台服务器,即“默认”。
从该授权服务器复制 Issuer URI。您的授权端点将是/v1/authorize
附加的 URI,令牌端点将以/v1/token
.
例如,如果您的 Issuer URI 是https://{yourOktaDomain}/oauth2/default
,那么您的授权端点将为https://{yourOktaDomain}/oauth2/default/v1/authorize
并且您的令牌端点将为https://{yourOktaDomain}/oauth2/default/v1/token
。在上一步创建的 JavaScript 配置对象中输入这两个值。
设置 HTML 结构
接下来,让我们向页面添加一些 HTML 以创建几个 UI 元素来帮助说明此流程。
<div class="flex-center full-height">
<div class="content">
<a href="#" id="start">Click to Sign In</a>
<div id="token" class="hidden">
<h2>Access Token</h2>
<div id="access_token" class="code"></div>
</div>
<div id="error" class="hidden">
<h2>Error</h2>
<div id="error_details" class="code"></div>
</div>
</div>
</div>
为了让它看起来不错,在下面添加以下 CSS。
<style>
body {
padding: 0;
margin: 0;
min-height: 100vh;
font-family: arial, sans-serif;
}
@media(max-width: 400px) {
body {
padding: 10px;
}
}
.full-height {
min-height: 100vh;
}
.flex-center {
align-items: center;
display: flex;
justify-content: center;
}
.content {
max-width: 400px;
}
h2 {
text-align: center;
}
.code {
font-family: "Courier New", "Courier", monospace;
width: 100%;
padding: 4px;
border: 1px #ccc solid;
border-radius: 4px;
word-break: break-all;
}
.hidden {
display: none;
}
</style>
开始 PKCE 请求
有了这个,我们就可以得到好的东西,实际上在 JavaScript 中启动 PKCE 流程。首先,添加一个新<script>
标签,这样我们就有了开始编写 JavaScript 的地方。
<script>
</script>
我们首先要定义一些辅助函数来处理 PKCE 的棘手部分:安全地生成一个随机字符串,并生成该字符串的 SHA256 散列。
将这些函数添加到<script>
您刚刚创建的标签中。
// PKCE HELPER FUNCTIONS
// Generate a secure random string using the browser crypto functions
function generateRandomString() {
var array = new Uint32Array(28);
window.crypto.getRandomValues(array);
return Array.from(array, dec => ('0' + dec.toString(16)).substr(-2)).join('');
}
// Calculate the SHA256 hash of the input text.
// Returns a promise that resolves to an ArrayBuffer
function sha256(plain) {
const encoder = new TextEncoder();
const data = encoder.encode(plain);
return window.crypto.subtle.digest('SHA-256', data);
}
// Base64-urlencodes the input string
function base64urlencode(str) {
// Convert the ArrayBuffer to string using Uint8 array to convert to what btoa accepts.
// btoa accepts chars only within ascii 0-255 and base64 encodes them.
// Then convert the base64 encoded to base64url encoded
// (replace + with -, replace / with _, trim trailing =)
return btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
// Return the base64-urlencoded sha256 hash for the PKCE challenge
async function pkceChallengeFromVerifier(v) {
hashed = await sha256(v);
return base64urlencode(hashed);
}
现在我们已准备好启动流程。PKCE 流程的第一步是生成一个秘密,对其进行哈希处理,然后将用户重定向到在 URL 中包含该哈希值的授权服务器。
我们将向我们在 HTML 中创建的链接添加一个onclick
侦听器。Click to Sign In
// Initiate the PKCE Auth Code flow when the link is clicked
document.getElementById("start").addEventListener("click", async function(e){
e.preventDefault();
// Create and store a random "state" value
var state = generateRandomString();
localStorage.setItem("pkce_state", state);
// Create and store a new PKCE code_verifier (the plaintext random secret)
var code_verifier = generateRandomString();
localStorage.setItem("pkce_code_verifier", code_verifier);
// Hash and base64-urlencode the secret to use as the challenge
var code_challenge = await pkceChallengeFromVerifier(code_verifier);
// Build the authorization URL
var url = config.authorization_endpoint
+ "?response_type=code"
+ "&client_id="+encodeURIComponent(config.client_id)
+ "&state="+encodeURIComponent(state)
+ "&scope="+encodeURIComponent(config.requested_scopes)
+ "&redirect_uri="+encodeURIComponent(config.redirect_uri)
+ "&code_challenge="+encodeURIComponent(code_challenge)
+ "&code_challenge_method=S256"
;
// Redirect to the authorization server
window.location = url;
});
这个函数做了几件事:
- 创建一个随机字符串用作
state
值并将其存储在LocalStorage
- 创建一个随机字符串用作 PKCE
code_verifier
值 - 哈希和 base64-urlencodes 代码验证器
- 使用您在开始时定义的配置值,构建具有所有必需参数的授权 URL
- 将浏览器重定向到授权 URL
此时,用户被交给授权服务器登录。授权服务器随后将用户重定向回应用程序,查询字符串中将有两个参数:code
和state
。
使用授权码获取访问令牌
此应用程序将需要验证该state
值是否与它在开始时生成的值相匹配,然后将授权代码交换为访问令牌。为此,我们需要添加更多辅助函数。
将以下函数添加到 JavaScript 的底部。此函数将查询字符串解析为 JavaScript 对象。
// Parse a query string into an object
function parseQueryString(string) {
if(string == "") { return {}; }
var segments = string.split("&").map(s => s.split("=") );
var queryString = {};
segments.forEach(s => queryString[s[0]] = s[1]);
return queryString;
}
还要添加下面的函数,这将为我们提供一种简单的方法来发出 POST 请求并解析 JSON 响应。
// Make a POST request and parse the response as JSON
function sendPostRequest(url, params, success, error) {
var request = new XMLHttpRequest();
request.open('POST', url, true);
request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
request.onload = function() {
var body = {};
try {
body = JSON.parse(request.response);
} catch(e) {}
if(request.status == 200) {
success(request, body);
} else {
error(request, body);
}
}
request.onerror = function() {
error(request, {});
}
var body = Object.keys(params).map(key => key + '=' + params[key]).join('&');
request.send(body);
}
现在您已准备好用授权代码交换访问令牌。如果您熟悉传统的授权代码流程,您会记得这一步通常需要客户端密码。但是由于我们没有此 JavaScript 应用程序的客户端机密,因此我们将在发出此请求时发送 PKCE 代码验证程序,以确保只有请求代码的应用程序才能将其交换为访问令牌。
将以下代码添加到您的 JavaScript 部分。
/ Handle the redirect back from the authorization server and
// get an access token from the token endpoint
var q = parseQueryString(window.location.search.substring(1));
// Check if the server returned an error string
if(q.error) {
alert("Error returned from authorization server: "+q.error);
document.getElementById("error_details").innerText = q.error+"\n\n"+q.error_description;
document.getElementById("error").classList = "";
}
// If the server returned an authorization code, attempt to exchange it for an access token
if(q.code) {
// Verify state matches what we set at the beginning
if(localStorage.getItem("pkce_state") != q.state) {
alert("Invalid state");
} else {
// Exchange the authorization code for an access token
sendPostRequest(config.token_endpoint, {
grant_type: "authorization_code",
code: q.code,
client_id: config.client_id,
redirect_uri: config.redirect_uri,
code_verifier: localStorage.getItem("pkce_code_verifier")
}, function(request, body) {
// Initialize your application now that you have an access token.
// Here we just display it in the browser.
document.getElementById("access_token").innerText = body.access_token;
document.getElementById("start").classList = "hidden";
document.getElementById("token").classList = "";
// Replace the history entry to remove the auth code from the browser address bar
window.history.replaceState({}, null, "/");
}, function(request, error) {
// This could be an error response from the OAuth server, or an error because the
// request failed such as if the OAuth server doesn't allow CORS requests
document.getElementById("error_details").innerText = error.error+"\n\n"+error.error_description;
document.getElementById("error").classList = "";
});
}
// Clean these up since we don't need them anymore
localStorage.removeItem("pkce_state");
localStorage.removeItem("pkce_code_verifier");
}
这段代码做了几件事:
- 检查授权服务器是否返回错误消息,如果是则显示给用户
- 检查授权服务器是否返回授权码,并将其交换为访问令牌
- 向令牌端点发送 POST 请求,其中包括
code_verifier
它在上一步中创建的参数 - 更新 UI 以指示错误消息或显示返回的访问令牌
- 使用会话历史管理 API 从地址栏中删除授权代码
此时,您已准备好试用该应用程序!您需要运行本地 Web 服务器,或将其托管在测试域上。在任何情况下,只需确保您的应用程序设置中的基本 URI和重定向 URI设置为您将访问此应用程序的 URL。(另请注意,由于浏览器对 URI 的跨域限制,仅从您的文件系统打开页面将无法正常工作file://
)。
您可以使用任何 Web 服务器来提供文件,但我发现启动此应用程序的一种简单方法是使用 PHP 的内置 Web 服务器。您可以运行以下命令在端口 8080 上启动 Web 服务器:
php -S localhost:8080
您现在可以在浏览器中访问http://localhost:8080/
,您将看到登录链接。
单击该链接,您将被重定向到 Okta。如果您已经登录,您将立即被重定向,应用程序将获得访问令牌!
恭喜!您已经使用 vanilla JavaScript 在浏览器中成功实现了 PKCE!
您可以在此处找到完整的示例代码:pkce-vanilla-js
希望这对在浏览器中执行 PKCE 所需的内容有所帮助!在实践中,您可能会使用一个 JavaScript 库在幕后为您处理这个问题,但了解它在幕后是如何工作的仍然很有用!
OAuth 2.0 PKCE Flow
本文来自博客园,作者:demo007x,转载请注明原文链接:https://www.cnblogs.com/demo007x/articles/17307370.html