.Net 5.0 通过IdentityServer4实现单点登录之oidc认证部分源码解析
接着前文.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), };
生成了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);