接着前文.Net 5.0 通过IdentityServer4实现单点登录之授权部分源码解析,本文主要分析在授权失败后,调用oidc认证的Chanllage方法部分.关于认证方案不理解的可以参考.Net Core 3.0 认证组件源码解析上文讲到因为第一次调用,请求的控制器方法没有带任何身份认证信息,且因为控制器默认打了Authorize特性,经过前文描述的一系列授权处理器处理,授权结果返回PolicyAuthorizationResult.Challenge(),接着执行如下代码:
if (authorizeResult.Challenged)
{
if (policy.AuthenticationSchemes.Count > 0)
{
foreach (var scheme in policy.AuthenticationSchemes)
{
await context.ChallengeAsync(scheme);
}
}
else
{
await context.ChallengeAsync();
}
return;
}
else if (authorizeResult.Forbidden)
{
if (policy.AuthenticationSchemes.Count > 0)
{
foreach (var scheme in policy.AuthenticationSchemes)
{
await context.ForbidAsync(scheme);
}
}
else
{
await context.ForbidAsync();
}
return;
}
await next(context);
}
demo中没有给控制器方法配置任何认证方案,所以进入context.ChallengeAsync()方法,其调用IAuthenticationService实例的ChallengeAsync方法,执行细节如下:
public virtual async Task ChallengeAsync(HttpContext context, string? scheme, AuthenticationProperties? properties)
{
if (scheme == null)
{
var defaultChallengeScheme = await Schemes.GetDefaultChallengeSchemeAsync();
scheme = defaultChallengeScheme?.Name;
if (scheme == null)
{
throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultChallengeScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).");
}
}
var handler = await Handlers.GetHandlerAsync(context, scheme);
if (handler == null)
{
throw await CreateMissingHandlerException(scheme);
}
await handler.ChallengeAsync(properties);
}
获取默认的ChallengeScheme,并根据上下文和传入的认证方案(这里获取的是配置的默认的认证方案demo是oidc),获取认证方案处理器,拿到处理器后调用ChallengeAsync方法,先看看处理器基类的ChallengeAsync方法代码如下:
public async Task ChallengeAsync(AuthenticationProperties? properties)
{
var target = ResolveTarget(Options.ForwardChallenge);
if (target != null)
{
await Context.ChallengeAsync(target, properties);
return;
}
properties ??= new AuthenticationProperties();
await HandleChallengeAsync(properties);
Logger.AuthenticationSchemeChallenged(Scheme.Name);
}
这里首先第一个if语句是,如果解析到配置的了ForwardChallenge方案,则调用配置的Challenge方案,如果没有配置,则调用HandleChallengeAsync方法,如下:
protected virtual Task HandleChallengeAsync(AuthenticationProperties properties)
{
Response.StatusCode = 401;
return Task.CompletedTask;
}
这里很明显,会被子类重写,要不然流程走不下去了.这里,应为默认的challenge方法配置的是oidc,所以查看下OpenIdConnectHandler实例的方法(关于这个跳转要理解,必须掌握认证组件的逻辑),代码如下:
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
await HandleChallengeAsyncInternal(properties);
var location = Context.Response.Headers[HeaderNames.Location];
if (location == StringValues.Empty)
{
location = "(not set)";
}
var cookie = Context.Response.Headers[HeaderNames.SetCookie];
if (cookie == StringValues.Empty)
{
cookie = "(not set)";
}
Logger.HandleChallenge(location, cookie);
}
接着看HandleChallengeAsyncInternal方法:
private async Task HandleChallengeAsyncInternal(AuthenticationProperties properties)
{
Logger.EnteringOpenIdAuthenticationHandlerHandleUnauthorizedAsync(GetType().FullName);
// order for local RedirectUri
// 1. challenge.Properties.RedirectUri
// 2. CurrentUri if RedirectUri is not set)
if (string.IsNullOrEmpty(properties.RedirectUri))
{
properties.RedirectUri = OriginalPathBase + OriginalPath + Request.QueryString;
}
Logger.PostAuthenticationLocalRedirect(properties.RedirectUri);
if (_configuration == null && Options.ConfigurationManager != null)
{
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
}
var message = new OpenIdConnectMessage
{
ClientId = Options.ClientId,
EnableTelemetryParameters = !Options.DisableTelemetry,
IssuerAddress = _configuration?.AuthorizationEndpoint ?? string.Empty,
RedirectUri = BuildRedirectUri(Options.CallbackPath),
Resource = Options.Resource,
ResponseType = Options.ResponseType,
Prompt = properties.GetParameter<string>(OpenIdConnectParameterNames.Prompt) ?? Options.Prompt,
Scope = string.Join(" ", properties.GetParameter<ICollection<string>>(OpenIdConnectParameterNames.Scope) ?? Options.Scope),
};
// https://tools.ietf.org/html/rfc7636
if (Options.UsePkce && Options.ResponseType == OpenIdConnectResponseType.Code)
{
var bytes = new byte[32];
RandomNumberGenerator.Fill(bytes);
var codeVerifier = Microsoft.AspNetCore.WebUtilities.Base64UrlTextEncoder.Encode(bytes);
// Store this for use during the code redemption. See RunAuthorizationCodeReceivedEventAsync.
properties.Items.Add(OAuthConstants.CodeVerifierKey, codeVerifier);
var challengeBytes = SHA256.HashData(Encoding.UTF8.GetBytes(codeVerifier));
var codeChallenge = WebEncoders.Base64UrlEncode(challengeBytes);
message.Parameters.Add(OAuthConstants.CodeChallengeKey, codeChallenge);
message.Parameters.Add(OAuthConstants.CodeChallengeMethodKey, OAuthConstants.CodeChallengeMethodS256);
}
// Add the 'max_age' parameter to the authentication request if MaxAge is not null.
// See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
var maxAge = properties.GetParameter<TimeSpan?>(OpenIdConnectParameterNames.MaxAge) ?? Options.MaxAge;
if (maxAge.HasValue)
{
message.MaxAge = Convert.ToInt64(Math.Floor((maxAge.Value).TotalSeconds))
.ToString(CultureInfo.InvariantCulture);
}
// Omitting the response_mode parameter when it already corresponds to the default
// response_mode used for the specified response_type is recommended by the specifications.
// See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes
if (!string.Equals(Options.ResponseType, OpenIdConnectResponseType.Code, StringComparison.Ordinal) ||
!string.Equals(Options.ResponseMode, OpenIdConnectResponseMode.Query, StringComparison.Ordinal))
{
message.ResponseMode = Options.ResponseMode;
}
if (Options.ProtocolValidator.RequireNonce)
{
message.Nonce = Options.ProtocolValidator.GenerateNonce();
WriteNonceCookie(message.Nonce);
}
GenerateCorrelationId(properties);
var redirectContext = new RedirectContext(Context, Scheme, Options, properties)
{
ProtocolMessage = message
};
await Events.RedirectToIdentityProvider(redirectContext);
if (redirectContext.Handled)
{
Logger.RedirectToIdentityProviderHandledResponse();
return;
}
message = redirectContext.ProtocolMessage;
if (!string.IsNullOrEmpty(message.State))
{
properties.Items[OpenIdConnectDefaults.UserstatePropertiesKey] = message.State;
}
// When redeeming a 'code' for an AccessToken, this value is needed
properties.Items.Add(OpenIdConnectDefaults.RedirectUriForCodePropertiesKey, message.RedirectUri);
message.State = Options.StateDataFormat.Protect(properties);
if (string.IsNullOrEmpty(message.IssuerAddress))
{
throw new InvalidOperationException(
"Cannot redirect to the authorization endpoint, the configuration may be missing or invalid.");
}
if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.RedirectGet)
{
var redirectUri = message.CreateAuthenticationRequestUrl();
if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute))
{
Logger.InvalidAuthenticationRequestUrl(redirectUri);
}
Response.Redirect(redirectUri);
return;
}
else if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.FormPost)
{
var content = message.BuildFormPost();
var buffer = Encoding.UTF8.GetBytes(content);
Response.ContentLength = buffer.Length;
Response.ContentType = "text/html;charset=UTF-8";
// Emit Cache-Control=no-cache to prevent client caching.
Response.Headers[HeaderNames.CacheControl] = "no-cache, no-store";
Response.Headers[HeaderNames.Pragma] = "no-cache";
Response.Headers[HeaderNames.Expires] = HeaderValueEpocDate;
await Response.Body.WriteAsync(buffer, 0, buffer.Length);
return;
}
throw new NotImplementedException($"An unsupported authentication method has been configured: {Options.AuthenticationMethod}");
}
此段代码篇幅很长,分块解析,代码如下:
if (string.IsNullOrEmpty(properties.RedirectUri))
{
properties.RedirectUri = OriginalPathBase + OriginalPath + Request.QueryString;
}
Logger.PostAuthenticationLocalRedirect(properties.RedirectUri);
如果challenge.Properties没有设置了RedirectUri,则按照指定逻辑生成RedirectUri,这段代码目前看不出有什么作用.接着看如下代码:
if (_configuration == null && Options.ConfigurationManager != null)
{
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
}
这段代码很重要,从id4服务拉取了配置相关信息,代码如下:
public async Task<T> GetConfigurationAsync(CancellationToken cancel)
{
DateTimeOffset now = DateTimeOffset.UtcNow;
if (_currentConfiguration != null && _syncAfter > now)
{
return _currentConfiguration;
}
await _refreshLock.WaitAsync(cancel).ConfigureAwait(false);
try
{
if (_syncAfter <= now)
{
try
{
// Don't use the individual CT here, this is a shared operation that shouldn't be affected by an individual's cancellation.
// The transport should have it's own timeouts, etc..
_currentConfiguration = await _configRetriever.GetConfigurationAsync(_metadataAddress, _docRetriever, CancellationToken.None).ConfigureAwait(false);
Contract.Assert(_currentConfiguration != null);
_lastRefresh = now;
_syncAfter = DateTimeUtil.Add(now.UtcDateTime, _automaticRefreshInterval);
}
catch (Exception ex)
{
_syncAfter = DateTimeUtil.Add(now.UtcDateTime, _automaticRefreshInterval < _refreshInterval ? _automaticRefreshInterval : _refreshInterval);
if (_currentConfiguration == null) // Throw an exception if there's no configuration to return.
throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant(LogMessages.IDX20803, (_metadataAddress ?? "null")), ex));
else
LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant(LogMessages.IDX20806, (_metadataAddress ?? "null")), ex));
}
}
// Stale metadata is better than no metadata
if (_currentConfiguration != null)
return _currentConfiguration;
else
{
throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant(LogMessages.IDX20803, (_metadataAddress ?? "null"))));
}
}
finally
{
_refreshLock.Release();
}
}
首先判断下配置是否过期,没有过期的话,返回缓存的配置,这一点保证了同步id4的配置同步到客户端,不会太损耗性能,接着通过SemaphoreSlim实例,做了下并发安全操作.
接着如果第一次初始化或者配置过期,则从id4同步一次配置.接着看如下代码:
_currentConfiguration = await _configRetriever.GetConfigurationAsync(_metadataAddress, _docRetriever, CancellationToken.None).ConfigureAwait(false);
首先_metadataAddress字段的值是取自如下OpenIdConnectPostConfigureOptions : IPostConfigureOptions<OpenIdConnectOptions>代码(关于IPostConfigureOptions可以理解未在给OpenIdConnectOptions配置实例注册一个行为,当程序配置完OpenIdConnectOptions配置实例后,会调用IPostConfigureOptions的PostConfigure方法执行配置的二次初始化,类似写入默认配置的功能):
public void PostConfigure(string name, OpenIdConnectOptions options)
{
options.DataProtectionProvider = options.DataProtectionProvider ?? _dp;
if (string.IsNullOrEmpty(options.SignOutScheme))
{
options.SignOutScheme = options.SignInScheme;
}
if (options.StateDataFormat == null)
{
var dataProtector = options.DataProtectionProvider.CreateProtector(
typeof(OpenIdConnectHandler).FullName, name, "v1");
options.StateDataFormat = new PropertiesDataFormat(dataProtector);
}
if (options.StringDataFormat == null)
{
var dataProtector = options.DataProtectionProvider.CreateProtector(
typeof(OpenIdConnectHandler).FullName,
typeof(string).FullName,
name,
"v1");
options.StringDataFormat = new SecureDataFormat<string>(new StringSerializer(), dataProtector);
}
if (string.IsNullOrEmpty(options.TokenValidationParameters.ValidAudience) && !string.IsNullOrEmpty(options.ClientId))
{
options.TokenValidationParameters.ValidAudience = options.ClientId;
}
if (options.Backchannel == null)
{
options.Backchannel = new HttpClient(options.BackchannelHttpHandler ?? new HttpClientHandler());
options.Backchannel.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET Core OpenIdConnect handler");
options.Backchannel.Timeout = options.BackchannelTimeout;
options.Backchannel.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB
}
if (options.ConfigurationManager == null)
{
if (options.Configuration != null)
{
options.ConfigurationManager = new StaticConfigurationManager<OpenIdConnectConfiguration>(options.Configuration);
}
else if (!(string.IsNullOrEmpty(options.MetadataAddress) && string.IsNullOrEmpty(options.Authority)))
{
if (string.IsNullOrEmpty(options.MetadataAddress) && !string.IsNullOrEmpty(options.Authority))
{
options.MetadataAddress = options.Authority;
if (!options.MetadataAddress.EndsWith("/", StringComparison.Ordinal))
{
options.MetadataAddress += "/";
}
options.MetadataAddress += ".well-known/openid-configuration";
}
if (options.RequireHttpsMetadata && !options.MetadataAddress.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("The MetadataAddress or Authority must use HTTPS unless disabled for development by setting RequireHttpsMetadata=false.");
}
options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(options.MetadataAddress, new OpenIdConnectConfigurationRetriever(),
new HttpDocumentRetriever(options.Backchannel) { RequireHttps = options.RequireHttpsMetadata })
{
RefreshInterval = options.RefreshInterval,
AutomaticRefreshInterval = options.AutomaticRefreshInterval,
};
}
}
}
中的如下代码:
if (string.IsNullOrEmpty(options.MetadataAddress) && !string.IsNullOrEmpty(options.Authority))
{
options.MetadataAddress = options.Authority;
if (!options.MetadataAddress.EndsWith("/", StringComparison.Ordinal))
{
options.MetadataAddress += "/";
}
options.MetadataAddress += ".well-known/openid-configuration";
}
实际_metadataAddress字段的值就是取自OpenIdConnectOptions配置实例的Authority值+"/.well-known/openid-configuration"而Authority值在demo中配置的就是id4服务的地址,那么很明显_metadataAddress字段指向的就是id4服务下的某个终结点,后续会介绍.接着回到获取配置的方法,这里篇幅太多直接解析重点,
public async Task<string> GetDocumentAsync(string address, CancellationToken cancel)
{
if (string.IsNullOrWhiteSpace(address))
throw new ArgumentNullException(nameof(address));
if (!Utility.IsHttps(address) && RequireHttps)
throw new Exception("");
Exception unsuccessfulHttpResponseException;
try
{
var httpClient = _httpClient ?? _defaultHttpClient;
var uri = new Uri(address, UriKind.RelativeOrAbsolute);
var response = await httpClient.GetAsync(uri, cancel).ConfigureAwait(false);
var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
if (response.IsSuccessStatusCode)
return responseContent;
unsuccessfulHttpResponseException = new IOException();
}
catch (Exception ex)
{
throw ex;
}
throw new Exception("");
}
通过demo中设置的id4服务的地址和默认的id4默认的配置发现服务,通过httpclient get请求,获取到id4对外公开的配置信息.并反序列化到OpenIdConnectConfiguration实例中.
接着执行如下代码:
OpenIdConnectConfiguration openIdConnectConfiguration = JsonConvert.DeserializeObject<OpenIdConnectConfiguration>(doc);
if (!string.IsNullOrEmpty(openIdConnectConfiguration.JwksUri))
{
string keys = await retriever.GetDocumentAsync(openIdConnectConfiguration.JwksUri, cancel).ConfigureAwait(false);
openIdConnectConfiguration.JsonWebKeySet = JsonConvert.DeserializeObject<JsonWebKeySet>(keys);
foreach (SecurityKey key in openIdConnectConfiguration.JsonWebKeySet.GetSigningKeys())
{
openIdConnectConfiguration.SigningKeys.Add(key);
}
}
这里拿到公开配置中的JwsUri的节点访问地址,通过httpclient拉取到id4服务端生成的jwk相关信息(解密令牌用)并写入到OpenIdConnectConfiguration实例中并返回.所以Challange方法第一步拉取了id4服务所有公开的配置和jwt信息相关信息并写入OpenIdConnectConfiguration实例返回.接着看oidc的处理器方法如下:
var message = new OpenIdConnectMessage
{
ClientId = Options.ClientId,
EnableTelemetryParameters = !Options.DisableTelemetry,
IssuerAddress = _configuration?.AuthorizationEndpoint ?? string.Empty,
RedirectUri = BuildRedirectUri(Options.CallbackPath),
Resource = Options.Resource,
ResponseType = Options.ResponseType,
Prompt = properties.GetParameter<string>(OpenIdConnectParameterNames.Prompt) ?? Options.Prompt,
Scope = string.Join(" ", properties.GetParameter<ICollection<string>>(OpenIdConnectParameterNames.Scope) ?? Options.Scope),
};
// https://tools.ietf.org/html/rfc7636
if (Options.UsePkce && Options.ResponseType == OpenIdConnectResponseType.Code)
{
var bytes = new byte[32];
RandomNumberGenerator.Fill(bytes);
var codeVerifier = Microsoft.AspNetCore.WebUtilities.Base64UrlTextEncoder.Encode(bytes);
// Store this for use during the code redemption. See RunAuthorizationCodeReceivedEventAsync.
properties.Items.Add(OAuthConstants.CodeVerifierKey, codeVerifier);
var challengeBytes = SHA256.HashData(Encoding.UTF8.GetBytes(codeVerifier));
var codeChallenge = WebEncoders.Base64UrlEncode(challengeBytes);
message.Parameters.Add(OAuthConstants.CodeChallengeKey, codeChallenge);
message.Parameters.Add(OAuthConstants.CodeChallengeMethodKey, OAuthConstants.CodeChallengeMethodS256);
}
生成了OpenIdConnectMessage实例,分析下配置来源,配置OIDC组件时如下代码:
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "http://localhost:5001";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.SaveTokens = true;
});
ClientId:来自客户端集成OIDC组件时设置的ClientId demo中式mvc
EnableTelemetryParameters:来自客户端集成OIDC组件时设置的EnableTelemetryParameters 默认为false
IssuerAddress:_来自_id4服务公开的配置信息中的认证终结点 id服务地址+/connect/authorize
RedirectUri:RedirectUri的值来自与两个地方:
(1)、OpenIdConnectOptions配置的默认值/signin-oidc
public OpenIdConnectOptions()
{
CallbackPath = new PathString("/signin-oidc");
SignedOutCallbackPath = new PathString("/signout-callback-oidc");
RemoteSignOutPath = new PathString("/signout-oidc");
SecurityTokenValidator = _defaultHandler;
Events = new OpenIdConnectEvents();
Scope.Add("openid");
Scope.Add("profile");
ClaimActions.DeleteClaim("nonce");
ClaimActions.DeleteClaim("aud");
ClaimActions.DeleteClaim("azp");
ClaimActions.DeleteClaim("acr");
ClaimActions.DeleteClaim("iss");
ClaimActions.DeleteClaim("iat");
ClaimActions.DeleteClaim("nbf");
ClaimActions.DeleteClaim("exp");
ClaimActions.DeleteClaim("at_hash");
ClaimActions.DeleteClaim("c_hash");
ClaimActions.DeleteClaim("ipaddr");
ClaimActions.DeleteClaim("platf");
ClaimActions.DeleteClaim("ver");
// http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
ClaimActions.MapUniqueJsonKey("sub", "sub");
ClaimActions.MapUniqueJsonKey("name", "name");
ClaimActions.MapUniqueJsonKey("given_name", "given_name");
ClaimActions.MapUniqueJsonKey("family_name", "family_name");
ClaimActions.MapUniqueJsonKey("profile", "profile");
ClaimActions.MapUniqueJsonKey("email", "email");
_nonceCookieBuilder = new OpenIdConnectNonceCookieBuilder(this)
{
Name = OpenIdConnectDefaults.CookieNoncePrefix,
HttpOnly = true,
SameSite = SameSiteMode.None,
SecurePolicy = CookieSecurePolicy.SameAsRequest,
IsEssential = true,
};
}
(2)、认证方案处理器基类 AuthenticationHandler<TOptions>的BuildRedirectUri方法进行组装,如下代码:
protected string BuildRedirectUri(string targetPath)
=> Request.Scheme + "://" + Request.Host + OriginalPathBase + targetPath;
Request.Scheme代表是http还是https协议,Request.Host当前客户端的ip地址加端口,OriginalPathBase可以通过IAuthenticationFeature设置值,目前不知道他的用途.
ok,打这里也就知道RedirectUri的值了当前客户端的/signin-oidc访问路径.
Resource:来自客户端集成OIDC组件时设置的Resource demo中为null
ResponseType:来自客户端集成OIDC组件时设置的ResponseType demo中为 code
Prompt:来自认证属性AuthenticationProperties实例(如果为空取自客户端集成OIDC组件时设置的Prompt demo中为空),demo中调用为null
Scope:自认证属性AuthenticationProperties实例 (如果为空取自客户端集成OIDC组件时设置的Scope) 上图中有OpenIdConnectOptions实例得默认构造
Scope.Add("openid");
Scope.Add("profile");
所以其默认值为openid、profile.
到这里OpenIdConnectMessage实例得构造分析完毕.
接着分析OIDC 认证方案得OpenIdConnectHandler实例的HandleChallengeAsyncInternal方法的剩余逻辑
if (Options.UsePkce && Options.ResponseType == OpenIdConnectResponseType.Code)
{
var bytes = new byte[32];
RandomNumberGenerator.Fill(bytes);
var codeVerifier = Microsoft.AspNetCore.WebUtilities.Base64UrlTextEncoder.Encode(bytes);
// Store this for use during the code redemption. See RunAuthorizationCodeReceivedEventAsync.
properties.Items.Add(OAuthConstants.CodeVerifierKey, codeVerifier);
var challengeBytes = SHA256.HashData(Encoding.UTF8.GetBytes(codeVerifier));
var codeChallenge = WebEncoders.Base64UrlEncode(challengeBytes);
message.Parameters.Add(OAuthConstants.CodeChallengeKey, codeChallenge);
message.Parameters.Add(OAuthConstants.CodeChallengeMethodKey, OAuthConstants.CodeChallengeMethodS256);
}
首先默认是开启PKCE模式的且这里demo中给定的响应类型确实是code,其实这里demo就是采用Authorization Code+PKCE模式,关于这个模式请参考https://mp.weixin.qq.com/s/p9PdwqpQYwv5iWkTlhfuew 下面解析分析源码,这个模式会干什么
(1)、生成32的随机数(RandomNumberGenerator 是一种密码强度的随机数生成器)并转成base64字符串
(2)、并向AuthenticationProperties实例的Items属性写入 key为code_verifier value为(1)中的32位随机数的base64字符串
(3)、通过SHA256加密(1)中的随机数.转成base64字符串 叫做codeChallenge
(4)、向OpenIdConnectMessage实例的Parameters属性写入key 为code_challenge value为(3)中的值和key为code_challenge_method,value为codeChallenge的加密方式
接着分析OIDC 认证方案得OpenIdConnectHandler实例的HandleChallengeAsyncInternal方法的剩余逻辑
var maxAge = properties.GetParameter<TimeSpan?>(OpenIdConnectParameterNames.MaxAge) ?? Options.MaxAge;
if (maxAge.HasValue)
{
message.MaxAge = Convert.ToInt64(Math.Floor((maxAge.Value).TotalSeconds))
.ToString(CultureInfo.InvariantCulture);
}
// Omitting the response_mode parameter when it already corresponds to the default
// response_mode used for the specified response_type is recommended by the specifications.
// See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes
if (!string.Equals(Options.ResponseType, OpenIdConnectResponseType.Code, StringComparison.Ordinal) ||
!string.Equals(Options.ResponseMode, OpenIdConnectResponseMode.Query, StringComparison.Ordinal))
{
message.ResponseMode = Options.ResponseMode;
}
这里设置了OpenIdConnectMessage实例的maxAge属性和ResponseMode属性
接着看如下源码:
if (Options.ProtocolValidator.RequireNonce)
{
message.Nonce = Options.ProtocolValidator.GenerateNonce();
WriteNonceCookie(message.Nonce);
}
这里设置了OpenIdConnectMessage实例的nonce值,值的内容如下:
public virtual string GenerateNonce()
{
LogHelper.LogVerbose(LogMessages.IDX21328);
string nonce = Convert.ToBase64String(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString() + Guid.NewGuid().ToString()));
if (RequireTimeStampInNonce)
{
return DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture) + "." + nonce;
}
return nonce;
}
Guid值加上时间戳.并写入到客户端的cookie中.关于nonce的作用主要用于安全,防止重放攻击.具体请参
https://blog.csdn.net/koastal/article/details/53456696,后续也会解析.
cookie的名称是.AspNetCore.OpenIdConnect.Nonce.,当然这个值是可以修改的,但是不建议这么做.
接着看如下代码:
GenerateCorrelationId(properties);
根据认证属性生成了CorrelationId,生成逻辑如下:
protected virtual void GenerateCorrelationId(AuthenticationProperties properties)
{
if (properties == null)
{
throw new ArgumentNullException(nameof(properties));
}
var bytes = new byte[32];
RandomNumberGenerator.Fill(bytes);
var correlationId = Base64UrlTextEncoder.Encode(bytes);
var cookieOptions = Options.CorrelationCookie.Build(Context, Clock.UtcNow);
properties.Items[CorrelationProperty] = correlationId;
var cookieName = Options.CorrelationCookie.Name + correlationId;
Response.Cookies.Append(cookieName, CorrelationMarker, cookieOptions);
}
首先生成了一个32位随机数转成base64字符串,作为correlationId写入AuthenticationProperties实例的Item属性,key为.xsrf,value为correlationId,并写入cookie,名称为.AspNetCore.Correlation.+correlationId
接着看如下代码:
var redirectContext = new RedirectContext(Context, Scheme, Options, properties)
{
ProtocolMessage = message
};
await Events.RedirectToIdentityProvider(redirectContext);
if (redirectContext.Handled)
{
Logger.RedirectToIdentityProviderHandledResponse();
return;
}
message = redirectContext.ProtocolMessage;
if (!string.IsNullOrEmpty(message.State))
{
properties.Items[OpenIdConnectDefaults.UserstatePropertiesKey] = message.State;
}
// When redeeming a 'code' for an AccessToken, this value is needed
properties.Items.Add(OpenIdConnectDefaults.RedirectUriForCodePropertiesKey, message.RedirectUri);
message.State = Options.StateDataFormat.Protect(properties);
首先生成一个RedirectConext实例,给外部订阅,说明这里可以自定义跳转.接着将AuthenticationProperties实例的Items属性写入key为OpenIdConnect.Code.RedirectUri,value为就是客户端跳转url,demo中为http://localhost:5002/signin-oidc.同时将AuthenticationProperties实例值通过配置ISecureDataFormat<AuthenticationProperties>接口进行加密写入到OpenIdConnectMessage实例的Sate属性中.
接着分析OIDC 认证方案得OpenIdConnectHandler实例的HandleChallengeAsyncInternal方法的剩余逻辑,如下代码:
if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.RedirectGet)
{
var redirectUri = message.CreateAuthenticationRequestUrl();
if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute))
{
Logger.InvalidAuthenticationRequestUrl(redirectUri);
}
Response.Redirect(redirectUri);
return;
}
else if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.FormPost)
{
var content = message.BuildFormPost();
var buffer = Encoding.UTF8.GetBytes(content);
Response.ContentLength = buffer.Length;
Response.ContentType = "text/html;charset=UTF-8";
// Emit Cache-Control=no-cache to prevent client caching.
Response.Headers[HeaderNames.CacheControl] = "no-cache, no-store";
Response.Headers[HeaderNames.Pragma] = "no-cache";
Response.Headers[HeaderNames.Expires] = HeaderValueEpocDate;
await Response.Body.WriteAsync(buffer, 0, buffer.Length);
return;
}
默认OIDC组件默认的跳转到身份认证服务的模式是OpenIdConnectRedirectBehavior.RedirectGet,当然这里可以改变,应为url过于明显.但是原理一样,那么这里走第一个if,接着分析,这里根据OpenIdConnectMessage实例内容创建url,创建代码如下:
public virtual string CreateAuthenticationRequestUrl()
{
OpenIdConnectMessage openIdConnectMessage = Clone();
openIdConnectMessage.RequestType = OpenIdConnectRequestType.Authentication;
EnsureTelemetryValues(openIdConnectMessage);
return openIdConnectMessage.BuildRedirectUrl();
}
通过Clone方法(本质new this),创建一个副本,设置了message的请求类型为Authentication,接着设置了监控相关的如下字段:
private void EnsureTelemetryValues(OpenIdConnectMessage clonedMessage)
{
if (this.EnableTelemetryParameters)
{
clonedMessage.SetParameter(OpenIdConnectParameterNames.SkuTelemetry, SkuTelemetryValue);
clonedMessage.SetParameter(OpenIdConnectParameterNames.VersionTelemetry, typeof(OpenIdConnectMessage).GetTypeInfo().Assembly.GetName().Version.ToString());
}
}
最后根据message实例生成访问url,代码如下:
public virtual string BuildRedirectUrl()
{
StringBuilder strBuilder = new StringBuilder(_issuerAddress);
bool issuerAddressHasQuery = _issuerAddress.Contains("?");
foreach (KeyValuePair<string, string> parameter in _parameters)
{
if (parameter.Value == null)
{
continue;
}
if (!issuerAddressHasQuery)
{
strBuilder.Append('?');
issuerAddressHasQuery = true;
}
else
{
strBuilder.Append('&');
}
strBuilder.Append(Uri.EscapeDataString(parameter.Key));
strBuilder.Append('=');
strBuilder.Append(Uri.EscapeDataString(parameter.Value));
}
return strBuilder.ToString();
}
这里_issuerAddress就是id4服务的认证终结点地址,上面有介绍.message实例值经过上述流程的转换,如下图:
最后根据这些值生成访问url,对应的url值如下:
http://localhost:5001/connect/authorize?client_id=mvc&redirect_uri=http%3A%2F%2Flocalhost%3A5002%2Fsignin-oidc&response_type=code&scope=openid%20profile&code_challenge=Ur1nNYQMb92VuIDvgeN9mJCvQRWyspeUvEjWDToyHqg&code_challenge_method=S256&response_mode=form_post&nonce=637914152486923476.OGM4MTZlNjktODgyYi00MDk3LThmYjMtMThhZjA2Y2I1NTRmZDI1NzIxYzYtZjkzNS00YzhjLTgzODctNGQyMmJhNmRhNGM4&state=CfDJ8HpC1EPIyftOtkkyJFkl1v9AcTjtWAadkF-ERJUSWQun-BBX0VMyqB5FFwNfPPTDI8B_17mXRXOCH_G55jpkiMMjer5IV1T5Skt2nDxn8WGS_inRbRntd04agnYBGCxXyIT6cuspg0sXcOvorCManimIgsxsg5tHNSYrh8dWtdJ1FvOknWcfYhbqR5QzZ44WZKEEdxUNn-9CB6FJnulndq_5CwkqjPMux2TsnE3Wok1MsSC8kKAoHTuvBwrxd1Su_xmooEg64NJCI4_ZbB9h9lBuv9YUSraDDUzAOzPA8zqwRlYA2SCevtIcmXxaT23bQ63Zv0dJ3kCoyTsoxf5OYoaOs8JkDzXl7cqglBb21cJ7CHQMW1IXdku6bHo1-BSHuw&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=1.0.0.0
拿到url之后进行Response.Redirect(redirectUri);