前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >.Net 5.0 通过IdentityServer4实现单点登录之oidc认证部分源码解析

.Net 5.0 通过IdentityServer4实现单点登录之oidc认证部分源码解析

作者头像
郑小超.
发布2022-06-22 17:00:51
1.1K0
发布2022-06-22 17:00:51
举报
文章被收录于专栏:GreenLeavesGreenLeaves

接着前文.Net 5.0 通过IdentityServer4实现单点登录之授权部分源码解析,本文主要分析在授权失败后,调用oidc认证的Chanllage方法部分.关于认证方案不理解的可以参考.Net Core 3.0 认证组件源码解析上文讲到因为第一次调用,请求的控制器方法没有带任何身份认证信息,且因为控制器默认打了Authorize特性,经过前文描述的一系列授权处理器处理,授权结果返回PolicyAuthorizationResult.Challenge(),接着执行如下代码:

代码语言:javascript
复制
            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方法,执行细节如下:

代码语言:javascript
复制
        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方法代码如下:

代码语言:javascript
复制
        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方法,如下:

代码语言:javascript
复制
        protected virtual Task HandleChallengeAsync(AuthenticationProperties properties)
        {
            Response.StatusCode = 401;
            return Task.CompletedTask;
        }

这里很明显,会被子类重写,要不然流程走不下去了.这里,应为默认的challenge方法配置的是oidc,所以查看下OpenIdConnectHandler实例的方法(关于这个跳转要理解,必须掌握认证组件的逻辑),代码如下:

代码语言:javascript
复制
        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方法:

代码语言:javascript
复制
        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}");
        }

此段代码篇幅很长,分块解析,代码如下:

代码语言:javascript
复制
            if (string.IsNullOrEmpty(properties.RedirectUri))
            {
                properties.RedirectUri = OriginalPathBase + OriginalPath + Request.QueryString;
            }
            Logger.PostAuthenticationLocalRedirect(properties.RedirectUri);

如果challenge.Properties没有设置了RedirectUri,则按照指定逻辑生成RedirectUri,这段代码目前看不出有什么作用.接着看如下代码:

代码语言:javascript
复制
            if (_configuration == null && Options.ConfigurationManager != null)
            {
                _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
            }

这段代码很重要,从id4服务拉取了配置相关信息,代码如下:

代码语言:javascript
复制
        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同步一次配置.接着看如下代码:

代码语言:javascript
复制
 _currentConfiguration = await _configRetriever.GetConfigurationAsync(_metadataAddress, _docRetriever, CancellationToken.None).ConfigureAwait(false);

首先_metadataAddress字段的值是取自如下OpenIdConnectPostConfigureOptions : IPostConfigureOptions<OpenIdConnectOptions>代码(关于IPostConfigureOptions可以理解未在给OpenIdConnectOptions配置实例注册一个行为,当程序配置完OpenIdConnectOptions配置实例后,会调用IPostConfigureOptions的PostConfigure方法执行配置的二次初始化,类似写入默认配置的功能):

代码语言:javascript
复制
       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,
                    };
                }
            }
        }

中的如下代码:

代码语言:javascript
复制
                    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服务下的某个终结点,后续会介绍.接着回到获取配置的方法,这里篇幅太多直接解析重点,

代码语言:javascript
复制
        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实例中.

接着执行如下代码:

代码语言:javascript
复制
            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的处理器方法如下:

代码语言:javascript
复制
            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组件时如下代码:

代码语言:javascript
复制
            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

代码语言:javascript
复制
        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方法进行组装,如下代码:

代码语言:javascript
复制
      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实例得默认构造

代码语言:javascript
复制
Scope.Add("openid");
Scope.Add("profile");

所以其默认值为openid、profile.

到这里OpenIdConnectMessage实例得构造分析完毕.

接着分析OIDC 认证方案得OpenIdConnectHandler实例的HandleChallengeAsyncInternal方法的剩余逻辑

代码语言:javascript
复制
           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方法的剩余逻辑

代码语言:javascript
复制
            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属性

接着看如下源码:

代码语言:javascript
复制
            if (Options.ProtocolValidator.RequireNonce)
            {
                message.Nonce = Options.ProtocolValidator.GenerateNonce();
                WriteNonceCookie(message.Nonce);
            }

这里设置了OpenIdConnectMessage实例的nonce值,值的内容如下:

代码语言:javascript
复制
        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.,当然这个值是可以修改的,但是不建议这么做.

接着看如下代码:

代码语言:javascript
复制
GenerateCorrelationId(properties);

根据认证属性生成了CorrelationId,生成逻辑如下:

代码语言:javascript
复制
        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 

接着看如下代码:

代码语言:javascript
复制
            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方法的剩余逻辑,如下代码:

代码语言:javascript
复制
            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,创建代码如下:

代码语言:javascript
复制
        public virtual string CreateAuthenticationRequestUrl()
        {
            OpenIdConnectMessage openIdConnectMessage = Clone();
            openIdConnectMessage.RequestType = OpenIdConnectRequestType.Authentication;
            EnsureTelemetryValues(openIdConnectMessage);
            return openIdConnectMessage.BuildRedirectUrl();
        }

通过Clone方法(本质new this),创建一个副本,设置了message的请求类型为Authentication,接着设置了监控相关的如下字段:

代码语言:javascript
复制
        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,代码如下:

代码语言:javascript
复制
        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值如下:

代码语言:javascript
复制
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);

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-06-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档