支撑其他电路板、器件和器件之间的相互连接,并为所支撑的器件提供电源和数据信号的电路板或框架,所以用粘合剂来说特别合适,通过身份粘合微软自身体系的所有业务,是基础底座)。
LEGACY AUTHENTICATIONS:
Pass-through Authentication (PTA)认证协议:支持Office登录、Exchange、Skype for Business以及Apple Device Enrollment Program (Apple DEP);
WS-FED:B2B和B2C支持WS-FED认证;
SCIM:支持SCIM认证方式;
OpenID:OpenID认证方式支持GraphAPI、B2B和B2C;
诊断:
RequestID:通过RequestID来进行Azure AD支持的登录异常和需要后台人员支持的Support服务;
登录诊断:包括登录过程、MFA认证、条件访问、登录上下文等进行针对,发现潜在问题;
应用配置针对:Enterprise企业应用配置诊断;企业应用事件日志;不正确的凭证事件;
Conditional Access(条件访问):
设备:支持Intune Managed Browser SSO认证的条件访问,等于终端上有一个信息采集器,用来让条件访问做对应的策略;终端访问Apps应用并没有Intune License信息,Intune保护设施未开启禁止访问Apps;是否是托管设备;
细粒度策略:针对细粒度的策略查看对应的告警和命中情况;支持服务应用访问条件访问、支持Azure Portal、SharePoint等;
应用:应用力度的条件访问,逐步扩大支撑范围;包括Exchange ActiveSync评估登录的地址,包括城市、地域或者IP地址,评估登录风险、设备平台登录访问条件、浏览器客户端以及浏览器版本、用户成功和失败登录记录;
浏览器:通过Edge、Firefox(Win10/11、Windows Server 2019)等浏览器设置对应的访问策略;
网络:SSPR密码重置和MFA支持可信网络策略、是否是托管设备;
用户:用户登录综合评估是低风险;
仅报告模式:针对条件访问规则,仅仅是报告和观察状态,并指定可以不进行策略执行动作;
命令行(Powershell模块):简化管理,使用Powershell来进行对应的管理;支持GraphAPI、Azure AD管理等命令行;
MFA:
统一控制台:针对手机号、邮箱、手机应用MFA、SSRP密码重置控制台等进行统一的界面,提高用户体验;
密码重置:通过管理员开启MFA可以重置密码功能,让用户进行密码重置通过MFA增强密码重置水位;
认证恢复:丢失MFA之后,可以通过临时访问功能,设置新的MFA;
数据确权:Azure AD认证数据可以符合GDPR等数据存储的要求,包括Gov政务云、Azure公有云、情报云等都属于独立的数据存储,有独有的数据存储安全需求;
Passwordless认证模块:
FIDO2:支持FIDO2 Passwordless认证模块;支持Hybrid混合认证方式,包括线下的WIN10加入Azure AD域设备认证;
SMS手机号登录:通过SMS手机号进行登录,而不是用密码的Passwordless方式;
软令牌:通过Microsoft Authencator进行软令牌认证;
SmartCard:认证高等级的认证,采用SmartCard硬件令牌进行认证;
Windows Hello:通过Windows Hello进行身份认证;
自动认证:IOS和Android设备自动填充登录密码;
零信任:
持续验证:持续访问评估(Continuos Access Evaluation)Azure AD安全状态(例如Azure用户删除)之后近乎实时状态禁止通过Token访问相关应用;
GraphAPI:
目录功能:Graph API目录API支持统计(Count)、搜索(Search)、Filter(过滤)、Sort(排序)等功能;
认证支持:GraphAPI支持通过配置SAML2来进行应用身份认证;
扩展更多目录:包括风险、用户、组、应用、服务、终端等;
更新属性:管理员可以更新用户的对应属性,包括MFA手机、邮箱、姓名等等属性;
减少隐私:GraphAPI对应的Secret Token信息泄露的问题进行更新;GraphAPI设置Password Secret认证方式,并且无法看到明文;
支持设备认证:GraphAPI支持MDM、MAM配置等;
权限设计:针对GraphAPI授予过多的访问权限进行限制;
混合标识身份认证:
身份同步:Azure AD Connect Cloud SYNC同步策略;
应用加固策略:
覆盖Workload身份:针对应用、服务、托管的身份来进行检测、调查、削减对应的身份相关的风险;条件访问策略可以同样适用在Workload身份上;
总结:MIP不断通过几个维度来扩展身份的安全性,逐步完整更大的身份安全版图:
用户认证维度:不断扩充认证协议,包括SAML2、OAuth2、OpenID等协议;
设备认证维度:Intune产品线就是不断支持更多操作系统,包括安卓、IOS、Windows、MacOS等;例如微软的KPI就是通过Intune 100%的托管设备;
不断增强MFA能力:包括Microsoft软令牌、手机认证、FIDO2认证等;
不断的权限管理:针对权限管理进行细粒度的管控;
条件访问:针对应用、用户、操作系统、浏览器等进行访问条件的风控策略,针对异常攻击进行检测;
增强威胁检测能力:针对前面覆盖的能力进行大量日志采集和分析以及跟第三方联动集成SIEM;
生态增强:下面会专门针对Intune生态、Passwordless生态维度展开讲一下微软身份的生态体系;
威胁情报和MSSP服务兜底:通过微软威胁情报体系和对应的MSSP专家服务不断构建身份安全的托管服务、威胁调查服务,来提升用户的安全水位;
非常多的大型ToB的客户针对设备管理,都有自己的成熟的管理方案,再上云的过程中,也许不会选择到Intune微软的设备管理套件,例如TOP 500强等很多企业都会采用以下的生成的MDM/MAM管理平台,包括BlackBerry UEM、Citrix Workspace device compliance、IBM MaaS360、JAMF Pro、MobileIron Device Compliance Cloud、MobileIron Device Compliance On-prem、SOTI MobiControl、VMware Workspace ONE UEM (formerly AirWatch)等,微软Intune为了更好的支持这些客户,扩展商业边界不断的侵蚀更多的大B客户;
POST https://login.microsoftonline.com/{TenantID}/oauth2/v2.0/token
Content-Type : application/x-www-form-urlencoded
scope=https://graph.microsoft.com/.default&grant_type=client_credentials&client_id=535fb089-9ff3-47b6-9bfb-4f1264799865&client_secret=qWgdYAmab0YSkuL1qKv5bPX
private async Task<IPrincipal> AuthenticateRequestAsync(HttpContext context)
{
IPrincipal user = context.get_User();
if (user == null)
{
NameValueCollection headers = context.get_Request().get_Headers();
if (headers != null)
{
string[] authHeaderValues = headers.GetValues("Authorization");
if (!authHeaderValues.IsNullOrEmpty())
{
string token = GetEncryptedBearerToken(authHeaderValues);
if (!string.IsNullOrEmpty(token))
{
if (config.ForwardDecryptedAuthorizationTokens)
{
headers["Authorization"] = "Bearer " + token;
}
}
else
{
token = GetBearerToken(authHeaderValues);
}
if (!string.IsNullOrEmpty(token))
{
try
{
JwtValidater validater = new JwtValidater(config, ValidationParametersCache, tracer);
user = await validater.ValidateToken(token, (AadApplicationId appIdFromToken) => allowedApplicationIds.Count < 1 || pathsThatAllowAllApplications.Contains(context.get_Request().get_AppRelativeCurrentExecutionFilePath()) || allowedApplicationIds.Contains(appIdFromToken)).ConfigureAwait(continueOnCapturedContext: false);
}
catch (ArgumentException e)
{
string message3 = string.Format(CultureInfo.InvariantCulture, "Failed to validate token. AuthorizationHeader={0}, Exception={1}", token, e);
tracer.Warning(message3, "AuthenticateRequestAsync", "X:\\bt\\1037330\\repo\\src\\Security\\Aad\\Core\\AadJwtAuthModule.cs", 155);
}
catch (Exception genericException)
{
string message2 = string.Format(CultureInfo.InvariantCulture, "Failed to validate token. AuthorizationHeader={0}", token);
tracer.UnexpectedException(genericException, message2, "AuthenticateRequestAsync", "X:\\bt\\1037330\\repo\\src\\Security\\Aad\\Core\\AadJwtAuthModule.cs", 165);
throw;
}
if (user is ClaimsPrincipal claimsPrincipal)
{
user = new ClaimsPrincipal(new ExtensionAadIdentity(claimsPrincipal, tracer, config));
}
else if (user != null)
{
string message = string.Format(CultureInfo.InvariantCulture, "Unable to authorize, the identity constructed from the auth header is not supported. typeof(user)={0}, AuthorizationHeader={1}", user.GetType(), token);
tracer.Warning(message, "AuthenticateRequestAsync", "X:\\bt\\1037330\\repo\\src\\Security\\Aad\\Core\\AadJwtAuthModule.cs", 184);
}
}
}
}
user = user ?? AnonymousUser;
context.set_User(user);
}
if ((user.Identity == null || !user.Identity!.IsAuthenticated) && !UrlAuthorizationModule.CheckUrlAccessForPrincipal(context.get_Request().get_Path(), user, context.get_Request().get_RequestType()))
{
context.get_Response().set_StatusCode(401);
}
return context.get_User();
}
private string GetBearerToken(IEnumerable<string> authHeaderValues)
{
return GetAuthHeaderToken("Bearer ", authHeaderValues);
}
private string GetAuthHeaderToken(string prefix, IEnumerable<string> authHeaderValues)
{
IEnumerable<string> bearerTokenValues = from value in authHeaderValues
where value?.StartsWith(prefix, StringComparison.Ordinal) ?? false
select value.Substring(prefix.Length);
return bearerTokenValues.FirstOrDefault();
}
3、继续跟进到比较核心的代码逻辑验证Token的有效性,这两行代码的重要性非常高的,等于整个微软Graph API的认证逻辑绝对是核心中的核心代码;继续跟进ValidateToken这个函数;
JwtValidater validater = new JwtValidater(config, ValidationParametersCache, tracer);
user = await validater.ValidateToken(token, (AadApplicationId appIdFromToken) => allowedApplicationIds.Count < 1 || pathsThatAllowAllApplications.Contains(context.get_Request().get_AppRelativeCurrentExecutionFilePath()) || allowedApplicationIds.Contains(appIdFromToken)).ConfigureAwait(continueOnCapturedContext: false);
4、ValidateToken有几个参数输入Token就是Client_ID和Client_Secrets认证后的令牌和需要验证的AppId也就是后来说的aud验证部分;
public async Task<IPrincipal> ValidateToken(string token, Func<AadApplicationId, bool> validateAppId)
{
if (string.IsNullOrEmpty(token))
{
return null;
}
ClaimsPrincipal claimsPrincipal = null;
try
{
JwtValidationParameters validationParams = await GetCachedValidationParameters().ConfigureAwait(continueOnCapturedContext: false);
if (validationParams != null)
{
IEnumerable<string> allowedAudiences = from audience in config.AllowedAudiences.MapNullToEmpty()
where audience != null
select audience;
if (token.StartsWith("Bearer ", StringComparison.Ordinal))
{
token = token.Substring("Bearer ".Length);
}
claimsPrincipal = GetClaimsPrincipalFromToken(token, config.TenantId, validationParams.Issuer, allowedAudiences, validationParams.SigningKeys, validateAppId);
}
}
catch (ArgumentException e4)
{
tracer.Information("Unable to get claims principal from access token: {0}".FormatInvariant(e4), "ValidateToken", "X:\\bt\\1037330\\repo\\src\\Security\\Aad\\Core\\AadJwtValidater.cs", 109);
}
catch (SecurityTokenExpiredException e3)
{
bool isShortLivedToken = false;
try
{
isShortLivedToken = IsShortLivedToken(token);
}
catch (Exception)
{
}
if (isShortLivedToken)
{
tracer.Warning(string.Format(CultureInfo.InvariantCulture, "Unable to get claims principal from expired short lived access token: {0}", e3), "ValidateToken", "X:\\bt\\1037330\\repo\\src\\Security\\Aad\\Core\\AadJwtValidater.cs", 126);
}
else
{
tracer.Information(string.Format(CultureInfo.InvariantCulture, "Unable to get claims principal from expired access token: {0}", e3), "ValidateToken", "X:\\bt\\1037330\\repo\\src\\Security\\Aad\\Core\\AadJwtValidater.cs", 130);
}
}
catch (SecurityTokenValidationException e2)
{
tracer.Warning(string.Format(CultureInfo.InvariantCulture, "Unable to get claims principal from access token: {0}", e2), "ValidateToken", "X:\\bt\\1037330\\repo\\src\\Security\\Aad\\Core\\AadJwtValidater.cs", 137);
}
catch (SignatureVerificationFailedException e)
{
tracer.Information(string.Format(CultureInfo.InvariantCulture, "Unable to get claims principal from access token: {0}", e), "ValidateToken", "X:\\bt\\1037330\\repo\\src\\Security\\Aad\\Core\\AadJwtValidater.cs", 147);
}
return claimsPrincipal;
}
private ClaimsPrincipal GetClaimsPrincipalFromToken(string token, string issuerTenantId, string issuer, IEnumerable<string> allowedAudiences, IEnumerable<SecurityKey> signingKeys, Func<AadApplicationId, bool> validateAppId)
{
bool shouldValidateIssuer = !string.Equals(issuerTenantId, "common", StringComparison.OrdinalIgnoreCase);
TokenValidationParameters validationParameters = new TokenValidationParameters
{
ValidAudiences = allowedAudiences,
ValidateIssuer = shouldValidateIssuer,
ValidIssuer = issuer,
ClockSkew = config.MaxValidationClockSkewInterval,
IssuerSigningKeys = signingKeys
};
SecurityToken validatedToken;
ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
if (validatedToken != null)
{
if (!ValidateApplicationId(validatedToken as JwtSecurityToken, validateAppId))
{
return null;
}
return claimsPrincipal;
}
return claimsPrincipal;
}
6、继续跟进tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
public override ClaimsPrincipal ValidateToken(string token, TokenValidationParameters validationParameters, out SecurityToken validatedToken)
{
if (string.IsNullOrWhiteSpace(token))
{
throw LogHelper.LogArgumentNullException("token");
}
if (validationParameters == null)
{
throw LogHelper.LogArgumentNullException("validationParameters");
}
if (token.Length > MaximumTokenSizeInBytes)
{
throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant("IDX10209: token has length: '{0}' which is larger than the MaximumTokenSizeInBytes: '{1}'.", token.Length, MaximumTokenSizeInBytes)));
}
string[] array = token.Split(new char[1] { '.' }, 6);
if (array.Length != 3 && array.Length != 5)
{
throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant("IDX12741: JWT: '{0}' must have three segments (JWS) or five segments (JWE).", token)));
}
if (array.Length == 5)
{
JwtSecurityToken jwtSecurityToken = ReadJwtToken(token);
string token2 = DecryptToken(jwtSecurityToken, validationParameters);
JwtSecurityToken jwtToken = (jwtSecurityToken.InnerToken = ValidateSignature(token2, validationParameters));
validatedToken = jwtSecurityToken;
return ValidateTokenPayload(jwtToken, validationParameters);
}
validatedToken = ValidateSignature(token, validationParameters);
return ValidateTokenPayload(validatedToken as JwtSecurityToken, validationParameters);
}
7、继续跟进jwtSecurityToken.Decode函数
internal void Decode(string[] tokenParts, string rawData)
{
LogHelper.LogInformation("IDX12716: Decoding token: '{0}' into header, payload and signature.", rawData);
try
{
Header = JwtHeader.Base64UrlDeserialize(tokenParts[0]);
}
catch (Exception innerException)
{
throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant("IDX12729: Unable to decode the header '{0}' as Base64Url encoded string. jwtEncodedString: '{1}'.", tokenParts[0], rawData), innerException));
}
if (tokenParts.Length == 5)
{
DecodeJwe(tokenParts);
}
else
{
DecodeJws(tokenParts);
}
RawData = rawData;
}
8、从上面代码可以看到Token分成了Jwe和Jws两种模式,看一下DecodeJwe和DecodeJws的区别;Jwe是比Jws的安全等级更高,Jwe可以针对JWT Payload部分进行加密;
private void DecodeJwe(string[] tokenParts)
{
RawHeader = tokenParts[0];
RawEncryptedKey = tokenParts[1];
RawInitializationVector = tokenParts[2];
RawCiphertext = tokenParts[3];
RawAuthenticationTag = tokenParts[4];
}
DecodeJws代码,针对JWT Header、Payload和签名进行解析;这里面的RawHeader里面包含了如何验证令牌,以及令牌的类型和相关的加密算法,例如{"typ":"JWT","alg":"RS256","kid":"i6lGk3FZzxRcUb2C3nEQ7syHJlY"}其中typ是类型JWT,ALG算法是RS265,KID是公钥验签的公钥签名,RawPayload就是相关的核心跟用户相关的参数信息,RawSignature就是JWT的签名部分,用来防篡改等操作;
private void DecodeJws(string[] tokenParts)
{
if (Header.Cty != null)
{
LogHelper.LogVerbose(LogHelper.FormatInvariant("IDX12738: Header.Cty != null, assuming JWS. Cty: '{0}'.", Header.Cty));
}
try
{
Payload = JwtPayload.Base64UrlDeserialize(tokenParts[1]);
}
catch (Exception innerException)
{
throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant("IDX12723: Unable to decode the payload '{0}' as Base64Url encoded string. jwtEncodedString: '{1}'.", tokenParts[1], RawData), innerException));
}
RawHeader = tokenParts[0];
RawPayload = tokenParts[1];
RawSignature = tokenParts[2];
}
9、继续跟验证JWT Token签名部分的核心逻辑,前面讲到了JWT的RawHeader获取部分,有Kid这个参数,Kid参数就是指定使用哪个JWT的公钥验签,首先获取IssuerSigningKey核心代码是enumerable = validationParameters.IssuerSigningKeyResolver(token, jwtSecurityToken, jwtSecurityToken.Header.Kid, validationParameters);,然后通过ValidateSignature(bytes, signature, item, jwtSecurityToken.Header.Alg, validationParameters)进行验签,Kid的获取还是需要讲透的,首先微软在JWT的OpenID配置Endpoint可以获取对应的信息,https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration访问获取到"jwks_uri":"https://login.microsoftonline.com/common/discovery/v2.0/keys",继续访问https://login.microsoftonline.com/common/discovery/v2.0/keys获取到全部的公钥,根据观察和分析这里微软有一个很变态的操作,就是24小时会进行公钥配置的轮转。继续讲验证签名的逻辑,通过Kid来获取对应的公钥验证签名,例如下图的nOo3ZDrODXEK1jKWhXslHR_KXEg来验签。
[{"kty":"RSA","use":"sig","kid":"nOo3ZDrODXEK1jKWhXslHR_KXEg","x5t":"nOo3ZDrODXEK1jKWhXslHR_KXEg","n":"oaLLT9hkcSj2tGfZsjbu7Xz1Krs0qEicXPmEsJKOBQHauZ_kRM1HdEkgOJbUznUspE6xOuOSXjlzErqBxXAu4SCvcvVOCYG2v9G3-uIrLF5dstD0sYHBo1VomtKxzF90Vslrkn6rNQgUGIWgvuQTxm1uRklYFPEcTIRw0LnYknzJ06GC9ljKR617wABVrZNkBuDgQKj37qcyxoaxIGdxEcmVFZXJyrxDgdXh9owRmZn6LIJlGjZ9m59emfuwnBnsIQG7DirJwe9SXrLXnexRQWqyzCdkYaOqkpKrsjuxUj2-MHX31FqsdpJJsOAvYXGOYBKJRjhGrGdONVrZdUdTBQ","e":"AQAB","x5c":["MIIDBTCCAe2gAwIBAgIQN33ROaIJ6bJBWDCxtmJEbjANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIwMTIyMTIwNTAxN1oXDTI1MTIyMDIwNTAxN1owLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKGiy0/YZHEo9rRn2bI27u189Sq7NKhInFz5hLCSjgUB2rmf5ETNR3RJIDiW1M51LKROsTrjkl45cxK6gcVwLuEgr3L1TgmBtr/Rt/riKyxeXbLQ9LGBwaNVaJrSscxfdFbJa5J+qzUIFBiFoL7kE8ZtbkZJWBTxHEyEcNC52JJ8ydOhgvZYykete8AAVa2TZAbg4ECo9+6nMsaGsSBncRHJlRWVycq8Q4HV4faMEZmZ+iyCZRo2fZufXpn7sJwZ7CEBuw4qycHvUl6y153sUUFqsswnZGGjqpKSq7I7sVI9vjB199RarHaSSbDgL2FxjmASiUY4RqxnTjVa2XVHUwUCAwEAAaMhMB8wHQYDVR0OBBYEFI5mN5ftHloEDVNoIa8sQs7kJAeTMA0GCSqGSIb3DQEBCwUAA4IBAQBnaGnojxNgnV4+TCPZ9br4ox1nRn9tzY8b5pwKTW2McJTe0yEvrHyaItK8KbmeKJOBvASf+QwHkp+F2BAXzRiTl4Z+gNFQULPzsQWpmKlz6fIWhc7ksgpTkMK6AaTbwWYTfmpKnQw/KJm/6rboLDWYyKFpQcStu67RZ+aRvQz68Ev2ga5JsXlcOJ3gP/lE5WC1S0rjfabzdMOGP8qZQhXk4wBOgtFBaisDnbjV5pcIrjRPlhoCxvKgC/290nZ9/DLBH3TbHk8xwHXeBAnAjyAqOZij92uksAv7ZLq4MODcnQshVINXwsYshG1pQqOLwMertNaY5WtrubMRku44Dw7R"],"issuer":"https://login.microsoftonline.com/{tenantid}/v2.0"}
protected virtual JwtSecurityToken ValidateSignature(string token, TokenValidationParameters validationParameters)
{
if (string.IsNullOrWhiteSpace(token))
{
throw LogHelper.LogArgumentNullException("token");
}
if (validationParameters == null)
{
throw LogHelper.LogArgumentNullException("validationParameters");
}
if (validationParameters.SignatureValidator != null)
{
SecurityToken securityToken = validationParameters.SignatureValidator(token, validationParameters);
if (securityToken == null)
{
throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant("IDX10505: Signature validation failed. The user defined 'Delegate' specified on TokenValidationParameters returned null when validating token: '{0}'.", token)));
}
if (!(securityToken is JwtSecurityToken result))
{
throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant("IDX10506: Signature validation failed. The user defined 'Delegate' specified on TokenValidationParameters did not return a '{0}', but returned a '{1}' when validating token: '{2}'.", typeof(JwtSecurityToken), securityToken.GetType(), token)));
}
return result;
}
JwtSecurityToken jwtSecurityToken = null;
if (validationParameters.TokenReader != null)
{
SecurityToken securityToken2 = validationParameters.TokenReader(token, validationParameters);
if (securityToken2 == null)
{
throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant("IDX10510: Signature validation failed. The user defined 'Delegate' specified in TokenValidationParameters returned null when reading token: '{0}'.", token)));
}
jwtSecurityToken = securityToken2 as JwtSecurityToken;
if (jwtSecurityToken == null)
{
throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant("IDX10509: Signature validation failed. The user defined 'Delegate' specified in TokenValidationParameters did not return a '{0}', but returned a '{1}' when reading token: '{2}'.", typeof(JwtSecurityToken), securityToken2.GetType(), token)));
}
}
else
{
jwtSecurityToken = ReadJwtToken(token);
}
byte[] bytes = Encoding.UTF8.GetBytes(jwtSecurityToken.RawHeader + "." + jwtSecurityToken.RawPayload);
if (string.IsNullOrEmpty(jwtSecurityToken.RawSignature))
{
if (validationParameters.RequireSignedTokens)
{
throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant("IDX10504: Unable to validate signature, token does not have a signature: '{0}'.", token)));
}
return jwtSecurityToken;
}
bool flag = false;
IEnumerable<SecurityKey> enumerable = null;
if (validationParameters.IssuerSigningKeyResolver != null)
{
enumerable = validationParameters.IssuerSigningKeyResolver(token, jwtSecurityToken, jwtSecurityToken.Header.Kid, validationParameters);
}
else
{
SecurityKey securityKey = ResolveIssuerSigningKey(token, jwtSecurityToken, validationParameters);
if (securityKey != null)
{
flag = true;
enumerable = new List<SecurityKey> { securityKey };
}
}
if (enumerable == null)
{
enumerable = GetAllSigningKeys(token, jwtSecurityToken, jwtSecurityToken.Header.Kid, validationParameters);
}
StringBuilder stringBuilder = new StringBuilder();
StringBuilder stringBuilder2 = new StringBuilder();
bool flag2 = !string.IsNullOrEmpty(jwtSecurityToken.Header.Kid);
byte[] signature;
try
{
signature = Base64UrlEncoder.DecodeBytes(jwtSecurityToken.RawSignature);
}
catch (FormatException innerException)
{
throw new SecurityTokenInvalidSignatureException("IDX10508: Signature validation failed. Signature is improperly formatted.", innerException);
}
foreach (SecurityKey item in enumerable)
{
try
{
if (ValidateSignature(bytes, signature, item, jwtSecurityToken.Header.Alg, validationParameters))
{
LogHelper.LogInformation("IDX10242: Security token: '{0}' has a valid signature.", token);
jwtSecurityToken.SigningKey = item;
return jwtSecurityToken;
}
}
catch (Exception ex)
{
stringBuilder.AppendLine(ex.ToString());
}
if (item != null)
{
stringBuilder2.AppendLine(item.ToString() + " , KeyId: " + item.KeyId);
if (flag2 && !flag && item.KeyId != null)
{
flag = jwtSecurityToken.Header.Kid.Equals(item.KeyId, (item is X509SecurityKey) ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
}
}
if (flag2)
{
if (flag)
{
throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant("IDX10511: Signature validation failed. Keys tried: '{0}'. \nkid: '{1}'. \nExceptions caught:\n '{2}'.\ntoken: '{3}'.", stringBuilder2, jwtSecurityToken.Header.Kid, stringBuilder, jwtSecurityToken)));
}
throw LogHelper.LogExceptionMessage(new SecurityTokenSignatureKeyNotFoundException(LogHelper.FormatInvariant("IDX10501: Signature validation failed. Unable to match keys: \nkid: '{0}', \ntoken: '{1}'.", jwtSecurityToken.Header.Kid, jwtSecurityToken)));
}
if (stringBuilder2.Length > 0)
{
throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant("IDX10503: Signature validation failed. Keys tried: '{0}'.\nExceptions caught:\n '{1}'.\ntoken: '{2}'.", stringBuilder2, stringBuilder, jwtSecurityToken)));
}
throw LogHelper.LogExceptionMessage(new SecurityTokenSignatureKeyNotFoundException("IDX10500: Signature validation failed. No security keys were provided to validate the signature."));
}
10、验签完成了,验证签名通过之后如何来验证用户和权限,整个访问的逻辑是首先通过aud参数来验证是否有权限访问Graph API这个应用,每一个Graph API都有对应的ID来标识应用;通过授予的令牌中的oid、roles、wids来验证调用Token的oid对应的用户是否有对应的权限和对应的角色roles,对应的权限和对应的角色可以授予用户能访问什么样的数据,最终完成整个Graph API的调用的验证。当然中间还有对应的签名过期时间、login.microsoftonline.com Endpoint发布的验证Nonce的有效性等,这里不做过多赘述。
11、分析完Graph API的整个认证逻辑,再来分析一下整个Graph API的攻击面,目前Graph API最大的攻击面就是可以直接通过获取Client_ID和Client_Secrets的方式来进行应用层的访问,直接获取敏感数据。另外Graph API中也存在了大量的敏感数据,一旦权限设计不合理,包括组织架构、人员信息、设备信息、组信息、邮件信息、SharePoint文件信息等,都可以通过Graph API Token的方式进行获取。另外一个攻击面,就是整个Graph API设计的最核心就是依靠私钥Keys来做的JWT的验证逻辑,一旦私钥被获取,那对平台的影响是毁灭的(当然概率较小),笔者目前也正在分析私钥的存储方式,通过公开渠道来获取对应的设计思想,后期如找到对应的文献笔者一定第一时间分享。还有一个攻击面就是可以用来做各种猥琐的后门,通过设计恶意的应用加上较大的权限来持续保持权限;还可以通过在原有的Application和ServicePriciple上增加密码认证,通过Graph API的Application API的passwordCredential来通过密码认证来持续保持权限;同时也可以导入证书,通过Application API的keyCredential方式进行持续权限的保持方式(历史上Application API出现过泄露私钥的漏洞,漏洞编号https://msrc-blog.microsoft.com/2021/11/17/guidance-for-azure-active-directory-ad-keycredential-property-information-disclosure-in-application-and-service-principal-apis/)这个漏洞的影响还是非常巨大的。
关于微软身份Token参数的扩展阅读:
https://docs.microsoft.com/zh-cn/azure/active-directory/develop/id-tokens