• home > tools > cloudServices > Azure >

    Microsoft Entra(Azure ADD)做 SSO单点登录 OAuth2认证详解

    Author:zhoulujun Date:

    本篇教你如何做Microsoft 标识平台身份验证,本篇微软原生组件出发,而是先讲解里面的原理,然后通过原生JS做一个完整项目演示,再看 《Microsoft 标识平台身份验证库 (MSAL) 使用:react+MsalProvider鉴权》

    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: 用于客户身份管理和访问控制。

    方案适用于以下应用场景:

    1. 企业内部应用: 使用Microsoft Entra ID进行用户身份验证,确保只有授权用户可以访问企业内部应用。

    2. 客户身份管理: 使用Azure Active Directory B2C管理客户身份,支持多种社交身份提供商,如Google、Facebook等。

    3. API保护: 通过Microsoft Entra ID或Azure Active Directory B2C保护Web API,确保只有经过身份验证的用户可以访问。

    4. 云原生应用开发: 结合Azure的各种服务,如App Services、Storage、Key Vault等,构建完整的云原生应用。

    本篇主要讲网页,其他的比这个还简单

    单页应用

    单页应用 (SPA) 前端, Microsoft 标识平台支持这些应用的方式是使用 OpenID Connect 协议进行身份验证,以及使用 OAuth 2.0 定义的两种授权类型之一。

     再安利下《OAuth 2.0 扩展协议之PKCE

    OAuth 2.0交互流程如下

    分为三个步骤:

    1. 请求授权代码(获取code)

    2. 兑换访问令牌的代码(通过code换取accessToken)

    3. 使用访问令牌获取用户信息(校验token并获取用户信息,并获取登陆用户信息)

    具体教程如下:https://learn.microsoft.com/zh-cn/entra/identity-platform/v2-oauth2-auth-code-flow#refresh-the-access-token

    该图显示了 OAuth 授权代码流。本机应用和 Web API 使用本文中描述的令牌进行交互。

    请求授权代码:/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} 值可用于控制哪些用户可以登录应用程序。 有效值为 commonorganizationsconsumers 和租户标识符。 有关详细信息,请参阅终结点
    client_id必答Microsoft Entra 管理中心 - 应用注册页分配给你的应用的应用程序(客户端)ID。
    scope可选范围的空格分隔列表。 范围必须全部来自单个资源,以及 OIDC范围(profileopenidemail)。 有关详细信息,请参阅 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