public void GetResources_ReturnsExpectedResources(string resource, string[] resources) { // Arrange var ticket = new AuthenticationTicket( new ClaimsIdentity(), new AuthenticationProperties()); ticket.Properties.Dictionary[OpenIdConnectConstants.Properties.Resources] = resource; // Act and assert Assert.Equal(resources, ticket.GetResources()); }
private async Task <bool> InvokeTokenEndpointAsync() { if (!string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase)) { Options.Logger.LogError("The token request was rejected because an invalid " + "HTTP method was received: {Method}.", Request.Method); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed token request has been received: make sure to use POST." })); } // See http://openid.net/specs/openid-connect-core-1_0.html#FormSerialization if (string.IsNullOrEmpty(Request.ContentType)) { Options.Logger.LogError("The token request was rejected because the " + "mandatory 'Content-Type' header was missing."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed token request has been received: " + "the mandatory 'Content-Type' header was missing from the POST request." })); } // May have media/type; charset=utf-8, allow partial match. if (!Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) { Options.Logger.LogError("The token request was rejected because an invalid 'Content-Type' " + "header was received: {ContentType}.", Request.ContentType); return(await SendTokenResponseAsync(new OpenIdConnectResponse { 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'." })); } var request = new OpenIdConnectRequest(await Request.ReadFormAsync()); // Note: set the message type before invoking the ExtractTokenRequest event. request.SetProperty(OpenIdConnectConstants.Properties.MessageType, OpenIdConnectConstants.MessageTypes.Token); // Store the token request in the OWIN context. Context.SetOpenIdConnectRequest(request); var @event = new ExtractTokenRequestContext(Context, Options, request); await Options.Provider.ExtractTokenRequest(@event); if (@event.HandledResponse) { return(true); } else if (@event.Skipped) { return(false); } else if (@event.IsRejected) { Options.Logger.LogError("The token request was rejected with the following error: {Error} ; {Description}", /* Error: */ @event.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ @event.ErrorDescription); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = @event.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = @event.ErrorDescription, ErrorUri = @event.ErrorUri })); } // Reject token requests missing the mandatory grant_type parameter. if (string.IsNullOrEmpty(request.GrantType)) { Options.Logger.LogError("The token request was rejected because the grant type was missing."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The mandatory 'grant_type' parameter was missing.", })); } // Reject grant_type=authorization_code requests if the authorization endpoint is disabled. else if (request.IsAuthorizationCodeGrantType() && !Options.AuthorizationEndpointPath.HasValue) { Options.Logger.LogError("The token request was rejected because the authorization code grant was disabled."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.UnsupportedGrantType, ErrorDescription = "The authorization code grant is not allowed by this authorization server." })); } // 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)) { Options.Logger.LogError("The token request was rejected because the authorization code was missing."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The mandatory 'code' parameter was missing." })); } // 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)) { Options.Logger.LogError("The token request was rejected because the refresh token was missing."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The mandatory 'refresh_token' parameter was missing." })); } // 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))) { Options.Logger.LogError("The token request was rejected because the resource owner credentials were missing."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The mandatory 'username' and/or 'password' parameters " + "was/were missing from the request message." })); } // 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)) { var header = Request.Headers.Get("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 context = new ValidateTokenRequestContext(Context, Options, request); await Options.Provider.ValidateTokenRequest(context); // If the validation context was set as fully validated, // mark the OpenID Connect request as confidential. if (context.IsValidated) { request.SetProperty(OpenIdConnectConstants.Properties.ConfidentialityLevel, OpenIdConnectConstants.ConfidentialityLevels.Private); } if (context.HandledResponse) { return(true); } else if (context.Skipped) { return(false); } else if (context.IsRejected) { Options.Logger.LogError("The token request was rejected with the following error: {Error} ; {Description}", /* Error: */ context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ context.ErrorDescription); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = context.ErrorDescription, ErrorUri = context.ErrorUri })); } // Reject grant_type=client_credentials requests if validation was skipped. else if (context.IsSkipped && request.IsClientCredentialsGrantType()) { Options.Logger.LogError("The token request must be fully validated to use the client_credentials grant type."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Client authentication is required when using client_credentials." })); } // Ensure that the client_id has been set from the ValidateTokenRequest event. else if (context.IsValidated && string.IsNullOrEmpty(request.ClientId)) { Options.Logger.LogError("The token request was validated but the client_id was not set."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.ServerError, ErrorDescription = "An internal server error occurred." })); } // At this stage, client_id cannot be null for grant_type=authorization_code requests, // as it must either be set in the ValidateTokenRequest 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)) { Options.Logger.LogError("The token request was rejected because the mandatory 'client_id' was missing."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "client_id was missing from the token request" })); } 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) { Options.Logger.LogError("The token request was rejected because the " + "authorization code or the refresh token was invalid."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Invalid ticket" })); } // 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() && !context.IsValidated && ticket.IsConfidential()) { Options.Logger.LogError("The token request was rejected because client authentication " + "was required to use the confidential refresh token."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Client authentication is required to use this ticket" })); } if (ticket.Properties.ExpiresUtc.HasValue && ticket.Properties.ExpiresUtc < Options.SystemClock.UtcNow) { Options.Logger.LogError("The token request was rejected because the " + "authorization code or the refresh token was expired."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Expired ticket" })); } // 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()) { Options.Logger.LogError("The token request was rejected because the authorization " + "code didn't contain any valid presenter."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.ServerError, ErrorDescription = "An internal server error occurred." })); } // 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)) { Options.Logger.LogError("The token request was rejected because the authorization " + "code was issued to a different client application."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Ticket does not contain matching client_id" })); } // 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 var address = ticket.GetProperty(OpenIdConnectConstants.Properties.RedirectUri); if (request.IsAuthorizationCodeGrantType() && !string.IsNullOrEmpty(address)) { ticket.SetProperty(OpenIdConnectConstants.Properties.RedirectUri, null); if (string.IsNullOrEmpty(request.RedirectUri)) { Options.Logger.LogError("The token request was rejected because the mandatory 'redirect_uri' " + "parameter was missing from the grant_type=authorization_code request."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "redirect_uri was missing from the token request" })); } else if (!string.Equals(address, request.RedirectUri, StringComparison.Ordinal)) { Options.Logger.LogError("The token request was rejected because the 'redirect_uri' " + "parameter didn't correspond to the expected value."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Authorization code does not contain matching redirect_uri" })); } } // If a code challenge was initially sent in the authorization request and associated with the // code, validate the code verifier to ensure the token request is sent by a legit caller. var challenge = ticket.GetProperty(OpenIdConnectConstants.Properties.CodeChallenge); if (request.IsAuthorizationCodeGrantType() && !string.IsNullOrEmpty(challenge)) { ticket.SetProperty(OpenIdConnectConstants.Properties.CodeChallenge, null); // Get the code verifier from the token request. // If it cannot be found, return an invalid_grant error. var verifier = request.CodeVerifier; if (string.IsNullOrEmpty(verifier)) { Options.Logger.LogError("The token request was rejected because the required 'code_verifier' " + "parameter was missing from the grant_type=authorization_code request."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The required 'code_verifier' was missing from the token request." })); } // Note: the code challenge method is always validated when receiving the authorization request. var method = ticket.GetProperty(OpenIdConnectConstants.Properties.CodeChallengeMethod); ticket.SetProperty(OpenIdConnectConstants.Properties.CodeChallengeMethod, null); Debug.Assert(string.IsNullOrEmpty(method) || string.Equals(method, OpenIdConnectConstants.CodeChallengeMethods.Plain, StringComparison.Ordinal) || string.Equals(method, OpenIdConnectConstants.CodeChallengeMethods.Sha256, StringComparison.Ordinal), "The specified code challenge method should be supported."); // If the S256 challenge method was used, compute the hash corresponding to the code verifier. if (string.Equals(method, OpenIdConnectConstants.CodeChallengeMethods.Sha256, StringComparison.Ordinal)) { using (var algorithm = SHA256.Create()) { // Compute the SHA-256 hash of the code verifier and encode it using base64-url. // See https://tools.ietf.org/html/rfc7636#section-4.6 for more information. var hash = algorithm.ComputeHash(Encoding.ASCII.GetBytes(request.CodeVerifier)); verifier = Base64UrlEncoder.Encode(hash); } } // Compare the verifier and the code challenge: if the two don't match, return an error. // Note: to prevent timing attacks, a time-constant comparer is always used. if (!OpenIdConnectServerHelpers.AreEqual(verifier, challenge)) { Options.Logger.LogError("The token request was rejected because the 'code_verifier' was invalid."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "The specified 'code_verifier' was invalid." })); } } if (request.IsRefreshTokenGrantType() && !string.IsNullOrEmpty(request.Resource)) { // When an explicit resource parameter has been included in the token request // but was missing from the initial request, the request MUST be rejected. var resources = ticket.GetResources(); if (!resources.Any()) { Options.Logger.LogError("The token request was rejected because the 'resource' parameter was not allowed."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Token request cannot contain a resource parameter " + "if the authorization request didn't contain one" })); } // 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 initial authorization/token request. else if (!new HashSet <string>(resources).IsSupersetOf(request.GetResources())) { Options.Logger.LogError("The token request was rejected because the 'resource' parameter was not valid."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Token request doesn't contain a valid resource parameter" })); } } if (request.IsRefreshTokenGrantType() && !string.IsNullOrEmpty(request.Scope)) { // When an explicit scope parameter has been included in the token request // but was missing from the initial request, the request MUST be rejected. // See http://tools.ietf.org/html/rfc6749#section-6 var scopes = ticket.GetScopes(); if (!scopes.Any()) { Options.Logger.LogError("The token request was rejected because the 'scope' parameter was not allowed."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Token request cannot contain a scope parameter " + "if the authorization request didn't contain one" })); } // 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 initial authorization/token request. // See https://tools.ietf.org/html/rfc6749#section-6 else if (!new HashSet <string>(scopes).IsSupersetOf(request.GetScopes())) { Options.Logger.LogError("The token request was rejected because the 'scope' parameter was not valid."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Token request doesn't contain a valid scope parameter" })); } } } var notification = new HandleTokenRequestContext(Context, Options, request, ticket); await Options.Provider.HandleTokenRequest(notification); if (notification.HandledResponse) { return(true); } else if (notification.Skipped) { return(false); } else if (notification.IsRejected) { Options.Logger.LogError("The token request was rejected with the following error: {Error} ; {Description}", /* Error: */ notification.Error ?? OpenIdConnectConstants.Errors.InvalidGrant, /* Description: */ notification.ErrorDescription); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = notification.Error ?? OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = notification.ErrorDescription, ErrorUri = notification.ErrorUri })); } // Flow the changes made to the ticket. ticket = notification.Ticket; // Ensure an authentication ticket has been provided or return // an error code indicating that the grant type is not supported. if (ticket == null) { Options.Logger.LogError("The token request was rejected because no authentication " + "ticket was returned by application code."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.UnsupportedGrantType, ErrorDescription = "The specified grant_type parameter is not supported." })); } return(await HandleSignInAsync(ticket)); }
private async Task <string> SerializeAccessTokenAsync( ClaimsPrincipal principal, AuthenticationProperties properties, OpenIdConnectRequest request, OpenIdConnectResponse response) { // Create a new principal containing only the filtered claims. // Actors identities are also filtered (delegation scenarios). principal = principal.Clone(claim => { // Never exclude the subject claim. if (string.Equals(claim.Type, OpenIdConnectConstants.Claims.Subject, StringComparison.OrdinalIgnoreCase)) { return(true); } // Claims whose destination is not explicitly referenced or doesn't // contain "access_token" are not included in the access token. if (!claim.HasDestination(OpenIdConnectConstants.Destinations.AccessToken)) { Logger.LogDebug("'{Claim}' was excluded from the access token claims.", claim.Type); return(false); } return(true); }); // Remove the destinations from the claim properties. foreach (var claim in principal.Claims) { claim.Properties.Remove(OpenIdConnectConstants.Properties.Destinations); } var identity = (ClaimsIdentity)principal.Identity; // Create a new ticket containing the updated properties and the filtered principal. var ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationScheme); ticket.Properties.IssuedUtc = Options.SystemClock.UtcNow; ticket.Properties.ExpiresUtc = ticket.Properties.IssuedUtc + (ticket.GetAccessTokenLifetime() ?? Options.AccessTokenLifetime); ticket.SetUsage(OpenIdConnectConstants.Usages.AccessToken); ticket.SetAudiences(ticket.GetResources()); // Associate a random identifier with the access token. ticket.SetTicketId(Guid.NewGuid().ToString()); // Remove the unwanted properties from the authentication ticket. ticket.RemoveProperty(OpenIdConnectConstants.Properties.AccessTokenLifetime) .RemoveProperty(OpenIdConnectConstants.Properties.AuthorizationCodeLifetime) .RemoveProperty(OpenIdConnectConstants.Properties.ClientId) .RemoveProperty(OpenIdConnectConstants.Properties.CodeChallenge) .RemoveProperty(OpenIdConnectConstants.Properties.CodeChallengeMethod) .RemoveProperty(OpenIdConnectConstants.Properties.IdentityTokenLifetime) .RemoveProperty(OpenIdConnectConstants.Properties.Nonce) .RemoveProperty(OpenIdConnectConstants.Properties.RedirectUri) .RemoveProperty(OpenIdConnectConstants.Properties.RefreshTokenLifetime); var notification = new SerializeAccessTokenContext(Context, Options, request, response, ticket) { DataFormat = Options.AccessTokenFormat, Issuer = Context.GetIssuer(Options), SecurityTokenHandler = Options.AccessTokenHandler, SigningCredentials = Options.SigningCredentials.FirstOrDefault(key => key.Key is SymmetricSecurityKey) ?? Options.SigningCredentials.FirstOrDefault() }; await Options.Provider.SerializeAccessToken(notification); if (notification.HandledResponse || !string.IsNullOrEmpty(notification.AccessToken)) { return(notification.AccessToken); } else if (notification.Skipped) { return(null); } if (!ReferenceEquals(ticket, notification.Ticket)) { throw new InvalidOperationException("The authentication ticket cannot be replaced."); } if (notification.SecurityTokenHandler == null) { return(notification.DataFormat?.Protect(ticket)); } // At this stage, throw an exception if no signing credentials were provided. if (notification.SigningCredentials == null) { throw new InvalidOperationException("A signing key must be provided."); } // Extract the main identity from the principal. identity = (ClaimsIdentity)ticket.Principal.Identity; // Store the "unique_id" property as a claim. identity.AddClaim(OpenIdConnectConstants.Claims.JwtId, ticket.GetTicketId()); // Store the "usage" property as a claim. identity.AddClaim(OpenIdConnectConstants.Claims.Usage, ticket.GetUsage()); // Store the "confidentiality_level" property as a claim. var confidentiality = ticket.GetProperty(OpenIdConnectConstants.Properties.ConfidentialityLevel); if (!string.IsNullOrEmpty(confidentiality)) { identity.AddClaim(OpenIdConnectConstants.Claims.ConfidentialityLevel, confidentiality); } // Create a new claim per scope item, that will result // in a "scope" array being added in the access token. foreach (var scope in notification.Scopes) { identity.AddClaim(OpenIdConnectConstants.Claims.Scope, scope); } // Store the audiences as claims. foreach (var audience in notification.Audiences) { identity.AddClaim(OpenIdConnectConstants.Claims.Audience, audience); } // Extract the presenters from the authentication ticket. var presenters = notification.Presenters.ToArray(); switch (presenters.Length) { case 0: break; case 1: identity.AddClaim(OpenIdConnectConstants.Claims.AuthorizedParty, presenters[0]); break; default: Logger.LogWarning("Multiple presenters have been associated with the access token " + "but the JWT format only accepts single values."); // Only add the first authorized party. identity.AddClaim(OpenIdConnectConstants.Claims.AuthorizedParty, presenters[0]); break; } var token = notification.SecurityTokenHandler.CreateToken(new SecurityTokenDescriptor { Subject = identity, Issuer = notification.Issuer, SigningCredentials = notification.SigningCredentials, IssuedAt = notification.Ticket.Properties.IssuedUtc?.UtcDateTime, NotBefore = notification.Ticket.Properties.IssuedUtc?.UtcDateTime, Expires = notification.Ticket.Properties.ExpiresUtc?.UtcDateTime }); return(notification.SecurityTokenHandler.WriteToken(token)); }
private async Task <string> SerializeAccessTokenAsync( ClaimsIdentity identity, AuthenticationProperties properties, OpenIdConnectRequest request, OpenIdConnectResponse response) { // Create a new identity containing only the filtered claims. // Actors identities are also filtered (delegation scenarios). identity = identity.Clone(claim => { // Never exclude ClaimTypes.NameIdentifier. if (string.Equals(claim.Type, ClaimTypes.NameIdentifier, StringComparison.OrdinalIgnoreCase)) { return(true); } // Claims whose destination is not explicitly referenced or doesn't // contain "access_token" are not included in the access token. return(claim.HasDestination(OpenIdConnectConstants.Destinations.AccessToken)); }); // Create a new ticket containing the updated properties and the filtered identity. var ticket = new AuthenticationTicket(identity, properties); ticket.Properties.IssuedUtc = Options.SystemClock.UtcNow; ticket.Properties.ExpiresUtc = ticket.Properties.IssuedUtc + (ticket.GetAccessTokenLifetime() ?? Options.AccessTokenLifetime); ticket.SetUsage(OpenIdConnectConstants.Usages.AccessToken); ticket.SetAudiences(ticket.GetResources()); // Associate a random identifier with the access token. ticket.SetTicketId(Guid.NewGuid().ToString()); // By default, add the client_id to the list of the // presenters allowed to use the access token. if (!string.IsNullOrEmpty(request.ClientId)) { ticket.SetPresenters(request.ClientId); } var notification = new SerializeAccessTokenContext(Context, Options, request, response, ticket) { DataFormat = Options.AccessTokenFormat, Issuer = Context.GetIssuer(Options), SecurityTokenHandler = Options.AccessTokenHandler, SigningCredentials = Options.SigningCredentials.FirstOrDefault() }; await Options.Provider.SerializeAccessToken(notification); if (notification.HandledResponse || !string.IsNullOrEmpty(notification.AccessToken)) { return(notification.AccessToken); } else if (notification.Skipped) { return(null); } if (!notification.Audiences.Any()) { Options.Logger.LogInformation("No explicit audience was associated with the access token."); } if (notification.SecurityTokenHandler == null) { return(notification.DataFormat?.Protect(ticket)); } if (notification.SigningCredentials == null) { throw new InvalidOperationException("A signing key must be provided."); } // Store the "unique_id" property as a claim. ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.JwtId, ticket.GetTicketId()); // Store the "usage" property as a claim. ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.Usage, ticket.GetUsage()); // Store the "confidentiality_level" property as a claim. var confidentiality = ticket.GetProperty(OpenIdConnectConstants.Properties.ConfidentialityLevel); if (!string.IsNullOrEmpty(confidentiality)) { identity.AddClaim(OpenIdConnectConstants.Claims.ConfidentialityLevel, confidentiality); } // Create a new claim per scope item, that will result // in a "scope" array being added in the access token. foreach (var scope in notification.Scopes) { ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.Scope, scope); } // Note: when used as an access token, a JWT token doesn't have to expose a "sub" claim // but the name identifier claim is used as a substitute when it has been explicitly added. // See https://tools.ietf.org/html/rfc7519#section-4.1.2 var subject = ticket.Identity.FindFirst(OpenIdConnectConstants.Claims.Subject); if (subject == null) { var identifier = ticket.Identity.FindFirst(ClaimTypes.NameIdentifier); if (identifier != null) { ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.Subject, identifier.Value); } } // Remove the ClaimTypes.NameIdentifier claims to avoid getting duplicate claims. // Note: the "sub" claim is automatically mapped by JwtSecurityTokenHandler // to ClaimTypes.NameIdentifier when validating a JWT token. // Note: make sure to call ToArray() to avoid an InvalidOperationException // on old versions of Mono, where FindAll() is implemented using an iterator. foreach (var claim in ticket.Identity.FindAll(ClaimTypes.NameIdentifier).ToArray()) { ticket.Identity.RemoveClaim(claim); } // Store the audiences as claims. foreach (var audience in notification.Audiences) { ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.Audience, audience); } // Extract the presenters from the authentication ticket. var presenters = notification.Presenters.ToArray(); switch (presenters.Length) { case 0: break; case 1: ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.AuthorizedParty, presenters[0]); break; default: Options.Logger.LogWarning("Multiple presenters have been associated with the access token " + "but the JWT format only accepts single values."); // Only add the first authorized party. ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.AuthorizedParty, presenters[0]); break; } if (ticket.Properties.IssuedUtc != null) { ticket.Identity.AddClaim(new Claim( OpenIdConnectConstants.Claims.IssuedAt, EpochTime.GetIntDate(ticket.Properties.IssuedUtc.Value.UtcDateTime).ToString(), ClaimValueTypes.Integer64)); } var descriptor = new SecurityTokenDescriptor { Subject = ticket.Identity, TokenIssuerName = notification.Issuer, EncryptingCredentials = notification.EncryptingCredentials, SigningCredentials = notification.SigningCredentials, Lifetime = new Lifetime( notification.Ticket.Properties.IssuedUtc?.UtcDateTime, notification.Ticket.Properties.ExpiresUtc?.UtcDateTime) }; var token = notification.SecurityTokenHandler.CreateToken(descriptor); return(notification.SecurityTokenHandler.WriteToken(token)); }
private async Task <bool> InvokeTokenEndpointAsync() { if (!string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase)) { Options.Logger.LogError("The token request was rejected because an invalid " + "HTTP method was received: {Method}.", Request.Method); return(await SendTokenResponseAsync(null, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed token request has been received: make sure to use POST." })); } // See http://openid.net/specs/openid-connect-core-1_0.html#FormSerialization if (string.IsNullOrEmpty(Request.ContentType)) { Options.Logger.LogError("The token request was rejected because the " + "mandatory 'Content-Type' header was missing."); return(await SendTokenResponseAsync(null, 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." })); } // May have media/type; charset=utf-8, allow partial match. if (!Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) { Options.Logger.LogError("The token request was rejected because an invalid 'Content-Type' " + "header was received: {ContentType}.", Request.ContentType); return(await SendTokenResponseAsync(null, 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'." })); } var request = new OpenIdConnectMessage(await Request.ReadFormAsync()) { RequestType = OpenIdConnectRequestType.TokenRequest }; // Store the token request in the OWIN context. Context.SetOpenIdConnectRequest(request); // Reject token requests missing the mandatory grant_type parameter. if (string.IsNullOrEmpty(request.GrantType)) { Options.Logger.LogError("The token request was rejected because the grant type was missing."); return(await SendTokenResponseAsync(request, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The mandatory 'grant_type' parameter was missing.", })); } // 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)) { Options.Logger.LogError("The token request was rejected because the authorization code was missing."); return(await SendTokenResponseAsync(request, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The mandatory 'code' parameter was missing." })); } // 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.GetRefreshToken())) { Options.Logger.LogError("The token request was rejected because the refresh token was missing."); return(await SendTokenResponseAsync(request, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The mandatory 'refresh_token' parameter was missing." })); } // 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))) { Options.Logger.LogError("The token request was rejected because the resource owner credentials were missing."); return(await SendTokenResponseAsync(request, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The mandatory 'username' and/or 'password' parameters " + "was/were missing from the request message." })); } // 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)) { var header = Request.Headers.Get("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 context = new ValidateTokenRequestContext(Context, Options, request); await Options.Provider.ValidateTokenRequest(context); if (context.IsRejected) { Options.Logger.LogError("The token request was rejected with the following error: {Error} ; {Description}", /* Error: */ context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ context.ErrorDescription); return(await SendTokenResponseAsync(request, new OpenIdConnectMessage { Error = context.Error ?? OpenIdConnectConstants.Errors.InvalidClient, ErrorDescription = context.ErrorDescription, ErrorUri = context.ErrorUri })); } // Reject grant_type=client_credentials requests if validation was skipped. else if (context.IsSkipped && request.IsClientCredentialsGrantType()) { Options.Logger.LogError("The token request must be fully validated to use the client_credentials grant type."); return(await SendTokenResponseAsync(request, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Client authentication is required when using client_credentials." })); } // Ensure that the client_id has been set from the ValidateTokenRequest event. else if (context.IsValidated && string.IsNullOrEmpty(request.ClientId)) { Options.Logger.LogError("The token request was validated but the client_id was not set."); return(await SendTokenResponseAsync(request, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.ServerError, ErrorDescription = "An internal server error occurred." })); } 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.GetRefreshToken(), request); if (ticket == null) { Options.Logger.LogError("The token request was rejected because the " + "authorization code or the refresh token was invalid."); return(await SendTokenResponseAsync(request, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Invalid ticket" })); } if (!ticket.Properties.ExpiresUtc.HasValue || ticket.Properties.ExpiresUtc < Options.SystemClock.UtcNow) { Options.Logger.LogError("The token request was rejected because the " + "authorization code or the refresh token was expired."); return(await SendTokenResponseAsync(request, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Expired ticket" })); } // 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() && !context.IsValidated && ticket.IsConfidential()) { Options.Logger.LogError("The token request was rejected because client authentication " + "was required to use the confidential refresh token."); return(await SendTokenResponseAsync(request, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Client authentication is required to use this ticket" })); } // 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()) { Options.Logger.LogError("The token request was rejected because the authorization " + "code didn't contain any valid presenter."); return(await SendTokenResponseAsync(request, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.ServerError, ErrorDescription = "An internal server error occurred." })); } // At this stage, client_id cannot be null for grant_type=authorization_code requests, // as it must either be set in the ValidateTokenRequest 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)) { Options.Logger.LogError("The token request was rejected because the mandatory 'client_id' was missing."); return(await SendTokenResponseAsync(request, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "client_id was missing from the token request" })); } // 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)) { Options.Logger.LogError("The token request was rejected because the authorization " + "code was issued to a different client application."); return(await SendTokenResponseAsync(request, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Ticket does not contain matching client_id" })); } // 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 var address = ticket.GetProperty(OpenIdConnectConstants.Properties.RedirectUri); if (request.IsAuthorizationCodeGrantType() && !string.IsNullOrEmpty(address)) { ticket.Properties.Dictionary.Remove(OpenIdConnectConstants.Properties.RedirectUri); if (string.IsNullOrEmpty(request.RedirectUri)) { Options.Logger.LogError("The token request was rejected because the mandatory 'redirect_uri' " + "parameter was missing from the grant_type=authorization_code request."); return(await SendTokenResponseAsync(request, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "redirect_uri was missing from the token request" })); } else if (!string.Equals(address, request.RedirectUri, StringComparison.Ordinal)) { Options.Logger.LogError("The token request was rejected because the 'redirect_uri' " + "parameter didn't correspond to the expected value."); return(await SendTokenResponseAsync(request, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Authorization code does not contain matching redirect_uri" })); } } if (request.IsRefreshTokenGrantType() && !string.IsNullOrEmpty(request.Resource)) { // When an explicit resource parameter has been included in the token request // but was missing from the initial request, the request MUST be rejected. var resources = ticket.GetResources(); if (!resources.Any()) { Options.Logger.LogError("The token request was rejected because the 'resource' parameter was not allowed."); return(await SendTokenResponseAsync(request, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Token request cannot contain a resource parameter" + "if the authorization request didn't contain one" })); } // 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 initial authorization/token request. else if (!new HashSet <string>(resources).IsSupersetOf(request.GetResources())) { Options.Logger.LogError("The token request was rejected because the 'resource' parameter was not valid."); return(await SendTokenResponseAsync(request, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Token request doesn't contain a valid resource parameter" })); } } if (request.IsRefreshTokenGrantType() && !string.IsNullOrEmpty(request.Scope)) { // When an explicit scope parameter has been included in the token request // but was missing from the initial request, the request MUST be rejected. // See http://tools.ietf.org/html/rfc6749#section-6 var scopes = ticket.GetScopes(); if (!scopes.Any()) { Options.Logger.LogError("The token request was rejected because the 'scope' parameter was not allowed."); return(await SendTokenResponseAsync(request, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Token request cannot contain a scope parameter" + "if the authorization request didn't contain one" })); } // 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 initial authorization/token request. // See https://tools.ietf.org/html/rfc6749#section-6 else if (!new HashSet <string>(scopes).IsSupersetOf(request.GetScopes())) { Options.Logger.LogError("The token request was rejected because the 'scope' parameter was not valid."); return(await SendTokenResponseAsync(request, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Token request doesn't contain a valid scope parameter" })); } } if (request.IsAuthorizationCodeGrantType()) { // Note: the authentication ticket is copied to avoid modifying the properties of the authorization code. var grant = new GrantAuthorizationCodeContext(Context, Options, request, ticket.Copy()); await Options.Provider.GrantAuthorizationCode(grant); if (!grant.IsValidated) { // Note: use invalid_grant as the default error if none has been explicitly provided. return(await SendTokenResponseAsync(request, new OpenIdConnectMessage { Error = grant.Error ?? OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = grant.ErrorDescription, ErrorUri = grant.ErrorUri })); } // By default, when using the authorization code grant, the authentication ticket extracted from the // authorization code is used as-is. To avoid aligning the expiration date of the generated tokens // with the lifetime of the authorization code, the ticket properties are automatically reset to null. if (grant.Ticket.Properties.IssuedUtc == ticket.Properties.IssuedUtc) { grant.Ticket.Properties.IssuedUtc = null; } if (grant.Ticket.Properties.ExpiresUtc == ticket.Properties.ExpiresUtc) { grant.Ticket.Properties.ExpiresUtc = null; } ticket = grant.Ticket; } else { // Note: the authentication ticket is copied to avoid modifying the properties of the refresh token. var grant = new GrantRefreshTokenContext(Context, Options, request, ticket.Copy()); await Options.Provider.GrantRefreshToken(grant); if (!grant.IsValidated) { // Note: use invalid_grant as the default error if none has been explicitly provided. return(await SendTokenResponseAsync(request, new OpenIdConnectMessage { Error = grant.Error ?? OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = grant.ErrorDescription, ErrorUri = grant.ErrorUri })); } // By default, when using the refresh token grant, the authentication ticket extracted from the // refresh token is used as-is. To avoid aligning the expiration date of the generated tokens // with the lifetime of the refresh token, the ticket properties are automatically reset to null. if (grant.Ticket.Properties.IssuedUtc == ticket.Properties.IssuedUtc) { grant.Ticket.Properties.IssuedUtc = null; } if (grant.Ticket.Properties.ExpiresUtc == ticket.Properties.ExpiresUtc) { grant.Ticket.Properties.ExpiresUtc = null; } ticket = grant.Ticket; } } // 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 grant = new GrantResourceOwnerCredentialsContext(Context, Options, request); await Options.Provider.GrantResourceOwnerCredentials(grant); if (!grant.IsValidated) { // Note: use invalid_grant as the default error if none has been explicitly provided. return(await SendTokenResponseAsync(request, new OpenIdConnectMessage { Error = grant.Error ?? OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = grant.ErrorDescription, ErrorUri = grant.ErrorUri })); } ticket = grant.Ticket; } // 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 grant = new GrantClientCredentialsContext(Context, Options, request); await Options.Provider.GrantClientCredentials(grant); if (!grant.IsValidated) { // Note: use unauthorized_client as the default error if none has been explicitly provided. return(await SendTokenResponseAsync(request, new OpenIdConnectMessage { Error = grant.Error ?? OpenIdConnectConstants.Errors.UnauthorizedClient, ErrorDescription = grant.ErrorDescription, ErrorUri = grant.ErrorUri })); } ticket = grant.Ticket; } // See http://tools.ietf.org/html/rfc6749#section-8.3 else { var grant = new GrantCustomExtensionContext(Context, Options, request); await Options.Provider.GrantCustomExtension(grant); if (!grant.IsValidated) { // Note: use unsupported_grant_type as the default error if none has been explicitly provided. return(await SendTokenResponseAsync(request, new OpenIdConnectMessage { Error = grant.Error ?? OpenIdConnectConstants.Errors.UnsupportedGrantType, ErrorDescription = grant.ErrorDescription, ErrorUri = grant.ErrorUri })); } ticket = grant.Ticket; } var notification = new HandleTokenRequestContext(Context, Options, request, ticket); await Options.Provider.HandleTokenRequest(notification); if (notification.HandledResponse) { return(true); } else if (notification.Skipped) { return(false); } // 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) { Options.Logger.LogError("The token request was rejected because no authentication " + "ticket was returned by application code."); return(await SendTokenResponseAsync(request, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.ServerError })); } if (context.IsValidated) { // Store a boolean indicating whether the ticket should be marked as confidential. ticket.Properties.Dictionary[OpenIdConnectConstants.Properties.Confidential] = "true"; } // Always include the "openid" scope when the developer doesn't explicitly call SetScopes. // Note: the application is allowed to specify a different "scopes": in this case, // don't replace the "scopes" property stored in the authentication ticket. if (!ticket.Properties.Dictionary.ContainsKey(OpenIdConnectConstants.Properties.Scopes) && request.HasScope(OpenIdConnectConstants.Scopes.OpenId)) { ticket.Properties.Dictionary[OpenIdConnectConstants.Properties.Scopes] = OpenIdConnectConstants.Scopes.OpenId; } string audiences; // When a "resources" property cannot be found in the authentication properties, infer it from the "audiences" property. if (!ticket.Properties.Dictionary.ContainsKey(OpenIdConnectConstants.Properties.Resources) && ticket.Properties.Dictionary.TryGetValue(OpenIdConnectConstants.Properties.Audiences, out audiences)) { ticket.Properties.Dictionary[OpenIdConnectConstants.Properties.Resources] = audiences; } 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(); // When receiving a grant_type=refresh_token request, determine whether the client application // requests a limited set of resources and replace the "resources" property if necessary. if (request.IsRefreshTokenGrantType() && !string.IsNullOrEmpty(request.Resource)) { // Replace the resources initially granted by the resources listed by the client application in the token request. // Note: at this stage, request.GetResources() cannot return more items than the ones that were initially granted // by the resource owner as the "resources" parameter is always validated when receiving the token request. properties.Dictionary[OpenIdConnectConstants.Properties.Resources] = string.Join(" ", request.GetResources()); } // Note: when the "resource" parameter added to the OpenID Connect response // is identical to the request parameter, returning it is not necessary. var resources = properties.GetProperty(OpenIdConnectConstants.Properties.Resources); if (request.IsAuthorizationCodeGrantType() || (!string.IsNullOrEmpty(resources) && !string.IsNullOrEmpty(request.Resource) && !string.Equals(request.Resource, resources, StringComparison.Ordinal))) { response.Resource = resources; } // When receiving a grant_type=refresh_token request, determine whether the client application // requests a limited set of scopes and replace the "scopes" property if necessary. if (request.IsRefreshTokenGrantType() && !string.IsNullOrEmpty(request.Scope)) { // Replace the scopes initially granted by the scopes listed by the client application in the token request. // Note: at this stage, request.GetScopes() cannot return more items than the ones that were initially granted // by the resource owner as the "scope" parameter is always validated when receiving the token request. properties.Dictionary[OpenIdConnectConstants.Properties.Scopes] = string.Join(" ", request.GetScopes()); } // Note: when the "scope" parameter added to the OpenID Connect response // is identical to the request parameter, returning it is not necessary. var scopes = properties.GetProperty(OpenIdConnectConstants.Properties.Scopes); if (request.IsAuthorizationCodeGrantType() || (!string.IsNullOrEmpty(scopes) && !string.IsNullOrEmpty(request.Scope) && !string.Equals(request.Scope, scopes, StringComparison.Ordinal))) { response.Scope = scopes; } response.TokenType = OpenIdConnectConstants.TokenTypes.Bearer; response.AccessToken = await SerializeAccessTokenAsync(ticket.Identity, 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)) { Options.Logger.LogError("An error occurred during the serialization of the " + "access token and a null value was returned."); return(await SendTokenResponseAsync(request, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.ServerError, ErrorDescription = "no valid access token was issued" })); } // 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(); response.IdToken = await SerializeIdentityTokenAsync(ticket.Identity, 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)) { Options.Logger.LogError("An error occurred during the serialization of the " + "identity token and a null value was returned."); return(await SendTokenResponseAsync(request, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.ServerError, ErrorDescription = "no valid identity token was issued" })); } } // 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) && (!request.IsRefreshTokenGrantType() || Options.UseSlidingExpiration) && (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(); response.SetRefreshToken(await SerializeRefreshTokenAsync(ticket.Identity, properties, request, response)); } return(await SendTokenResponseAsync(request, response, ticket)); }
private async Task <string> SerializeAccessTokenAsync( ClaimsPrincipal principal, AuthenticationProperties properties, OpenIdConnectMessage request, OpenIdConnectMessage response) { // properties.IssuedUtc and properties.ExpiresUtc // should always be preferred when explicitly set. if (properties.IssuedUtc == null) { properties.IssuedUtc = Options.SystemClock.UtcNow; } if (properties.ExpiresUtc == null) { properties.ExpiresUtc = properties.IssuedUtc + Options.AccessTokenLifetime; } // Create a new principal containing only the filtered claims. // Actors identities are also filtered (delegation scenarios). principal = principal.Clone(claim => { // Never exclude ClaimTypes.NameIdentifier. if (string.Equals(claim.Type, ClaimTypes.NameIdentifier, StringComparison.OrdinalIgnoreCase)) { return(true); } // Claims whose destination is not explicitly referenced or doesn't // contain "access_token" are not included in the access token. return(claim.HasDestination(OpenIdConnectConstants.Destinations.AccessToken)); }); var identity = (ClaimsIdentity)principal.Identity; // Create a new ticket containing the updated properties and the filtered principal. var ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationScheme); ticket.SetUsage(OpenIdConnectConstants.Usages.AccessToken); ticket.SetAudiences(ticket.GetResources()); // By default, add the client_id to the list of the // presenters allowed to use the access token. if (!string.IsNullOrEmpty(request.ClientId)) { ticket.SetPresenters(request.ClientId); } var notification = new SerializeAccessTokenContext(Context, Options, request, response, ticket) { DataFormat = Options.AccessTokenFormat, Issuer = Context.GetIssuer(Options), SecurityTokenHandler = Options.AccessTokenHandler, SigningCredentials = Options.SigningCredentials.FirstOrDefault() }; await Options.Provider.SerializeAccessToken(notification); if (!string.IsNullOrEmpty(notification.AccessToken)) { return(notification.AccessToken); } if (notification.SecurityTokenHandler == null) { return(notification.DataFormat?.Protect(ticket)); } // Extract the main identity from the principal. identity = (ClaimsIdentity)ticket.Principal.Identity; // Store the "usage" property as a claim. identity.AddClaim(OpenIdConnectConstants.Claims.Usage, ticket.GetUsage()); // If the ticket is marked as confidential, add a new // "confidential" claim in the security token. if (ticket.IsConfidential()) { identity.AddClaim(new Claim(OpenIdConnectConstants.Claims.Confidential, "true", ClaimValueTypes.Boolean)); } // Create a new claim per scope item, that will result // in a "scope" array being added in the access token. foreach (var scope in ticket.GetScopes()) { identity.AddClaim(OpenIdConnectConstants.Claims.Scope, scope); } var handler = notification.SecurityTokenHandler as JwtSecurityTokenHandler; if (handler != null) { // Note: when used as an access token, a JWT token doesn't have to expose a "sub" claim // but the name identifier claim is used as a substitute when it has been explicitly added. // See https://tools.ietf.org/html/rfc7519#section-4.1.2 var subject = identity.FindFirst(JwtRegisteredClaimNames.Sub); if (subject == null) { var identifier = identity.FindFirst(ClaimTypes.NameIdentifier); if (identifier != null) { identity.AddClaim(JwtRegisteredClaimNames.Sub, identifier.Value); } } // Remove the ClaimTypes.NameIdentifier claims to avoid getting duplicate claims. // Note: the "sub" claim is automatically mapped by JwtSecurityTokenHandler // to ClaimTypes.NameIdentifier when validating a JWT token. // Note: make sure to call ToArray() to avoid an InvalidOperationException // on old versions of Mono, where FindAll() is implemented using an iterator. foreach (var claim in identity.FindAll(ClaimTypes.NameIdentifier).ToArray()) { identity.RemoveClaim(claim); } // Store the audiences as claims. foreach (var audience in ticket.GetAudiences()) { identity.AddClaim(JwtRegisteredClaimNames.Aud, audience); } // Extract the presenters from the authentication ticket. var presenters = ticket.GetPresenters().ToArray(); switch (presenters.Length) { case 0: break; case 1: identity.AddClaim(JwtRegisteredClaimNames.Azp, presenters[0]); break; default: Logger.LogWarning("Multiple presenters have been associated with the access token " + "but the JWT format only accepts single values."); // Only add the first authorized party. identity.AddClaim(JwtRegisteredClaimNames.Azp, presenters[0]); break; } var token = handler.CreateJwtSecurityToken( subject: identity, issuer: notification.Issuer, signingCredentials: notification.SigningCredentials, issuedAt: ticket.Properties.IssuedUtc.Value.UtcDateTime, notBefore: ticket.Properties.IssuedUtc.Value.UtcDateTime, expires: ticket.Properties.ExpiresUtc.Value.UtcDateTime); var x509SecurityKey = notification.SigningCredentials?.Key as X509SecurityKey; if (x509SecurityKey != null) { // Note: unlike "kid", "x5t" is not automatically added by JwtHeader's constructor in IdentityModel for .NET Core. // Though not required by the specifications, this property is needed for IdentityModel for Katana to work correctly. // See https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server/issues/132 // and https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/181. token.Header[JwtHeaderParameterNames.X5t] = Base64UrlEncoder.Encode(x509SecurityKey.Certificate.GetCertHash()); } return(handler.WriteToken(token)); } else { var token = notification.SecurityTokenHandler.CreateToken(new SecurityTokenDescriptor { Subject = identity, Issuer = notification.Issuer, Audience = notification.Audiences.ElementAtOrDefault(0), SigningCredentials = notification.SigningCredentials, IssuedAt = notification.Ticket.Properties.IssuedUtc.Value.UtcDateTime, NotBefore = notification.Ticket.Properties.IssuedUtc.Value.UtcDateTime, Expires = notification.Ticket.Properties.ExpiresUtc.Value.UtcDateTime }); // Note: the security token is manually serialized to prevent // an exception from being thrown if the handler doesn't implement // the SecurityTokenHandler.WriteToken overload returning a string. var builder = new StringBuilder(); using (var writer = XmlWriter.Create(builder, new XmlWriterSettings { Encoding = new UTF8Encoding(false), OmitXmlDeclaration = true })) { notification.SecurityTokenHandler.WriteToken(writer, token); } return(builder.ToString()); } }
private async Task <bool> HandleSignInAsync(AuthenticationTicket ticket) { // Extract the OpenID Connect request from the ASP.NET context. // If it cannot be found or doesn't correspond to an authorization // or a token request, throw an InvalidOperationException. var request = Context.GetOpenIdConnectRequest(); if (request == null || (!request.IsAuthorizationRequest() && !request.IsTokenRequest())) { throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint."); } // Note: if an OpenID Connect response was already generated, throw an exception. var response = Context.GetOpenIdConnectResponse(); if (response != null) { throw new InvalidOperationException("An OpenID Connect response has already been sent."); } if (string.IsNullOrEmpty(ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject))) { throw new InvalidOperationException("The authentication ticket was rejected because " + "it doesn't contain the mandatory subject claim."); } // Prepare a new OpenID Connect response. response = new OpenIdConnectResponse(); if (request.IsAuthorizationRequest()) { response.RedirectUri = request.GetProperty <string>(OpenIdConnectConstants.Properties.RedirectUri); response.State = request.State; } // Copy the confidentiality level associated with the request to the authentication ticket. if (!ticket.HasProperty(OpenIdConnectConstants.Properties.ConfidentialityLevel)) { ticket.SetProperty(OpenIdConnectConstants.Properties.ConfidentialityLevel, request.GetProperty <string>(OpenIdConnectConstants.Properties.ConfidentialityLevel)); } // Always include the "openid" scope when the developer doesn't explicitly call SetScopes. // Note: the application is allowed to specify a different "scopes": in this case, // don't replace the "scopes" property stored in the authentication ticket. if (request.HasScope(OpenIdConnectConstants.Scopes.OpenId) && !ticket.HasScope()) { ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId); } // When a "resources" property cannot be found in the ticket, // infer it from the "audiences" property. if (ticket.HasAudience() && !ticket.HasResource()) { ticket.SetResources(ticket.GetAudiences()); } // Add the validated client_id to the list of authorized presenters, // unless the presenters were explicitly set by the developer. var presenter = request.GetProperty <string>(OpenIdConnectConstants.Properties.ClientId); if (!string.IsNullOrEmpty(presenter) && !ticket.HasPresenter()) { ticket.SetPresenters(presenter); } // Only return an authorization code if the request is an authorization request and has response_type=code. if (request.IsAuthorizationRequest() && request.HasResponseType(OpenIdConnectConstants.ResponseTypes.Code)) { // 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(); response.Code = await SerializeAuthorizationCodeAsync(ticket.Principal, properties, request, response); } // Only return an access token if the request is a token request // or an authorization request that specifies response_type=token. if (request.IsTokenRequest() || (request.IsAuthorizationRequest() && 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(); // When receiving a grant_type=refresh_token request, determine whether the client application // requests a limited set of scopes/resources and replace the corresponding properties if necessary. // Note: at this stage, request.GetResources() cannot return more items than the ones that were initially granted // by the resource owner as the "resources" parameter is always validated when receiving the token request. if (request.IsTokenRequest() && request.IsRefreshTokenGrantType()) { if (!string.IsNullOrEmpty(request.Resource)) { Logger.LogDebug("The access token resources will be limited to the resources " + "requested by the client application: {Resources}.", request.GetResources()); // Replace the resources initially granted by the resources listed by the client application in the token request. // Note: request.GetResources() automatically removes duplicate entries, so additional filtering is not necessary. properties.SetProperty(OpenIdConnectConstants.Properties.Resources, request.GetResources()); } if (!string.IsNullOrEmpty(request.Scope)) { Logger.LogDebug("The access token scopes will be limited to the scopes " + "requested by the client application: {Scopes}.", request.GetScopes()); // Replace the scopes initially granted by the scopes listed by the client application in the token request. // Note: request.GetScopes() automatically removes duplicate entries, so additional filtering is not necessary. properties.SetProperty(OpenIdConnectConstants.Properties.Scopes, request.GetScopes()); } } var resources = ticket.GetResources(); if (request.IsAuthorizationCodeGrantType() || !new HashSet <string>(resources).SetEquals(request.GetResources())) { response.Resource = string.Join(" ", resources); } var scopes = ticket.GetScopes(); if (request.IsAuthorizationCodeGrantType() || !new HashSet <string>(scopes).SetEquals(request.GetScopes())) { response.Scope = string.Join(" ", scopes); } response.TokenType = OpenIdConnectConstants.TokenTypes.Bearer; response.AccessToken = await SerializeAccessTokenAsync(ticket.Principal, properties, request, response); // 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; response.ExpiresIn = (long)(lifetime.TotalSeconds + .5); } } // Only return a refresh token if the request is a token request that specifies scope=offline_access. if (request.IsTokenRequest() && ticket.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess)) { // Note: when sliding expiration is disabled, don't return a new refresh token, // unless the token request is not a grant_type=refresh_token request. if (Options.UseSlidingExpiration || !request.IsRefreshTokenGrantType()) { // 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(); response.RefreshToken = await SerializeRefreshTokenAsync(ticket.Principal, properties, request, response); } } // Only return an identity token if the openid scope was requested and granted // to avoid generating and returning an unnecessary token to pure OAuth2 clients. if (ticket.HasScope(OpenIdConnectConstants.Scopes.OpenId)) { // Note: don't return an identity token if the request is an // authorization request that doesn't use response_type=id_token. if (request.IsTokenRequest() || 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(); response.IdToken = await SerializeIdentityTokenAsync(ticket.Principal, properties, request, response); } } if (request.IsAuthorizationRequest()) { return(await SendAuthorizationResponseAsync(response, ticket)); } return(await SendTokenResponseAsync(response, ticket)); }
private async Task <string> SerializeAccessTokenAsync( ClaimsIdentity identity, AuthenticationProperties properties, OpenIdConnectMessage request, OpenIdConnectMessage response) { // properties.IssuedUtc and properties.ExpiresUtc // should always be preferred when explicitly set. if (properties.IssuedUtc == null) { properties.IssuedUtc = Options.SystemClock.UtcNow; } if (properties.ExpiresUtc == null) { properties.ExpiresUtc = properties.IssuedUtc + Options.AccessTokenLifetime; } // Create a new identity containing only the filtered claims. // Actors identities are also filtered (delegation scenarios). identity = identity.Clone(claim => { // Never exclude ClaimTypes.NameIdentifier. if (string.Equals(claim.Type, ClaimTypes.NameIdentifier, StringComparison.OrdinalIgnoreCase)) { return(true); } // Claims whose destination is not explicitly referenced or doesn't // contain "access_token" are not included in the access token. return(claim.HasDestination(OpenIdConnectConstants.Destinations.AccessToken)); }); // Create a new ticket containing the updated properties and the filtered identity. var ticket = new AuthenticationTicket(identity, properties); ticket.SetUsage(OpenIdConnectConstants.Usages.AccessToken); ticket.SetAudiences(ticket.GetResources()); // Associate a random identifier with the access token. ticket.SetTicketId(Guid.NewGuid().ToString()); // By default, add the client_id to the list of the // presenters allowed to use the access token. if (!string.IsNullOrEmpty(request.ClientId)) { ticket.SetPresenters(request.ClientId); } var notification = new SerializeAccessTokenContext(Context, Options, request, response, ticket) { DataFormat = Options.AccessTokenFormat, Issuer = Context.GetIssuer(Options), SecurityTokenHandler = Options.AccessTokenHandler, SigningCredentials = Options.SigningCredentials.FirstOrDefault() }; await Options.Provider.SerializeAccessToken(notification); if (!string.IsNullOrEmpty(notification.AccessToken)) { return(notification.AccessToken); } if (notification.SecurityTokenHandler == null) { return(notification.DataFormat?.Protect(ticket)); } // Store the "unique_id" property as a claim. ticket.Identity.AddClaim(notification.SecurityTokenHandler is JwtSecurityTokenHandler ? OpenIdConnectConstants.Claims.JwtId : OpenIdConnectConstants.Claims.TokenId, ticket.GetTicketId()); // Store the "usage" property as a claim. ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.Usage, ticket.GetUsage()); // If the ticket is marked as confidential, add a new // "confidential" claim in the security token. if (ticket.IsConfidential()) { ticket.Identity.AddClaim(new Claim(OpenIdConnectConstants.Claims.Confidential, "true", ClaimValueTypes.Boolean)); } // Create a new claim per scope item, that will result // in a "scope" array being added in the access token. foreach (var scope in ticket.GetScopes()) { ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.Scope, scope); } var handler = notification.SecurityTokenHandler as JwtSecurityTokenHandler; if (handler != null) { // Note: when used as an access token, a JWT token doesn't have to expose a "sub" claim // but the name identifier claim is used as a substitute when it has been explicitly added. // See https://tools.ietf.org/html/rfc7519#section-4.1.2 var subject = identity.FindFirst(OpenIdConnectConstants.Claims.Subject); if (subject == null) { var identifier = identity.FindFirst(ClaimTypes.NameIdentifier); if (identifier != null) { identity.AddClaim(OpenIdConnectConstants.Claims.Subject, identifier.Value); } } // Remove the ClaimTypes.NameIdentifier claims to avoid getting duplicate claims. // Note: the "sub" claim is automatically mapped by JwtSecurityTokenHandler // to ClaimTypes.NameIdentifier when validating a JWT token. // Note: make sure to call ToArray() to avoid an InvalidOperationException // on old versions of Mono, where FindAll() is implemented using an iterator. foreach (var claim in ticket.Identity.FindAll(ClaimTypes.NameIdentifier).ToArray()) { ticket.Identity.RemoveClaim(claim); } // Store the audiences as claims. foreach (var audience in ticket.GetAudiences()) { ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.Audience, audience); } // Extract the presenters from the authentication ticket. var presenters = ticket.GetPresenters().ToArray(); switch (presenters.Length) { case 0: break; case 1: identity.AddClaim(OpenIdConnectConstants.Claims.AuthorizedParty, presenters[0]); break; default: Options.Logger.LogWarning("Multiple presenters have been associated with the access token " + "but the JWT format only accepts single values."); // Only add the first authorized party. identity.AddClaim(OpenIdConnectConstants.Claims.AuthorizedParty, presenters[0]); break; } var token = handler.CreateToken( subject: ticket.Identity, issuer: notification.Issuer, signingCredentials: notification.SigningCredentials, notBefore: ticket.Properties.IssuedUtc.Value.UtcDateTime, expires: ticket.Properties.ExpiresUtc.Value.UtcDateTime); token.Payload[OpenIdConnectConstants.Claims.IssuedAt] = EpochTime.GetIntDate(ticket.Properties.IssuedUtc.Value.UtcDateTime); // Try to extract a key identifier from the signing credentials // and add the "kid" property to the JWT header if applicable. LocalIdKeyIdentifierClause clause = null; if (notification.SigningCredentials?.SigningKeyIdentifier != null && notification.SigningCredentials.SigningKeyIdentifier.TryFind(out clause)) { token.Header[JwtHeaderParameterNames.Kid] = clause.LocalId; } return(handler.WriteToken(token)); } else { var descriptor = new SecurityTokenDescriptor { Subject = ticket.Identity, AppliesToAddress = notification.Audiences.ElementAtOrDefault(0), TokenIssuerName = notification.Issuer, EncryptingCredentials = notification.EncryptingCredentials, SigningCredentials = notification.SigningCredentials, Lifetime = new Lifetime( notification.Ticket.Properties.IssuedUtc.Value.UtcDateTime, notification.Ticket.Properties.ExpiresUtc.Value.UtcDateTime) }; // When the encrypting credentials use an asymmetric key, replace them by a // EncryptedKeyEncryptingCredentials instance to generate a symmetric key. if (descriptor.EncryptingCredentials != null && descriptor.EncryptingCredentials.SecurityKey is AsymmetricSecurityKey) { // Note: EncryptedKeyEncryptingCredentials automatically generates an in-memory key // that will be encrypted using the original credentials and added to the resulting token // if the security token handler fully supports token encryption (e.g SAML or SAML2). descriptor.EncryptingCredentials = new EncryptedKeyEncryptingCredentials( wrappingCredentials: notification.EncryptingCredentials, keySizeInBits: 256, encryptionAlgorithm: SecurityAlgorithms.Aes256Encryption); } var token = notification.SecurityTokenHandler.CreateToken(descriptor); // Note: the security token is manually serialized to prevent // an exception from being thrown if the handler doesn't implement // the SecurityTokenHandler.WriteToken overload returning a string. var builder = new StringBuilder(); using (var writer = XmlWriter.Create(builder, new XmlWriterSettings { Encoding = new UTF8Encoding(false), OmitXmlDeclaration = true })) { notification.SecurityTokenHandler.WriteToken(writer, token); } return(builder.ToString()); } }
private async Task <string> SerializeAccessTokenAsync( ClaimsIdentity identity, AuthenticationProperties properties, OpenIdConnectRequest request, OpenIdConnectResponse response) { // Create a new identity containing only the filtered claims. // Actors identities are also filtered (delegation scenarios). identity = identity.Clone(claim => { // Never exclude the subject claim. if (string.Equals(claim.Type, OpenIdConnectConstants.Claims.Subject, StringComparison.OrdinalIgnoreCase)) { return(true); } // Claims whose destination is not explicitly referenced or doesn't // contain "access_token" are not included in the access token. if (!claim.HasDestination(OpenIdConnectConstants.Destinations.AccessToken)) { Logger.LogDebug("'{Claim}' was excluded from the access token claims.", claim.Type); return(false); } return(true); }); // Remove the destinations from the claim properties. foreach (var claim in identity.Claims) { claim.Properties.Remove(OpenIdConnectConstants.Properties.Destinations); } // Create a new ticket containing the updated properties and the filtered identity. var ticket = new AuthenticationTicket(identity, properties); ticket.Properties.IssuedUtc = Options.SystemClock.UtcNow; ticket.Properties.ExpiresUtc = ticket.Properties.IssuedUtc; ticket.Properties.ExpiresUtc += ticket.GetAccessTokenLifetime() ?? Options.AccessTokenLifetime; // Associate a random identifier with the access token. ticket.SetTokenId(Guid.NewGuid().ToString()); ticket.SetAudiences(ticket.GetResources()); // Remove the unwanted properties from the authentication ticket. ticket.RemoveProperty(OpenIdConnectConstants.Properties.AccessTokenLifetime) .RemoveProperty(OpenIdConnectConstants.Properties.AuthorizationCodeLifetime) .RemoveProperty(OpenIdConnectConstants.Properties.CodeChallenge) .RemoveProperty(OpenIdConnectConstants.Properties.CodeChallengeMethod) .RemoveProperty(OpenIdConnectConstants.Properties.IdentityTokenLifetime) .RemoveProperty(OpenIdConnectConstants.Properties.Nonce) .RemoveProperty(OpenIdConnectConstants.Properties.OriginalRedirectUri) .RemoveProperty(OpenIdConnectConstants.Properties.RefreshTokenLifetime) .RemoveProperty(OpenIdConnectConstants.Properties.TokenUsage); var notification = new SerializeAccessTokenContext(Context, Options, request, response, ticket) { DataFormat = Options.AccessTokenFormat, Issuer = Context.GetIssuer(Options), SecurityTokenHandler = Options.AccessTokenHandler, SigningCredentials = Options.SigningCredentials.FirstOrDefault(key => key.SigningKey is SymmetricSecurityKey) ?? Options.SigningCredentials.FirstOrDefault() }; await Options.Provider.SerializeAccessToken(notification); if (notification.HandledResponse || !string.IsNullOrEmpty(notification.AccessToken)) { return(notification.AccessToken); } else if (notification.Skipped) { return(null); } if (notification.SecurityTokenHandler == null) { if (notification.DataFormat == null) { throw new InvalidOperationException("A security token handler or data formatter must be provided."); } var value = notification.DataFormat.Protect(ticket); Logger.LogTrace("A new access token was successfully generated using the " + "specified data format: {Token} ; {Claims} ; {Properties}.", value, ticket.Identity.Claims, ticket.Properties.Dictionary); return(value); } // At this stage, throw an exception if no signing credentials were provided. if (notification.SigningCredentials == null) { throw new InvalidOperationException("A signing key must be provided."); } // Store the "usage" property as a claim. ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.TokenUsage, OpenIdConnectConstants.TokenUsages.AccessToken); // Store the "unique_id" property as a claim. ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.JwtId, ticket.GetTokenId()); // Store the "confidentiality_level" property as a claim. var confidentiality = ticket.GetProperty(OpenIdConnectConstants.Properties.ConfidentialityLevel); if (!string.IsNullOrEmpty(confidentiality)) { identity.AddClaim(OpenIdConnectConstants.Claims.ConfidentialityLevel, confidentiality); } // Create a new claim per scope item, that will result // in a "scope" array being added in the access token. foreach (var scope in notification.Scopes) { ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.Scope, scope); } // Store the audiences as claims. foreach (var audience in notification.Audiences) { ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.Audience, audience); } // Extract the presenters from the authentication ticket. var presenters = notification.Presenters.ToArray(); switch (presenters.Length) { case 0: break; case 1: ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.AuthorizedParty, presenters[0]); break; default: Logger.LogWarning("Multiple presenters have been associated with the access token " + "but the JWT format only accepts single values."); // Only add the first authorized party. ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.AuthorizedParty, presenters[0]); break; } if (ticket.Properties.IssuedUtc != null) { ticket.Identity.AddClaim(new Claim( OpenIdConnectConstants.Claims.IssuedAt, EpochTime.GetIntDate(ticket.Properties.IssuedUtc.Value.UtcDateTime).ToString(), ClaimValueTypes.Integer64)); } var token = notification.SecurityTokenHandler.CreateToken(new SecurityTokenDescriptor { Subject = ticket.Identity, TokenIssuerName = notification.Issuer, SigningCredentials = notification.SigningCredentials, Lifetime = new Lifetime( notification.Ticket.Properties.IssuedUtc?.UtcDateTime, notification.Ticket.Properties.ExpiresUtc?.UtcDateTime) }); // When the access token is a JWT token, directly use RawData instead of calling // JwtSecurityTokenHandler.WriteToken(token) to avoid signing the token twice. var result = token is JwtSecurityToken jwtSecurityToken ? jwtSecurityToken.RawData : notification.SecurityTokenHandler.WriteToken(token); Logger.LogTrace("A new access token was successfully generated using the specified " + "security token handler: {Token} ; {Claims} ; {Properties}.", result, ticket.Identity.Claims, ticket.Properties.Dictionary); return(result); }
private async Task <bool> HandleSignInAsync(AuthenticationTicket ticket) { // Extract the OpenID Connect request from the OWIN context. // If it cannot be found or doesn't correspond to an authorization // or a token request, throw an InvalidOperationException. var request = Context.GetOpenIdConnectRequest(); if (request == null || (!request.IsAuthorizationRequest() && !request.IsTokenRequest())) { throw new InvalidOperationException("An authorization or token response cannot be returned from this endpoint."); } // Note: if a response was already generated, throw an exception. var response = Context.GetOpenIdConnectResponse(); if (response != null) { throw new InvalidOperationException("A response has already been sent."); } if (string.IsNullOrEmpty(ticket.Identity.GetClaim(OpenIdConnectConstants.Claims.Subject))) { throw new InvalidOperationException("The authentication ticket was rejected because " + "the mandatory subject claim was missing."); } Logger.LogTrace("A sign-in operation was triggered: {Claims} ; {Properties}.", ticket.Identity.Claims, ticket.Properties.Dictionary); // Prepare a new OpenID Connect response. response = new OpenIdConnectResponse(); // Copy the confidentiality level associated with the request to the authentication ticket. if (!ticket.HasProperty(OpenIdConnectConstants.Properties.ConfidentialityLevel)) { ticket.SetConfidentialityLevel(request.GetProperty <string>(OpenIdConnectConstants.Properties.ConfidentialityLevel)); } // Always include the "openid" scope when the developer doesn't explicitly call SetScopes. // Note: the application is allowed to specify a different "scopes": in this case, // don't replace the "scopes" property stored in the authentication ticket. if (request.HasScope(OpenIdConnectConstants.Scopes.OpenId) && !ticket.HasScope()) { ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId); } // When a "resources" property cannot be found in the ticket, // infer it from the "audiences" property. if (ticket.HasAudience() && !ticket.HasResource()) { ticket.SetResources(ticket.GetAudiences()); } // Add the validated client_id to the list of authorized presenters, // unless the presenters were explicitly set by the developer. var presenter = request.GetProperty <string>(OpenIdConnectConstants.Properties.ValidatedClientId); if (!string.IsNullOrEmpty(presenter) && !ticket.HasPresenter()) { ticket.SetPresenters(presenter); } var notification = new ProcessSigninResponseContext(Context, Options, ticket, request, response); if (request.IsAuthorizationRequest()) { // By default, return an authorization code if a response type containing code was specified. notification.IncludeAuthorizationCode = request.HasResponseType(OpenIdConnectConstants.ResponseTypes.Code); // By default, return an access token if a response type containing token was specified. notification.IncludeAccessToken = request.HasResponseType(OpenIdConnectConstants.ResponseTypes.Token); // By default, prevent a refresh token from being returned as the OAuth2 specification // explicitly disallows returning a refresh token from the authorization endpoint. // See https://tools.ietf.org/html/rfc6749#section-4.2.2 for more information. notification.IncludeRefreshToken = false; // By default, return an identity token if a response type containing code // was specified and if the openid scope was explicitly or implicitly granted. notification.IncludeIdentityToken = request.HasResponseType(OpenIdConnectConstants.ResponseTypes.IdToken) && ticket.HasScope(OpenIdConnectConstants.Scopes.OpenId); } else { // By default, prevent an authorization code from being returned as this type of token // cannot be issued from the token endpoint in the standard OAuth2/OpenID Connect flows. notification.IncludeAuthorizationCode = false; // By default, always return an access token. notification.IncludeAccessToken = true; // By default, only return a refresh token is the offline_access scope was granted and if // sliding expiration is disabled or if the request is not a grant_type=refresh_token request. notification.IncludeRefreshToken = ticket.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess) && (Options.UseSlidingExpiration || !request.IsRefreshTokenGrantType()); // By default, only return an identity token if the openid scope was granted. notification.IncludeIdentityToken = ticket.HasScope(OpenIdConnectConstants.Scopes.OpenId); } await Options.Provider.ProcessSigninResponse(notification); if (notification.HandledResponse) { Logger.LogDebug("The sign-in response was handled in user code."); return(true); } else if (notification.Skipped) { Logger.LogDebug("The default sign-in handling was skipped from user code."); return(false); } else if (notification.IsRejected) { Logger.LogError("The request was rejected with the following error: {Error} ; {Description}", /* Error: */ notification.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ notification.ErrorDescription); if (request.IsAuthorizationRequest()) { return(await SendAuthorizationResponseAsync(new OpenIdConnectResponse { Error = notification.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = notification.ErrorDescription, ErrorUri = notification.ErrorUri })); } return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = notification.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = notification.ErrorDescription, ErrorUri = notification.ErrorUri })); } // Flow the changes made to the ticket. ticket = notification.Ticket; // Ensure an authentication ticket has been provided or return // an error code indicating that the request was rejected. if (ticket == null) { Logger.LogError("The request was rejected because no authentication ticket was provided."); if (request.IsAuthorizationRequest()) { return(await SendAuthorizationResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.AccessDenied, ErrorDescription = "The authorization was denied by the resource owner." })); } return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "The token request was rejected by the authorization server." })); } if (notification.IncludeAuthorizationCode) { // 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(); response.Code = await SerializeAuthorizationCodeAsync(ticket.Identity, properties, request, response); } if (notification.IncludeAccessToken) { // 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 receiving a grant_type=refresh_token request, determine whether the client application // requests a limited set of scopes/resources and replace the corresponding properties if necessary. // Note: at this stage, request.GetResources() cannot return more items than the ones that were initially granted // by the resource owner as the "resources" parameter is always validated when receiving the token request. if (request.IsTokenRequest() && request.IsRefreshTokenGrantType()) { if (!string.IsNullOrEmpty(request.Resource)) { Logger.LogDebug("The access token resources will be limited to the resources requested " + "by the client application: {Resources}.", request.GetResources()); // Replace the resources initially granted by the resources listed by the client // application in the token request. Note: request.GetResources() automatically // removes duplicate entries, so additional filtering is not necessary. properties.SetProperty(OpenIdConnectConstants.Properties.Resources, new JArray(request.GetResources()).ToString(Formatting.None)); } if (!string.IsNullOrEmpty(request.Scope)) { Logger.LogDebug("The access token scopes will be limited to the scopes requested " + "by the client application: {Scopes}.", request.GetScopes()); // Replace the scopes initially granted by the scopes listed by the client // application in the token request. Note: request.GetScopes() automatically // removes duplicate entries, so additional filtering is not necessary. properties.SetProperty(OpenIdConnectConstants.Properties.Scopes, new JArray(request.GetScopes()).ToString(Formatting.None)); } } var resources = ticket.GetResources(); if (request.IsAuthorizationCodeGrantType() || !new HashSet <string>(resources).SetEquals(request.GetResources())) { response.Resource = string.Join(" ", resources); } var scopes = ticket.GetScopes(); if (request.IsAuthorizationCodeGrantType() || !new HashSet <string>(scopes).SetEquals(request.GetScopes())) { response.Scope = string.Join(" ", scopes); } response.TokenType = OpenIdConnectConstants.TokenTypes.Bearer; response.AccessToken = await SerializeAccessTokenAsync(ticket.Identity, properties, request, response); // 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; response.ExpiresIn = (long)(lifetime.TotalSeconds + .5); } } if (notification.IncludeRefreshToken) { // 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(); response.RefreshToken = await SerializeRefreshTokenAsync(ticket.Identity, properties, request, response); } if (notification.IncludeIdentityToken) { // 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(); response.IdToken = await SerializeIdentityTokenAsync(ticket.Identity, properties, request, response); } if (request.IsAuthorizationRequest()) { return(await SendAuthorizationResponseAsync(response, ticket)); } return(await SendTokenResponseAsync(response, ticket)); }
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); } }