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