private async Task <bool> TryRevokeTokensAsync([NotNull] AuthenticationTicket ticket) { // Note: if the authorization identifier is null, return true as no tokens need to be revoked. var identifier = ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId); if (string.IsNullOrEmpty(identifier)) { return(true); } var result = true; foreach (var token in await _tokenManager.FindByAuthorizationIdAsync(identifier)) { // Don't change the status of the token used in the token request. if (string.Equals(ticket.GetTokenId(), await _tokenManager.GetIdAsync(token), StringComparison.Ordinal)) { continue; } result &= await TryRevokeTokenAsync(token); } return(result); }
private async Task <bool> TryExtendTokenAsync( [NotNull] object token, [NotNull] AuthenticationTicket ticket, [NotNull] OpenIddictServerOptions options) { var identifier = ticket.GetTokenId(); Debug.Assert(!string.IsNullOrEmpty(identifier), "The token identifier shouldn't be null or empty."); try { // Compute the new expiration date of the refresh token. var date = options.SystemClock.UtcNow; date += ticket.GetRefreshTokenLifetime() ?? options.RefreshTokenLifetime; // Note: the request cancellation token is deliberately not used here to ensure the caller // cannot prevent this operation from being executed by resetting the TCP connection. await _tokenManager.ExtendAsync(token, date); _logger.LogInformation("The expiration date of the refresh token '{Identifier}' " + "was automatically updated: {Date}.", identifier, date); return(true); } catch (Exception exception) { _logger.LogDebug(exception, "An exception occurred while trying to update the " + "expiration date of the token '{Identifier}'.", identifier); return(false); } }
private async Task <bool> TryExtendTokenAsync( [NotNull] AuthenticationTicket ticket, [NotNull] HttpContext context, [NotNull] OpenIddictOptions options) { var identifier = ticket.GetTokenId(); if (string.IsNullOrEmpty(identifier)) { return(false); } var token = await Tokens.FindByIdAsync(identifier, context.RequestAborted); if (token == null) { return(false); } try { // Compute the new expiration date of the refresh token. var date = options.SystemClock.UtcNow; date += ticket.GetRefreshTokenLifetime() ?? options.RefreshTokenLifetime; await Tokens.ExtendAsync(token, date, context.RequestAborted); Logger.LogInformation("The expiration date of the refresh token '{Identifier}' " + "was automatically updated: {Date}.", identifier, date); return(true); } catch (Exception exception) { Logger.LogWarning(exception, "An exception occurred while trying to update the " + "expiration date of the token '{Identifier}'.", identifier); return(false); } }
private async Task <bool> TryRevokeTokensAsync([NotNull] AuthenticationTicket ticket, [NotNull] HttpContext context) { // Note: if the authorization identifier is null, return true as no tokens need to be revoked. var identifier = ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId); if (string.IsNullOrEmpty(identifier)) { return(true); } foreach (var token in await Tokens.FindByAuthorizationIdAsync(identifier, context.RequestAborted)) { // Don't overwrite the status of the token used in the token request. if (string.Equals(ticket.GetTokenId(), await Tokens.GetIdAsync(token, context.RequestAborted))) { continue; } try { await Tokens.RevokeAsync(token, context.RequestAborted); Logger.LogInformation("The token '{Identifier}' was automatically revoked.", await Tokens.GetIdAsync(token, context.RequestAborted)); } catch (Exception exception) { Logger.LogWarning(exception, "An exception occurred while trying to revoke the tokens " + "associated with the token '{Identifier}'.", await Tokens.GetIdAsync(token, context.RequestAborted)); return(false); } } return(true); }
private async Task <bool> TryRedeemTokenAsync([NotNull] AuthenticationTicket ticket, [NotNull] HttpContext context) { // Note: if the token identifier or the token itself // cannot be found, return true as the token doesn't need // to be revoked if it doesn't exist or is already invalid. var identifier = ticket.GetTokenId(); if (string.IsNullOrEmpty(identifier)) { return(true); } var token = await Tokens.FindByIdAsync(identifier, context.RequestAborted); if (token == null) { return(true); } try { await Tokens.RedeemAsync(token, context.RequestAborted); Logger.LogInformation("The token '{Identifier}' was automatically marked as redeemed.", identifier); return(true); } catch (Exception exception) { Logger.LogWarning(exception, "An exception occurred while trying to " + "redeem the token '{Identifier}'.", identifier); return(false); } }
private async Task <bool> InvokeIntrospectionEndpointAsync() { OpenIdConnectRequest request; // See https://tools.ietf.org/html/rfc7662#section-2.1 // and https://tools.ietf.org/html/rfc7662#section-4 if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase)) { request = new OpenIdConnectRequest(Request.Query); } else if (string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase)) { // See http://openid.net/specs/openid-connect-core-1_0.html#FormSerialization if (string.IsNullOrEmpty(Request.ContentType)) { Logger.LogError("The introspection request was rejected because " + "the mandatory 'Content-Type' header was missing."); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The mandatory 'Content-Type' header must be specified." })); } // May have media/type; charset=utf-8, allow partial match. if (!Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) { Logger.LogError("The introspection request was rejected because an invalid 'Content-Type' " + "header was specified: {ContentType}.", Request.ContentType); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The specified 'Content-Type' header is not valid." })); } request = new OpenIdConnectRequest(await Request.ReadFormAsync(Context.RequestAborted)); } else { Logger.LogError("The introspection request was rejected because an invalid " + "HTTP method was specified: {Method}.", Request.Method); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The specified HTTP method is not valid." })); } // Note: set the message type before invoking the ExtractIntrospectionRequest event. request.SetProperty(OpenIdConnectConstants.Properties.MessageType, OpenIdConnectConstants.MessageTypes.IntrospectionRequest); // Store the introspection request in the ASP.NET context. Context.SetOpenIdConnectRequest(request); var @event = new ExtractIntrospectionRequestContext(Context, Options, request); await Options.Provider.ExtractIntrospectionRequest(@event); if (@event.HandledResponse) { Logger.LogDebug("The introspection request was handled in user code."); return(true); } else if (@event.Skipped) { Logger.LogDebug("The default introspection request handling was skipped from user code."); return(false); } else if (@event.IsRejected) { Logger.LogError("The introspection request was rejected with the following error: {Error} ; {Description}", /* Error: */ @event.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ @event.ErrorDescription); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { Error = @event.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = @event.ErrorDescription, ErrorUri = @event.ErrorUri })); } Logger.LogInformation("The introspection request was successfully extracted " + "from the HTTP request: {Request}.", request); if (string.IsNullOrEmpty(request.Token)) { return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The mandatory 'token' parameter is missing." })); } // Try to resolve the client credentials specified in the 'Authorization' header. // If they cannot be extracted, fallback to the client_id/client_secret parameters. var credentials = Request.Headers.GetClientCredentials(); if (credentials != null) { // Reject requests that use multiple client authentication methods. // See https://tools.ietf.org/html/rfc6749#section-2.3 for more information. if (!string.IsNullOrEmpty(request.ClientSecret)) { Logger.LogError("The introspection request was rejected because " + "multiple client credentials were specified."); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "Multiple client credentials cannot be specified." })); } request.ClientId = credentials?.Key; request.ClientSecret = credentials?.Value; } var context = new ValidateIntrospectionRequestContext(Context, Options, request); await Options.Provider.ValidateIntrospectionRequest(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) { Logger.LogDebug("The introspection request was handled in user code."); return(true); } else if (context.Skipped) { Logger.LogDebug("The default introspection request handling was skipped from user code."); return(false); } else if (context.IsRejected) { Logger.LogError("The introspection request was rejected with the following error: {Error} ; {Description}", /* Error: */ context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ context.ErrorDescription); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { Error = context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = context.ErrorDescription, ErrorUri = context.ErrorUri })); } // Store the validated client_id as a request property. request.SetProperty(OpenIdConnectConstants.Properties.ValidatedClientId, context.ClientId); Logger.LogInformation("The introspection request was successfully validated."); AuthenticationTicket ticket = null; // Note: use the "token_type_hint" parameter to determine // the type of the token sent by the client application. // See https://tools.ietf.org/html/rfc7662#section-2.1 switch (request.TokenTypeHint) { case OpenIdConnectConstants.TokenTypeHints.AccessToken: ticket = await DeserializeAccessTokenAsync(request.Token, request); break; case OpenIdConnectConstants.TokenTypeHints.AuthorizationCode: ticket = await DeserializeAuthorizationCodeAsync(request.Token, request); break; case OpenIdConnectConstants.TokenTypeHints.IdToken: ticket = await DeserializeIdentityTokenAsync(request.Token, request); break; case OpenIdConnectConstants.TokenTypeHints.RefreshToken: ticket = await DeserializeRefreshTokenAsync(request.Token, request); break; } // Note: if the token can't be found using "token_type_hint", // the search must be extended to all supported token types. // See https://tools.ietf.org/html/rfc7662#section-2.1 if (ticket == null) { ticket = await DeserializeAccessTokenAsync(request.Token, request) ?? await DeserializeAuthorizationCodeAsync(request.Token, request) ?? await DeserializeIdentityTokenAsync(request.Token, request) ?? await DeserializeRefreshTokenAsync(request.Token, request); } if (ticket == null) { Logger.LogInformation("The introspection request was rejected because the token was invalid."); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { [OpenIdConnectConstants.Parameters.Active] = false })); } // Note: unlike refresh or identity tokens that can only be validated by client applications, // access tokens can be validated by either resource servers or client applications: // in both cases, the caller must be authenticated if the ticket is marked as confidential. if (context.IsSkipped && ticket.IsConfidential()) { Logger.LogError("The introspection request was rejected because the caller was not authenticated."); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { [OpenIdConnectConstants.Parameters.Active] = false })); } // If the ticket is already expired, directly return active=false. if (ticket.Properties.ExpiresUtc.HasValue && ticket.Properties.ExpiresUtc < Options.SystemClock.UtcNow) { Logger.LogInformation("The introspection request was rejected because the token was expired."); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { [OpenIdConnectConstants.Parameters.Active] = false })); } // When a client_id can be inferred from the introspection request, // ensure that the client application is a valid audience/presenter. if (!string.IsNullOrEmpty(context.ClientId)) { if (ticket.IsAuthorizationCode() && ticket.HasPresenter() && !ticket.HasPresenter(context.ClientId)) { Logger.LogError("The introspection request was rejected because the " + "authorization code was issued to a different client."); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { [OpenIdConnectConstants.Parameters.Active] = false })); } // Ensure the caller is listed as a valid audience or authorized presenter. else if (ticket.IsAccessToken() && ticket.HasAudience() && !ticket.HasAudience(context.ClientId) && ticket.HasPresenter() && !ticket.HasPresenter(context.ClientId)) { Logger.LogError("The introspection request was rejected because the access token " + "was issued to a different client or for another resource server."); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { [OpenIdConnectConstants.Parameters.Active] = false })); } // Reject the request if the caller is not listed as a valid audience. else if (ticket.IsIdentityToken() && ticket.HasAudience() && !ticket.HasAudience(context.ClientId)) { Logger.LogError("The introspection request was rejected because the " + "identity token was issued to a different client."); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { [OpenIdConnectConstants.Parameters.Active] = false })); } // Reject the introspection request if the caller doesn't // correspond to the client application the token was issued to. else if (ticket.IsRefreshToken() && ticket.HasPresenter() && !ticket.HasPresenter(context.ClientId)) { Logger.LogError("The introspection request was rejected because the " + "refresh token was issued to a different client."); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { [OpenIdConnectConstants.Parameters.Active] = false })); } } var notification = new HandleIntrospectionRequestContext(Context, Options, request, ticket) { Active = true, Issuer = Context.GetIssuer(Options), TokenId = ticket.GetTokenId(), TokenUsage = ticket.GetProperty(OpenIdConnectConstants.Properties.TokenUsage), Subject = ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject) }; // Note: only set "token_type" when the received token is an access token. // See https://tools.ietf.org/html/rfc7662#section-2.2 // and https://tools.ietf.org/html/rfc6749#section-5.1 if (ticket.IsAccessToken()) { notification.TokenType = OpenIdConnectConstants.TokenTypes.Bearer; } notification.IssuedAt = ticket.Properties.IssuedUtc; notification.NotBefore = ticket.Properties.IssuedUtc; notification.ExpiresAt = ticket.Properties.ExpiresUtc; // Infer the audiences/client_id claims from the properties stored in the authentication ticket. // Note: the client_id claim must be a unique string so multiple presenters cannot be returned. // To work around this limitation, only the first one is returned if multiple values are listed. notification.Audiences.UnionWith(ticket.GetAudiences()); notification.ClientId = ticket.GetPresenters().FirstOrDefault(); // Note: non-metadata claims are only added if the caller's client_id is known // AND is in the specified audiences, unless there's no explicit audience. if (!ticket.HasAudience() || (!string.IsNullOrEmpty(context.ClientId) && ticket.HasAudience(context.ClientId))) { notification.Username = ticket.Principal.Identity?.Name; notification.Scopes.UnionWith(ticket.GetScopes()); // Potentially sensitive claims are only exposed if the client was authenticated // and if the authentication ticket corresponds to an identity or access token. if (context.IsValidated && (ticket.IsAccessToken() || ticket.IsIdentityToken())) { foreach (var grouping in ticket.Principal.Claims.GroupBy(claim => claim.Type)) { // Exclude standard claims, that are already handled via strongly-typed properties. // Make sure to always update this list when adding new built-in claim properties. var type = grouping.Key; switch (type) { case OpenIdConnectConstants.Claims.Audience: case OpenIdConnectConstants.Claims.ExpiresAt: case OpenIdConnectConstants.Claims.IssuedAt: case OpenIdConnectConstants.Claims.Issuer: case OpenIdConnectConstants.Claims.NotBefore: case OpenIdConnectConstants.Claims.Scope: case OpenIdConnectConstants.Claims.Subject: case OpenIdConnectConstants.Claims.TokenType: case OpenIdConnectConstants.Claims.TokenUsage: continue; } var claims = grouping.ToArray(); switch (claims.Length) { case 0: continue; // When there's only one claim with the same type, directly // convert the claim as an OpenIdConnectParameter instance, // whose token type is determined from the claim value type. case 1: { notification.Claims[type] = claims[0].AsParameter(); continue; } // When multiple claims share the same type, convert all the claims // to OpenIdConnectParameter instances, retrieve the underlying // JSON values and add everything to a new JSON array. default: { notification.Claims[type] = new JArray(claims.Select(claim => claim.AsParameter().Value)); continue; } } } } } await Options.Provider.HandleIntrospectionRequest(notification); if (notification.HandledResponse) { Logger.LogDebug("The introspection request was handled in user code."); return(true); } else if (notification.Skipped) { Logger.LogDebug("The default introspection request handling was skipped from user code."); return(false); } else if (notification.IsRejected) { Logger.LogError("The introspection request was rejected with the following error: {Error} ; {Description}", /* Error: */ notification.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ notification.ErrorDescription); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { Error = notification.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = notification.ErrorDescription, ErrorUri = notification.ErrorUri })); } var response = new OpenIdConnectResponse { [OpenIdConnectConstants.Claims.Active] = notification.Active }; // Only add the other properties if // the token is considered as active. if (notification.Active) { response[OpenIdConnectConstants.Claims.Issuer] = notification.Issuer; response[OpenIdConnectConstants.Claims.Username] = notification.Username; response[OpenIdConnectConstants.Claims.Subject] = notification.Subject; response[OpenIdConnectConstants.Claims.Scope] = string.Join(" ", notification.Scopes); response[OpenIdConnectConstants.Claims.JwtId] = notification.TokenId; response[OpenIdConnectConstants.Claims.TokenType] = notification.TokenType; response[OpenIdConnectConstants.Claims.TokenUsage] = notification.TokenUsage; response[OpenIdConnectConstants.Claims.ClientId] = notification.ClientId; if (notification.IssuedAt != null) { response[OpenIdConnectConstants.Claims.IssuedAt] = EpochTime.GetIntDate(notification.IssuedAt.Value.UtcDateTime); } if (notification.NotBefore != null) { response[OpenIdConnectConstants.Claims.NotBefore] = EpochTime.GetIntDate(notification.NotBefore.Value.UtcDateTime); } if (notification.ExpiresAt != null) { response[OpenIdConnectConstants.Claims.ExpiresAt] = EpochTime.GetIntDate(notification.ExpiresAt.Value.UtcDateTime); } switch (notification.Audiences.Count) { case 0: break; case 1: response[OpenIdConnectConstants.Claims.Audience] = notification.Audiences.ElementAt(0); break; default: response[OpenIdConnectConstants.Claims.Audience] = new JArray(notification.Audiences); break; } foreach (var claim in notification.Claims) { response.SetParameter(claim.Key, claim.Value); } } return(await SendIntrospectionResponseAsync(response)); }
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, Scheme.Name); ticket.Properties.IssuedUtc = Options.SystemClock.UtcNow; // Only set the expiration date if a lifetime was specified in either the ticket or the options. var lifetime = ticket.GetAccessTokenLifetime() ?? Options.AccessTokenLifetime; if (lifetime.HasValue) { ticket.Properties.ExpiresUtc = ticket.Properties.IssuedUtc + lifetime.Value; } // 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, Scheme, Options, request, response, ticket) { DataFormat = Options.AccessTokenFormat, EncryptingCredentials = Options.EncryptingCredentials.FirstOrDefault( credentials => credentials.Key is SymmetricSecurityKey), Issuer = Context.GetIssuer(Options), SecurityTokenHandler = Options.AccessTokenHandler, SigningCredentials = Options.SigningCredentials.FirstOrDefault( credentials => credentials.Key is SymmetricSecurityKey) ?? Options.SigningCredentials.FirstOrDefault() }; await Provider.SerializeAccessToken(notification); if (notification.IsHandled || !string.IsNullOrEmpty(notification.AccessToken)) { return(notification.AccessToken); } 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.Principal.Claims, ticket.Properties.Items); 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."); } // Extract the main identity from the principal. identity = (ClaimsIdentity)ticket.Principal.Identity; // Store the "usage" property as a claim. identity.AddClaim(OpenIdConnectConstants.Claims.TokenUsage, OpenIdConnectConstants.TokenUsages.AccessToken); // Store the "unique_id" property as a claim. 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) { 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.CreateEncodedJwt(new SecurityTokenDescriptor { Subject = identity, Issuer = notification.Issuer, EncryptingCredentials = notification.EncryptingCredentials, SigningCredentials = notification.SigningCredentials, IssuedAt = notification.Ticket.Properties.IssuedUtc?.UtcDateTime, NotBefore = notification.Ticket.Properties.IssuedUtc?.UtcDateTime, Expires = notification.Ticket.Properties.ExpiresUtc?.UtcDateTime }); Logger.LogTrace("A new access token was successfully generated using the specified " + "security token handler: {Token} ; {Claims} ; {Properties}.", token, ticket.Principal.Claims, ticket.Properties.Items); return(token); }
private async Task <string> SerializeIdentityTokenAsync( ClaimsPrincipal principal, AuthenticationProperties properties, OpenIdConnectRequest request, OpenIdConnectResponse response) { // Replace the principal by a new one 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 "id_token" are not included in the identity token. if (!claim.HasDestination(OpenIdConnectConstants.Destinations.IdentityToken)) { Logger.LogDebug("'{Claim}' was excluded from the identity 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, Scheme.Name); ticket.Properties.IssuedUtc = Options.SystemClock.UtcNow; // Only set the expiration date if a lifetime was specified in either the ticket or the options. var lifetime = ticket.GetIdentityTokenLifetime() ?? Options.IdentityTokenLifetime; if (lifetime.HasValue) { ticket.Properties.ExpiresUtc = ticket.Properties.IssuedUtc + lifetime.Value; } // Associate a random identifier with the identity token. ticket.SetTokenId(Guid.NewGuid().ToString()); // 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.OriginalRedirectUri) .RemoveProperty(OpenIdConnectConstants.Properties.RefreshTokenLifetime) .RemoveProperty(OpenIdConnectConstants.Properties.TokenUsage); ticket.SetAudiences(ticket.GetPresenters()); var notification = new SerializeIdentityTokenContext(Context, Scheme, Options, request, response, ticket) { Issuer = Context.GetIssuer(Options), SecurityTokenHandler = Options.IdentityTokenHandler, SigningCredentials = Options.SigningCredentials.FirstOrDefault( credentials => credentials.Key is AsymmetricSecurityKey) }; await Provider.SerializeIdentityToken(notification); if (notification.IsHandled || !string.IsNullOrEmpty(notification.IdentityToken)) { return(notification.IdentityToken); } if (notification.SecurityTokenHandler == null) { throw new InvalidOperationException("A security token handler must be provided."); } // Extract the main identity from the principal. identity = (ClaimsIdentity)ticket.Principal.Identity; if (string.IsNullOrEmpty(identity.GetClaim(OpenIdConnectConstants.Claims.Subject))) { throw new InvalidOperationException("The authentication ticket was rejected because " + "the mandatory subject claim was missing."); } // Note: identity tokens must be signed but an exception is made by the OpenID Connect specification // when they are returned from the token endpoint: in this case, signing is not mandatory, as the TLS // server validation can be used as a way to ensure an identity token was issued by a trusted party. // See http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation for more information. if (notification.SigningCredentials == null && request.IsAuthorizationRequest()) { throw new InvalidOperationException("A signing key must be provided."); } // Store the "usage" property as a claim. identity.AddClaim(OpenIdConnectConstants.Claims.TokenUsage, OpenIdConnectConstants.TokenUsages.IdToken); // Store the "unique_id" property as a claim. 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); } // Store the audiences as claims. foreach (var audience in notification.Audiences) { identity.AddClaim(OpenIdConnectConstants.Claims.Audience, audience); } // If a nonce was present in the authorization request, it MUST // be included in the id_token generated by the token endpoint. // See http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation var nonce = request.Nonce; if (request.IsAuthorizationCodeGrantType()) { // Restore the nonce stored in the authentication // ticket extracted from the authorization code. nonce = ticket.GetProperty(OpenIdConnectConstants.Properties.Nonce); } if (!string.IsNullOrEmpty(nonce)) { identity.AddClaim(OpenIdConnectConstants.Claims.Nonce, nonce); } if (notification.SigningCredentials != null && (!string.IsNullOrEmpty(response.Code) || !string.IsNullOrEmpty(response.AccessToken))) { using (var algorithm = OpenIdConnectServerHelpers.GetHashAlgorithm(notification.SigningCredentials.Algorithm)) { // Create an authorization code hash if necessary. if (!string.IsNullOrEmpty(response.Code)) { var hash = algorithm.ComputeHash(Encoding.ASCII.GetBytes(response.Code)); // Note: only the left-most half of the hash of the octets is used. // See http://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken identity.AddClaim(OpenIdConnectConstants.Claims.CodeHash, Base64UrlEncoder.Encode(hash, 0, hash.Length / 2)); } // Create an access token hash if necessary. if (!string.IsNullOrEmpty(response.AccessToken)) { var hash = algorithm.ComputeHash(Encoding.ASCII.GetBytes(response.AccessToken)); // Note: only the left-most half of the hash of the octets is used. // See http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken identity.AddClaim(OpenIdConnectConstants.Claims.AccessTokenHash, Base64UrlEncoder.Encode(hash, 0, hash.Length / 2)); } } } // 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 identity 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.CreateEncodedJwt(new SecurityTokenDescriptor { Subject = identity, Issuer = notification.Issuer, EncryptingCredentials = notification.EncryptingCredentials, SigningCredentials = notification.SigningCredentials, IssuedAt = notification.Ticket.Properties.IssuedUtc?.UtcDateTime, NotBefore = notification.Ticket.Properties.IssuedUtc?.UtcDateTime, Expires = notification.Ticket.Properties.ExpiresUtc?.UtcDateTime }); Logger.LogTrace("A new identity token was successfully generated using the specified " + "security token handler: {Token} ; {Claims} ; {Properties}.", token, ticket.Principal.Claims, ticket.Properties.Items); return(token); }