Microsoft Entra(Azure ADD)做 SSO单点登录 OAuth2认证详解
Author:zhoulujun Date:
《Microsoft 标识平台身份验证库 (MSAL) 使用:react+MsalProvider鉴权》 使用起来比较简单,但是交互流程是怎么样的呢?
Microsoft 标识平台支持各种新式应用体系结构的身份验证,所有这些体系结构都基于行业标准协议 OAuth 2.0 或 OpenID Connect。
这里安利下:《单点登录之身份验证与授权:CAS/OAuth/SAML/OpenID》
在明白授权逻辑前,前期概念准备
JavaScript: 作为前端开发的主要语言,JavaScript在单页应用中扮演着核心角色。
MSAL.js: Microsoft Authentication Library for JavaScript,用于处理身份验证和授权流程。
Microsoft Graph API: 用于访问Microsoft 365数据和服务。
Azure App Services: 用于部署和管理Web应用。
Azure Storage: 用于存储和管理应用数据。
Azure Key Vault: 用于安全地存储和管理密钥、证书和机密。
Azure Functions: 用于构建无服务器应用程序。
Microsoft Entra ID: 用于企业级身份管理和访问控制。
Azure Active Directory B2C: 用于客户身份管理和访问控制。
方案适用于以下应用场景:
企业内部应用: 使用Microsoft Entra ID进行用户身份验证,确保只有授权用户可以访问企业内部应用。
客户身份管理: 使用Azure Active Directory B2C管理客户身份,支持多种社交身份提供商,如Google、Facebook等。
API保护: 通过Microsoft Entra ID或Azure Active Directory B2C保护Web API,确保只有经过身份验证的用户可以访问。
云原生应用开发: 结合Azure的各种服务,如App Services、Storage、Key Vault等,构建完整的云原生应用。
本篇主要讲网页,其他的比这个还简单
单页应用
单页应用 (SPA) 前端, Microsoft 标识平台支持这些应用的方式是使用 OpenID Connect 协议进行身份验证,以及使用 OAuth 2.0 定义的两种授权类型之一。
再安利下《OAuth 2.0 扩展协议之PKCE》
OAuth 2.0交互流程如下
分为三个步骤:
请求授权代码(获取code)
兑换访问令牌的代码(通过code换取accessToken)
使用访问令牌获取用户信息(校验token并获取用户信息,并获取登陆用户信息)
请求授权代码:/oauth2/v2.0/authorize
https://login.microsoftonline.com/common/oauth2/v2.0/authorize
重定向进入登录页:重定向页面接口,跳转P10登陆窗口,并回调code给 业务
https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize?
client_id=00001111-aaaa-2222-bbbb-3333cccc4444
&response_type=code
&redirect_uri=http%3A%2F%2Flocalhost%2Fmyapp%2F
&response_mode=query
&scope=https%3A%2F%2Fgraph.microsoft.com%2Fmail.read
&state=12345
&code_challenge=YTFjNjI1OWYzMzA3MTI4ZDY2Njg5M2RkNmVjNDE5YmEyZGRhOGYyM2IzNjdmZWFhMTQ1ODg3NDcxY2Nl
&code_challenge_method=S256用户经过身份验证并授予同意后,Microsoft 标识平台将使用 response_mode 参数中指定的方法,将响应返回到位于所指示的 redirect_uri 的应用。
GET http://localhost? code=AwABAAAAvPM1KaPlrEqdFSBzjqfTGBCmLdgfSTLEMPGYuNHSUYBrq... &state=12345
发现已经存在登陆态,通过接口https://login.microsoftonline.com/kmsi 返回302重定向返回code
参数说明:
| 参数 | 选择 | 说明 |
|---|---|---|
| tenant | 必需 | 请求路径中的 {tenant} 值可用于控制哪些用户可以登录应用程序。 有效值为 common、organizations、consumers 和租户标识符。 对于用户从一个租户登录到另一个租户的来宾场景,必须提供租户标识符才能让其登录到资源租户。 有关详细信息,请参阅终结点。 |
| client_id | 必答 | Microsoft Entra 管理中心 - 应用注册体验分配给应用的“应用程序(客户端) ID”。 |
| response_type | 必答 | 必须包括授权代码流的 code 。 如果使用混合流,则还可以包括 id_token 或 token。 |
| redirect_uri | 必需 | 应用的 redirect_uri,你的应用可通过该应用发送和接收身份验证响应。 其必须完全符合在 Microsoft Entra 管理中心中注册的其中一个重定向 URI,否则必须是编码的 URL。 对于本机应用和移动应用,请使用一个建议的值:https://login.microsoftonline.com/common/oauth2/nativeclient(适用于使用嵌入式浏览器的应用)或 http://localhost(适用于使用系统浏览器的应用)。 |
| scope | 必需 | 希望用户同意的范围的空格分隔列表。 对于请求的 /authorize 分支,此参数可以涵盖多个资源。 此值允许应用获取你要调用的多个 Web API 的同意。 |
| response_mode | 建议 | 指定标识平台应如何将请求的令牌返回到应用。 支持的值: - query:请求访问令牌时的默认值。 在重定向 URI 上提供代码作为查询字符串参数。 使用隐式流请求 ID 令牌时不支持该 query 参数。 - fragment:通过使用隐式流请求 ID 令牌时的默认值。 如果只请求一个代码,也支持。 - form_post:对重定向 URI 执行包含代码的 POST。 请求代码时支持。 |
| state | 建议 | 同时随令牌响应返回的请求中所包含的值。 可以是想要的任何内容的字符串。 随机生成的唯一值通常用于 防止跨站点请求伪造攻击。 该值还可用于在身份验证请求发生前,对有关用户在应用中的状态信息进行编码。 例如,它可以对用户所在的页面或视图进行编码。 |
| prompt | 可选 | 表示需要的用户交互类型。 有效值为 login、none、consent 和 select_account。 - prompt=login 强制用户在该请求上输入其凭据,从而使单一登录无效。 - prompt=none 则相反。 它确保不向用户显示任何交互式提示。 如果请求无法通过单一登录以无提示方式完成,则 Microsoft 标识平台将返回 interaction_required 错误。 - prompt=consent 在用户登录后触发 OAuth 同意对话框,要求用户向应用授予权限。 - prompt=select_account 将中断单一登录,提供帐户选择体验,列出会话或任何记住的帐户中的所有帐户,或者提供选择一起使用其他帐户的选项。 |
| login_hint | 可选 | 可使用此参数预先填充用户登录页面的用户名和电子邮件地址字段。 应用在已经从前次登录提取 login_hint 可选声明后,可在重新身份验证时使用此参数。 |
| domain_hint | 可选 | 如果包含,应用将跳过用户在登录页面上经历的基于电子邮件的发现过程,导致稍微更加流畅的用户体验。 例如,将其发送到其联合标识提供者。 应用可以在重新身份验证期间使用此参数,方法是从前次登录提取 tid。 |
| code_challenge | 建议/必需 | 用于通过“用于代码交换的证明密钥”(PKCE) 来保护授权代码授予。 如果包含 code_challenge_method,则需要。 有关详细信息,请参阅 PKCE RFC。 目前建议将参数用于所有应用程序类型(公共和机密客户端),Microsoft 标识平台则要求使用授权代码流的单页应用使用此参数。 |
| code_challenge_method | 建议/必需 | 用于为 code_challenge 参数编码 code_verifier 的方法。 此方法应为 S256,但是如果客户端不能支持 SHA256,则该规范允许使用 plain。 如果已排除在外,且包含了 code_challenge,则假定 code_challenge 为纯文本。 Microsoft 标识平台支持 plain 和 S256。 有关详细信息,请参阅 PKCE RFC。 使用授权代码流的单页应用需要此参数。 |
此时,系统会要求用户输入凭据并完成身份验证。 Microsoft 标识平台还将确保用户已同意 scope 查询参数中指定的权限。 如果用户未同意其中的任一权限,就让用户同意所需的权限。 有关详细信息,请参阅 Microsoft 标识平台中的权限和同意。
兑换访问令牌的代码:/oauth2/v2.0/token
https://login.microsoftonline.com/common/oauth2/v2.0/token
通过code换取accessToken:
POST /{tenant}/oauth2/v2.0/token HTTP/1.1
Host: https://login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded
client_id=11112222-bbbb-3333-cccc-4444dddd5555
&scope=https%3A%2F%2Fgraph.microsoft.com%2Fmail.read
&code=OAAABAAAAiL9Kn2Z27UubvWFPbm0gLWQJVzCTE9UkP3pSx1aXxUjq3n8b2JRLk4OxVXr...
&redirect_uri=http%3A%2F%2Flocalhost%2Fmyapp%2F
&grant_type=authorization_code
&code_verifier=ThisIsntRandomButItNeedsToBe43CharactersLong
&client_secret=sampleCredentia1s获取 authorization_code 并获取用户授予的权限后,接下来可以兑换 code 以获取资源的 access_token。 通过向 /token 终结点发送 POST 请求来兑换 code
参数说明:
| 参数 | 必需/可选 | 说明 |
|---|---|---|
tenant | 必需 | 请求路径中的 {tenant} 值可用于控制哪些用户可以登录应用程序。 有效值为 common、organizations、consumers 和租户标识符。 有关详细信息,请参阅终结点。 |
client_id | 必答 | Microsoft Entra 管理中心 - 应用注册页分配给你的应用的应用程序(客户端)ID。 |
scope | 可选 | 范围的空格分隔列表。 范围必须全部来自单个资源,以及 OIDC范围(profile、openid、email)。 有关详细信息,请参阅 Microsoft 标识平台中的权限和同意。 此参数是授权代码流的 Microsoft 扩展,旨在允许应用在令牌兑换期间声明其需要令牌的资源。 |
code | 必需的 | 在流的第一个阶段获取的 authorization_code。 |
redirect_uri | 必需的 | 用于获取 authorization_code 的相同 redirect_uri 值。 |
grant_type | 必需 | 必须是授权代码流的 authorization_code 。 |
code_verifier | 建议 | 用于获取 authorization_code 的相同 code_verifier。 如果在授权码授权请求中使用 PKCE,则需要。 有关详细信息,请参阅 PKCE RFC。 |
client_secret | 机密 Web 应用所需 | 在应用注册门户中为应用创建的应用程序机密。 请不要在本机应用或单页应用中使用应用程序机密,因为 client_secret 无法可靠地存储在设备或网页上。 可将 client_secret 安全地存储在服务器端的 Web 应用和 Web API 需要应用程序机密。 与此处讨论的所有参数一样,客户端机密在发送之前必须进行 URL 编码。 此步骤由 SDK 完成。 有关 URI 编码的详细信息,请参阅 URI 常规语法规范。 还支持根据 RFC 6749 在授权标头中提供凭据的基本身份验证模式。 |
获取用户信息
https://graph.microsoft.com/v1.0/me
通过accessToken获取登陆用户信息,并校验accessToken有效性
import requests
user_info_url = "https://graph.microsoft.com/v1.0/me"
headers = {'Authorization': f'Bearer {access_token}'}
user_info_response = requests.get(user_info_url, headers=headers)
user_info = user_info_response.json()
print(user_info)完整流程
const axios = require('axios');
const querystring = require('querystring');
// 替换以下占位符为你的实际值
const tenantId = '{tenant-id}';
const clientId = '{client-id}';
const clientSecret = '{client-secret}';
const redirectUri = '{redirect-uri}';
const authorizationCode = '{authorization-code}';
// Step 2: Exchange authorization code for tokens
const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
const tokenPayload = querystring.stringify({
grant_type: 'authorization_code',
client_id: clientId,
redirect_uri: redirectUri,
code: authorizationCode,
client_secret: clientSecret,
});
axios.post(tokenUrl, tokenPayload, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
})
.then(tokenResponse => {
const accessToken = tokenResponse.data.access_token;
// Step 3: Get user info using the access token
const graphUrl = 'https://graph.microsoft.com/v1.0/me';
const config = {
headers: {
Authorization: `Bearer ${accessToken}`,
},
};
return axios.get(graphUrl, config);
})
.then(userInfoResponse => {
console.log(JSON.stringify(userInfoResponse.data, null, 2));
})
.catch(error => {
console.error('Error:', error);
});当然更加推进直接用的套件,比如《Microsoft 标识平台身份验证库 (MSAL) 使用:react+MsalProvider鉴权》
原生JS完整项目流程
前端:设置登录按钮和OAuth 2.0授权URL
后端:处理授权码交换
前端:获取访问令牌并调用API
前端就是一个登录按钮,重定向到Microsoft登录页面 (微软的授权服务中心),拿到code去后端拿token
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Microsoft Authentication</title> </head> <body> <button id="loginButton">Login with Microsoft</button> <script src="script.js"></script> </body> </html>
然后,在script.js文件中添加以下代码:
document.getElementById('loginButton').addEventListener('click', function() {
const clientId = 'YOUR_CLIENT_ID'; // 替换为你的客户端ID
const tenantId = 'YOUR_TENANT_ID'; // 替换为你的租户ID
const redirectUri = 'YOUR_REDIRECT_URI'; // 替换为你的重定向URI
const scope = 'https://graph.microsoft.com/User.Read'; // 替换为你需要的权限范围
const authUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?
response_type=code
&client_id=${clientId}
&redirect_uri=${redirectUri}
&scope=${scope}
&state=12345
&code_challenge_method=S256
&code_challenge=YOUR_CODE_CHALLENGE`; // 替换为生成的code_challenge
// 生成code_challenge(这是一个简化的例子,实际生产环境中需要安全生成)
function generateCodeVerifier() {
return Math.random().toString(36).substr(2, 128);
}
function generateCodeChallenge(codeVerifier) {
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const hashBuffer = crypto.subtle.digest('SHA-256', data);
return btoa(String.fromCharCode(...new Uint8Array(hashBuffer))).replace(/=+$/, '');
}
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);
// 临时存储codeVerifier(实际应用中应存储在安全的地方,如浏览器本地存储)
sessionStorage.setItem('codeVerifier', codeVerifier);
// 重定向到Microsoft登录页面
window.location.href = authUrl;
});
// 处理重定向后的回调
window.addEventListener('load', function() {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
if (code) {
// 发送POST请求到你的后端服务器以交换访问令牌
fetch('/token-endpoint', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
code: code,
codeVerifier: sessionStorage.getItem('codeVerifier'),
clientId: 'YOUR_CLIENT_ID',
redirectUri: 'YOUR_REDIRECT_URI',
grantType: 'authorization_code',
tenantId: 'YOUR_TENANT_ID',
clientSecret: 'YOUR_CLIENT_SECRET' // 如果你的应用是机密客户端,需要client_secret
})
})
.then(response => response.json())
.then(data => {
console.log('Access Token:', data.access_token);
// 你可以在这里使用访问令牌调用API
})
.catch(error => {
console.error('Error exchanging code for token:', error);
});
}
});下面是一个简单的Node.js后端服务器示例,用于处理授权码交换:
const express = require('express');
const axios = require('axios');
const bodyParser = require('body-parser');
const app = express();
const port = 3000;
app.use(bodyParser.json());
app.post('/token-endpoint', async (req, res) => {
const { code, codeVerifier, clientId, redirectUri, grantType, tenantId, clientSecret } = req.body;
const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
const response = await axios.post(tokenUrl, {
grant_type: grantType,
code: code,
redirect_uri: redirectUri,
client_id: clientId,
code_verifier: codeVerifier,
client_secret: clientSecret // 如果你的应用是机密客户端,需要client_secret
}, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
transformRequest: [(data, headers) => {
return axios.defaults.transformRequest[0](qs.stringify(data), headers);
}],
qs: require('qs')
});
res.json(response.data);
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}/`);
});个人还是建议用后端的
document.getElementById('loginButton').addEventListener('click', function() {
// 构建授权URL
const clientId = 'YOUR_CLIENT_ID';
const tenantId = 'YOUR_TENANT_ID';
const redirectUri = encodeURIComponent('http://localhost:3000'); // 替换为你的重定向URI
const authUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?client_id=${clientId}&response_type=code&redirect_uri=${redirectUri}&scope=openid%20profile%20email&response_mode=query`;
// 重定向到授权URL
window.location.href = authUrl;
});
// 处理重定向后的回调
window.addEventListener('load', function() {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
if (code) {
// 使用授权码获取访问令牌
fetchAccessToken(code).then(accessToken => {
// 使用访问令牌获取用户信息
fetchUserInfo(accessToken).then(userInfo => {
document.getElementById('userProfile').innerText = JSON.stringify(userInfo, null, 2);
}).catch(error => {
console.error('Error fetching user info:', error);
});
}).catch(error => {
console.error('Error fetching access token:', error);
});
}
});
function fetchAccessToken(code) {
const clientId = 'YOUR_CLIENT_ID';
const tenantId = 'YOUR_TENANT_ID';
const redirectUri = encodeURIComponent('http://localhost:3000'); // 替换为你的重定向URI
const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
const requestBody = new URLSearchParams({
client_id: clientId,
scope: 'openid profile email',
code: code,
redirect_uri: redirectUri,
grant_type: 'authorization_code'
});
return fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: requestBody
}).then(response => response.json()).then(data => data.access_token);
}
function fetchUserInfo(accessToken) {
const userInfoUrl = 'https://graph.microsoft.com/v1.0/me';
return fetch(userInfoUrl, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
}).then(response => response.json());
}在前端直接调用Microsoft Graph API,可能会遇到CORS问题,这里需要配置下。
转载本站文章《Microsoft Entra(Azure ADD)做 SSO单点登录 OAuth2认证详解》,
请注明出处:https://www.zhoulujun.cn/html/tools/cloudServices/Azure/9290.html