public override Task ValidateClientAuthentication(ValidateClientAuthenticationContext context) { // JS applications cannot keep their credentials secret: since this Aurelia demo uses a JS app, // client authentication must be skipped to indicate to the OIDC server that the client cannot be trusted. context.Skipped(); return Task.FromResult<object>(null); }
public override Task ValidateClientAuthentication(ValidateClientAuthenticationContext context) { // Since there's only one application and since it's a public client // (i.e a client that cannot keep its credentials private), call Skipped() // to inform the server the request should be accepted without // enforcing client authentication. context.Skipped(); return Task.FromResult<object>(null); }
public override Task ValidateClientAuthentication(ValidateClientAuthenticationContext context) { if (context.ClientId == "AspNetContribSample") { // Note: the context is marked as skipped instead of validated because the client // is not trusted (JavaScript applications cannot keep their credentials secret). context.Skipped(); } else { // If the client_id doesn't correspond to the // intended identifier, reject the request. context.Rejected(); } return Task.FromResult(0); }
public override async Task ValidateClientAuthentication(ValidateClientAuthenticationContext context) { // Note: client authentication is not mandatory for non-confidential client applications like mobile apps // (except when using the client credentials grant type) but this authorization server uses a safer policy // that makes client authentication mandatory and returns an error if client_id or client_secret is missing. // You may consider relaxing it to support the resource owner password credentials grant type // with JavaScript or desktop applications, where client credentials cannot be safely stored. // In this case, call context.Skipped() to inform the server middleware the client is not trusted. if (string.IsNullOrEmpty(context.ClientId) || string.IsNullOrEmpty(context.ClientSecret)) { context.Rejected( error: "invalid_request", description: "Missing credentials: ensure that your credentials were correctly " + "flowed in the request body or in the authorization header"); return; } var database = context.HttpContext.RequestServices.GetRequiredService<ApplicationContext>(); // Retrieve the application details corresponding to the requested client_id. var application = await (from entity in database.Applications where entity.ApplicationID == context.ClientId select entity).SingleOrDefaultAsync(context.HttpContext.RequestAborted); if (application == null) { context.Rejected( error: "invalid_client", description: "Application not found in the database: ensure that your client_id is correct"); return; } if (!string.Equals(context.ClientSecret, application.Secret, StringComparison.Ordinal)) { context.Rejected( error: "invalid_client", description: "Invalid credentials: ensure that you specified a correct client_secret"); return; } context.Validated(); }
/// <summary> /// Called to validate that the origin of the request is a registered "client_id", and that the correct credentials for that client are /// present on the request. If the web application accepts Basic authentication credentials, /// context.TryGetBasicCredentials(out clientId, out clientSecret) may be called to acquire those values if present in the request header. If the web /// application accepts "client_id" and "client_secret" as form encoded POST parameters, /// context.TryGetFormCredentials(out clientId, out clientSecret) may be called to acquire those values if present in the request body. /// If context.Validated is not called the request will not proceed further. /// </summary> /// <param name="context">The context of the event carries information in and results out.</param> /// <returns>Task to enable asynchronous execution</returns> public virtual Task ValidateClientAuthentication(ValidateClientAuthenticationContext context) => OnValidateClientAuthentication(context);
private async Task InvokeValidationEndpointAsync() { OpenIdConnectMessage request; // See https://tools.ietf.org/html/rfc7662#section-2.1 // and https://tools.ietf.org/html/rfc7662#section-4 if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase)) { request = new OpenIdConnectMessage(Request.Query.ToDictionary()) { RequestType = OpenIdConnectRequestType.AuthenticationRequest }; } else if (string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase)) { // See http://openid.net/specs/openid-connect-core-1_0.html#FormSerialization if (string.IsNullOrEmpty(Request.ContentType)) { await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed validation request has been received: " + "the mandatory 'Content-Type' header was missing from the POST request." }); return; } // May have media/type; charset=utf-8, allow partial match. if (!Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) { await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed validation request has been received: " + "the 'Content-Type' header contained an unexcepted value. " + "Make sure to use 'application/x-www-form-urlencoded'." }); return; } var form = await Request.ReadFormAsync(Context.RequestAborted); request = new OpenIdConnectMessage(form.ToDictionary()) { RequestType = OpenIdConnectRequestType.AuthenticationRequest }; } else { Logger.LogInformation("A malformed request has been received by the validation endpoint."); await SendErrorPageAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed validation request has been received: " + "make sure to use either GET or POST." }); return; } if (string.IsNullOrWhiteSpace(request.GetToken())) { await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed validation request has been received: " + "a 'token' parameter with an access, refresh, or identity token is required." }); return; } // When client_id and client_secret are both null, try to extract them from the Authorization header. // See http://tools.ietf.org/html/rfc6749#section-2.3.1 and // http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication if (string.IsNullOrEmpty(request.ClientId) && string.IsNullOrEmpty(request.ClientSecret)) { string header = Request.Headers[HeaderNames.Authorization]; if (!string.IsNullOrEmpty(header) && header.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase)) { try { var value = header.Substring("Basic ".Length).Trim(); var data = Encoding.UTF8.GetString(Convert.FromBase64String(value)); var index = data.IndexOf(':'); if (index >= 0) { request.ClientId = data.Substring(0, index); request.ClientSecret = data.Substring(index + 1); } } catch (FormatException) { } catch (ArgumentException) { } } } var clientNotification = new ValidateClientAuthenticationContext(Context, Options, request); await Options.Provider.ValidateClientAuthentication(clientNotification); // Reject the request if client authentication was rejected. if (clientNotification.IsRejected) { Logger.LogError("The validation request was rejected " + "because client authentication was invalid."); await SendPayloadAsync(new JObject { [OpenIdConnectConstants.Claims.Active] = false }); return; } // Ensure that the client_id has been set from the ValidateClientAuthentication event. else if (clientNotification.IsValidated && string.IsNullOrEmpty(request.ClientId)) { Logger.LogError("Client authentication was validated but the client_id was not set."); await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.ServerError, ErrorDescription = "An internal server error occurred." }); return; } AuthenticationTicket ticket = null; // Note: use the "token_type_hint" parameter to determine // the type of the token sent by the client application. // See https://tools.ietf.org/html/rfc7662#section-2.1 switch (request.GetTokenTypeHint()) { case OpenIdConnectConstants.Usages.AccessToken: ticket = await DeserializeAccessTokenAsync(request.GetToken(), request); break; case OpenIdConnectConstants.Usages.RefreshToken: ticket = await DeserializeRefreshTokenAsync(request.GetToken(), request); break; case OpenIdConnectConstants.Usages.IdToken: ticket = await DeserializeIdentityTokenAsync(request.GetToken(), request); break; } // Note: if the token can't be found using "token_type_hint", // the search must be extended to all supported token types. // See https://tools.ietf.org/html/rfc7662#section-2.1 if (ticket == null) { ticket = await DeserializeAccessTokenAsync(request.GetToken(), request) ?? await DeserializeIdentityTokenAsync(request.GetToken(), request) ?? await DeserializeRefreshTokenAsync(request.GetToken(), request); } if (ticket == null) { Logger.LogInformation("The validation request was rejected because the token was invalid."); await SendPayloadAsync(new JObject { [OpenIdConnectConstants.Claims.Active] = false }); return; } // Note: unlike refresh or identity tokens that can only be validated by client applications, // access tokens can be validated by either resource servers or client applications: // in both cases, the caller must be authenticated if the ticket is marked as confidential. if (clientNotification.IsSkipped && ticket.IsConfidential()) { Logger.LogWarning("The validation request was rejected " + "because the caller was not authenticated."); await SendPayloadAsync(new JObject { [OpenIdConnectConstants.Claims.Active] = false }); return; } // If the ticket is already expired, directly return active=false. if (ticket.Properties.ExpiresUtc.HasValue && ticket.Properties.ExpiresUtc < Options.SystemClock.UtcNow) { Logger.LogDebug("expired token"); await SendPayloadAsync(new JObject { [OpenIdConnectConstants.Claims.Active] = false }); return; } switch (ticket.GetUsage()) { case OpenIdConnectConstants.Usages.AccessToken: { // When the caller is authenticated, ensure it is // listed as a valid audience or authorized presenter. if (clientNotification.IsValidated && !ticket.HasAudience(clientNotification.ClientId) && !ticket.HasPresenter(clientNotification.ClientId)) { Logger.LogWarning("The validation request was rejected because the access token " + "was issued to a different client or for another resource server."); await SendPayloadAsync(new JObject { [OpenIdConnectConstants.Claims.Active] = false }); return; } break; } case OpenIdConnectConstants.Usages.IdToken: { // When the caller is authenticated, reject the validation // request if the caller is not listed as a valid audience. if (clientNotification.IsValidated && !ticket.HasAudience(clientNotification.ClientId)) { Logger.LogWarning("The validation request was rejected because the " + "identity token was issued to a different client."); await SendPayloadAsync(new JObject { [OpenIdConnectConstants.Claims.Active] = false }); return; } break; } case OpenIdConnectConstants.Usages.RefreshToken: { // When the caller is authenticated, reject the validation request if the caller // doesn't correspond to the client application the token was issued to. if (clientNotification.IsValidated && !ticket.HasPresenter(clientNotification.ClientId)) { Logger.LogWarning("The validation request was rejected because the " + "refresh token was issued to a different client."); await SendPayloadAsync(new JObject { [OpenIdConnectConstants.Claims.Active] = false }); return; } break; } } // Insert the validation request in the ASP.NET context. Context.SetOpenIdConnectRequest(request); var notification = new ValidationEndpointContext(Context, Options, request, ticket); notification.Active = true; // Note: "token_type" may be null when the received token is not an access token. // See https://tools.ietf.org/html/rfc7662#section-2.2 and https://tools.ietf.org/html/rfc6749#section-5.1 notification.TokenType = ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.TokenType); notification.Issuer = Context.GetIssuer(Options); notification.Subject = ticket.Principal.GetClaim(ClaimTypes.NameIdentifier); notification.IssuedAt = ticket.Properties.IssuedUtc; notification.ExpiresAt = ticket.Properties.ExpiresUtc; // Copy the audiences extracted from the "aud" claim. foreach (var audience in ticket.GetAudiences()) { notification.Audiences.Add(audience); } // Note: non-metadata claims are only added if the caller is authenticated AND is in the specified audiences. if (clientNotification.IsValidated && notification.Audiences.Contains(clientNotification.ClientId)) { // Extract the main identity associated with the principal. var identity = (ClaimsIdentity)ticket.Principal.Identity; notification.Username = identity.Name; notification.Scope = ticket.GetProperty(OpenIdConnectConstants.Properties.Scopes); // Potentially sensitive claims are only exposed to trusted callers // if the ticket corresponds to an access or identity token. if (ticket.IsAccessToken() || ticket.IsIdentityToken()) { foreach (var claim in ticket.Principal.Claims) { // Exclude standard claims, that are already handled via strongly-typed properties. // Make sure to always update this list when adding new built-in claim properties. if (string.Equals(claim.Type, identity.NameClaimType, StringComparison.Ordinal) || string.Equals(claim.Type, ClaimTypes.NameIdentifier, StringComparison.Ordinal)) { continue; } if (string.Equals(claim.Type, JwtRegisteredClaimNames.Aud, StringComparison.Ordinal) || string.Equals(claim.Type, JwtRegisteredClaimNames.Exp, StringComparison.Ordinal) || string.Equals(claim.Type, JwtRegisteredClaimNames.Iat, StringComparison.Ordinal) || string.Equals(claim.Type, JwtRegisteredClaimNames.Iss, StringComparison.Ordinal) || string.Equals(claim.Type, JwtRegisteredClaimNames.Nbf, StringComparison.Ordinal) || string.Equals(claim.Type, JwtRegisteredClaimNames.Sub, StringComparison.Ordinal)) { continue; } if (string.Equals(claim.Type, OpenIdConnectConstants.Claims.TokenType, StringComparison.Ordinal) || string.Equals(claim.Type, OpenIdConnectConstants.Claims.Scope, StringComparison.Ordinal)) { continue; } string type; // Try to resolve the short name associated with the claim type: // if none can be found, the claim type is used as-is. if (!JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.TryGetValue(claim.Type, out type)) { type = claim.Type; } // Note: make sure to use the indexer // syntax to avoid duplicate properties. notification.Claims[type] = claim.Value; } } } await Options.Provider.ValidationEndpoint(notification); // Flow the changes made to the authentication ticket. ticket = notification.AuthenticationTicket; if (notification.HandledResponse) { return; } var payload = new JObject(); payload.Add(OpenIdConnectConstants.Claims.Active, notification.Active); // Only add the other properties if // the token is considered as active. if (notification.Active) { if (!string.IsNullOrEmpty(notification.Issuer)) { payload.Add(JwtRegisteredClaimNames.Iss, notification.Issuer); } if (!string.IsNullOrEmpty(notification.Username)) { payload.Add(OpenIdConnectConstants.Claims.Username, notification.Username); } if (!string.IsNullOrEmpty(notification.Subject)) { payload.Add(JwtRegisteredClaimNames.Sub, notification.Subject); } if (!string.IsNullOrEmpty(notification.Scope)) { payload.Add(OpenIdConnectConstants.Claims.Scope, notification.Scope); } if (notification.IssuedAt.HasValue) { payload.Add(JwtRegisteredClaimNames.Iat, EpochTime.GetIntDate(notification.IssuedAt.Value.UtcDateTime)); payload.Add(JwtRegisteredClaimNames.Nbf, EpochTime.GetIntDate(notification.IssuedAt.Value.UtcDateTime)); } if (notification.ExpiresAt.HasValue) { payload.Add(JwtRegisteredClaimNames.Exp, EpochTime.GetIntDate(notification.ExpiresAt.Value.UtcDateTime)); } if (!string.IsNullOrEmpty(notification.TokenType)) { payload.Add(OpenIdConnectConstants.Claims.TokenType, notification.TokenType); } switch (notification.Audiences.Count) { case 0: break; case 1: payload.Add(JwtRegisteredClaimNames.Aud, notification.Audiences[0]); break; default: payload.Add(JwtRegisteredClaimNames.Aud, JArray.FromObject(notification.Audiences)); break; } foreach (var claim in notification.Claims) { // Ignore claims whose value is null. if (claim.Value == null) { continue; } // Note: make sure to use the indexer // syntax to avoid duplicate properties. payload[claim.Key] = claim.Value; } } var context = new ValidationEndpointResponseContext(Context, Options, payload); await Options.Provider.ValidationEndpointResponse(context); if (context.HandledResponse) { return; } using (var buffer = new MemoryStream()) using (var writer = new JsonTextWriter(new StreamWriter(buffer))) { payload.WriteTo(writer); writer.Flush(); Response.ContentLength = buffer.Length; Response.ContentType = "application/json;charset=UTF-8"; Response.Headers[HeaderNames.CacheControl] = "no-cache"; Response.Headers[HeaderNames.Pragma] = "no-cache"; Response.Headers[HeaderNames.Expires] = "-1"; buffer.Seek(offset: 0, loc: SeekOrigin.Begin); await buffer.CopyToAsync(Response.Body, 4096, Context.RequestAborted); } }
private async Task InvokeTokenEndpointAsync() { if (!string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase)) { await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed token request has been received: make sure to use POST." }); return; } // See http://openid.net/specs/openid-connect-core-1_0.html#FormSerialization if (string.IsNullOrEmpty(Request.ContentType)) { await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed token request has been received: " + "the mandatory 'Content-Type' header was missing from the POST request." }); return; } // May have media/type; charset=utf-8, allow partial match. if (!Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) { await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed token request has been received: " + "the 'Content-Type' header contained an unexcepted value. " + "Make sure to use 'application/x-www-form-urlencoded'." }); return; } var form = await Request.ReadFormAsync(Context.RequestAborted); var request = new OpenIdConnectMessage(form.ToDictionary()) { RequestType = OpenIdConnectRequestType.TokenRequest }; // Reject token requests missing the mandatory grant_type parameter. if (string.IsNullOrEmpty(request.GrantType)) { Logger.LogError("The token request was rejected because the grant type was missing."); await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The mandatory 'grant_type' parameter was missing.", }); return; } // Reject grant_type=authorization_code requests missing the authorization code. // See https://tools.ietf.org/html/rfc6749#section-4.1.3 else if (request.IsAuthorizationCodeGrantType() && string.IsNullOrEmpty(request.Code)) { Logger.LogError("The token request was rejected because the authorization code was missing."); await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The mandatory 'code' parameter was missing." }); return; } // Reject grant_type=refresh_token requests missing the refresh token. // See https://tools.ietf.org/html/rfc6749#section-6 else if (request.IsRefreshTokenGrantType() && string.IsNullOrEmpty(request.RefreshToken)) { Logger.LogError("The token request was rejected because the refresh token was missing."); await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The mandatory 'refresh_token' parameter was missing." }); return; } // Reject grant_type=password requests missing username or password. // See https://tools.ietf.org/html/rfc6749#section-4.3.2 else if (request.IsPasswordGrantType() && (string.IsNullOrEmpty(request.Username) || string.IsNullOrEmpty(request.Password))) { Logger.LogError("The token request was rejected because the resource owner credentials were missing."); await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The mandatory 'username' and/or 'password' parameters " + "was/were missing from the request message." }); return; } // When client_id and client_secret are both null, try to extract them from the Authorization header. // See http://tools.ietf.org/html/rfc6749#section-2.3.1 and // http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication if (string.IsNullOrEmpty(request.ClientId) && string.IsNullOrEmpty(request.ClientSecret)) { string header = Request.Headers[HeaderNames.Authorization]; if (!string.IsNullOrEmpty(header) && header.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase)) { try { var value = header.Substring("Basic ".Length).Trim(); var data = Encoding.UTF8.GetString(Convert.FromBase64String(value)); var index = data.IndexOf(':'); if (index >= 0) { request.ClientId = data.Substring(0, index); request.ClientSecret = data.Substring(index + 1); } } catch (FormatException) { } catch (ArgumentException) { } } } var clientNotification = new ValidateClientAuthenticationContext(Context, Options, request); await Options.Provider.ValidateClientAuthentication(clientNotification); // Reject the request if client authentication was rejected. if (clientNotification.IsRejected) { Logger.LogError("invalid client authentication."); await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = clientNotification.Error ?? OpenIdConnectConstants.Errors.InvalidClient, ErrorDescription = clientNotification.ErrorDescription, ErrorUri = clientNotification.ErrorUri }); return; } // Reject grant_type=client_credentials requests if client authentication was skipped. else if (clientNotification.IsSkipped && request.IsClientCredentialsGrantType()) { Logger.LogError("client authentication is required for client_credentials grant type."); await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "client authentication is required when using client_credentials" }); return; } // Ensure that the client_id has been set from the ValidateClientAuthentication event. else if (clientNotification.IsValidated && string.IsNullOrEmpty(request.ClientId)) { Logger.LogError("Client authentication was validated but the client_id was not set."); await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.ServerError, ErrorDescription = "An internal server error occurred." }); return; } var validatingContext = new ValidateTokenRequestContext(Context, Options, request); // Validate the token request immediately if the grant type used by // the client application doesn't rely on a previously-issued token/code. if (!request.IsAuthorizationCodeGrantType() && !request.IsRefreshTokenGrantType()) { await Options.Provider.ValidateTokenRequest(validatingContext); if (!validatingContext.IsValidated) { // Note: use invalid_request as the default error if none has been explicitly provided. await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = validatingContext.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = validatingContext.ErrorDescription, ErrorUri = validatingContext.ErrorUri }); return; } } AuthenticationTicket ticket = null; // See http://tools.ietf.org/html/rfc6749#section-4.1 // and http://tools.ietf.org/html/rfc6749#section-4.1.3 (authorization code grant). // See http://tools.ietf.org/html/rfc6749#section-6 (refresh token grant). if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType()) { ticket = request.IsAuthorizationCodeGrantType() ? await DeserializeAuthorizationCodeAsync(request.Code, request) : await DeserializeRefreshTokenAsync(request.RefreshToken, request); if (ticket == null) { Logger.LogError("invalid ticket"); await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Invalid ticket" }); return; } if (!ticket.Properties.ExpiresUtc.HasValue || ticket.Properties.ExpiresUtc < Options.SystemClock.UtcNow) { Logger.LogError("expired ticket"); await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Expired ticket" }); return; } // If the client was fully authenticated when retrieving its refresh token, // the current request must be rejected if client authentication was not enforced. if (request.IsRefreshTokenGrantType() && !clientNotification.IsValidated && ticket.IsConfidential()) { Logger.LogError("client authentication is required to use this ticket"); await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Client authentication is required to use this ticket" }); return; } // Note: presenters may be empty during a grant_type=refresh_token request if the refresh token // was issued to a public client but cannot be null for an authorization code grant request. var presenters = ticket.GetPresenters(); if (request.IsAuthorizationCodeGrantType() && !presenters.Any()) { Logger.LogError("The client the authorization code was issued to cannot be resolved."); await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.ServerError, ErrorDescription = "An internal server error occurred." }); return; } // At this stage, client_id cannot be null for grant_type=authorization_code requests, // as it must either be set in the ValidateClientAuthentication notification // by the developer or manually flowed by non-confidential client applications. // See https://tools.ietf.org/html/rfc6749#section-4.1.3 if (request.IsAuthorizationCodeGrantType() && string.IsNullOrEmpty(request.ClientId)) { Logger.LogError("client_id was missing from the token request"); await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "client_id was missing from the token request" }); return; } // Ensure the authorization code/refresh token was issued to the client application making the token request. // Note: when using the refresh token grant, client_id is optional but must validated if present. // As a consequence, this check doesn't depend on the actual status of client authentication. // See https://tools.ietf.org/html/rfc6749#section-6 // and http://openid.net/specs/openid-connect-core-1_0.html#RefreshingAccessToken if (!string.IsNullOrEmpty(request.ClientId) && presenters.Any() && !presenters.Contains(request.ClientId, StringComparer.Ordinal)) { Logger.LogError("ticket does not contain matching client_id"); await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Ticket does not contain matching client_id" }); return; } // Validate the redirect_uri flowed by the client application during this token request. // Note: for pure OAuth2 requests, redirect_uri is only mandatory if the authorization request // contained an explicit redirect_uri. OpenID Connect requests MUST include a redirect_uri // but the specifications allow proceeding the token request without returning an error // if the authorization request didn't contain an explicit redirect_uri. // See https://tools.ietf.org/html/rfc6749#section-4.1.3 // and http://openid.net/specs/openid-connect-core-1_0.html#TokenRequestValidation string address; if (request.IsAuthorizationCodeGrantType() && ticket.Properties.Items.TryGetValue(OpenIdConnectConstants.Properties.RedirectUri, out address)) { ticket.Properties.Items.Remove(OpenIdConnectConstants.Properties.RedirectUri); if (string.IsNullOrEmpty(request.RedirectUri)) { Logger.LogError("redirect_uri was missing from the grant_type=authorization_code request."); await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "redirect_uri was missing from the token request" }); return; } else if (!string.Equals(address, request.RedirectUri, StringComparison.Ordinal)) { Logger.LogError("authorization code does not contain matching redirect_uri"); await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Authorization code does not contain matching redirect_uri" }); return; } } if (!string.IsNullOrEmpty(request.Resource)) { // When an explicit resource parameter has been included in the token request // but was missing from the authorization request, the request MUST be rejected. var resources = ticket.GetResources(); if (!resources.Any()) { Logger.LogError("token request cannot contain a resource"); await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Token request cannot contain a resource parameter" + "if the authorization request didn't contain one" }); return; } // When an explicit resource parameter has been included in the token request, // the authorization server MUST ensure that it doesn't contain resources // that were not allowed during the authorization request. else if (!new HashSet <string>(resources).IsSupersetOf(request.GetResources())) { Logger.LogError("token request does not contain matching resource"); await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Token request doesn't contain a valid resource parameter" }); return; } // Replace the resources initially granted by the resources // listed by the client application in the token request. ticket.SetResources(request.GetResources()); } if (!string.IsNullOrEmpty(request.Scope)) { // When an explicit scope parameter has been included in the token request // but was missing from the authorization request, the request MUST be rejected. // See http://tools.ietf.org/html/rfc6749#section-6 var scopes = ticket.GetScopes(); if (!scopes.Any()) { Logger.LogError("token request cannot contain a scope"); await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Token request cannot contain a scope parameter" + "if the authorization request didn't contain one" }); return; } // When an explicit scope parameter has been included in the token request, // the authorization server MUST ensure that it doesn't contain scopes // that were not allowed during the authorization request. // See https://tools.ietf.org/html/rfc6749#section-6 else if (!new HashSet <string>(scopes).IsSupersetOf(request.GetScopes())) { Logger.LogError("token request does not contain matching scope"); await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Token request doesn't contain a valid scope parameter" }); return; } // Replace the scopes initially granted by the scopes // listed by the client application in the token request. ticket.SetScopes(request.GetScopes()); } // Expose the authentication ticket extracted from the authorization // code or the refresh token before invoking ValidateTokenRequest. validatingContext.AuthenticationTicket = ticket; await Options.Provider.ValidateTokenRequest(validatingContext); if (!validatingContext.IsValidated) { // Note: use invalid_request as the default error if none has been explicitly provided. await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = validatingContext.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = validatingContext.ErrorDescription, ErrorUri = validatingContext.ErrorUri }); return; } if (request.IsAuthorizationCodeGrantType()) { // Note: the authentication ticket is copied to avoid modifying the properties of the authorization code. var context = new GrantAuthorizationCodeContext(Context, Options, request, ticket.Copy()); await Options.Provider.GrantAuthorizationCode(context); if (!context.IsValidated) { // Note: use invalid_grant as the default error if none has been explicitly provided. await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = context.Error ?? OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = context.ErrorDescription, ErrorUri = context.ErrorUri }); return; } ticket = context.AuthenticationTicket; } else { // Note: the authentication ticket is copied to avoid modifying the properties of the refresh token. var context = new GrantRefreshTokenContext(Context, Options, request, ticket.Copy()); await Options.Provider.GrantRefreshToken(context); if (!context.IsValidated) { // Note: use invalid_grant as the default error if none has been explicitly provided. await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = context.Error ?? OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = context.ErrorDescription, ErrorUri = context.ErrorUri }); return; } ticket = context.AuthenticationTicket; } // By default, when using the authorization code or the refresh token grants, the authentication ticket // extracted from the code/token is used as-is. If the developer didn't provide his own ticket // or didn't set an explicit expiration date, the ticket properties are reset to avoid aligning the // expiration date of the generated tokens with the lifetime of the authorization code/refresh token. if (ticket.Properties.IssuedUtc == validatingContext.AuthenticationTicket.Properties.IssuedUtc) { ticket.Properties.IssuedUtc = null; } if (ticket.Properties.ExpiresUtc == validatingContext.AuthenticationTicket.Properties.ExpiresUtc) { ticket.Properties.ExpiresUtc = null; } } // See http://tools.ietf.org/html/rfc6749#section-4.3 // and http://tools.ietf.org/html/rfc6749#section-4.3.2 else if (request.IsPasswordGrantType()) { var context = new GrantResourceOwnerCredentialsContext(Context, Options, request); await Options.Provider.GrantResourceOwnerCredentials(context); if (!context.IsValidated) { // Note: use invalid_grant as the default error if none has been explicitly provided. await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = context.Error ?? OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = context.ErrorDescription, ErrorUri = context.ErrorUri }); return; } ticket = context.AuthenticationTicket; } // See http://tools.ietf.org/html/rfc6749#section-4.4 // and http://tools.ietf.org/html/rfc6749#section-4.4.2 else if (request.IsClientCredentialsGrantType()) { var context = new GrantClientCredentialsContext(Context, Options, request); await Options.Provider.GrantClientCredentials(context); if (!context.IsValidated) { // Note: use unauthorized_client as the default error if none has been explicitly provided. await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = context.Error ?? OpenIdConnectConstants.Errors.UnauthorizedClient, ErrorDescription = context.ErrorDescription, ErrorUri = context.ErrorUri }); return; } ticket = context.AuthenticationTicket; } // See http://tools.ietf.org/html/rfc6749#section-8.3 else { var context = new GrantCustomExtensionContext(Context, Options, request); await Options.Provider.GrantCustomExtension(context); if (!context.IsValidated) { // Note: use unsupported_grant_type as the default error if none has been explicitly provided. await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = context.Error ?? OpenIdConnectConstants.Errors.UnsupportedGrantType, ErrorDescription = context.ErrorDescription, ErrorUri = context.ErrorUri }); return; } ticket = context.AuthenticationTicket; } var notification = new TokenEndpointContext(Context, Options, request, ticket); await Options.Provider.TokenEndpoint(notification); if (notification.HandledResponse) { return; } // Flow the changes made to the ticket. ticket = notification.Ticket; // Ensure an authentication ticket has been provided: // a null ticket MUST result in an internal server error. if (ticket == null) { Logger.LogError("authentication ticket missing"); await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.ServerError }); return; } if (clientNotification.IsValidated) { // Store a boolean indicating whether the ticket should be marked as confidential. ticket.Properties.Items[OpenIdConnectConstants.Properties.Confidential] = "true"; } // Note: the application is allowed to specify a different "scope": in this case, // don't replace the "scope" property stored in the authentication ticket. if (!ticket.Properties.Items.ContainsKey(OpenIdConnectConstants.Properties.Scopes) && request.HasScope(OpenIdConnectConstants.Scopes.OpenId)) { // Always include the "openid" scope when the developer didn't explicitly call SetScopes. ticket.Properties.Items[OpenIdConnectConstants.Properties.Scopes] = OpenIdConnectConstants.Scopes.OpenId; } var response = new OpenIdConnectMessage(); // Note: by default, an access token is always returned, but the client application can use the "response_type" parameter // to only include specific types of tokens. When this parameter is missing, an access token is always generated. if (string.IsNullOrEmpty(request.ResponseType) || request.HasResponseType(OpenIdConnectConstants.ResponseTypes.Token)) { // Make sure to create a copy of the authentication properties // to avoid modifying the properties set on the original ticket. var properties = ticket.Properties.Copy(); string resources; if (!properties.Items.TryGetValue(OpenIdConnectConstants.Properties.Resources, out resources)) { Logger.LogInformation("No explicit resource has been associated with the authentication ticket: " + "the access token will thus be issued without any audience attached."); } // Note: when the "resource" parameter added to the OpenID Connect response // is identical to the request parameter, keeping it is not necessary. if (request.IsAuthorizationCodeGrantType() || (!string.IsNullOrEmpty(request.Resource) && !string.Equals(request.Resource, resources, StringComparison.Ordinal))) { response.Resource = resources; } // Note: when the "scope" parameter added to the OpenID Connect response // is identical to the request parameter, keeping it is not necessary. string scopes; properties.Items.TryGetValue(OpenIdConnectConstants.Properties.Scopes, out scopes); if (request.IsAuthorizationCodeGrantType() || (!string.IsNullOrEmpty(request.Scope) && !string.Equals(request.Scope, scopes, StringComparison.Ordinal))) { response.Scope = scopes; } // When sliding expiration is disabled, the access token added to the response // cannot live longer than the refresh token that was used in the token request. if (request.IsRefreshTokenGrantType() && !Options.UseSlidingExpiration && validatingContext.AuthenticationTicket.Properties.ExpiresUtc.HasValue && validatingContext.AuthenticationTicket.Properties.ExpiresUtc.Value < (Options.SystemClock.UtcNow + Options.AccessTokenLifetime)) { properties.ExpiresUtc = validatingContext.AuthenticationTicket.Properties.ExpiresUtc; } response.TokenType = OpenIdConnectConstants.TokenTypes.Bearer; response.AccessToken = await SerializeAccessTokenAsync(ticket.Principal, properties, request, response); // Ensure that an access token is issued to avoid returning an invalid response. // See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Combinations if (string.IsNullOrEmpty(response.AccessToken)) { Logger.LogError("SerializeAccessTokenAsync returned no access token."); await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.ServerError, ErrorDescription = "no valid access token was issued" }); return; } // properties.ExpiresUtc is automatically set by SerializeAccessTokenAsync but the end user // is free to set a null value directly in the SerializeAccessToken event. if (properties.ExpiresUtc.HasValue && properties.ExpiresUtc > Options.SystemClock.UtcNow) { var lifetime = properties.ExpiresUtc.Value - Options.SystemClock.UtcNow; var expiration = (long)(lifetime.TotalSeconds + .5); response.ExpiresIn = expiration.ToString(CultureInfo.InvariantCulture); } } // Note: by default, an identity token is always returned when the "openid" scope has been requested, // but the client application can use the "response_type" parameter to only include specific types of tokens. // When this parameter is missing, an identity token is always generated. if (ticket.HasScope(OpenIdConnectConstants.Scopes.OpenId) && (string.IsNullOrEmpty(request.ResponseType) || request.HasResponseType(OpenIdConnectConstants.ResponseTypes.IdToken))) { // Make sure to create a copy of the authentication properties // to avoid modifying the properties set on the original ticket. var properties = ticket.Properties.Copy(); // When sliding expiration is disabled, the identity token added to the response // cannot live longer than the refresh token that was used in the token request. if (request.IsRefreshTokenGrantType() && !Options.UseSlidingExpiration && validatingContext.AuthenticationTicket.Properties.ExpiresUtc.HasValue && validatingContext.AuthenticationTicket.Properties.ExpiresUtc.Value < (Options.SystemClock.UtcNow + Options.IdentityTokenLifetime)) { properties.ExpiresUtc = validatingContext.AuthenticationTicket.Properties.ExpiresUtc; } response.IdToken = await SerializeIdentityTokenAsync(ticket.Principal, properties, request, response); // Ensure that an identity token is issued to avoid returning an invalid response. // See http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse // and http://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse if (string.IsNullOrEmpty(response.IdToken)) { Logger.LogError("SerializeIdentityTokenAsync returned no identity token."); await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.ServerError, ErrorDescription = "no valid identity token was issued" }); return; } } // Note: by default, a refresh token is always returned when the "offline_access" scope has been requested, // but the client application can use the "response_type" parameter to only include specific types of tokens. // When this parameter is missing, a refresh token is always generated. if (ticket.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess) && (string.IsNullOrEmpty(request.ResponseType) || request.HasResponseType(OpenIdConnectConstants.Parameters.RefreshToken))) { // Make sure to create a copy of the authentication properties // to avoid modifying the properties set on the original ticket. var properties = ticket.Properties.Copy(); // When sliding expiration is disabled, the refresh token added to the response // cannot live longer than the refresh token that was used in the token request. if (request.IsRefreshTokenGrantType() && !Options.UseSlidingExpiration && validatingContext.AuthenticationTicket.Properties.ExpiresUtc.HasValue && validatingContext.AuthenticationTicket.Properties.ExpiresUtc.Value < (Options.SystemClock.UtcNow + Options.RefreshTokenLifetime)) { properties.ExpiresUtc = validatingContext.AuthenticationTicket.Properties.ExpiresUtc; } response.RefreshToken = await SerializeRefreshTokenAsync(ticket.Principal, properties, request, response); } var payload = new JObject(); foreach (var parameter in response.Parameters) { payload.Add(parameter.Key, parameter.Value); } var responseNotification = new TokenEndpointResponseContext(Context, Options, ticket, request, payload); await Options.Provider.TokenEndpointResponse(responseNotification); if (responseNotification.HandledResponse) { return; } using (var buffer = new MemoryStream()) using (var writer = new JsonTextWriter(new StreamWriter(buffer))) { payload.WriteTo(writer); writer.Flush(); Response.ContentLength = buffer.Length; Response.ContentType = "application/json;charset=UTF-8"; Response.Headers[HeaderNames.CacheControl] = "no-cache"; Response.Headers[HeaderNames.Pragma] = "no-cache"; Response.Headers[HeaderNames.Expires] = "-1"; buffer.Seek(offset: 0, loc: SeekOrigin.Begin); await buffer.CopyToAsync(Response.Body, 4096, Context.RequestAborted); } }
/// <summary> /// Called to validate that the origin of the request is a registered "client_id", and that the correct credentials for that client are /// present on the request. If the web application accepts Basic authentication credentials, /// context.TryGetBasicCredentials(out clientId, out clientSecret) may be called to acquire those values if present in the request header. If the web /// application accepts "client_id" and "client_secret" as form encoded POST parameters, /// context.TryGetFormCredentials(out clientId, out clientSecret) may be called to acquire those values if present in the request body. /// If context.Validated is not called the request will not proceed further. /// </summary> /// <param name="context">The context of the event carries information in and results out.</param> /// <returns>Task to enable asynchronous execution</returns> public virtual Task ValidateClientAuthentication(ValidateClientAuthenticationContext context) => OnValidateClientAuthentication(context);