在带有 PKCE 的 OAuth 授权流中使用时如何在 Azure 应用注册中启用 CORS?
Posted
技术标签:
【中文标题】在带有 PKCE 的 OAuth 授权流中使用时如何在 Azure 应用注册中启用 CORS?【英文标题】:How to enable CORS in an Azure App Registration when used in an OAuth Authorization Flow with PKCE? 【发布时间】:2020-05-07 17:10:39 【问题描述】:我有一个纯 javascript 应用程序,它尝试使用带有 PKCE 的 OAuth 授权流从 Azure 获取访问令牌。
应用程序未托管在 Azure 中。我只使用 Azure 作为 OAuth 授权服务器。
//Based on: https://developer.okta.com/blog/2019/05/01/is-the-oauth-implicit-flow-dead
var config =
client_id: "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx",
redirect_uri: "http://localhost:8080/",
authorization_endpoint: "https://login.microsoftonline.com/tenant-id/oauth2/v2.0/authorize",
token_endpoint: "https://login.microsoftonline.com/tenant-id/oauth2/v2.0/token",
requested_scopes: "openid api://tenant-id/user_impersonation"
;
// 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)
const hashed = await sha256(v);
return base64urlencode(hashed);
// 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;
// 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);
function component()
const element = document.createElement('div');
const btn = document.createElement('button');
element.innerhtml = 'Hello'+ 'webpack';
element.classList.add('hello');
return element;
(async function ()
document.body.appendChild(component());
const isAuthenticating = JSON.parse(window.localStorage.getItem('IsAuthenticating'));
console.log('init -> isAuthenticating', isAuthenticating);
if (!isAuthenticating)
window.localStorage.setItem('IsAuthenticating', JSON.stringify(true));
// 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;
else
// 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));
console.log('queryString', q);
// 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
// !!!!!!! This POST fails because of CORS policy.
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");
());
在 Azure 中,我只有一个应用注册(不是应用服务)。
Azure App Registration
获取授权码的第一步工作。
但是获取访问令牌的 POST 失败。 (图片来自here)
OAuth Authorization Code Flow with PKCE
访问 XMLHttpRequest 在 'https://login.microsoftonline.com/tenant-id/oauth2/v2.0/token' 来自 来源“http://localhost:8080”已被 CORS 策略阻止:否 请求中存在“Access-Control-Allow-Origin”标头 资源。
我在 Azure 的哪个位置配置应用注册的 CORS 策略?
【问题讨论】:
【参考方案1】:好的,经过几天对 Azure 实施的愚蠢性的抨击后,我偶然发现了一些隐藏的信息:https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-browser#prerequisites
如果您将清单中的 redirectUri 类型从“Web”更改为“Spa”,它会给我一个访问令牌!我们在做生意! 它破坏了 Azure 中的 UI,但就这样吧。
【讨论】:
嗨,刚刚尝试过,但显然只是在清单中更改它是不够的。我所做的是删除我当前的“身份验证”并使用模板“单页应用程序”创建一个新的并且它有效。好的答案伙计,因为它真的没有很好的记录。 这是正确的答案。我尝试将“平台”更改为“单页应用程序”并将localhost(用于本地开发)和我的应用程序的 URL 添加到重定向 URI 列表。这就是造成差异的原因。为这些 URL 启用了 CORS。谢谢你们。【参考方案2】:您应该使用您的本地主机地址定义内部 url。
https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/application-proxy-understand-cors-issues
【讨论】:
即使注册了应用程序,您也应该能够定义应用程序 URL。【参考方案3】:当我第一次发布时,Azure AD 令牌端点不允许从浏览器到令牌端点的 CORS 请求,但现在可以了。这些帖子和代码中解释了有关范围和令牌验证的一些 Azure AD 特性,以防万一:
Code Sample Blog Post【讨论】:
以上是关于在带有 PKCE 的 OAuth 授权流中使用时如何在 Azure 应用注册中启用 CORS?的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 PKCE 为 React 单页应用程序实现 OAuth2 授权代码授予?