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


前文介绍了oidc组件整合了相关的配置信息和从id4服务配置节点拉去了相关的配置信息和一些默认的信息,生成了OpenIdConnectMessage实例,内容如下:

,通过该实例生成了跳转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

最后调用Response.Redirect跳转到上面的url,id4的认证终结点,这里因为配置节点的相关源码比较简单,本文不做介绍.

所以这里会进入到id4的认证终结点,这里关于id4如果跳转终结点的因为源码比较简单,这里也不做介绍.大致逻辑事通过配置访问url,跳转到对应的处理终结点.url和终结点通过id4默认配置产生.接着看下id4demo服务的StartUp文件的调用代码如下:

// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.


using IdentityServer4;
using IdentityServerHost.Quickstart.UI;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;

namespace IdentityServer
{
    public class Startup
    {
        void CheckSameSite(HttpContext httpContext, CookieOptions options)
        {
            if (options.SameSite == SameSiteMode.None)
            {
                var userAgent = httpContext.Request.Headers["User-Agent"].ToString();
                if (true)
                {
                    options.SameSite = SameSiteMode.Unspecified;
                }
            }
        }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
          
            var builder = services.AddIdentityServer()
                .AddInMemoryIdentityResources(Config.IdentityResources)
                .AddInMemoryApiScopes(Config.ApiScopes)
                .AddInMemoryClients(Config.Clients)
                .AddTestUsers(TestUsers.Users);

            builder.AddDeveloperSigningCredential();

            services.AddAuthentication()
                .AddGoogle("Google", options =>
                {
                    options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;

                    options.ClientId = "";
                    options.ClientSecret = "";
                })
                .AddOpenIdConnect("oidc", "Demo IdentityServer", options =>
                {
                    options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
                    options.SignOutScheme = IdentityServerConstants.SignoutScheme;
                    options.SaveTokens = true;

                    options.Authority = "https://demo.identityserver.io/";
                    options.ClientId = "interactive.confidential";
                    options.ClientSecret = "secret";
                    options.ResponseType = "code";

                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        NameClaimType = "name",
                        RoleClaimType = "role"
                    };
                });
            services.Configure(options =>
            {
                options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
                options.OnAppendCookie = cookieContext =>
                CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
                options.OnDeleteCookie = cookieContext =>
                CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
            });
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseStaticFiles();
            app.UseRouting();

            app.UseIdentityServer();
            app.UseCookiePolicy();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapDefaultControllerRoute();
            });
        }
    }
}

接着分析认证终结点执行逻辑,源码如下:

        public override async Task ProcessAsync(HttpContext context)
        {
            Logger.LogDebug("Start authorize request");

            NameValueCollection values;

            if (HttpMethods.IsGet(context.Request.Method))
            {
                values = context.Request.Query.AsNameValueCollection();
            }
            else if (HttpMethods.IsPost(context.Request.Method))
            {
                if (!context.Request.HasApplicationFormContentType())
                {
                    return new StatusCodeResult(HttpStatusCode.UnsupportedMediaType);
                }

                values = context.Request.Form.AsNameValueCollection();
            }
            else
            {
                return new StatusCodeResult(HttpStatusCode.MethodNotAllowed);
            }

            var user = await UserSession.GetUserAsync();
            var result = await ProcessAuthorizeRequestAsync(values, user, null);

            Logger.LogTrace("End authorize request. result type: {0}", result?.GetType().ToString() ?? "-none-");

            return result;
        }

首先通过跳转时通过get方式,所以看下内部方法(将querystring转换成键值对集合),如下:

        public static NameValueCollection AsNameValueCollection(this IEnumerablestring, StringValues>> collection)
        {
            var nv = new NameValueCollection();

            foreach (var field in collection)
            {
                nv.Add(field.Key, field.Value.First());
            }

            return nv;
        }

转换后的内容如下:

 看过上文应该知道这就是OpenIdConnectMessage实例的值.

接着看认证终结点的源码:

var user = await UserSession.GetUserAsync();

这里尝试从用户绘画中获取httpcontext上下文的用户信息,接着解析:

        protected virtual async Task AuthenticateAsync()
        {
            if (Principal == null || Properties == null)
            {
                var scheme = await HttpContext.GetCookieAuthenticationSchemeAsync();

                var handler = await Handlers.GetHandlerAsync(HttpContext, scheme);
                if (handler == null)
                {
                    throw new InvalidOperationException($"No authentication handler is configured to authenticate for the scheme: {scheme}");
                }

                var result = await handler.AuthenticateAsync();
                if (result != null && result.Succeeded)
                {
                    Principal = result.Principal;
                    Properties = result.Properties;
                }
            }
        }

这里进入认证解析流程,获取默认配置的cookie认证方案.源码如下:

        internal static async Task<string> GetCookieAuthenticationSchemeAsync(this HttpContext context)
        {
            var options = context.RequestServices.GetRequiredService();
            //获取配置中的认证方案,如果配置了,则采用自定义的认证方案
            if (options.Authentication.CookieAuthenticationScheme != null)
            {
                return options.Authentication.CookieAuthenticationScheme;
            }

            //这里默认时名称为idsrv的Cookie认证方案
            var schemes = context.RequestServices.GetRequiredService();
            var scheme = await schemes.GetDefaultAuthenticateSchemeAsync();
            if (scheme == null)
            {
                throw new InvalidOperationException("No DefaultAuthenticateScheme found or no CookieAuthenticationScheme configured on IdentityServerOptions.");
            }

            return scheme.Name;
        }

这里获取IdentityServerOptions配置的认证方案,说明这里认证方案是可以自定义的,但是demo中并没有配置,且在StratUp类中ConfigureServices方法中配置IdentityServer4时,默认采用的就是Cookie认证方案,其认证方案名称为idsrv,源码如下:

        public static IIdentityServerBuilder AddIdentityServer(this IServiceCollection services)
        {
            var builder = services.AddIdentityServerBuilder();

            builder
                .AddRequiredPlatformServices()
                .AddCookieAuthentication()
                .AddCoreServices()
                .AddDefaultEndpoints()
                .AddPluggableServices()
                .AddValidators()
                .AddResponseGenerators()
                .AddDefaultSecretParsers()
                .AddDefaultSecretValidators();

            // provide default in-memory implementation, not suitable for most production scenarios
            builder.AddInMemoryPersistedGrants();

            return builder;
        }
        public static IIdentityServerBuilder AddCookieAuthentication(this IIdentityServerBuilder builder)
        {
            builder.Services.AddAuthentication(IdentityServerConstants.DefaultCookieAuthenticationScheme)
                .AddCookie(IdentityServerConstants.DefaultCookieAuthenticationScheme)
                .AddCookie(IdentityServerConstants.ExternalCookieAuthenticationScheme);

            builder.Services.AddSingleton, ConfigureInternalCookieOptions>();
            builder.Services.AddSingleton, PostConfigureInternalCookieOptions>();
            builder.Services.AddTransientDecorator();
            builder.Services.AddTransientDecorator();

            return builder;
        }

ok,这里返回了默认的cookie认证方案后,回到认证流程如下代码:

               var handler = await Handlers.GetHandlerAsync(HttpContext, scheme);
                if (handler == null)
                {
                    throw new InvalidOperationException($"No authentication handler is configured to authenticate for the scheme: {scheme}");
                }

                var result = await handler.AuthenticateAsync();
                if (result != null && result.Succeeded)
                {
                    Principal = result.Principal;
                    Properties = result.Properties;
                }

根据认证方案名称获取认证处理器,逻辑如下:

        public async Task GetHandlerAsync(HttpContext context, string authenticationScheme)
        {
            var handler = await _provider.GetHandlerAsync(context, authenticationScheme);
            if (handler is IAuthenticationRequestHandler requestHandler)
            {
                if (requestHandler is IAuthenticationSignInHandler signinHandler)
                {
                    return new AuthenticationRequestSignInHandlerWrapper(signinHandler, _httpContextAccessor);
                }

                if (requestHandler is IAuthenticationSignOutHandler signoutHandler)
                {
                    return new AuthenticationRequestSignOutHandlerWrapper(signoutHandler, _httpContextAccessor);
                }

                return new AuthenticationRequestHandlerWrapper(requestHandler, _httpContextAccessor);
            }

            return handler;
        }

这里因为.CookieAuthenticationHandler处理器不是认证请求处理器,所以直接返回该处理器实例.接处理器实例的AuthenticateAsync从客户端加密的cookie中解析出用户信息写入到上下文中,应为这里是第一次调用,所以必然用户信息为空.关于cookie认证方案如果不清楚请参考https://www.cnblogs.com/GreenLeaves/p/12100568.html

 接着回到认证终结点源码如下:

            var user = await UserSession.GetUserAsync();
            var result = await ProcessAuthorizeRequestAsync(values, user, null);

            Logger.LogTrace("End authorize request. result type: {0}", result?.GetType().ToString() ?? "-none-");

            return result;

这里user为空,原因说了,接着分析ProcessAuthorizeRequestAsync方法:

       internal async Task ProcessAuthorizeRequestAsync(NameValueCollection parameters, ClaimsPrincipal user, ConsentResponse consent)
        {
            if (user != null)
            {
                Logger.LogDebug("User in authorize request: {subjectId}", user.GetSubjectId());
            }
            else
            {
                Logger.LogDebug("No user present in authorize request");
            }

            // validate request
            var result = await _validator.ValidateAsync(parameters, user);
            if (result.IsError)
            {
                return await CreateErrorResultAsync(
                    "Request validation failed",
                    result.ValidatedRequest,
                    result.Error,
                    result.ErrorDescription);
            }

            var request = result.ValidatedRequest;
            LogRequest(request);

            // determine user interaction
            var interactionResult = await _interactionGenerator.ProcessInteractionAsync(request, consent);
            if (interactionResult.IsError)
            {
                return await CreateErrorResultAsync("Interaction generator error", request, interactionResult.Error, interactionResult.ErrorDescription, false);
            }
            if (interactionResult.IsLogin)
            {
                return new LoginPageResult(request);
            }
            if (interactionResult.IsConsent)
            {
                return new ConsentPageResult(request);
            }
            if (interactionResult.IsRedirect)
            {
                return new CustomRedirectResult(request, interactionResult.RedirectUrl);
            }

            var response = await _authorizeResponseGenerator.CreateResponseAsync(request);

            await RaiseResponseEventAsync(response);

            LogResponse(response);

            return new AuthorizeResult(response);
        }

这里根据id4服务的配置和客户端传入的OpenIdConnectMessage实例值,校验传入OpenIdConnectMessage实例值是否符合要求,通过如下代码:

var result = await _validator.ValidateAsync(parameters, user);

开启检验,方法如下:

        public async Task ValidateAsync(NameValueCollection parameters, ClaimsPrincipal subject = null)
        {
            _logger.LogDebug("Start authorize request protocol validation");

            var request = new ValidatedAuthorizeRequest
            {
                Options = _options,
                Subject = subject ?? Principal.Anonymous,
                Raw = parameters ?? throw new ArgumentNullException(nameof(parameters))
            };
            
            // load client_id
            // client_id must always be present on the request
            var loadClientResult = await LoadClientAsync(request);
            if (loadClientResult.IsError)
            {
                return loadClientResult;
            }

            // load request object
            var roLoadResult = await LoadRequestObjectAsync(request);
            if (roLoadResult.IsError)
            {
                return roLoadResult;
            }

            // validate request object
            var roValidationResult = await ValidateRequestObjectAsync(request);
            if (roValidationResult.IsError)
            {
                return roValidationResult;
            }

            // validate client_id and redirect_uri
            var clientResult = await ValidateClientAsync(request);
            if (clientResult.IsError)
            {
                return clientResult;
            }

            // state, response_type, response_mode
            var mandatoryResult = ValidateCoreParameters(request);
            if (mandatoryResult.IsError)
            {
                return mandatoryResult;
            }

            // scope, scope restrictions and plausability
            var scopeResult = await ValidateScopeAsync(request);
            if (scopeResult.IsError)
            {
                return scopeResult;
            }

            // nonce, prompt, acr_values, login_hint etc.
            var optionalResult = await ValidateOptionalParametersAsync(request);
            if (optionalResult.IsError)
            {
                return optionalResult;
            }

            // custom validator
            _logger.LogDebug("Calling into custom validator: {type}", _customValidator.GetType().FullName);
            var context = new CustomAuthorizeRequestValidationContext
            {
                Result = new AuthorizeRequestValidationResult(request)
            };
            await _customValidator.ValidateAsync(context);

            var customResult = context.Result;
            if (customResult.IsError)
            {
                LogError("Error in custom validation", customResult.Error, request);
                return Invalid(request, customResult.Error, customResult.ErrorDescription);
            }

            _logger.LogTrace("Authorize request protocol validation successful");

            return Valid(request);
        }

这里首先生成了ValidatedAuthorizeRequest实例

(1)、检验客户端是否有效 设置ClientId和Client信息

        private async Task LoadClientAsync(ValidatedAuthorizeRequest request)
        {
            //从请求的QuerString获取客户端id
            var clientId = request.Raw.Get(OidcConstants.AuthorizeRequest.ClientId);

            //clientId值长度和非空检验 默认clientId不能超过100
            if (clientId.IsMissingOrTooLong(_options.InputLengthRestrictions.ClientId))
            {
                LogError("client_id is missing or too long", request);
                return Invalid(request, description: "Invalid client_id");
            }

            request.ClientId = clientId;

            //判断客户端是否在仓储中是否存在 demo中采用内存仓储
            var client = await _clients.FindEnabledClientByIdAsync(request.ClientId);
            if (client == null)
            {
                LogError("Unknown client or not enabled", request.ClientId, request);
                return Invalid(request, OidcConstants.AuthorizeErrors.UnauthorizedClient, "Unknown client or client not enabled");
            }

            //设置请求的客户端信息
            request.SetClient(client);

            return Valid(request);
        }

关于id4客户端的配置代码如下:

            var builder = services.AddIdentityServer()
                .AddInMemoryIdentityResources(Config.IdentityResources)
                .AddInMemoryApiScopes(Config.ApiScopes)
                .AddInMemoryClients(Config.Clients)
                .AddTestUsers(TestUsers.Users);

通过.AddInMemoryClients(Config.Clients)配置,id4官方提供了ef core实现,当然这里可以选择重写,如Dapper.

        public static IEnumerable Clients =>
            new List
            {
                // machine to machine client
                new Client
                {
                    ClientId = "client",
                    ClientSecrets = { new Secret("secret".Sha256()) },

                    AllowedGrantTypes = GrantTypes.ClientCredentials,
                    // scopes that client has access to
                    AllowedScopes = { "api1" }
                },
                
                // interactive ASP.NET Core MVC client
                new Client
                {
                    ClientId = "mvc",
                    ClientSecrets = { new Secret("secret".Sha256()) },

                    AllowedGrantTypes = GrantTypes.Code,
                    
                    // where to redirect to after login
                    RedirectUris = { "http://localhost:5002/signin-oidc" },

                    // where to redirect to after logout
                    PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },

                    AllowedScopes = new List<string>
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        "api1"
                    }
                }
            };

可以看到这里确实配置了ClientId为mvc的客户端信息.

(2)、检验传入的redirectUri和id4客户端配置的是否一致,并设置RedirectUri,如下代码:

        private async Task ValidateClientAsync(ValidatedAuthorizeRequest request)
        {
            //////////////////////////////////////////////////////////
            // check request object requirement
            //////////////////////////////////////////////////////////
            if (request.Client.RequireRequestObject)
            {
                if (!request.RequestObjectValues.Any())
                {
                    return Invalid(request, description: "Client must use request object, but no request or request_uri parameter present");
                }
            }

            //////////////////////////////////////////////////////////
            // redirect_uri must be present, and a valid uri
            //////////////////////////////////////////////////////////
            var redirectUri = request.Raw.Get(OidcConstants.AuthorizeRequest.RedirectUri);

            if (redirectUri.IsMissingOrTooLong(_options.InputLengthRestrictions.RedirectUri))
            {
                LogError("redirect_uri is missing or too long", request);
                return Invalid(request, description: "Invalid redirect_uri");
            }

            if (!Uri.TryCreate(redirectUri, UriKind.Absolute, out _))
            {
                LogError("malformed redirect_uri", redirectUri, request);
                return Invalid(request, description: "Invalid redirect_uri");
            }

            //////////////////////////////////////////////////////////
            // check if client protocol type is oidc
            //////////////////////////////////////////////////////////
            if (request.Client.ProtocolType != IdentityServerConstants.ProtocolTypes.OpenIdConnect)
            {
                LogError("Invalid protocol type for OIDC authorize endpoint", request.Client.ProtocolType, request);
                return Invalid(request, OidcConstants.AuthorizeErrors.UnauthorizedClient, description: "Invalid protocol");
            }

            //////////////////////////////////////////////////////////
            // check if redirect_uri is valid
            //////////////////////////////////////////////////////////
            if (await _uriValidator.IsRedirectUriValidAsync(redirectUri, request.Client) == false)
            {
                LogError("Invalid redirect_uri", redirectUri, request);
                return Invalid(request, OidcConstants.AuthorizeErrors.InvalidRequest, "Invalid redirect_uri");
            }

            request.RedirectUri = redirectUri;

            return Valid(request);
        }

 (3)、检验核心参数,代码如下:

        private AuthorizeRequestValidationResult ValidateCoreParameters(ValidatedAuthorizeRequest request)
        {
            //设置State值
            var state = request.Raw.Get(OidcConstants.AuthorizeRequest.State);
            if (state.IsPresent())
            {
                request.State = state;
            }

            //检验responseType长度
            var responseType = request.Raw.Get(OidcConstants.AuthorizeRequest.ResponseType);
            if (responseType.IsMissing())
            {
                LogError("Missing response_type", request);
                return Invalid(request, OidcConstants.AuthorizeErrors.UnsupportedResponseType, "Missing response_type");
            }

            // The responseType may come in in an unconventional order.
            // Use an IEqualityComparer that doesn't care about the order of multiple values.
            // Per https://tools.ietf.org/html/rfc6749#section-3.1.1 -
            // 'Extension response types MAY contain a space-delimited (%x20) list of
            // values, where the order of values does not matter (e.g., response
            // type "a b" is the same as "b a").'
            // http://openid.net/specs/oauth-v2-multiple-response-types-1_0-03.html#terminology -
            // 'If a response type contains one of more space characters (%20), it is compared
            // as a space-delimited list of values in which the order of values does not matter.'

            //判断responseType是否支持
            if (!Constants.SupportedResponseTypes.Contains(responseType, _responseTypeEqualityComparer))
            {
                LogError("Response type not supported", responseType, request);
                return Invalid(request, OidcConstants.AuthorizeErrors.UnsupportedResponseType, "Response type not supported");
            }

            // Even though the responseType may have come in in an unconventional order,
            // we still need the request's ResponseType property to be set to the
            // conventional, supported response type.

            //判断ResponseType是否支持
            request.ResponseType = Constants.SupportedResponseTypes.First(
                supportedResponseType => _responseTypeEqualityComparer.Equals(supportedResponseType, responseType));

            //////////////////////////////////////////////////////////
            // match response_type to grant type
            //////////////////////////////////////////////////////////

            //设置GrantType
            request.GrantType = Constants.ResponseTypeToGrantTypeMapping[request.ResponseType];

            // set default response mode for flow; this is needed for any client error processing below

            //设置ResponseMode
            request.ResponseMode = Constants.AllowedResponseModesForGrantType[request.GrantType].First();

            //////////////////////////////////////////////////////////
            // check if flow is allowed at authorize endpoint
            //////////////////////////////////////////////////////////

            //判断下GrantType是否支持
            if (!Constants.AllowedGrantTypesForAuthorizeEndpoint.Contains(request.GrantType))
            {
                LogError("Invalid grant type", request.GrantType, request);
                return Invalid(request, description: "Invalid response_type");
            }

            //////////////////////////////////////////////////////////
            // check if PKCE is required and validate parameters
            //////////////////////////////////////////////////////////
            
            //设置Pkce模式
            if (request.GrantType == GrantType.AuthorizationCode || request.GrantType == GrantType.Hybrid)
            {
                _logger.LogDebug("Checking for PKCE parameters");

                /////////////////////////////////////////////////////////////////////////////
                // validate code_challenge and code_challenge_method
                /////////////////////////////////////////////////////////////////////////////
                var proofKeyResult = ValidatePkceParameters(request);

                if (proofKeyResult.IsError)
                {
                    return proofKeyResult;
                }
            }

            //////////////////////////////////////////////////////////
            // check response_mode parameter and set response_mode
            //////////////////////////////////////////////////////////

            // check if response_mode parameter is present and valid

            //判断responseMode是否支持
            var responseMode = request.Raw.Get(OidcConstants.AuthorizeRequest.ResponseMode);
            if (responseMode.IsPresent())
            {
                if (Constants.SupportedResponseModes.Contains(responseMode))
                {
                    if (Constants.AllowedResponseModesForGrantType[request.GrantType].Contains(responseMode))
                    {
                        request.ResponseMode = responseMode;
                    }
                    else
                    {
                        LogError("Invalid response_mode for response_type", responseMode, request);
                        return Invalid(request, OidcConstants.AuthorizeErrors.InvalidRequest, description: "Invalid response_mode for response_type");
                    }
                }
                else
                {
                    LogError("Unsupported response_mode", responseMode, request);
                    return Invalid(request, OidcConstants.AuthorizeErrors.UnsupportedResponseType, description: "Invalid response_mode");
                }
            }


            //////////////////////////////////////////////////////////
            // check if grant type is allowed for client
            //////////////////////////////////////////////////////////

            //判断传入id4服务端配置的GrantType集合是否支持客户端传入的GrantType
            if (!request.Client.AllowedGrantTypes.Contains(request.GrantType))
            {
                LogError("Invalid grant type for client", request.GrantType, request);
                return Invalid(request, OidcConstants.AuthorizeErrors.UnauthorizedClient, "Invalid grant type for client");
            }

            //////////////////////////////////////////////////////////
            // check if response type contains an access token,
            // and if client is allowed to request access token via browser
            //////////////////////////////////////////////////////////
            
            //这里demo调用中不涉及
            var responseTypes = responseType.FromSpaceSeparatedString();
            if (responseTypes.Contains(OidcConstants.ResponseTypes.Token))
            {
                if (!request.Client.AllowAccessTokensViaBrowser)
                {
                    LogError("Client requested access token - but client is not configured to receive access tokens via browser", request);
                    return Invalid(request, description: "Client not configured to receive access tokens via browser");
                }
            }

            return Valid(request);
        }

这里注意下pkce模式,关于pkce模式前文中有介绍,其检验逻辑如下:

        private AuthorizeRequestValidationResult ValidatePkceParameters(ValidatedAuthorizeRequest request)
        {
            var fail = Invalid(request);

            //获取codeChallenge值
            var codeChallenge = request.Raw.Get(OidcConstants.AuthorizeRequest.CodeChallenge);
            if (codeChallenge.IsMissing())
            {
                if (request.Client.RequirePkce)
                {
                    LogError("code_challenge is missing", request);
                    fail.ErrorDescription = "code challenge required";
                }
                else
                {
                    _logger.LogDebug("No PKCE used.");
                    return Valid(request);
                }

                return fail;
            }

            //判断codeChallenge值长度是否合法
            if (codeChallenge.Length < _options.InputLengthRestrictions.CodeChallengeMinLength ||
                codeChallenge.Length > _options.InputLengthRestrictions.CodeChallengeMaxLength)
            {
                LogError("code_challenge is either too short or too long", request);
                fail.ErrorDescription = "Invalid code_challenge";
                return fail;
            }

            //设置CodeChallenge值
            request.CodeChallenge = codeChallenge;

            //获取pkce CodeChallenge值的加密方式
            var codeChallengeMethod = request.Raw.Get(OidcConstants.AuthorizeRequest.CodeChallengeMethod);
            if (codeChallengeMethod.IsMissing())
            {
                _logger.LogDebug("Missing code_challenge_method, defaulting to plain");
                codeChallengeMethod = OidcConstants.CodeChallengeMethods.Plain;
            }

            //判断CodeChallenge加密方式是否合法  除了sha256还支持plain
            if (!Constants.SupportedCodeChallengeMethods.Contains(codeChallengeMethod))
            {
                LogError("Unsupported code_challenge_method", codeChallengeMethod, request);
                fail.ErrorDescription = "Transform algorithm not supported";
                return fail;
            }

            // check if plain method is allowed
            if (codeChallengeMethod == OidcConstants.CodeChallengeMethods.Plain)
            {
                if (!request.Client.AllowPlainTextPkce)
                {
                    LogError("code_challenge_method of plain is not allowed", request);
                    fail.ErrorDescription = "Transform algorithm not supported";
                    return fail;
                }
            }

            //设置CodeChallenge的加密方法
            request.CodeChallengeMethod = codeChallengeMethod;

            return Valid(request);
        }

(4)、校验客户端传入scope 代码如下: 

        private async Task ValidateScopeAsync(ValidatedAuthorizeRequest request)
        {
            //////////////////////////////////////////////////////////
            // scope must be present
            //////////////////////////////////////////////////////////
            
            //scope非空检验
            var scope = request.Raw.Get(OidcConstants.AuthorizeRequest.Scope);
            if (scope.IsMissing())
            {
                LogError("scope is missing", request);
                return Invalid(request, description: "Invalid scope");
            }

            //scope长度检验
            if (scope.Length > _options.InputLengthRestrictions.Scope)
            {
                LogError("scopes too long.", request);
                return Invalid(request, description: "Invalid scope");
            }

            //写入scope信息
            request.RequestedScopes = scope.FromSpaceSeparatedString().Distinct().ToList();

            //如果客户端传入的scope中包含opid,那设置当前请求是openid请求,这里客户端调用oidc组件默认的设置的openid和profile
            if (request.RequestedScopes.Contains(IdentityServerConstants.StandardScopes.OpenId))
            {
                request.IsOpenIdRequest = true;
            }

            //////////////////////////////////////////////////////////
            // check scope vs response_type plausability
            //////////////////////////////////////////////////////////
            var requirement = Constants.ResponseTypeToScopeRequirement[request.ResponseType];
            if (requirement == Constants.ScopeRequirement.Identity ||
                requirement == Constants.ScopeRequirement.IdentityOnly)
            {
                if (request.IsOpenIdRequest == false)
                {
                    LogError("response_type requires the openid scope", request);
                    return Invalid(request, description: "Missing openid scope");
                }
            }

            //////////////////////////////////////////////////////////
            // check if scopes are valid/supported and check for resource scopes
            //////////////////////////////////////////////////////////
            
            //根据传入的scope和id4服务端配置的客户端的相关资源进行校验
            var validatedResources = await _resourceValidator.ValidateRequestedResourcesAsync(new ResourceValidationRequest
            {
                Client = request.Client,
                Scopes = request.RequestedScopes
            });

            if (!validatedResources.Succeeded)
            {
                return Invalid(request, OidcConstants.AuthorizeErrors.InvalidScope, "Invalid scope");
            }

            //如果id4服务端配置的客户端存在Identity Resource,那么调用客户端必须传递OpenId和profile的scope
            if (validatedResources.Resources.IdentityResources.Any() && !request.IsOpenIdRequest)
            {
                LogError("Identity related scope requests, but no openid scope", request);
                return Invalid(request, OidcConstants.AuthorizeErrors.InvalidScope, "Identity scopes requested, but openid scope is missing");
            }

            //如果scope中包含api scope,设置当前请求位apirescouce请求  
            if (validatedResources.Resources.ApiScopes.Any())
            {
                request.IsApiResourceRequest = true;
            }
                
            //////////////////////////////////////////////////////////
            // check id vs resource scopes and response types plausability
            //////////////////////////////////////////////////////////
            var responseTypeValidationCheck = true;
            switch (requirement)
            {
                case Constants.ScopeRequirement.Identity:
                    if (!validatedResources.Resources.IdentityResources.Any())
                    {
                        _logger.LogError("Requests for id_token response type must include identity scopes");
                        responseTypeValidationCheck = false;
                    }
                    break;
                case Constants.ScopeRequirement.IdentityOnly:
                    if (!validatedResources.Resources.IdentityResources.Any() || validatedResources.Resources.ApiScopes.Any())
                    {
                        _logger.LogError("Requests for id_token response type only must not include resource scopes");
                        responseTypeValidationCheck = false;
                    }
                    break;
                case Constants.ScopeRequirement.ResourceOnly:
                    if (validatedResources.Resources.IdentityResources.Any() || !validatedResources.Resources.ApiScopes.Any())
                    {
                        _logger.LogError("Requests for token response type only must include resource scopes, but no identity scopes.");
                        responseTypeValidationCheck = false;
                    }
                    break;
            }

            if (!responseTypeValidationCheck)
            {
                return Invalid(request, OidcConstants.AuthorizeErrors.InvalidScope, "Invalid scope for response type");
            }

            //设置通过校验的资源集合
            request.ValidatedResources = validatedResources;

            return Valid(request);
        }

 这里篇幅过长,分析一下几个核心的关键点

i、scope是可以在服务端进行拦截的,源码如下:

      public ParsedScopesResult ParseScopeValues(IEnumerable<string> scopeValues)
        {
            if (scopeValues == null) throw new ArgumentNullException(nameof(scopeValues));

            var result = new ParsedScopesResult();

            foreach (var scopeValue in scopeValues)
            {
                var ctx = new ParseScopeContext(scopeValue);

                //这里可以通过重写DefaultScopeParser的ParseScopeValue方法,来实现改变客户端传入scope的值
                ParseScopeValue(ctx);
                
                if (ctx.Succeeded)
                {
                    var parsedScope = ctx.ParsedName != null ?
                        new ParsedScopeValue(ctx.RawValue, ctx.ParsedName, ctx.ParsedParameter) :
                        new ParsedScopeValue(ctx.RawValue);

                    result.ParsedScopes.Add(parsedScope);
                }
                else if (!ctx.Ignore)
                {
                    result.Errors.Add(new ParsedScopeValidationError(scopeValue, ctx.Error));
                }
                else
                {
                    _logger.LogDebug("Scope parsing ignoring scope {scope}", scopeValue);
                }
            }

            return result;
        }

ii、client和其相关资源仓储可以在id4服务端重写,其加载逻辑就是通过客户端传入的scope去对应的仓储中查找,代码如下:

        public static async Task FindResourcesByScopeAsync(this IResourceStore store, IEnumerable<string> scopeNames)
        {
            //根据客户端传入的scope从store中查询配置的identityrescoce资源
            var identity = await store.FindIdentityResourcesByScopeNameAsync(scopeNames);
            //根据客户端传入的scope从store中查询配置的apiResources资源
            var apiResources = await store.FindApiResourcesByScopeNameAsync(scopeNames);
            //根据客户端传入的scope从store中查询配置的apiScope资源
            var scopes = await store.FindApiScopesByNameAsync(scopeNames);

            //校验identityrescoce资源、apiResources资源、apiScope资源
            Validate(identity, apiResources, scopes);

            
            var resources = new Resources(identity, apiResources, scopes)
            {
                //这个scope('offline_access')用于生成refreshtoken
                OfflineAccess = scopeNames.Contains(IdentityServerConstants.StandardScopes.OfflineAccess)
            };

            return resources;
        }

资源的核心校验逻辑如下:

private static void Validate(IEnumerable identity, IEnumerable apiResources, IEnumerable apiScopes)
        {
            // attempt to detect invalid configuration. this is about the only place
            // we can do this, since it's hard to get the values in the store.

            //判断identity scope是否存在重复
            var identityScopeNames = identity.Select(x => x.Name).ToArray();
            var dups = GetDuplicates(identityScopeNames);
            if (dups.Any())
            {
                var names = dups.Aggregate((x, y) => x + ", " + y);
                throw new Exception(
                    $"Duplicate identity scopes found. This is an invalid configuration. Use different names for identity scopes. Scopes found: {names}");
            }

            //判断api rescouce是否存在重复
            var apiNames = apiResources.Select(x => x.Name);
            dups = GetDuplicates(apiNames);
            if (dups.Any())
            {
                var names = dups.Aggregate((x, y) => x + ", " + y);
                throw new Exception(
                    $"Duplicate api resources found. This is an invalid configuration. Use different names for API resources. Names found: {names}");
            }
            
            //判断api scope是否存在重复的
            var scopesNames = apiScopes.Select(x => x.Name);
            dups = GetDuplicates(scopesNames);
            if (dups.Any())
            {
                var names = dups.Aggregate((x, y) => x + ", " + y);
                throw new Exception(
                    $"Duplicate scopes found. This is an invalid configuration. Use different names for scopes. Names found: {names}");
            }

            //判断identity相关的scope和api相关的scope是否有交集
            var overlap = identityScopeNames.Intersect(scopesNames).ToArray();
            if (overlap.Any())
            {
                var names = overlap.Aggregate((x, y) => x + ", " + y);
                throw new Exception(
                    $"Found identity scopes and API scopes that use the same names. This is an invalid configuration. Use different names for identity scopes and API scopes. Scopes found: {names}");
            }
        }

iii、在从仓储中读取到相关资源后,会将相关值写入到ResourceValidationResult实例,后续的操作将不会在查询仓储,而是总内存判断

v、根据客户端传入的scope判断id4服务端配置客户端是否支持,如果支持写入ResourceValidationResult实例,不支持写入ResourceValidationResult实例的InvalidScopes,源码如下:

        protected virtual async Task ValidateScopeAsync(
            Client client, 
            Resources resourcesFromStore, 
            ParsedScopeValue requestedScope, 
            ResourceValidationResult result)
        {
            //refreshtoken scope特殊处理
            if (requestedScope.ParsedName == IdentityServerConstants.StandardScopes.OfflineAccess)
            {
                if (await IsClientAllowedOfflineAccessAsync(client))
                {
                    result.Resources.OfflineAccess = true;
                    result.ParsedScopes.Add(new ParsedScopeValue(IdentityServerConstants.StandardScopes.OfflineAccess));
                }
                else
                {
                    result.InvalidScopes.Add(IdentityServerConstants.StandardScopes.OfflineAccess);
                }
            }
            else
            {
                //根据客户端传入的scope从resources实例中的identityrescoce资源
                var identity = resourcesFromStore.FindIdentityResourcesByScope(requestedScope.ParsedName);
                if (identity != null)
                {
                    //判断id4服务端配置的客户端是否配置了identityrescoce资源
                    if (await IsClientAllowedIdentityResourceAsync(client, identity))
                    {
                        result.ParsedScopes.Add(requestedScope);
                        result.Resources.IdentityResources.Add(identity);
                    }
                    else
                    {
                        result.InvalidScopes.Add(requestedScope.RawValue);
                    }
                }
                else
                {
                    //根据客户端传入的scope从resources实例中的identityrescoce资源
                    var apiScope = resourcesFromStore.FindApiScope(requestedScope.ParsedName);
                    if (apiScope != null)
                    {
                        //判断id4服务端配置的客户端是否配置了ApiScope资源
                        if (await IsClientAllowedApiScopeAsync(client, apiScope))
                        {
                            result.ParsedScopes.Add(requestedScope);
                            result.Resources.ApiScopes.Add(apiScope);

                            //根据客户端传入的scope从resources实例中的是否存在关联的scope资源,如果apiresource配置了向result写入ApiResources
                            var apis = resourcesFromStore.FindApiResourcesByScope(apiScope.Name);
                            foreach (var api in apis)
                            {
                                result.Resources.ApiResources.Add(api);
                            }
                        }
                        else
                        {
                            result.InvalidScopes.Add(requestedScope.RawValue);
                        }
                    }
                    else
                    {
                        _logger.LogError("Scope {scope} not found in store.", requestedScope.ParsedName);
                        result.InvalidScopes.Add(requestedScope.RawValue);
                    }
                }
            }
        }

(5)、校验可选参数

这里暂时不做分析

(6)、构造了CustomAuthorizeRequestValidationContext实例传入通过ValidatedAuthorizeRequest实例,给外部自定义修改,代码如下:

            var context = new CustomAuthorizeRequestValidationContext
            {
                Result = new AuthorizeRequestValidationResult(request)
            };
            await _customValidator.ValidateAsync(context);

这里就可以实现ICustomAuthorizeRequestValidator来重写ValidateAsync方法,来实现定制化.

(7)、返回最终的ValidatedAuthorizeRequest实例

ok,在经过上述一系列操作之后,获取到了一个最终的ValidatedAuthorizeRequest实例,接着执行如下代码:

            var interactionResult = await _interactionGenerator.ProcessInteractionAsync(request, consent);
            if (interactionResult.IsError)
            {
                return await CreateErrorResultAsync("Interaction generator error", request, interactionResult.Error, interactionResult.ErrorDescription, false);
            }
            if (interactionResult.IsLogin)
            {
                return new LoginPageResult(request);
            }
            if (interactionResult.IsConsent)
            {
                return new ConsentPageResult(request);
            }
            if (interactionResult.IsRedirect)
            {
                return new CustomRedirectResult(request, interactionResult.RedirectUrl);
            }

            var response = await _authorizeResponseGenerator.CreateResponseAsync(request);

            await RaiseResponseEventAsync(response);

            LogResponse(response);

            return new AuthorizeResult(response);

很明显,到这里开始处理客户端 的交互了,看一下IAuthorizeInteractionResponseGenerator的默认实现的ProcessInteractionAsync方法,代码如下:

        public virtual async Task ProcessInteractionAsync(ValidatedAuthorizeRequest request, ConsentResponse consent = null)
        {
            Logger.LogTrace("ProcessInteractionAsync");

            //处理需要用户点击授权相关 demo第一次登录调用不会触发
            if (consent != null && consent.Granted == false && consent.Error.HasValue && request.Subject.IsAuthenticated() == false)
            {
                // special case when anonymous user has issued an error prior to authenticating
                Logger.LogInformation("Error: User consent result: {error}", consent.Error);

                var error = consent.Error switch
                {
                    AuthorizationError.AccountSelectionRequired => OidcConstants.AuthorizeErrors.AccountSelectionRequired,
                    AuthorizationError.ConsentRequired => OidcConstants.AuthorizeErrors.ConsentRequired,
                    AuthorizationError.InteractionRequired => OidcConstants.AuthorizeErrors.InteractionRequired,
                    AuthorizationError.LoginRequired => OidcConstants.AuthorizeErrors.LoginRequired,
                    _ => OidcConstants.AuthorizeErrors.AccessDenied
                };
                
                return new InteractionResponse
                {
                    Error = error,
                    ErrorDescription = consent.ErrorDescription
                };
            }

            //处理登录
            var result = await ProcessLoginAsync(request);
            
            if (!result.IsLogin && !result.IsError && !result.IsRedirect)
            {
                result = await ProcessConsentAsync(request, consent);
            }

            //如果判断到结果是需要登录或者需要用户授权或者需要跳转 且请求的prompt设置为none的话(代表不需要展示客户端ui)  报错
            if ((result.IsLogin || result.IsConsent || result.IsRedirect) && request.PromptModes.Contains(OidcConstants.PromptModes.None))
            {
                // prompt=none means do not show the UI
                Logger.LogInformation("Changing response to LoginRequired: prompt=none was requested");
                result = new InteractionResponse
                {
                    Error = result.IsLogin ? OidcConstants.AuthorizeErrors.LoginRequired :
                                result.IsConsent ? OidcConstants.AuthorizeErrors.ConsentRequired : 
                                    OidcConstants.AuthorizeErrors.InteractionRequired
                };
            }

            return result;
        }

下面是决定交互行为的逻辑,代码如下:

        protected internal virtual async Task ProcessLoginAsync(ValidatedAuthorizeRequest request)
        {
            if (request.PromptModes.Contains(OidcConstants.PromptModes.Login) ||
                request.PromptModes.Contains(OidcConstants.PromptModes.SelectAccount))
            {
                Logger.LogInformation("Showing login: request contains prompt={0}", request.PromptModes.ToSpaceSeparatedString());

                // remove prompt so when we redirect back in from login page
                // we won't think we need to force a prompt again
                request.RemovePrompt();
                
                return new InteractionResponse { IsLogin = true };
            }

            // unauthenticated user
            //判断是否处于认证成功之后的状态
            var isAuthenticated = request.Subject.IsAuthenticated();
            
            // user de-activated
            bool isActive = false;

            //如果当前用户已经认证成功
            if (isAuthenticated)
            {
                //判断当前用户是否处于活跃状态
                var isActiveCtx = new IsActiveContext(request.Subject, request.Client, IdentityServerConstants.ProfileIsActiveCallers.AuthorizeEndpoint);
                await Profile.IsActiveAsync(isActiveCtx);
                
                isActive = isActiveCtx.IsActive;
            }

            //如果不是认证成功状态或者不处于活跃状态
            if (!isAuthenticated || !isActive)
            {
                if (!isAuthenticated)
                {
                    Logger.LogInformation("Showing login: User is not authenticated");
                }
                else if (!isActive)
                {
                    Logger.LogInformation("Showing login: User is not active");
                }

                //返回交互返回值,需要登陆了
                return new InteractionResponse { IsLogin = true };
            }

            // check current idp
            var currentIdp = request.Subject.GetIdentityProvider();

            // check if idp login hint matches current provider
            var idp = request.GetIdP();
            if (idp.IsPresent())
            {
                if (idp != currentIdp)
                {
                    Logger.LogInformation("Showing login: Current IdP ({currentIdp}) is not the requested IdP ({idp})", currentIdp, idp);
                    return new InteractionResponse { IsLogin = true };
                }
            }

            // check authentication freshness
            if (request.MaxAge.HasValue)
            {
                var authTime = request.Subject.GetAuthenticationTime();
                if (Clock.UtcNow > authTime.AddSeconds(request.MaxAge.Value))
                {
                    Logger.LogInformation("Showing login: Requested MaxAge exceeded.");

                    return new InteractionResponse { IsLogin = true };
                }
            }

            // check local idp restrictions
            if (currentIdp == IdentityServerConstants.LocalIdentityProvider)
            {
                if (!request.Client.EnableLocalLogin)
                {
                    Logger.LogInformation("Showing login: User logged in locally, but client does not allow local logins");
                    return new InteractionResponse { IsLogin = true };
                }
            }
            // check external idp restrictions if user not using local idp
            else if (request.Client.IdentityProviderRestrictions != null && 
                request.Client.IdentityProviderRestrictions.Any() &&
                !request.Client.IdentityProviderRestrictions.Contains(currentIdp))
            {
                Logger.LogInformation("Showing login: User is logged in with idp: {idp}, but idp not in client restriction list.", currentIdp);
                return new InteractionResponse { IsLogin = true };
            }

            // check client's user SSO timeout
            if (request.Client.UserSsoLifetime.HasValue)
            {
                var authTimeEpoch = request.Subject.GetAuthenticationTimeEpoch();
                var nowEpoch = Clock.UtcNow.ToUnixTimeSeconds();

                var diff = nowEpoch - authTimeEpoch;
                if (diff > request.Client.UserSsoLifetime.Value)
                {
                    Logger.LogInformation("Showing login: User's auth session duration: {sessionDuration} exceeds client's user SSO lifetime: {userSsoLifetime}.", diff, request.Client.UserSsoLifetime);
                    return new InteractionResponse { IsLogin = true };
                }
            }

            return new InteractionResponse();
        }

第一次调用,必然走这个返回值,代码如下:

           //如果不是认证成功状态或者不处于活跃状态
            if (!isAuthenticated || !isActive)
            {
                if (!isAuthenticated)
                {
                    Logger.LogInformation("Showing login: User is not authenticated");
                }
                else if (!isActive)
                {
                    Logger.LogInformation("Showing login: User is not active");
                }

                //返回交互返回值,需要登陆了
                return new InteractionResponse { IsLogin = true };
            }

最后的结果返回值如下:

 接着执行如下代码:

            if (interactionResult.IsLogin)
            {
                return new LoginPageResult(request);
            }
            if (interactionResult.IsConsent)
            {
                return new ConsentPageResult(request);
            }
            if (interactionResult.IsRedirect)
            {
                return new CustomRedirectResult(request, interactionResult.RedirectUrl);
            }

            var response = await _authorizeResponseGenerator.CreateResponseAsync(request);

            await RaiseResponseEventAsync(response);

            LogResponse(response);

            return new AuthorizeResult(response);

返回LoginPageResult,id4的设计是url通过特定的终结点处理,然后,执行终结点返回值的ExecuteAsync方法,下面解析下源码,如下:

        public async Task ExecuteAsync(HttpContext context)
        {
            Init(context);

            //从上下文的Items属性中获取key为idsvr:IdentityServerBasePath的value 作为id4服务的根路径  加上 connect/authorize/callback 作为returnurl的值(不包含querystring)
            var returnUrl = context.GetIdentityServerBasePath().EnsureTrailingSlash() + Constants.ProtocolRoutePaths.AuthorizeCallback;
            if (_authorizationParametersMessageStore != null)
            {
                var msg = new Messagestring, string[]>>(_request.Raw.ToFullDictionary());
                var id = await _authorizationParametersMessageStore.WriteAsync(msg);
                returnUrl = returnUrl.AddQueryString(Constants.AuthorizationParamsStore.MessageStoreIdParameterName, id);
            }
            else
            {
                //并且returanurl的querystring 值就是客户端oidc组件根据配置参数和id4服务拉取的配置参数生成OpenIdConnectMessage实例值
                returnUrl = returnUrl.AddQueryString(_request.Raw.ToQueryString());
            }

            //获取id4服务配置的登录url
            var loginUrl = _options.UserInteraction.LoginUrl;
            //存在不是本地url的情况,访问其他认证server的情况
            if (!loginUrl.IsLocalUrl())
            {
                // this converts the relative redirect path to an absolute one if we're 
                // redirecting to a different server
                returnUrl = context.GetIdentityServerHost().EnsureTrailingSlash() + returnUrl.RemoveLeadingSlash();
            }

            //Account/Login?ReturnUrl=id4服务的根路径/connect/authorize/callback?客户端oidc组件根据配置参数和id4服务拉取的配置参数生成OpenIdConnectMessage实例值组成的querystring
            var url = loginUrl.AddQueryString(_options.UserInteraction.LoginReturnUrlParameter, returnUrl);
            
            //跳转到id4服务的登录页
            context.Response.RedirectToAbsoluteUrl(url);
        }

 ok,到这里就跳转去了id4服务的登录功能.

相关