Azure AD认证和Azure AD B2C的token获取

Azure AD认证和Azure AD B2C的token获取


工作当中使用过Azure AD认证和B2C的认证,今天抽时间再回顾一下。

个人理解比较浅显,我认为Azure AD和Azure AD B2C都可作为用户管理的系统,他们提供了自己的登录认证画面,统一使用Graph API对自己的用户和其他功能做管理。

Azure AD功能强大,微软的老牌认证方式,可以很方便跟其他三方应用集成,可做单点登录。
而Azure AD B2C更像是三方的用户系统,最大的特点是可自定义UI画面。

感觉总结的不是很好,纯纯自己的理解,这里就不多说了,让我们进入正题。

这里主要贴一下,当时使用的认证相关获取token的代码。

一、Azure AD

1、获取认证登录地址
下面的代码是nodejs的示例,需要用到msal-node库·,当然可以根据官网自己拼接URL也是可以的,在之前曾经尝试过,没有任何问题。
只是在当时不知道为什么,老是提示我需要change_codeverifier两个参数,因为不明白这两个参数的原理,所以采用了msal-node库去生成。
msal参考地址: https://azuread.github.io/microsoft-authentication-library-for-js/ref/classes/_azure_msal_browser.publicclientapplication.html

const msal = require("@azure/msal-node");

function getAuthCodeUrl (){
  return new Promise((resolve, reject) => {
    const cryptoProvider = new msal.CryptoProvider();
    cryptoProvider.generatePkceCodes().then(({ verifier, challenge }) => {
	  const publiClient= new msal.PublicClientApplication({
	    auth: {
	      "clientId": SETTINGS.AD_CLIENT_ID,
	      "authority": `https://login.microsoftonline.com/${SETTINGS.AD_TENANT_ID}`
	    }
	  });
      publiClient.getAuthCodeUrl({
        scopes: ["user.read"],
        redirectUri: SETTINGS.REDIRECT_URL,
        codeChallenge: challenge,
        codeChallengeMethod: "S256"
      }).then((response) => {
        let result = {
          authUrl: response,
          verifier: verifier,
        };
        resolve(result);
      }).catch((error) => {
        reject(JSON.stringify(error));
      });
    });
  });
}

2、access_token的获取
① 通过认证code获取access_token。
在通过认证地址登录后,会跳转到重定向地址,附带client_infocode参数,verifier参数由1、中获取。
官方参考地址: https://learn.microsoft.com/zh-cn/graph/auth-v2-user

const rp = require("request-promise");

function getADToken (code, clientInfo, verifier) {
  return new Promise((resolve, reject) => {
    let options = {
      method: "POST",
      uri: `https://login.microsoftonline.com/${SETTINGS.AD_TENANT_ID}/oauth2/v2.0/token`,
      form: {
        "client_id": SETTINGS.AD_CLIENT_ID,
        "scope": "openid offline_access profile",
        "grant_type": "authorization_code",
        "redirect_uri": SETTINGS.REDIRECT_URL,
        "code": code,
        "client_secret": SETTINGS.AD_SECRIT_ID,
        "client_info": clientInfo,
        "code_verifier": verifier,
      },
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
      },
    };

    rp(options)
      .then((res) => {
        resolve(JSON.parse(res)["access_token"]);
      })
      .catch((err) => {
        reject(err);
      });
  });
}

二、Azure AD B2C

1、获取认证登录地址

// B2C配置对象
 const b2cConfig= {
    auth: {
      clientId: SETTINGS.B2C_CLIENT_ID,
      authority: `https://${SETTINGS.B2C_TENANT_NAME}.b2clogin.com/${SETTINGS.B2C_TENANT_NAME}.onmicrosoft.com/${SETTINGS.B2C_POLICY}`,
      knownAuthorities: [`${SETTINGS.B2C_TENANT_NAME}.b2clogin.com`],
      redirectUri: SETTINGS.REDIRECT_URL,
    }
  };

// 创建msal对象
const publicClient = new msal.PublicClientApplication(b2cConfig);

function getAuthCodeUrl (){
  return new Promise((resolve, reject) => {
    const cryptoProvider = new msal.CryptoProvider();
    cryptoProvider.generatePkceCodes().then(({ verifier, challenge }) => {
      publicClient.getAuthCodeUrl({
        redirectUri: b2cConfig.auth.redirectUri,
        authority: b2cConfig.auth.authority,
        scopes: ["openid", "offline_access"],
        state: "login",
        codeChallenge: challenge,
        codeChallengeMethod: "S256",
      }).then((response) => {
        let result = {
          authUrl: response,
          verifier: verifier,
        };
        resolve(result);
      }).catch((error) => {
        reject(JSON.stringify(error));
      });
    });
  });
}

2、获取IdToken
B2C认证拿到的code只能换取idToken, 就算拿到了access_token也是属于web_token,不能用于调用graph api。这是跟微软support确认后的结果。

function getIdToken (code, codeVerifier) {
  return new Promise((resolve, reject) => {
    // prepare the request for authentication
    const tokenRequest = {
      redirectUri: b2cConfig.auth.redirectUri,
      code: code,
      codeVerifier: codeVerifier,
    };
    pca.acquireTokenByCode(tokenRequest).then((response) => {
      resolve(response);
    }).catch((error) => {
      reject(error);
    });
  });
}

3、解析IdToken
IdToken中包含用户的相关信息,但是没法查看,需要用到jwt-decode库解析。

const jwtDecode = require("jwt-decode");
let tokenObj = jwtDecode(idToken);

4、获取access_token
B2C不能使用认证code获取access_token,所以采用了Azure AD免登录的方式获取了access_token。
官方参考地址: https://learn.microsoft.com/zh-cn/graph/auth-v2-service

const confidentialClientPca = new msal.ConfidentialClientApplication({
	auth: {
      "clientId": SETTINGS.B2C_CLIENT_ID,
      "authority": `https://login.microsoftonline.com/${SETTINGS.B2C_TENANT_ID}`,
      "clientSecret": SETTINGS.B2C_SECRIT_ID,
    }
  });

function getClientCredentialsToken () {
  return new Promise((resolve, reject) => {
    const clientCredentialRequest = {
      scopes: ["https://graph.microsoft.com/.default"],
      azureRegion: null, // (optional) specify the region you will deploy your application to here (e.g. "westus2")
      skipCache: false, // (optional) this skips the cache and forces MSAL to get a new token from Azure AD
    };
    confidentialClientPca.acquireTokenByClientCredential(clientCredentialRequest)
      .then((response) => {
        resolve(response.accessToken);
      }).catch((error) => {
        reject(JSON.stringify(error));
      });
  });
}

三、C# 操作示例

依赖包<PackageReference Include="Microsoft.Graph.Beta" Version="4.35.0-preview" />

using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Microsoft.Graph;
using Microsoft.Identity.Client;
using System.IO;

namespace AZ.Functuon
{
    public class GraphAPI
    {
        public GraphServiceClient client = null;
        public StreamWriter logWriter;
        public string tenantName;

        public GraphAPI(string tenantId,string tenantName,string clientId,string clientSecret,StreamWriter logWriter)
        {
            try
            {
                this.tenantName = tenantName;
                this.logWriter = logWriter;
                string[] scopes = new[] { "https://graph.microsoft.com/.default" };
                IConfidentialClientApplication cca = ConfidentialClientApplicationBuilder
                    .Create(clientId)
                    .WithTenantId(tenantId)
                    .WithClientSecret(clientSecret)
                    .Build();
                Task<AuthenticationResult> taskResult = cca.AcquireTokenForClient(scopes).ExecuteAsync();
                taskResult.Wait();
                InitClint(cca, scopes);
            }
            catch (Exception e)
            {
                CommonFunction.WriteLog(logWriter, "ERROR", $"{e.Message} | {e.StackTrace}");
                this.client = null;
            }
        }
        
        public void InitClint(IConfidentialClientApplication cca,string[] scopes)
        {
            DelegateAuthenticationProvider authProvider = new DelegateAuthenticationProvider(async (request) =>
            {
                AuthenticationResult result = await cca.AcquireTokenForClient(scopes).ExecuteAsync();
                request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", result.AccessToken);
            });
            this.client = new GraphServiceClient(authProvider);
        }

        // ユーザのObjectIdを取得する
        public async Task<string> GetObjectId(string employeeId)
        {
            try
            {
                var result = await this.client.Users
                    .Request()
                    .Filter($"identities/any(c:c/issuerAssignedId eq '{employeeId}' and c/issuer eq '{this.tenantName}@onmicrosoft.com')")
                    .Select(e => new
                    {
                        e.DisplayName,
                        e.Id,
                        e.Identities
                    })
                    .GetAsync();
                if (result != null)
                {
                    JObject jo = (JObject)JsonConvert.DeserializeObject(System.Text.Json.JsonSerializer.Serialize(result).Substring(1, System.Text.Json.JsonSerializer.Serialize(result).Length-2));
                    return jo["id"].ToString();
                }
                else
                {
                    return null;
                }
            }
            catch(Exception e)
            {
                CommonFunction.WriteLog(logWriter, "ERROR", $"{e.Message} | {e.StackTrace}");
                return null;
            }
        }

        // ユーザのObjectIdをtest取得する
        public async Task<string> GetObjectIdByUserprincipalname(string userPrincipalName)
        {
            try
            {
                String [] userPrincipalNamefirst=userPrincipalName.Split("@");
                var result = await this.client.Users[$"{userPrincipalNamefirst[0]}@{this.tenantName}.onmicrosoft.com"]
                    .Request(new Option[] { new QueryOption("$count", "true")})
                    .Header("ConsistencyLevel", "eventual")
                    .GetAsync();
                if (result != null)
                {
                    JObject jo = (JObject)JsonConvert.DeserializeObject(System.Text.Json.JsonSerializer.Serialize(result));
                    return jo["id"].ToString();
                }
                else
                {
                    return null;
                }
            }
            catch(Exception e)
            {
                CommonFunction.WriteLog(logWriter, "ERROR", $"{e.Message} | {e.StackTrace}");
                return null;
            }
        }

        // B2Cにユーザを新規追加する
        public async Task<bool> CreateB2CUser(string employeeId, string userName,string userPrincipalName, string birthday)
        {
            try
            {
                userPrincipalName = userPrincipalName.Split("@")[0];
                var user = new User
                {
                    AccountEnabled = true,
                    DisplayName = userName,
                    MailNickname = userPrincipalName,
                    UserPrincipalName = $"{userPrincipalName}@{this.tenantName}.onmicrosoft.com",
                    PasswordProfile = new PasswordProfile
                    {
                        ForceChangePasswordNextSignIn = false,
                        Password = $"Fj_{birthday}"
                    },
                    Identities = new List<ObjectIdentity>()
                    {
                        new ObjectIdentity
                        {
                            SignInType = "userName",
                            Issuer = "{this.tenantName}.onmicrosoft.com",
                            IssuerAssignedId = employeeId
                        },
                    }
                };
                var result = await this.client.Users
                    .Request()
                    .AddAsync(user);
                return true;
            }
            catch(Exception e)
            {
                CommonFunction.WriteLog(logWriter, "ERROR", $"{e.Message} | {e.StackTrace}");
                return false;
            }
        }

        // B2Cユーザの認証電話番号追加
        public async Task<bool> AddAuthPhoneNumber(string objectID, string phone)
        {
            try
            {
                var phoneAuthenticationMethod = new PhoneAuthenticationMethod
                {
                    PhoneNumber = $"+81 {phone}",
                    PhoneType = AuthenticationPhoneType.Mobile
                };
                await this.client.Users[objectID].Authentication.PhoneMethods
                    .Request()
                    .AddAsync(phoneAuthenticationMethod);
                return true;
            }
            catch(Exception e)
            {
                CommonFunction.WriteLog(logWriter, "ERROR", $"{e.Message} | {e.StackTrace}");
                return false;
            }
        }

        // 人事情報システム側で更新したB2Cユーザの認証電話番号を更新する
        public async Task<bool> UpdateAuthPhoneNumber(string objectID, string phone)
        {
            try
            {
                var phoneAuthenticationMethod = new PhoneAuthenticationMethod
                {
                    PhoneNumber = $"+81 {phone}",
                    PhoneType = AuthenticationPhoneType.Mobile
                };

                await this.client.Users[objectID].Authentication.PhoneMethods["3179e48a-750b-4051-897c-87b9720928f7"]
                    .Request()
                    .PutAsync(phoneAuthenticationMethod);
                return true;
            }
            catch(Exception e)
            {
                CommonFunction.WriteLog(logWriter, "ERROR", $"{e.Message} | {e.StackTrace}");
                return false;
            }
        }

        // 明細照会サービス側で追加したB2CユーザのユーザIDを更新する
        public async Task<bool> UpdateUserId(string objectID, string userId, string singleName)
        {
            try
            {
            var user = new User
            {
                Identities = new List<ObjectIdentity>()
                {
                    new ObjectIdentity
                    {
                        SignInType = "userName",
                        Issuer = "{this.tenantName}.onmicrosoft.com",
                        IssuerAssignedId = userId
                    },
                },
            };
            await this.client.Users[objectID]
                .Request()
                .UpdateAsync(user);
                return true;
            }
            catch(Exception e)
            {
                CommonFunction.WriteLog(logWriter, "ERROR", $"{e.Message} | {e.StackTrace}");
                return false;
            }
        }

        // B2Cにユーザを削除する
        public async Task<bool> DeleteUserByObjId(string objectId)
        {
            try
            {
                await this.client.Users[objectId]
                    .Request()
                    .DeleteAsync();
                return true;
            }
            catch(Exception e)
            {
                CommonFunction.WriteLog(logWriter, "ERROR", $"{e.Message} | {e.StackTrace}");
                return false;
            }
        }
    }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Anesthesia丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值