.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
到这里又回去了客户端.