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

前文.Net 5.0 通过IdentityServer4实现单点登录之oidc认证部分源码解析介绍了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 = "<insert here>";
                    options.ClientSecret = "<insert here>";
                })
                .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<CookiePolicyOptions>(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<IEndpointResult> 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 IEnumerable<KeyValuePair<string, 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<IdentityServerOptions>();
            //获取配置中的认证方案,如果配置了,则采用自定义的认证方案
            if (options.Authentication.CookieAuthenticationScheme != null)
            {
                return options.Authentication.CookieAuthenticationScheme;
            }

            //这里默认时名称为idsrv的Cookie认证方案
            var schemes = context.RequestServices.GetRequiredService<IAuthenticationSchemeProvider>();
            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<IConfigureOptions<CookieAuthenticationOptions>, ConfigureInternalCookieOptions>();
            builder.Services.AddSingleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureInternalCookieOptions>();
            builder.Services.AddTransientDecorator<IAuthenticationService, IdentityServerAuthenticationService>();
            builder.Services.AddTransientDecorator<IAuthenticationHandlerProvider, FederatedSignoutAuthenticationHandlerProvider>();

            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<IAuthenticationHandler> 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<IEndpointResult> 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<AuthorizeRequestValidationResult> 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<AuthorizeRequestValidationResult> 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<Client> Clients =>
            new List<Client>
            {
                // 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<AuthorizeRequestValidationResult> 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<AuthorizeRequestValidationResult> 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<Resources> 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<IdentityResource> identity, IEnumerable<ApiResource> apiResources, IEnumerable<ApiScope> 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<InteractionResponse> 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<InteractionResponse> 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 Message<IDictionary<string, 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,到这里就带着生成ReturnUrl跳转去了id4服务的登录功能.

接着解析下下id4服务的登录控制器试图如下步骤:

i、第一步校验ReturnUrl,源码如下:

 public bool IsValidReturnUrl(string returnUrl)
        {
            if (returnUrl.IsLocalUrl())
            {
                var index = returnUrl.IndexOf('?');
                if (index >= 0)
                {
                    returnUrl = returnUrl.Substring(0, index);
                }

                if (returnUrl.EndsWith(Constants.ProtocolRoutePaths.Authorize, StringComparison.Ordinal) ||
                    returnUrl.EndsWith(Constants.ProtocolRoutePaths.AuthorizeCallback, StringComparison.Ordinal))
                {
                    _logger.LogTrace("returnUrl is valid");
                    return true;
                }
            }

            _logger.LogTrace("returnUrl is not valid");
            return false;
        }

这里首先校验下是不是本地url,接着校验url的主体部分是不是connect/authorize或者connect/authorize/callback oidc的returnurl只能是二者

ii、接着还是从url中解析出客户端传递的OpenIdConnectMessage实例值

                var parameters = returnUrl.ReadQueryStringAsNameValueCollection();
                if (_authorizationParametersMessageStore != null)
                {
                    var messageStoreId = parameters[Constants.AuthorizationParamsStore.MessageStoreIdParameterName];
                    var entry = await _authorizationParametersMessageStore.ReadAsync(messageStoreId);
                    parameters = entry?.Data.FromFullDictionary() ?? new NameValueCollection();
                }

这里注意_authorizationParametersMessageStore,校验,这段代码说明客户端OpenIdConnectMessage实例值是可以写入向redis这种的缓存的,而客户端只需要传递缓存之后,向OpenIdConnectMessage实例值的authzId从而服务端根据这个authzId值从redis中获取对应的oidc的参数值.

iii、还是校验下用户有没有登录和检验参数的过程和上面一样,代码如下:

//根据id4默认配置的认证的cookie认证方案中获取用户信息
                var user = await _userSession.GetUserAsync();
                var result = await _validator.ValidateAsync(parameters, user);
                if (!result.IsError)
                {
                    _logger.LogTrace("AuthorizationRequest being returned");
                    return new AuthorizationRequest(result.ValidatedRequest);
                }

应为到这里没有触发过登录方法,所以这里依然为空.

demo中其他的一些比如google登录这里暂时不说了

ok,登录页面渲染完成了.

接着看登录逻辑.

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Login(LoginInputModel model, string button)
        {
            //校验渲染登录视图时设置到页面隐藏域中的ReturnUrl值和渲染登录试图的检验逻辑一样
            var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);if (ModelState.IsValid)
            {
                //校验用户名密码
                if (_users.ValidateCredentials(model.Username, model.Password))
                {
                    var user = _users.FindByUsername(model.Username);
                    await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username, clientId: context?.Client.ClientId));

                    AuthenticationProperties props = null;

                    //认证成功,设置了记住登录
                    if (AccountOptions.AllowRememberLogin && model.RememberLogin)
                    {
                        //持久化登录信息
                        props = new AuthenticationProperties
                        {
                            IsPersistent = true,
                            ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
                        };
                    };

                    // 生成登录用户
                    var isuser = new IdentityServerUser(user.SubjectId)
                    {
                        DisplayName = user.Username
                    };

                    //登录
                    await HttpContext.SignInAsync(isuser, props);

                    if (context != null)
                    {
                        //如果是本地客户端(id4和客户端集成到一起的情况)  检验条件是判断RedirectUri前缀是否已http或者https开头
                        if (context.IsNativeClient())
                        {
                            // The client is native, so this change in how to
                            // return the response is for better UX for the end user.
                            return this.LoadingPage("Redirect", model.ReturnUrl);
                        }

                        //重定向到/connect/authorize/callback的url
                        return Redirect(model.ReturnUrl);
                    }
                }

                await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials", clientId:context?.Client.ClientId));
                ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage);
            }

            // something went wrong, show form with error
            var vm = await BuildLoginViewModelAsync(model);
            return View(vm);
        }

第一步校验ReturnUrl值,本质还是校验客户端OpenIdConnectMessage实例值

第二步校验登录名密码,验证通过,写入cookie,通过如下代码:

await HttpContext.SignInAsync(isuser, props);

生成Principal的源码如下:

        public ClaimsPrincipal CreatePrincipal()
        {
            //向claims写入用户id
            if (SubjectId.IsMissing()) throw new ArgumentException("SubjectId is mandatory", nameof(SubjectId));
            var claims = new List<Claim> { new Claim(JwtClaimTypes.Subject, SubjectId) };

            //向claims写入用户名
            if (DisplayName.IsPresent())
            {
                claims.Add(new Claim(JwtClaimTypes.Name, DisplayName));
            }

            //向claims写入身份提供商,如果id4服务和identity部署在一台服务器上,那么默认时local
            if (IdentityProvider.IsPresent())
            {
                claims.Add(new Claim(JwtClaimTypes.IdentityProvider, IdentityProvider));
            }

            //向claims写入认证通过的时间
            if (AuthenticationTime.HasValue)
            {
                claims.Add(new Claim(JwtClaimTypes.AuthenticationTime, new DateTimeOffset(AuthenticationTime.Value).ToUnixTimeSeconds().ToString()));
            }

            //向claims写入认证方式
            if (AuthenticationMethods.Any())
            {
                foreach (var amr in AuthenticationMethods)
                {
                    claims.Add(new Claim(JwtClaimTypes.AuthenticationMethod, amr));
                }
            }

            claims.AddRange(AdditionalClaims);

            var id = new ClaimsIdentity(claims.Distinct(new ClaimComparer()), Constants.IdentityServerAuthenticationType, JwtClaimTypes.Name, JwtClaimTypes.Role);
            return new ClaimsPrincipal(id);
        }

demo中只写入了用户id和用户名,这里还要注意的时,在引入id4服务的默认实现后,前文说过,id4服务端通过巧妙的di版本的装饰者对cookie认证做了重写.源码如下:

        internal static void AddDecorator<TService>(this IServiceCollection services)
        {
            //找到service中默认的传入TService的实现
            var registration = services.LastOrDefault(x => x.ServiceType == typeof(TService));
            if (registration == null)
            {
                throw new InvalidOperationException("Service type: " + typeof(TService).Name + " not registered.");
            }
            if (services.Any(x => x.ServiceType == typeof(Decorator<TService>)))
            {
                throw new InvalidOperationException("Decorator already registered for type: " + typeof(TService).Name + ".");
            }

            //移除原有的
            services.Remove(registration);

            if (registration.ImplementationInstance != null)
            {
                var type = registration.ImplementationInstance.GetType();
                var innerType = typeof(Decorator<,>).MakeGenericType(typeof(TService), type);
                services.Add(new ServiceDescriptor(typeof(Decorator<TService>), innerType, ServiceLifetime.Transient));
                services.Add(new ServiceDescriptor(type, registration.ImplementationInstance));
            }
            else if (registration.ImplementationFactory != null)
            {
                services.Add(new ServiceDescriptor(typeof(Decorator<TService>), provider =>
                {
                    return new DisposableDecorator<TService>((TService)registration.ImplementationFactory(provider));
                }, registration.Lifetime));
            }
            else
            {
                //通过Decorator<TService,TService的原有实现类型> 注册为Decorator<TService> 写入DI中
                var type = registration.ImplementationType;
                var innerType = typeof(Decorator<,>).MakeGenericType(typeof(TService), registration.ImplementationType);
                services.Add(new ServiceDescriptor(typeof(Decorator<TService>), innerType, ServiceLifetime.Transient));

                //写入默认实现,要不然Decorator<,>构造器拿不到实现
                services.Add(new ServiceDescriptor(type, type, registration.Lifetime));
            }
        }

添加了一些默认Claim,如下:

        private void AugmentMissingClaims(ClaimsPrincipal principal, DateTime authTime)
        {
            var identity = principal.Identities.First();

            // ASP.NET Identity issues this claim type and uses the authentication middleware name
            // such as "Google" for the value. this code is trying to correct/convert that for
            // our scenario. IOW, we take their old AuthenticationMethod value of "Google"
            // and issue it as the idp claim. we then also issue a amr with "external"
            var amr = identity.FindFirst(ClaimTypes.AuthenticationMethod);
            if (amr != null &&
                identity.FindFirst(JwtClaimTypes.IdentityProvider) == null &&
                identity.FindFirst(JwtClaimTypes.AuthenticationMethod) == null)
            {
                _logger.LogDebug("Removing amr claim with value: {value}", amr.Value);
                identity.RemoveClaim(amr);

                _logger.LogDebug("Adding idp claim with value: {value}", amr.Value);
                identity.AddClaim(new Claim(JwtClaimTypes.IdentityProvider, amr.Value));

                _logger.LogDebug("Adding amr claim with value: {value}", Constants.ExternalAuthenticationMethod);
                identity.AddClaim(new Claim(JwtClaimTypes.AuthenticationMethod, Constants.ExternalAuthenticationMethod));
            }

            //如果在认证端没有写入IdentityProvider(idp)身份提供端,id4服务默认向Cliam添加idp为local的值 代表本地提供身份
            if (identity.FindFirst(JwtClaimTypes.IdentityProvider) == null)
            {
                _logger.LogDebug("Adding idp claim with value: {value}", IdentityServerConstants.LocalIdentityProvider);
                identity.AddClaim(new Claim(JwtClaimTypes.IdentityProvider, IdentityServerConstants.LocalIdentityProvider));
            }

            //如果在认证端没有写入AuthenticationMethod(amr)身份认证方式,id4服务默认向Cliam添加amr为local的值 代表本地提供身份认证
            if (identity.FindFirst(JwtClaimTypes.AuthenticationMethod) == null)
            {
                if (identity.FindFirst(JwtClaimTypes.IdentityProvider).Value == IdentityServerConstants.LocalIdentityProvider)
                {
                    _logger.LogDebug("Adding amr claim with value: {value}", OidcConstants.AuthenticationMethods.Password);
                    identity.AddClaim(new Claim(JwtClaimTypes.AuthenticationMethod, OidcConstants.AuthenticationMethods.Password));
                }
                else
                {
                    _logger.LogDebug("Adding amr claim with value: {value}", Constants.ExternalAuthenticationMethod);
                    identity.AddClaim(new Claim(JwtClaimTypes.AuthenticationMethod, Constants.ExternalAuthenticationMethod));
                }
            }

            //如果在认证端没有写入AuthenticationTime(auth_time)身份认证通过时间,id4服务默认向Cliam添加auth_time为当前时间戳的值
            if (identity.FindFirst(JwtClaimTypes.AuthenticationTime) == null)
            {
                var time = new DateTimeOffset(authTime).ToUnixTimeSeconds().ToString();

                _logger.LogDebug("Adding auth_time claim with value: {value}", time);
                identity.AddClaim(new Claim(JwtClaimTypes.AuthenticationTime, time, ClaimValueTypes.Integer64));
            }
        }

claim详情如下:

写入cookie源码,如不明白请参考.Net Core 3.0认证组件之Cookie认证组件解析源码如下:

protected async override Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties? properties)
        {
            if (user == null)
            {
                throw new ArgumentNullException(nameof(user));
            }

            properties = properties ?? new AuthenticationProperties();

            _signInCalled = true;

            // Process the request cookie to initialize members like _sessionKey.
            await EnsureCookieTicket();
            var cookieOptions = BuildCookieOptions();

            var signInContext = new CookieSigningInContext(
                Context,
                Scheme,
                Options,
                user,
                properties,
                cookieOptions);

            DateTimeOffset issuedUtc;
            if (signInContext.Properties.IssuedUtc.HasValue)
            {
                issuedUtc = signInContext.Properties.IssuedUtc.Value;
            }
            else
            {
                issuedUtc = Clock.UtcNow;
                signInContext.Properties.IssuedUtc = issuedUtc;
            }

            if (!signInContext.Properties.ExpiresUtc.HasValue)
            {
                signInContext.Properties.ExpiresUtc = issuedUtc.Add(Options.ExpireTimeSpan);
            }

            await Events.SigningIn(signInContext);

            if (signInContext.Properties.IsPersistent)
            {
                var expiresUtc = signInContext.Properties.ExpiresUtc ?? issuedUtc.Add(Options.ExpireTimeSpan);
                signInContext.CookieOptions.Expires = expiresUtc.ToUniversalTime();
            }

            var ticket = new AuthenticationTicket(signInContext.Principal!, signInContext.Properties, signInContext.Scheme.Name);

            if (Options.SessionStore != null)
            {
                if (_sessionKey != null)
                {
                    // Renew the ticket in cases of multiple requests see: https://github.com/dotnet/aspnetcore/issues/22135
                    await Options.SessionStore.RenewAsync(_sessionKey, ticket);
                }
                else
                {
                    _sessionKey = await Options.SessionStore.StoreAsync(ticket);
                }

                var principal = new ClaimsPrincipal(
                    new ClaimsIdentity(
                        new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) },
                        Options.ClaimsIssuer));
                ticket = new AuthenticationTicket(principal, null, Scheme.Name);
            }

            var cookieValue = Options.TicketDataFormat.Protect(ticket, GetTlsTokenBinding());

            Options.CookieManager.AppendResponseCookie(
                Context,
                Options.Cookie.Name!,
                cookieValue,
                signInContext.CookieOptions);

            var signedInContext = new CookieSignedInContext(
                Context,
                Scheme,
                signInContext.Principal!,
                signInContext.Properties,
                Options);

            await Events.SignedIn(signedInContext);

            // Only redirect on the login path
            var shouldRedirect = Options.LoginPath.HasValue && OriginalPath == Options.LoginPath;
            await ApplyHeaders(shouldRedirect, signedInContext.Properties);

            Logger.AuthenticationSchemeSignedIn(Scheme.Name);
        }

这里RemeberMe的逻辑如下:

            if (signInContext.Properties.IsPersistent)
            {
                var expiresUtc = signInContext.Properties.ExpiresUtc ?? issuedUtc.Add(Options.ExpireTimeSpan);
                signInContext.CookieOptions.Expires = expiresUtc.ToUniversalTime();
            }

 

第三步 在id4端写入cookie之后,重定向到id4 /connect/authorize/callback终结点,并带着客户端OpenIdConnectMessage实例值转换成的querystring.

ok,接着分析/connect/authorize/callback终结点源码如下:

        public override async Task<IEndpointResult> ProcessAsync(HttpContext context)
        {
            if (!HttpMethods.IsGet(context.Request.Method))
            {
                Logger.LogWarning("Invalid HTTP method for authorize endpoint.");
                return new StatusCodeResult(HttpStatusCode.MethodNotAllowed);
            }

            Logger.LogDebug("Start authorize callback request");

            //从url中解析出parameters(客户端OpenIdConnectMessage实例值)
            var parameters = context.Request.Query.AsNameValueCollection();
            //客户端OpenIdConnectMessage实例值是可以被写入到如redis等缓存中的(客户端只需要传一个authId,缓存的key)
            if (_authorizationParametersMessageStore != null)
            {
                //从缓存中读取OpenIdConnectMessage实例值
                var messageStoreId = parameters[Constants.AuthorizationParamsStore.MessageStoreIdParameterName];
                var entry = await _authorizationParametersMessageStore.ReadAsync(messageStoreId);
                parameters = entry?.Data.FromFullDictionary() ?? new NameValueCollection();

                await _authorizationParametersMessageStore.DeleteAsync(messageStoreId);
            }

            //从cookie中读取用户信息
            var user = await UserSession.GetUserAsync();

            //授权相关demo中不涉及后续介绍 这里consent为null
            var consentRequest = new ConsentRequest(parameters, user?.GetSubjectId());
            var consent = await _consentResponseStore.ReadAsync(consentRequest.Id);

            if (consent != null && consent.Data == null)
            {
                return await CreateErrorResultAsync("consent message is missing data");
            }

            try
            {
                //处理授权请求
                var result = await ProcessAuthorizeRequestAsync(parameters, user, consent?.Data);

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

                return result;
            }
            finally
            {
                if (consent != null)
                {
                    await _consentResponseStore.DeleteAsync(consentRequest.Id);
                }
            }
        }

ok,源码中做了相关注释,这里开始解析授权请求相关源码

第一步:校验参数 应为有大量源码前面解析过,所以说下大致流程

1、检验客户端信息是否合法

2、检验客户端传递的redirect_uri是否合法(常规校验和判断id4的客户端设置的redirect_uri和客户端本身的是否一直)

3、设置State值、检验responseType长度、判断responseType是否支持、判断ResponseType是否支持、设置GrantType、设置ResponseMode、判断下GrantType是否支持、设置Pkce模式相关参数、判断responseMode是否支持、判断传入id4服务端配置的GrantType集合是否支持客户端传入的GrantType

4、校验scope,并设置

5、校验可选参数和之前不一样的是,应为当前流程用户已认证成功,会设置SessionId,源码如下:

            if (_options.Endpoints.EnableCheckSessionEndpoint)
            {
                if (request.Subject.IsAuthenticated())
                {
                    var sessionId = await _userSession.GetSessionIdAsync();
                    if (sessionId.IsPresent())
                    {
                        request.SessionId = sessionId;
                    }
                    else
                    {
                        LogError("Check session endpoint enabled, but SessionId is missing", request);
                    }
                }
                else
                {
                    //当前用户如果没有认证成功,清空sessionid
                    request.SessionId = ""; // empty string for anonymous users
                }
            }

 

第二步:处理接下去该走什么流程了,这里和上面的区别在于,当前用户已经认证成功,所以会触发判断用户是否处于活跃状态的流程,源码如下:

            //判断是否处于认证成功之后的状态
            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;
            }

默认实现是返回true,如果不自定义,那么只要登录成功且没有过期的用户,一直处于活跃状态.客户端也可以配置sso的生命周期源码如下:

      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 };
                }
            }

第三步:生成code,源码如下:

        public virtual async Task<AuthorizeResponse> CreateResponseAsync(ValidatedAuthorizeRequest request)
        {
            if (request.GrantType == GrantType.AuthorizationCode)
            {
                return await CreateCodeFlowResponseAsync(request);
            }
            if (request.GrantType == GrantType.Implicit)
            {
                return await CreateImplicitFlowResponseAsync(request);
            }
            if (request.GrantType == GrantType.Hybrid)
            {
                return await CreateHybridFlowResponseAsync(request);
            }

            Logger.LogError("Unsupported grant type: " + request.GrantType);
            throw new InvalidOperationException("invalid grant type: " + request.GrantType);
        }

 demo中采用的是AuthorizationCode模式,分析下源码

1、创建code,源码如下:

protected virtual async Task<AuthorizationCode> CreateCodeAsync(ValidatedAuthorizeRequest request)
        {
            string stateHash = null;
            //加密state,通过id4服务端默认加密服务(公私钥加密)
            if (request.State.IsPresent())
            {
                var credential = await KeyMaterialService.GetSigningCredentialsAsync(request.Client.AllowedIdentityTokenSigningAlgorithms);
                if (credential == null)
                {
                    throw new InvalidOperationException("No signing credential is configured.");
                }

                var algorithm = credential.Algorithm;
                stateHash = CryptoHelper.CreateHashClaimValue(request.State, algorithm);
            }

            var code = new AuthorizationCode
            {
                //code创建时间
                CreationTime = Clock.UtcNow.UtcDateTime,
                //客户端id
                ClientId = request.Client.ClientId,
                //生命周期
                Lifetime = request.Client.AuthorizationCodeLifetime,
                //当前认证通过用户的ClaimsPrincipal
                Subject = request.Subject,
                //当前认证通过用户的会话id
                SessionId = request.SessionId,
                Description = request.Description,
                //再次加密已经在客户端加密过的CodeChallenge,安全考虑,到时候客户端会校验值
                CodeChallenge = request.CodeChallenge.Sha256(),
                //加密方式
                CodeChallengeMethod = request.CodeChallengeMethod,
                //是否openid请求
                IsOpenId = request.IsOpenIdRequest,
                //请求的scope集合
                RequestedScopes = request.ValidatedResources.RawScopeValues,
                //跳转到客户端的url
                RedirectUri = request.RedirectUri,
                //nonce值
                Nonce = request.Nonce,
                //StateHash值 安全相关
                StateHash = stateHash,
                //是否设置了用户同意相关
                WasConsentShown = request.WasConsentShown
            };

            return code;
        }

code内容如下:

 

2、存储code值,通过IPersistedGrantStore,默认实现是存储到ConcurrentDictionary中

3、将code(这里的code并不是1中的core,而是PersistedGrant实例的Key属性)写入response实例

     Logger.LogDebug("Creating Authorization Code Flow response.");

            var code = await CreateCodeAsync(request);
            var id = await AuthorizationCodeStore.StoreAuthorizationCodeAsync(code);

            var response = new AuthorizeResponse
            {
                Request = request,
                Code = id,
                SessionState = request.GenerateSessionStateValue()
            };

            return response;

 

4、将response实例写入AuthorizeResult实例

return new AuthorizeResult(response);

 

第三步:执行授权AuthorizeResult实例的ExecuteAsync方法

1、添加客户端id到cookie中,源码如下:

        public virtual async Task AddClientIdAsync(string clientId)
        {
            if (clientId == null) throw new ArgumentNullException(nameof(clientId));

            //再次进行cookie认证填充认证Properties
            await AuthenticateAsync();
            if (Properties != null)
            {
                var clientIds = Properties.GetClientList();
                if (!clientIds.Contains(clientId))
                {
                    //向认证Properties写入客户端id
                    Properties.AddClientId(clientId);
                    //更新cookie
                    await UpdateSessionCookie();
                }
            }
        }

2、根据相关参数生成一段html

<html><head><meta http-equiv='X-UA-Compatible' content='IE=edge' /><base target='_self'/></head><body><form method='post' action='http://localhost:5002/signin-oidc'><input type='hidden' name='code' value='A2E7A4E75DB0663DECA93937D6376FB735F249F5D3C939EF85681E5FC07D2424' />
<input type='hidden' name='scope' value='openid profile api1' />
<input type='hidden' name='state' value='CfDJ8HpC1EPIyftOtkkyJFkl1v-gnmUzTayM1rqhzWpMkDLveWVyWrcB2_PjZ5Koy-flZE2XXi34wnhhJYHq0T8WAx6XcNDgupbLE-94ej9vb5oxoOcb53hiMhEDxX8BBy94S5qkK6vSy2LiAmZc0NnsXirmq0QebaDdON43Nex7L1mZHUCeVEj26cNXRd-nrePfrj1JRVXxrBHjGfK0E--wQas_lsXT-3X0xhFJ5zvmJAx9T-Hf70PxEdwKMnhQzCjUhFYd0_rXVUFXoCNGbQblp8XXdmQX-2oXMVZxEwDWyv0sDp4GTYiF5JzDh3a9QtAKnWtFtLlGA1gu8w7Nx6vew_icH6nAZlUbd9sxQpC3ZZPbQ2ZkwK0OXca88IMVXzyi2A' />
<input type='hidden' name='session_state' value='9f6KAPCgCNnb8aDm4tV4IbVhBAIXEFCrf5Z7mGa4rkY.359378DC06BECE3035CC1313C7B7F83D' />
<noscript><button>Click to continue</button></noscript></form><script>window.addEventListener('load', function(){document.forms[0].submit();});</script></body></html>

Response输出这段html时,会触发表单的自动提交,提交的地址是http://localhost:5002/signin-oidc

表单参数:code、scope、state、session_state

到这里又回去了客户端.

posted @ 2022-06-22 20:27  郑小超  阅读(1397)  评论(0编辑  收藏  举报