.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 TaskProcessAsync(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 TaskGetHandlerAsync(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 TaskProcessAuthorizeRequestAsync(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 TaskValidateAsync(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 TaskLoadClientAsync(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 IEnumerableClients => 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 TaskValidateClientAsync(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 TaskValidateScopeAsync(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 TaskFindResourcesByScopeAsync(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(IEnumerableidentity, 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 TaskProcessInteractionAsync(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 TaskProcessLoginAsync(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服务的登录功能.