工作当中使用过Azure AD认证和B2C的认证,今天抽时间再回顾一下。
个人理解比较浅显,我认为Azure AD和Azure AD B2C都可作为用户管理的系统,他们提供了自己的登录认证画面,统一使用Graph API对自己的用户和其他功能做管理。
Azure AD功能强大,微软的老牌认证方式,可以很方便跟其他三方应用集成,可做单点登录。
而Azure AD B2C更像是三方的用户系统,最大的特点是可自定义UI画面。
感觉总结的不是很好,纯纯自己的理解,这里就不多说了,让我们进入正题。
这里主要贴一下,当时使用的认证相关获取token的代码。
1、获取认证登录地址
下面的代码是nodejs的示例,需要用到msal-node
库·,当然可以根据官网自己拼接URL也是可以的,在之前曾经尝试过,没有任何问题。
只是在当时不知道为什么,老是提示我需要change_code
和verifier
两个参数,因为不明白这两个参数的原理,所以采用了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_info
和code
参数,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);});});
}
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 authenticationconst 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));});});
}
依赖包
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 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 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 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 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(){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 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 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 UpdateUserId(string objectID, string userId, string singleName){try{var user = new User{Identities = new List(){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 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;}}}
}