oidc服务需要提供token接口,提供AccessToken,IdToken,以及RefreshToken(可选)。在授权码模式下,token接口必须使用https。
必须使用POST方法,使用x-www-form-urlencoded序列化参数,clientId:clientSecret使用Basic加密放在Authorization头中
POST /token HTTP/1.1Host: server.example.comContent-Type: application/x-www-form-urlencodedAuthorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JWgrant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
认证服务必须校验下列内容:
在收到token请求,并校验通过之后,认证服务返回成功报文,报文包含了身份令牌和通行令牌。数据格式使用application/json。token_type必须返回Bearer,其他类型token不在本协议范围内。在OAuth2.0响应报文基础上,oidc增加了id_tken。所有token包含了token或者其他敏感信息的响应报文,必须包含以下响应头。
Cache-Control no-storePragma no-cache
如果认证失败返回application/json格式错误消息,状态码400
HTTP/1.1 400 Bad Request Content-Type: application/json Cache-Control: no-store Pragma: no-cache { "error": "invalid_request" }
客户端必须校验返回的id token, 校验条件如下。对照这些条件,就可以更懂Microsoft.Authentication.OpenIdConnect里面的代码了,要做的事情很多。
如果id_token中包含了at_hash声明,需要做下面的校验。at_hash标明了access_token和id_token之间的会话关联关系,做这个校验可以防跨站伪造。
校验规则很多,了解一下即可,绝大部分属于客户端需要做的部分,绝大部分跟安全有关。这一块的实现可以参考Microsoft.Authentication.OpenIdConnect,这是客户端的实现。我们现在看的IdentityServer是认证服务端的实现。
有下面几种授权模式可以请求token接口
注意:简化模式所有的token都是由认证接口(authorize)一次性返回的,不能使用token接口。
token接口仅允许POST方法,Content-Type必须为application/x-www-form-urlencoded,否则抛出InvalidRequest错误。
public async Task<IEndpointResult> ProcessAsync(HttpContext context) { _logger.LogTrace("Processing token request."); // validate HTTP if (!HttpMethods.IsPost(context.Request.Method) || !context.Request.HasFormContentType) { _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); 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);}
public async Task<ClientSecretValidationResult> ValidateAsync(HttpContext context){ _logger.LogDebug("Start client validation"); var fail = new ClientSecretValidationResult { IsError = true }; var parsedSecret = await _parser.ParseAsync(context); if (parsedSecret == null) { await RaiseFailureEventAsync("unknown", "No client id found"); _logger.LogError("No client identifier found"); return fail; } // load client var client = await _clients.FindEnabledClientByIdAsync(parsedSecret.Id); if (client == null) { await RaiseFailureEventAsync(parsedSecret.Id, "Unknown client"); _logger.LogError("No client with id '{clientId}' found. aborting", parsedSecret.Id); return fail; } SecretValidationResult secretValidationResult = null; if (!client.RequireClientSecret || client.IsImplicitOnly()) { _logger.LogDebug("Public Client - skipping secret validation success"); } else { secretValidationResult = await _validator.ValidateAsync(parsedSecret, client.ClientSecrets); if (secretValidationResult.Success == false) { await RaiseFailureEventAsync(client.ClientId, "Invalid client secret"); _logger.LogError("Client secret validation failed for client: {clientId}.", client.ClientId); return fail; } } _logger.LogDebug("Client validation success"); var success = new ClientSecretValidationResult { IsError = false, Client = client, Secret = parsedSecret, Confirmation = secretValidationResult?.Confirmation }; await RaiseSuccessEventAsync(client.ClientId, parsedSecret.Type); return success;}
public async Task<TokenRequestValidationResult> ValidateRequestAsync(NameValueCollection parameters, ClientSecretValidationResult clientValidationResult){ _logger.LogDebug("Start token request validation"); _validatedRequest = new ValidatedTokenRequest { Raw = parameters ?? throw new ArgumentNullException(nameof(parameters)), Options = _options }; if (clientValidationResult == null) throw new ArgumentNullException(nameof(clientValidationResult)); _validatedRequest.SetClient(clientValidationResult.Client, clientValidationResult.Secret, clientValidationResult.Confirmation); ///////////////////////////////////////////// // check client protocol type ///////////////////////////////////////////// if (_validatedRequest.Client.ProtocolType != IdentityServerConstants.ProtocolTypes.OpenIdConnect) { LogError("Invalid protocol type for client", new { clientId = _validatedRequest.Client.ClientId, expectedProtocolType = IdentityServerConstants.ProtocolTypes.OpenIdConnect, actualProtocolType = _validatedRequest.Client.ProtocolType }); return Invalid(OidcConstants.TokenErrors.InvalidClient); } ///////////////////////////////////////////// // check grant type ///////////////////////////////////////////// var grantType = parameters.Get(OidcConstants.TokenRequest.GrantType); if (grantType.IsMissing()) { LogError("Grant type is missing"); return Invalid(OidcConstants.TokenErrors.UnsupportedGrantType); } if (grantType.Length > _options.InputLengthRestrictions.GrantType) { LogError("Grant type is too long"); return Invalid(OidcConstants.TokenErrors.UnsupportedGrantType); } _validatedRequest.GrantType = grantType; switch (grantType) { case OidcConstants.GrantTypes.AuthorizationCode: return await RunValidationAsync(ValidateAuthorizationCodeRequestAsync, parameters); case OidcConstants.GrantTypes.ClientCredentials: return await RunValidationAsync(ValidateClientCredentialsRequestAsync, parameters); case OidcConstants.GrantTypes.Password: return await RunValidationAsync(ValidateResourceOwnerCredentialRequestAsync, parameters); case OidcConstants.GrantTypes.RefreshToken: return await RunValidationAsync(ValidateRefreshTokenRequestAsync, parameters); case OidcConstants.GrantTypes.DeviceCode: return await RunValidationAsync(ValidateDeviceCodeRequestAsync, parameters); default: return await RunValidationAsync(ValidateExtensionGrantRequestAsync, parameters); }}
private async Task<TokenRequestValidationResult> ValidateAuthorizationCodeRequestAsync(NameValueCollection parameters) { _logger.LogDebug("Start validation of authorization code token request"); ///////////////////////////////////////////// // check if client is authorized for grant type ///////////////////////////////////////////// if (!_validatedRequest.Client.AllowedGrantTypes.ToList().Contains(GrantType.AuthorizationCode) && !_validatedRequest.Client.AllowedGrantTypes.ToList().Contains(GrantType.Hybrid)) { LogError("Client not authorized for code flow"); return Invalid(OidcConstants.TokenErrors.UnauthorizedClient); } ///////////////////////////////////////////// // validate authorization code ///////////////////////////////////////////// var code = parameters.Get(OidcConstants.TokenRequest.Code); if (code.IsMissing()) { LogError("Authorization code is missing"); return Invalid(OidcConstants.TokenErrors.InvalidGrant); } if (code.Length > _options.InputLengthRestrictions.AuthorizationCode) { LogError("Authorization code is too long"); return Invalid(OidcConstants.TokenErrors.InvalidGrant); } _validatedRequest.AuthorizationCodeHandle = code; var authZcode = await _authorizationCodeStore.GetAuthorizationCodeAsync(code); if (authZcode == null) { LogError("Invalid authorization code", new { code }); return Invalid(OidcConstants.TokenErrors.InvalidGrant); } await _authorizationCodeStore.RemoveAuthorizationCodeAsync(code); if (authZcode.CreationTime.HasExceeded(authZcode.Lifetime, _clock.UtcNow.UtcDateTime)) { LogError("Authorization code expired", new { code }); return Invalid(OidcConstants.TokenErrors.InvalidGrant); } ///////////////////////////////////////////// // populate session id ///////////////////////////////////////////// if (authZcode.SessionId.IsPresent()) { _validatedRequest.SessionId = authZcode.SessionId; } ///////////////////////////////////////////// // validate client binding ///////////////////////////////////////////// if (authZcode.ClientId != _validatedRequest.Client.ClientId) { LogError("Client is trying to use a code from a different client", new { clientId = _validatedRequest.Client.ClientId, codeClient = authZcode.ClientId }); return Invalid(OidcConstants.TokenErrors.InvalidGrant); } ///////////////////////////////////////////// // validate code expiration ///////////////////////////////////////////// if (authZcode.CreationTime.HasExceeded(_validatedRequest.Client.AuthorizationCodeLifetime, _clock.UtcNow.UtcDateTime)) { LogError("Authorization code is expired"); return Invalid(OidcConstants.TokenErrors.InvalidGrant); } _validatedRequest.AuthorizationCode = authZcode; _validatedRequest.Subject = authZcode.Subject; ///////////////////////////////////////////// // validate redirect_uri ///////////////////////////////////////////// var redirectUri = parameters.Get(OidcConstants.TokenRequest.RedirectUri); if (redirectUri.IsMissing()) { LogError("Redirect URI is missing"); return Invalid(OidcConstants.TokenErrors.UnauthorizedClient); } if (redirectUri.Equals(_validatedRequest.AuthorizationCode.RedirectUri, StringComparison.Ordinal) == false) { LogError("Invalid redirect_uri", new { redirectUri, expectedRedirectUri = _validatedRequest.AuthorizationCode.RedirectUri }); return Invalid(OidcConstants.TokenErrors.UnauthorizedClient); } ///////////////////////////////////////////// // validate scopes are present ///////////////////////////////////////////// if (_validatedRequest.AuthorizationCode.RequestedScopes == null || !_validatedRequest.AuthorizationCode.RequestedScopes.Any()) { LogError("Authorization code has no associated scopes"); return Invalid(OidcConstants.TokenErrors.InvalidRequest); } ///////////////////////////////////////////// // validate PKCE parameters ///////////////////////////////////////////// var codeVerifier = parameters.Get(OidcConstants.TokenRequest.CodeVerifier); if (_validatedRequest.Client.RequirePkce || _validatedRequest.AuthorizationCode.CodeChallenge.IsPresent()) { _logger.LogDebug("Client required a proof key for code exchange. Starting PKCE validation"); var proofKeyResult = ValidateAuthorizationCodeWithProofKeyParameters(codeVerifier, _validatedRequest.AuthorizationCode); if (proofKeyResult.IsError) { return proofKeyResult; } _validatedRequest.CodeVerifier = codeVerifier; } else { if (codeVerifier.IsPresent()) { LogError("Unexpected code_verifier: {codeVerifier}. This happens when the client is trying to use PKCE, but it is not enabled. Set RequirePkce to true.", codeVerifier); return Invalid(OidcConstants.TokenErrors.InvalidGrant); } } ///////////////////////////////////////////// // make sure user is enabled ///////////////////////////////////////////// var isActiveCtx = new IsActiveContext(_validatedRequest.AuthorizationCode.Subject, _validatedRequest.Client, IdentityServerConstants.ProfileIsActiveCallers.AuthorizationCodeValidation); await _profile.IsActiveAsync(isActiveCtx); if (isActiveCtx.IsActive == false) { LogError("User has been disabled", new { subjectId = _validatedRequest.AuthorizationCode.Subject.GetSubjectId() }); return Invalid(OidcConstants.TokenErrors.InvalidGrant); } _logger.LogDebug("Validation of authorization code token request success"); return Valid(); }
private async Task<TokenRequestValidationResult> ValidateClientCredentialsRequestAsync(NameValueCollection parameters) { _logger.LogDebug("Start client credentials token request validation"); ///////////////////////////////////////////// // check if client is authorized for grant type ///////////////////////////////////////////// if (!_validatedRequest.Client.AllowedGrantTypes.ToList().Contains(GrantType.ClientCredentials)) { LogError("Client not authorized for client credentials flow, check the AllowedGrantTypes setting", new { clientId = _validatedRequest.Client.ClientId }); return Invalid(OidcConstants.TokenErrors.UnauthorizedClient); } ///////////////////////////////////////////// // check if client is allowed to request scopes ///////////////////////////////////////////// if (!await ValidateRequestedScopesAsync(parameters, ignoreImplicitIdentityScopes: true, ignoreImplicitOfflineAccess: true)) { return Invalid(OidcConstants.TokenErrors.InvalidScope); } if (_validatedRequest.ValidatedScopes.ContainsOpenIdScopes) { LogError("Client cannot request OpenID scopes in client credentials flow", new { clientId = _validatedRequest.Client.ClientId }); return Invalid(OidcConstants.TokenErrors.InvalidScope); } if (_validatedRequest.ValidatedScopes.ContainsOfflineAccessScope) { LogError("Client cannot request a refresh token in client credentials flow", new { clientId = _validatedRequest.Client.ClientId }); return Invalid(OidcConstants.TokenErrors.InvalidScope); } _logger.LogDebug("{clientId} credentials token request validation success", _validatedRequest.Client.ClientId); return Valid(); }
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 = 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);}
RefreshToken-刷新令牌。顾名思义,用于刷新通行令牌的凭证。拥有offline_access权限的客户端可以使用刷新令牌。只有授权码、混合流程等由后端参与的授权模式才允许使用刷新令牌。
private async Task<TokenRequestValidationResult> ValidateRefreshTokenRequestAsync(NameValueCollection parameters){ _logger.LogDebug("Start validation of refresh token request"); var refreshTokenHandle = parameters.Get(OidcConstants.TokenRequest.RefreshToken); if (refreshTokenHandle.IsMissing()) { LogError("Refresh token is missing"); return Invalid(OidcConstants.TokenErrors.InvalidRequest); } if (refreshTokenHandle.Length > _options.InputLengthRestrictions.RefreshToken) { LogError("Refresh token too long"); return Invalid(OidcConstants.TokenErrors.InvalidGrant); } var result = await _tokenValidator.ValidateRefreshTokenAsync(refreshTokenHandle, _validatedRequest.Client); if (result.IsError) { LogWarning("Refresh token validation failed. aborting"); return Invalid(OidcConstants.TokenErrors.InvalidGrant); } _validatedRequest.RefreshToken = result.RefreshToken; _validatedRequest.RefreshTokenHandle = refreshTokenHandle; _validatedRequest.Subject = result.RefreshToken.Subject; _logger.LogDebug("Validation of refresh token request success"); return Valid();}
protected virtual async Task<TokenResponse> ProcessAuthorizationCodeRequestAsync(TokenRequestValidationResult request) { Logger.LogTrace("Creating response for authorization code request"); ////////////////////////// // access token ///////////////////////// (var accessToken, var 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 resources = await Resources.FindEnabledResourcesByScopeAsync(request.ValidatedRequest.AuthorizationCode.RequestedScopes); var tokenRequest = new TokenCreationRequest { Subject = request.ValidatedRequest.AuthorizationCode.Subject, Resources = resources, 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; }
protected virtual Task<TokenResponse> ProcessClientCredentialsRequestAsync(TokenRequestValidationResult request){ Logger.LogTrace("Creating response for client credentials request"); return ProcessTokenRequestAsync(request);}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.Scopes.ToSpaceSeparatedString() }; if (refreshToken.IsPresent()) { response.RefreshToken = refreshToken; } return response; }
protected virtual Task<TokenResponse> ProcessPasswordRequestAsync(TokenRequestValidationResult request) { Logger.LogTrace("Creating response for password request"); return ProcessTokenRequestAsync(request); }
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; var creationRequest = new TokenCreationRequest { Subject = subject, ValidatedRequest = request.ValidatedRequest, Resources = await Resources.FindEnabledResourcesByScopeAsync(oldAccessToken.Scopes) }; 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() }; }
联系客服