• .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);

  • 相关阅读:
    20175314 《Java程序设计》第六周学习总结
    20175314 结队编程项目——四则运算第一周
    20175314 《Java程序设计》迭代和JDB
    20175314 实验一 Java开发环境的熟悉
    20175314 《Java程序设计》第五周学习总结
    20175314 《Java程序设计》第四周学习总结
    20175314 《Java程序设计》第三周学习总结
    20175314 《Java程序设计》第二周学习总结
    20175314 《Java程序设计》第一周学习总结
    多态的成员特点
  • 原文地址:https://www.cnblogs.com/GreenLeaves/p/16394688.html
Copyright © 2020-2023  润新知