IdentityServer 4 学习笔记 - 源码解读
一、简介
IdentityServer 4 是一个基于ASP.NET Core免费开源的 OpenID Connect 和 OAuth 2.0 框架。IdentityServer 4 整合了在应用程序中集成基于令牌的身份验证、单点登录和 API 访问控制所需的所有协议实现和扩展点。IdentityServer 4由 OpenID基金会正式认证,因此符合规范且可互操作。它是.NET基金会的一部分,根据他们的行为准则运作。它根据Apache 2(OSI批准的许可证)获得许可。
说白了,IdentityServer 4 其实就是ASP.NET Core的一个中间件,可以无缝的接入 ASP.NET Core 处理管道中。
那什么是ASP.NET Core 中间件?ASP.NET Core的处理流程是一个管道,而中间件是装配到管道中的用于处理请求和响应的组件。中间件按照装配的先后顺序执行,并决定是否进入下一个组件。中间件管道的处理流程如下图(图片来源于官网)
IdentityServer 4 源码地址 : https://github.com/IdentityServer/IdentityServer4 ,我下载的版本是 4.0.4
二、处理过程
整个处理过程分为启动初始化和监听响应请求两个部分:
1、如图左侧为Asp.net core 创建WebHost通过Startup类注册 IdentityServer服务、使用 IdentityServer中间件。
中间件配置类
路径 : \Configuration\IdentityServerApplicationBuilderExtensions.cs
public static class IdentityServerApplicationBuilderExtensions { public static IApplicationBuilder UseIdentityServer(this IApplicationBuilder app, IdentityServerMiddlewareOptions options = null) { app.Validate(); app.UseMiddleware<BaseUrlMiddleware>(); app.ConfigureCors(); // it seems ok if we have UseAuthentication more than once in the pipeline -- // this will just re-run the various callback handlers and the default authN // handler, which just re-assigns the user on the context. claims transformation // will run twice, since that's not cached (whereas the authN handler result is) // related: https://github.com/aspnet/Security/issues/1399 if (options == null) options = new IdentityServerMiddlewareOptions(); options.AuthenticationMiddleware(app); app.UseMiddleware<MutualTlsEndpointMiddleware>(); app.UseMiddleware<IdentityServerMiddleware>(); return app; } }
2、右侧为WebHost进行请求监听
下面以Client采用 用户名密码模式(GrantTypes.ResourceOwnerPassword)请求Token (地址 http://localhost:5000/connect/token)为例进行Post请求
(1)将请求传入 IdentityServerMiddleware ,中间件会注入节点IEndpointRouter(默认EndpointRouter类)通过执行 Invoke 的 router.Find(context) 根据 context.Request.Path 查找对应的IEndpointHandler终结点处理类。
中间件实现类:
public class IdentityServerMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; /// <summary> /// Initializes a new instance of the <see cref="IdentityServerMiddleware"/> class. /// </summary> /// <param name="next">The next.</param> /// <param name="logger">The logger.</param> public IdentityServerMiddleware(RequestDelegate next, ILogger<IdentityServerMiddleware> logger) { _next = next; _logger = logger; } /// <summary> /// Invokes the middleware. 调用中间件。 /// </summary> /// <param name="context">The context.</param> /// <param name="router">The router.</param> /// <param name="session">The user session.</param> /// <param name="events">The event service.</param> /// <param name="backChannelLogoutService"></param> /// <returns></returns> public async Task Invoke(HttpContext context, IEndpointRouter router, IUserSession session, IEventService events, IBackChannelLogoutService backChannelLogoutService) { // this will check the authentication session and from it emit the check session // cookie needed from JS-based signout clients. // 这将检查身份验证会话,并从中发出基于js的签出客户端所需的检查会话cookie。 await session.EnsureSessionIdCookieAsync(); context.Response.OnStarting(async () => { if (context.GetSignOutCalled()) { _logger.LogDebug("SignOutCalled set; processing post-signout session cleanup."); // this clears our session id cookie so JS clients can detect the user has signed out // 这将清除我们的会话id cookie,以便JS客户端可以检测用户已经退出 await session.RemoveSessionIdCookieAsync(); // back channel logout 回发通道注销 var logoutContext = await session.GetLogoutNotificationContext(); if (logoutContext != null) { await backChannelLogoutService.SendLogoutNotificationsAsync(logoutContext); } } }); try { var endpoint = router.Find(context); if (endpoint != null) { _logger.LogInformation("Invoking IdentityServer endpoint: {endpointType} for {url}", endpoint.GetType().FullName, context.Request.Path.ToString()); var result = await endpoint.ProcessAsync(context); if (result != null) { _logger.LogTrace("Invoking result: {type}", result.GetType().FullName); await result.ExecuteAsync(context); } return; } } catch (Exception ex) { await events.RaiseAsync(new UnhandledExceptionEvent(ex)); _logger.LogCritical(ex, "Unhandled exception: {exception}", ex.Message); throw; } await _next(context); } }
节点路由类:
internal class EndpointRouter : IEndpointRouter { private readonly IEnumerable<Endpoint> _endpoints; private readonly IdentityServerOptions _options; private readonly ILogger _logger; public EndpointRouter(IEnumerable<Endpoint> endpoints, IdentityServerOptions options, ILogger<EndpointRouter> logger) { _endpoints = endpoints; _options = options; _logger = logger; } public IEndpointHandler Find(HttpContext context) { if (context == null) throw new ArgumentNullException(nameof(context)); foreach(var endpoint in _endpoints) { var path = endpoint.Path; if (context.Request.Path.Equals(path, StringComparison.OrdinalIgnoreCase)) { var endpointName = endpoint.Name; _logger.LogDebug("Request path {path} matched to endpoint type {endpoint}", context.Request.Path, endpointName); return GetEndpointHandler(endpoint, context); } } _logger.LogTrace("No endpoint entry found for request path: {path}", context.Request.Path); return null; } private IEndpointHandler GetEndpointHandler(Endpoint endpoint, HttpContext context) { if (_options.Endpoints.IsEndpointEnabled(endpoint)) { if (context.RequestServices.GetService(endpoint.Handler) is IEndpointHandler handler) { _logger.LogDebug("Endpoint enabled: {endpoint}, successfully created handler: {endpointHandler}", endpoint.Name, endpoint.Handler.FullName); return handler; } _logger.LogDebug("Endpoint enabled: {endpoint}, failed to create handler: {endpointHandler}", endpoint.Name, endpoint.Handler.FullName); } else { _logger.LogWarning("Endpoint disabled: {endpoint}", endpoint.Name); } return null; } }
(2)根据 地址 http://localhost:5000/connect/token 请求会找到,TokenEndpoint 这个类,这个类会注入ITokenRequestValidator(默认TokenRequestValidator)、ITokenResponseGenerator(默认TokenResponseGenerator )、IEndpointResult(默认TokenResult )等处理类,这个终结点实例会返回给中间件,由中间件调用
var result = await endpoint.ProcessAsync(context);
endpoint.ProcessAsync 会依次执行validate client、validate request、create response、return result,最终调用:
await result.ExecuteAsync(context);
返回相应信息。
终结点处理程序:
internal class TokenEndpoint : IEndpointHandler { private readonly IClientSecretValidator _clientValidator; private readonly ITokenRequestValidator _requestValidator; private readonly ITokenResponseGenerator _responseGenerator; private readonly IEventService _events; private readonly ILogger _logger; /// <summary> /// Initializes a new instance of the <see cref="TokenEndpoint" /> class. /// </summary> /// <param name="clientValidator">The client validator.</param> /// <param name="requestValidator">The request validator.</param> /// <param name="responseGenerator">The response generator.</param> /// <param name="events">The events.</param> /// <param name="logger">The logger.</param> public TokenEndpoint( IClientSecretValidator clientValidator, ITokenRequestValidator requestValidator, ITokenResponseGenerator responseGenerator, IEventService events, ILogger<TokenEndpoint> logger) { _clientValidator = clientValidator; _requestValidator = requestValidator; _responseGenerator = responseGenerator; _events = events; _logger = logger; } /// <summary> /// Processes the request. /// </summary> /// <param name="context">The HTTP context.</param> /// <returns></returns> public async Task<IEndpointResult> ProcessAsync(HttpContext context) { _logger.LogTrace("Processing token request."); // validate HTTP if (!HttpMethods.IsPost(context.Request.Method) || !context.Request.HasApplicationFormContentType()) { _logger.LogWarning("Invalid HTTP request for token endpoint"); return Error(OidcConstants.TokenErrors.InvalidRequest); } return await ProcessTokenRequestAsync(context); } private async Task<IEndpointResult> ProcessTokenRequestAsync(HttpContext context) { _logger.LogDebug("Start token request."); // validate client var clientResult = await _clientValidator.ValidateAsync(context); if (clientResult.Client == null) { return Error(OidcConstants.TokenErrors.InvalidClient); } // validate request var form = (await context.Request.ReadFormAsync()).AsNameValueCollection(); _logger.LogTrace("Calling into token request validator: {type}", _requestValidator.GetType().FullName); var requestResult = await _requestValidator.ValidateRequestAsync(form, clientResult); //默认调用TokenRequestValidator类的ValidateRequestAsync if (requestResult.IsError) { await _events.RaiseAsync(new TokenIssuedFailureEvent(requestResult)); return Error(requestResult.Error, requestResult.ErrorDescription, requestResult.CustomResponse); } // create response _logger.LogTrace("Calling into token request response generator: {type}", _responseGenerator.GetType().FullName); var response = await _responseGenerator.ProcessAsync(requestResult); await _events.RaiseAsync(new TokenIssuedSuccessEvent(response, requestResult)); LogTokens(response, requestResult); // return result _logger.LogDebug("Token request success."); return new TokenResult(response); } private TokenErrorResult Error(string error, string errorDescription = null, Dictionary<string, object> custom = null) { var response = new TokenErrorResponse { Error = error, ErrorDescription = errorDescription, Custom = custom }; return new TokenErrorResult(response); } private void LogTokens(TokenResponse response, TokenRequestValidationResult requestResult) { var clientId = $"{requestResult.ValidatedRequest.Client.ClientId} ({requestResult.ValidatedRequest.Client?.ClientName ?? "no name set"})"; var subjectId = requestResult.ValidatedRequest.Subject?.GetSubjectId() ?? "no subject"; if (response.IdentityToken != null) { _logger.LogTrace("Identity token issued for {clientId} / {subjectId}: {token}", clientId, subjectId, response.IdentityToken); } if (response.RefreshToken != null) { _logger.LogTrace("Refresh token issued for {clientId} / {subjectId}: {token}", clientId, subjectId, response.RefreshToken); } if (response.AccessToken != null) { _logger.LogTrace("Access token issued for {clientId} / {subjectId}: {token}", clientId, subjectId, response.AccessToken); } } }
验证处理类:
internal class TokenRequestValidator : ITokenRequestValidator { ... private readonly IResourceOwnerPasswordValidator _resourceOwnerValidator; ... public TokenRequestValidator(IdentityServerOptions options, IAuthorizationCodeStore authorizationCodeStore, IResourceOwnerPasswordValidator resourceOwnerValidator, IProfileService profile, IDeviceCodeValidator deviceCodeValidator, ExtensionGrantValidator extensionGrantValidator, ICustomTokenRequestValidator customRequestValidator, IResourceValidator resourceValidator, IResourceStore resourceStore, ITokenValidator tokenValidator, IRefreshTokenService refreshTokenService, IEventService events, ISystemClock clock, ILogger<TokenRequestValidator> logger) { ... _resourceOwnerValidator = resourceOwnerValidator; ... } private async Task<TokenRequestValidationResult> ValidateResourceOwnerCredentialRequestAsync(NameValueCollection parameters) { _logger.LogDebug("Start resource owner password token request validation"); ///////////////////////////////////////////// // check if client is authorized for grant type ///////////////////////////////////////////// if (!_validatedRequest.Client.AllowedGrantTypes.Contains(GrantType.ResourceOwnerPassword)) { LogError("Client not authorized for resource owner flow, check the AllowedGrantTypes setting", new { client_id = _validatedRequest.Client.ClientId }); return Invalid(OidcConstants.TokenErrors.UnauthorizedClient); } ///////////////////////////////////////////// // check if client is allowed to request scopes ///////////////////////////////////////////// if (!(await ValidateRequestedScopesAsync(parameters))) { return Invalid(OidcConstants.TokenErrors.InvalidScope); } ///////////////////////////////////////////// // check resource owner credentials ///////////////////////////////////////////// var userName = parameters.Get(OidcConstants.TokenRequest.UserName); var password = parameters.Get(OidcConstants.TokenRequest.Password); if (userName.IsMissing()) { LogError("Username is missing"); return Invalid(OidcConstants.TokenErrors.InvalidGrant); } if (password.IsMissing()) { password = ""; } if (userName.Length > _options.InputLengthRestrictions.UserName || password.Length > _options.InputLengthRestrictions.Password) { LogError("Username or password too long"); return Invalid(OidcConstants.TokenErrors.InvalidGrant); } _validatedRequest.UserName = userName; ///////////////////////////////////////////// // authenticate user ///////////////////////////////////////////// var resourceOwnerContext = new ResourceOwnerPasswordValidationContext { UserName = userName, Password = password, Request = _validatedRequest }; await _resourceOwnerValidator.ValidateAsync(resourceOwnerContext); if (resourceOwnerContext.Result.IsError) { // protect against bad validator implementations resourceOwnerContext.Result.Error ??= OidcConstants.TokenErrors.InvalidGrant; if (resourceOwnerContext.Result.Error == OidcConstants.TokenErrors.UnsupportedGrantType) { LogError("Resource owner password credential grant type not supported"); await RaiseFailedResourceOwnerAuthenticationEventAsync(userName, "password grant type not supported", resourceOwnerContext.Request.Client.ClientId); return Invalid(OidcConstants.TokenErrors.UnsupportedGrantType, customResponse: resourceOwnerContext.Result.CustomResponse); } var errorDescription = "invalid_username_or_password"; if (resourceOwnerContext.Result.ErrorDescription.IsPresent()) { errorDescription = resourceOwnerContext.Result.ErrorDescription; } LogInformation("User authentication failed: ", errorDescription ?? resourceOwnerContext.Result.Error); await RaiseFailedResourceOwnerAuthenticationEventAsync(userName, errorDescription, resourceOwnerContext.Request.Client.ClientId); return Invalid(resourceOwnerContext.Result.Error, errorDescription, resourceOwnerContext.Result.CustomResponse); } if (resourceOwnerContext.Result.Subject == null) { var error = "User authentication failed: no principal returned"; LogError(error); await RaiseFailedResourceOwnerAuthenticationEventAsync(userName, error, resourceOwnerContext.Request.Client.ClientId); return Invalid(OidcConstants.TokenErrors.InvalidGrant); } ///////////////////////////////////////////// // make sure user is enabled ///////////////////////////////////////////// var isActiveCtx = new IsActiveContext(resourceOwnerContext.Result.Subject, _validatedRequest.Client, IdentityServerConstants.ProfileIsActiveCallers.ResourceOwnerValidation); await _profile.IsActiveAsync(isActiveCtx); if (isActiveCtx.IsActive == false) { LogError("User has been disabled", new { subjectId = resourceOwnerContext.Result.Subject.GetSubjectId() }); await RaiseFailedResourceOwnerAuthenticationEventAsync(userName, "user is inactive", resourceOwnerContext.Request.Client.ClientId); return Invalid(OidcConstants.TokenErrors.InvalidGrant); } _validatedRequest.UserName = userName; _validatedRequest.Subject = resourceOwnerContext.Result.Subject; await RaiseSuccessfulResourceOwnerAuthenticationEventAsync(userName, resourceOwnerContext.Result.Subject.GetSubjectId(), resourceOwnerContext.Request.Client.ClientId); _logger.LogDebug("Resource owner password token request validation success."); return Valid(resourceOwnerContext.Result.CustomResponse); } }
响应处理类:
public class TokenResponseGenerator : ITokenResponseGenerator { /// <summary> /// The logger /// </summary> protected readonly ILogger Logger; /// <summary> /// The token service /// </summary> protected readonly ITokenService TokenService; /// <summary> /// The refresh token service /// </summary> protected readonly IRefreshTokenService RefreshTokenService; /// <summary> /// The scope parser /// </summary> public IScopeParser ScopeParser { get; } /// <summary> /// The resource store /// </summary> protected readonly IResourceStore Resources; /// <summary> /// The clients store /// </summary> protected readonly IClientStore Clients; /// <summary> /// The clock /// </summary> protected readonly ISystemClock Clock; /// <summary> /// Initializes a new instance of the <see cref="TokenResponseGenerator" /> class. /// </summary> /// <param name="clock">The clock.</param> /// <param name="tokenService">The token service.</param> /// <param name="refreshTokenService">The refresh token service.</param> /// <param name="scopeParser">The scope parser.</param> /// <param name="resources">The resources.</param> /// <param name="clients">The clients.</param> /// <param name="logger">The logger.</param> public TokenResponseGenerator(ISystemClock clock, ITokenService tokenService, IRefreshTokenService refreshTokenService, IScopeParser scopeParser, IResourceStore resources, IClientStore clients, ILogger<TokenResponseGenerator> logger) { Clock = clock; TokenService = tokenService; RefreshTokenService = refreshTokenService; ScopeParser = scopeParser; Resources = resources; Clients = clients; Logger = logger; } /// <summary> /// Processes the response. /// </summary> /// <param name="request">The request.</param> /// <returns></returns> public virtual async Task<TokenResponse> ProcessAsync(TokenRequestValidationResult request) { switch (request.ValidatedRequest.GrantType) { case OidcConstants.GrantTypes.ClientCredentials: return await ProcessClientCredentialsRequestAsync(request); case OidcConstants.GrantTypes.Password: return await ProcessPasswordRequestAsync(request); case OidcConstants.GrantTypes.AuthorizationCode: return await ProcessAuthorizationCodeRequestAsync(request); case OidcConstants.GrantTypes.RefreshToken: return await ProcessRefreshTokenRequestAsync(request); case OidcConstants.GrantTypes.DeviceCode: return await ProcessDeviceCodeRequestAsync(request); default: return await ProcessExtensionGrantRequestAsync(request); } } /// <summary> /// Creates the response for an client credentials request. /// 为客户端凭证请求创建响应。 /// </summary> /// <param name="request">The request.</param> /// <returns></returns> protected virtual Task<TokenResponse> ProcessClientCredentialsRequestAsync(TokenRequestValidationResult request) { Logger.LogTrace("Creating response for client credentials request"); return ProcessTokenRequestAsync(request); } /// <summary> /// Creates the response for a password request. /// 为密码请求创建响应。 /// </summary> /// <param name="request">The request.</param> /// <returns></returns> protected virtual Task<TokenResponse> ProcessPasswordRequestAsync(TokenRequestValidationResult request) { Logger.LogTrace("Creating response for password request"); return ProcessTokenRequestAsync(request); } /// <summary> /// Creates the response for an authorization code request. /// 为授权代码请求创建响应。 /// </summary> /// <param name="request">The request.</param> /// <returns></returns> /// <exception cref="System.InvalidOperationException">Client does not exist anymore.</exception> protected virtual async Task<TokenResponse> ProcessAuthorizationCodeRequestAsync(TokenRequestValidationResult request) { Logger.LogTrace("Creating response for authorization code request"); ////////////////////////// // access token ///////////////////////// var (accessToken, refreshToken) = await CreateAccessTokenAsync(request.ValidatedRequest); var response = new TokenResponse { AccessToken = accessToken, AccessTokenLifetime = request.ValidatedRequest.AccessTokenLifetime, Custom = request.CustomResponse, Scope = request.ValidatedRequest.AuthorizationCode.RequestedScopes.ToSpaceSeparatedString() }; ////////////////////////// // refresh token ///////////////////////// if (refreshToken.IsPresent()) { response.RefreshToken = refreshToken; } ////////////////////////// // id token ///////////////////////// if (request.ValidatedRequest.AuthorizationCode.IsOpenId) { // load the client that belongs to the authorization code Client client = null; if (request.ValidatedRequest.AuthorizationCode.ClientId != null) { client = await Clients.FindEnabledClientByIdAsync(request.ValidatedRequest.AuthorizationCode.ClientId); } if (client == null) { throw new InvalidOperationException("Client does not exist anymore."); } var parsedScopesResult = ScopeParser.ParseScopeValues(request.ValidatedRequest.AuthorizationCode.RequestedScopes); var validatedResources = await Resources.CreateResourceValidationResult(parsedScopesResult); var tokenRequest = new TokenCreationRequest { Subject = request.ValidatedRequest.AuthorizationCode.Subject, ValidatedResources = validatedResources, Nonce = request.ValidatedRequest.AuthorizationCode.Nonce, AccessTokenToHash = response.AccessToken, StateHash = request.ValidatedRequest.AuthorizationCode.StateHash, ValidatedRequest = request.ValidatedRequest }; var idToken = await TokenService.CreateIdentityTokenAsync(tokenRequest); var jwt = await TokenService.CreateSecurityTokenAsync(idToken); response.IdentityToken = jwt; } return response; } /// <summary> /// Creates the response for a refresh token request. /// 为刷新令牌请求创建响应。 /// </summary> /// <param name="request">The request.</param> /// <returns></returns> protected virtual async Task<TokenResponse> ProcessRefreshTokenRequestAsync(TokenRequestValidationResult request) { Logger.LogTrace("Creating response for refresh token request"); var oldAccessToken = request.ValidatedRequest.RefreshToken.AccessToken; string accessTokenString; if (request.ValidatedRequest.Client.UpdateAccessTokenClaimsOnRefresh) { var subject = request.ValidatedRequest.RefreshToken.Subject; // todo: do we want to just parse here and build up validated result // or do we want to fully re-run validation here. var parsedScopesResult = ScopeParser.ParseScopeValues(oldAccessToken.Scopes); var validatedResources = await Resources.CreateResourceValidationResult(parsedScopesResult); var creationRequest = new TokenCreationRequest { Subject = subject, Description = request.ValidatedRequest.RefreshToken.Description, ValidatedRequest = request.ValidatedRequest, ValidatedResources = validatedResources }; var newAccessToken = await TokenService.CreateAccessTokenAsync(creationRequest); accessTokenString = await TokenService.CreateSecurityTokenAsync(newAccessToken); } else { oldAccessToken.CreationTime = Clock.UtcNow.UtcDateTime; oldAccessToken.Lifetime = request.ValidatedRequest.AccessTokenLifetime; accessTokenString = await TokenService.CreateSecurityTokenAsync(oldAccessToken); } var handle = await RefreshTokenService.UpdateRefreshTokenAsync(request.ValidatedRequest.RefreshTokenHandle, request.ValidatedRequest.RefreshToken, request.ValidatedRequest.Client); return new TokenResponse { IdentityToken = await CreateIdTokenFromRefreshTokenRequestAsync(request.ValidatedRequest, accessTokenString), AccessToken = accessTokenString, AccessTokenLifetime = request.ValidatedRequest.AccessTokenLifetime, RefreshToken = handle, Custom = request.CustomResponse, Scope = request.ValidatedRequest.RefreshToken.Scopes.ToSpaceSeparatedString() }; } /// <summary> /// Processes the response for device code grant request. /// 处理设备代码授予请求的响应。 /// </summary> /// <param name="request">The request.</param> /// <returns></returns> protected virtual async Task<TokenResponse> ProcessDeviceCodeRequestAsync(TokenRequestValidationResult request) { Logger.LogTrace("Creating response for device code request"); ////////////////////////// // access token ///////////////////////// var (accessToken, refreshToken) = await CreateAccessTokenAsync(request.ValidatedRequest); var response = new TokenResponse { AccessToken = accessToken, AccessTokenLifetime = request.ValidatedRequest.AccessTokenLifetime, Custom = request.CustomResponse, Scope = request.ValidatedRequest.DeviceCode.AuthorizedScopes.ToSpaceSeparatedString() }; ////////////////////////// // refresh token ///////////////////////// if (refreshToken.IsPresent()) { response.RefreshToken = refreshToken; } ////////////////////////// // id token ///////////////////////// if (request.ValidatedRequest.DeviceCode.IsOpenId) { // load the client that belongs to the device code Client client = null; if (request.ValidatedRequest.DeviceCode.ClientId != null) { client = await Clients.FindEnabledClientByIdAsync(request.ValidatedRequest.DeviceCode.ClientId); } if (client == null) { throw new InvalidOperationException("Client does not exist anymore."); } var parsedScopesResult = ScopeParser.ParseScopeValues(request.ValidatedRequest.DeviceCode.AuthorizedScopes); var validatedResources = await Resources.CreateResourceValidationResult(parsedScopesResult); var tokenRequest = new TokenCreationRequest { Subject = request.ValidatedRequest.DeviceCode.Subject, ValidatedResources = validatedResources, AccessTokenToHash = response.AccessToken, ValidatedRequest = request.ValidatedRequest }; var idToken = await TokenService.CreateIdentityTokenAsync(tokenRequest); var jwt = await TokenService.CreateSecurityTokenAsync(idToken); response.IdentityToken = jwt; } return response; } /// <summary> /// Creates the response for an extension grant request. /// 为扩展授权请求创建响应。 /// </summary> /// <param name="request">The request.</param> /// <returns></returns> protected virtual Task<TokenResponse> ProcessExtensionGrantRequestAsync(TokenRequestValidationResult request) { Logger.LogTrace("Creating response for extension grant request"); return ProcessTokenRequestAsync(request); } /// <summary> /// Creates the response for a token request. /// 为令牌请求创建响应。 /// </summary> /// <param name="validationResult">The validation result.</param> /// <returns></returns> protected virtual async Task<TokenResponse> ProcessTokenRequestAsync(TokenRequestValidationResult validationResult) { (var accessToken, var refreshToken) = await CreateAccessTokenAsync(validationResult.ValidatedRequest); var response = new TokenResponse { AccessToken = accessToken, AccessTokenLifetime = validationResult.ValidatedRequest.AccessTokenLifetime, Custom = validationResult.CustomResponse, Scope = validationResult.ValidatedRequest.ValidatedResources.RawScopeValues.ToSpaceSeparatedString() }; if (refreshToken.IsPresent()) { response.RefreshToken = refreshToken; } return response; } /// <summary> /// Creates the access/refresh token. /// 创建访问/刷新令牌。 /// </summary> /// <param name="request">The request.</param> /// <returns></returns> /// <exception cref="System.InvalidOperationException">Client does not exist anymore.</exception> protected virtual async Task<(string accessToken, string refreshToken)> CreateAccessTokenAsync(ValidatedTokenRequest request) { TokenCreationRequest tokenRequest; bool createRefreshToken; if (request.AuthorizationCode != null) { createRefreshToken = request.AuthorizationCode.RequestedScopes.Contains(IdentityServerConstants.StandardScopes.OfflineAccess); // load the client that belongs to the authorization code // 加载属于授权代码的客户端 Client client = null; if (request.AuthorizationCode.ClientId != null) { client = await Clients.FindEnabledClientByIdAsync(request.AuthorizationCode.ClientId); } if (client == null) { throw new InvalidOperationException("Client does not exist anymore."); } var parsedScopesResult = ScopeParser.ParseScopeValues(request.AuthorizationCode.RequestedScopes); var validatedResources = await Resources.CreateResourceValidationResult(parsedScopesResult); tokenRequest = new TokenCreationRequest { Subject = request.AuthorizationCode.Subject, Description = request.AuthorizationCode.Description, ValidatedResources = validatedResources, ValidatedRequest = request }; } else if (request.DeviceCode != null) { createRefreshToken = request.DeviceCode.AuthorizedScopes.Contains(IdentityServerConstants.StandardScopes.OfflineAccess); Client client = null; if (request.DeviceCode.ClientId != null) { client = await Clients.FindEnabledClientByIdAsync(request.DeviceCode.ClientId); } if (client == null) { throw new InvalidOperationException("Client does not exist anymore."); } var parsedScopesResult = ScopeParser.ParseScopeValues(request.DeviceCode.AuthorizedScopes); var validatedResources = await Resources.CreateResourceValidationResult(parsedScopesResult); tokenRequest = new TokenCreationRequest { Subject = request.DeviceCode.Subject, Description = request.DeviceCode.Description, ValidatedResources = validatedResources, ValidatedRequest = request }; } else { createRefreshToken = request.ValidatedResources.Resources.OfflineAccess; tokenRequest = new TokenCreationRequest { Subject = request.Subject, ValidatedResources = request.ValidatedResources, ValidatedRequest = request }; } var at = await TokenService.CreateAccessTokenAsync(tokenRequest); var accessToken = await TokenService.CreateSecurityTokenAsync(at); if (createRefreshToken) { var refreshToken = await RefreshTokenService.CreateRefreshTokenAsync(tokenRequest.Subject, at, request.Client); return (accessToken, refreshToken); } return (accessToken, null); } /// <summary> /// Creates an id_token for a refresh token request if identity resources have been requested. /// 如果已请求标识资源,则为刷新令牌请求创建id_token。 /// </summary> /// <param name="request">The request.</param> /// <param name="newAccessToken">The new access token.</param> /// <returns></returns> protected virtual async Task<string> CreateIdTokenFromRefreshTokenRequestAsync(ValidatedTokenRequest request, string newAccessToken) { // todo: can we just check for "openid" scope? 我们能检查一下“openid”范围吗? //var identityResources = await Resources.FindEnabledIdentityResourcesByScopeAsync(request.RefreshToken.Scopes); //if (identityResources.Any()) if (request.RefreshToken.Scopes.Contains(OidcConstants.StandardScopes.OpenId)) { var oldAccessToken = request.RefreshToken.AccessToken; var parsedScopesResult = ScopeParser.ParseScopeValues(oldAccessToken.Scopes); var validatedResources = await Resources.CreateResourceValidationResult(parsedScopesResult); var tokenRequest = new TokenCreationRequest { Subject = request.RefreshToken.Subject, ValidatedResources = validatedResources, ValidatedRequest = request, AccessTokenToHash = newAccessToken }; var idToken = await TokenService.CreateIdentityTokenAsync(tokenRequest); return await TokenService.CreateSecurityTokenAsync(idToken); } return null; } }
返回结果类:
internal class TokenResult : IEndpointResult { public TokenResponse Response { get; set; } public TokenResult(TokenResponse response) { Response = response ?? throw new ArgumentNullException(nameof(response)); } public async Task ExecuteAsync(HttpContext context) { context.Response.SetNoCache(); var dto = new ResultDto { id_token = Response.IdentityToken, access_token = Response.AccessToken, refresh_token = Response.RefreshToken, expires_in = Response.AccessTokenLifetime, token_type = OidcConstants.TokenResponse.BearerTokenType, scope = Response.Scope, Custom = Response.Custom }; await context.Response.WriteJsonAsync(dto); } internal class ResultDto { public string id_token { get; set; } public string access_token { get; set; } public int expires_in { get; set; } public string token_type { get; set; } public string refresh_token { get; set; } public string scope { get; set; } [JsonExtensionData] public Dictionary<string, object> Custom { get; set; } } }
(3)用户名密码验证
在 TokenRequestValidator 类中 会调用 IResourceOwnerPasswordValidator 接口的实现类 ,验证用户密码返回验证结果信息GrantValidationResult,组件默认注入的是 NotSupportedResourceOwnerPasswordValidator,
//IdentityServerBuilderExtensionsCore类
public static IIdentityServerBuilder AddValidators(this IIdentityServerBuilder builder) { ... builder.Services.TryAddTransient<ITokenRequestValidator, TokenRequestValidator>(); ... builder.Services.TryAddTransient<IResourceOwnerPasswordValidator, NotSupportedResourceOwnerPasswordValidator>(); ... }
//NotSupportedResourceOwnerPasswordValidator位置在\Validation\Default\NotSupportedResouceOwnerCredentialValidator.cs
public class NotSupportedResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator { private readonly ILogger _logger; /// <summary> /// Initializes a new instance of the <see cref="NotSupportedResourceOwnerPasswordValidator"/> class. /// </summary> /// <param name="logger">The logger.</param> public NotSupportedResourceOwnerPasswordValidator(ILogger<NotSupportedResourceOwnerPasswordValidator> logger) { _logger = logger; } /// <summary> /// Validates the resource owner password credential /// </summary> /// <param name="context">The context.</param> /// <returns></returns> public Task ValidateAsync(ResourceOwnerPasswordValidationContext context) { context.Result = new GrantValidationResult(TokenRequestErrors.UnsupportedGrantType); _logger.LogInformation("Resource owner password credential type not supported. Configure an IResourceOwnerPasswordValidator."); return Task.CompletedTask; } }
在开发中我们通常采用 .AddTestUsers(TestUsers.Users) 添加测试用户,这方法会注入 TestUserResourceOwnerPasswordValidatorc类,从添加的用户列表中验证用户密码,返回验证结果信息GrantValidationResult,
public static class IdentityServerBuilderExtensions { /// <summary> /// Adds test users. /// </summary> /// <param name="builder">The builder.</param> /// <param name="users">The users.</param> /// <returns></returns> public static IIdentityServerBuilder AddTestUsers(this IIdentityServerBuilder builder, List<TestUser> users) { builder.Services.AddSingleton(new TestUserStore(users)); builder.AddProfileService<TestUserProfileService>(); builder.AddResourceOwnerValidator<TestUserResourceOwnerPasswordValidator>(); return builder; } }
public class TestUserResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator { private readonly TestUserStore _users; private readonly ISystemClock _clock; /// <summary> /// Initializes a new instance of the <see cref="TestUserResourceOwnerPasswordValidator"/> class. /// </summary> /// <param name="users">The users.</param> /// <param name="clock">The clock.</param> public TestUserResourceOwnerPasswordValidator(TestUserStore users, ISystemClock clock) { _users = users; _clock = clock; } /// <summary> /// Validates the resource owner password credential /// 验证资源所有者密码凭据 /// </summary> /// <param name="context">The context.</param> /// <returns></returns> public Task ValidateAsync(ResourceOwnerPasswordValidationContext context) { if (_users.ValidateCredentials(context.UserName, context.Password)) { var user = _users.FindByUsername(context.UserName); context.Result = new GrantValidationResult( user.SubjectId ?? throw new ArgumentException("Subject ID not set", nameof(user.SubjectId)), OidcConstants.AuthenticationMethods.Password, _clock.UtcNow.UtcDateTime, user.Claims); } return Task.CompletedTask; } }
因此我们可以继承IResourceOwnerPasswordValidator接口实现自己的用户密码校验逻辑,如自己定义的数据库表结构中通过传入的ResourceOwnerPasswordValidationContext类的 context.UserName, context.Password参数完成自己的用户校验逻辑,通过配置 .AddResourceOwnerValidator<T>()替换默认的验证行为。
\\IdentityServerBuilderExtensionsAdditional类 \\路径\Configuration\DependencyInjection\BuilderExtensions\Additional.cs public static IIdentityServerBuilder AddResourceOwnerValidator<T>(this IIdentityServerBuilder builder) where T : class, IResourceOwnerPasswordValidator { builder.Services.AddTransient<IResourceOwnerPasswordValidator, T>(); return builder; }