2.4JwtBearerHandler 【AuthenticationHandler】
using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Security.Claims; using System.Text; using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Authentication.JwtBearer { /// <summary> /// An <see cref="AuthenticationHandler{TOptions}"/> that can perform JWT-bearer based authentication. /// </summary> public class JwtBearerHandler : AuthenticationHandler<JwtBearerOptions> { private OpenIdConnectConfiguration? _configuration; /// <summary> /// Initializes a new instance of <see cref="JwtBearerHandler"/>. /// </summary> /// <inheritdoc /> public JwtBearerHandler(IOptionsMonitor<JwtBearerOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } /// <summary> /// The handler calls methods on the events which give the application control at certain points where processing is occurring. /// If it is not provided a default instance is supplied which does nothing when the methods are called. /// </summary> protected new JwtBearerEvents Events { get => (JwtBearerEvents)base.Events!; set => base.Events = value; } /// <inheritdoc /> protected override Task<object> CreateEventsAsync() => Task.FromResult<object>(new JwtBearerEvents()); /// <summary> /// Searches the 'Authorization' header for a 'Bearer' token. If the 'Bearer' token is found, it is validated using <see cref="TokenValidationParameters"/> set in the options. /// </summary> /// <returns></returns> protected override async Task<AuthenticateResult> HandleAuthenticateAsync() { string? token = null; try { // Give application opportunity to find from a different location, adjust, or reject token var messageReceivedContext = new MessageReceivedContext(Context, Scheme, Options); // event can set the token await Events.MessageReceived(messageReceivedContext); if (messageReceivedContext.Result != null) { return messageReceivedContext.Result; } // If application retrieved token from somewhere else, use that. token = messageReceivedContext.Token; if (string.IsNullOrEmpty(token)) { string authorization = Request.Headers.Authorization.ToString(); // If no authorization header found, nothing to process further if (string.IsNullOrEmpty(authorization)) { return AuthenticateResult.NoResult(); } if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) { token = authorization.Substring("Bearer ".Length).Trim(); } // If no token found, no further work possible if (string.IsNullOrEmpty(token)) { return AuthenticateResult.NoResult(); } } if (_configuration == null && Options.ConfigurationManager != null) { _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); } var validationParameters = Options.TokenValidationParameters.Clone(); if (_configuration != null) { var issuers = new[] { _configuration.Issuer }; validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuers) ?? issuers; validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys) ?? _configuration.SigningKeys; } List<Exception>? validationFailures = null; SecurityToken? validatedToken = null; foreach (var validator in Options.SecurityTokenValidators) { if (validator.CanReadToken(token)) { ClaimsPrincipal principal; try {
//ISecurityTokenValidator principal = validator.ValidateToken(token, validationParameters, out validatedToken); } catch (Exception ex) { Logger.TokenValidationFailed(ex); // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event. if (Options.RefreshOnIssuerKeyNotFound && Options.ConfigurationManager != null && ex is SecurityTokenSignatureKeyNotFoundException) { Options.ConfigurationManager.RequestRefresh(); } if (validationFailures == null) { validationFailures = new List<Exception>(1); } validationFailures.Add(ex); continue; } Logger.TokenValidationSucceeded(); var tokenValidatedContext = new TokenValidatedContext(Context, Scheme, Options) { Principal = principal, SecurityToken = validatedToken }; tokenValidatedContext.Properties.ExpiresUtc = GetSafeDateTime(validatedToken.ValidTo); tokenValidatedContext.Properties.IssuedUtc = GetSafeDateTime(validatedToken.ValidFrom); await Events.TokenValidated(tokenValidatedContext); if (tokenValidatedContext.Result != null) { return tokenValidatedContext.Result; } if (Options.SaveToken) { tokenValidatedContext.Properties.StoreTokens(new[] { new AuthenticationToken { Name = "access_token", Value = token } }); } tokenValidatedContext.Success(); return tokenValidatedContext.Result!; } } if (validationFailures != null) { var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options) { Exception = (validationFailures.Count == 1) ? validationFailures[0] : new AggregateException(validationFailures) }; await Events.AuthenticationFailed(authenticationFailedContext); if (authenticationFailedContext.Result != null) { return authenticationFailedContext.Result; } return AuthenticateResult.Fail(authenticationFailedContext.Exception); } return AuthenticateResult.Fail("No SecurityTokenValidator available for token."); } catch (Exception ex) { Logger.ErrorProcessingMessage(ex); var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options) { Exception = ex }; await Events.AuthenticationFailed(authenticationFailedContext); if (authenticationFailedContext.Result != null) { return authenticationFailedContext.Result; } throw; } } private static DateTime? GetSafeDateTime(DateTime dateTime) { // Assigning DateTime.MinValue or default(DateTime) to a DateTimeOffset when in a UTC+X timezone will throw // Since we don't really care about DateTime.MinValue in this case let's just set the field to null if (dateTime == DateTime.MinValue) { return null; } return dateTime; } /// <inheritdoc /> protected override async Task HandleChallengeAsync(AuthenticationProperties properties) { var authResult = await HandleAuthenticateOnceSafeAsync(); var eventContext = new JwtBearerChallengeContext(Context, Scheme, Options, properties) { AuthenticateFailure = authResult?.Failure }; // Avoid returning error=invalid_token if the error is not caused by an authentication failure (e.g missing token). if (Options.IncludeErrorDetails && eventContext.AuthenticateFailure != null) { eventContext.Error = "invalid_token"; eventContext.ErrorDescription = CreateErrorDescription(eventContext.AuthenticateFailure); } await Events.Challenge(eventContext); if (eventContext.Handled) { return; } Response.StatusCode = 401; if (string.IsNullOrEmpty(eventContext.Error) && string.IsNullOrEmpty(eventContext.ErrorDescription) && string.IsNullOrEmpty(eventContext.ErrorUri)) { Response.Headers.Append(HeaderNames.WWWAuthenticate, Options.Challenge); } else { // https://tools.ietf.org/html/rfc6750#section-3.1 // WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="The access token expired" var builder = new StringBuilder(Options.Challenge); if (Options.Challenge.IndexOf(' ') > 0) { // Only add a comma after the first param, if any builder.Append(','); } if (!string.IsNullOrEmpty(eventContext.Error)) { builder.Append(" error=\""); builder.Append(eventContext.Error); builder.Append('\"'); } if (!string.IsNullOrEmpty(eventContext.ErrorDescription)) { if (!string.IsNullOrEmpty(eventContext.Error)) { builder.Append(','); } builder.Append(" error_description=\""); builder.Append(eventContext.ErrorDescription); builder.Append('\"'); } if (!string.IsNullOrEmpty(eventContext.ErrorUri)) { if (!string.IsNullOrEmpty(eventContext.Error) || !string.IsNullOrEmpty(eventContext.ErrorDescription)) { builder.Append(','); } builder.Append(" error_uri=\""); builder.Append(eventContext.ErrorUri); builder.Append('\"'); } Response.Headers.Append(HeaderNames.WWWAuthenticate, builder.ToString()); } } /// <inheritdoc /> protected override Task HandleForbiddenAsync(AuthenticationProperties properties) { var forbiddenContext = new ForbiddenContext(Context, Scheme, Options); Response.StatusCode = 403; return Events.Forbidden(forbiddenContext); } private static string CreateErrorDescription(Exception authFailure) { IReadOnlyCollection<Exception> exceptions; if (authFailure is AggregateException agEx) { exceptions = agEx.InnerExceptions; } else { exceptions = new[] { authFailure }; } var messages = new List<string>(exceptions.Count); foreach (var ex in exceptions) { // Order sensitive, some of these exceptions derive from others // and we want to display the most specific message possible. switch (ex) { case SecurityTokenInvalidAudienceException stia: messages.Add($"The audience '{stia.InvalidAudience ?? "(null)"}' is invalid"); break; case SecurityTokenInvalidIssuerException stii: messages.Add($"The issuer '{stii.InvalidIssuer ?? "(null)"}' is invalid"); break; case SecurityTokenNoExpirationException _: messages.Add("The token has no expiration"); break; case SecurityTokenInvalidLifetimeException stil: messages.Add("The token lifetime is invalid; NotBefore: " + $"'{stil.NotBefore?.ToString(CultureInfo.InvariantCulture) ?? "(null)"}'" + $", Expires: '{stil.Expires?.ToString(CultureInfo.InvariantCulture) ?? "(null)"}'"); break; case SecurityTokenNotYetValidException stnyv: messages.Add($"The token is not valid before '{stnyv.NotBefore.ToString(CultureInfo.InvariantCulture)}'"); break; case SecurityTokenExpiredException ste: messages.Add($"The token expired at '{ste.Expires.ToString(CultureInfo.InvariantCulture)}'"); break; case SecurityTokenSignatureKeyNotFoundException _: messages.Add("The signature key was not found"); break; case SecurityTokenInvalidSignatureException _: messages.Add("The signature is invalid"); break; } } return string.Join("; ", messages); } } }
using System; using System.Security.Claims; namespace Microsoft.IdentityModel.Tokens { /// <summary> /// ISecurityTokenValidator /// </summary> public interface ISecurityTokenValidator { /// <summary> /// Returns true if the token can be read, false otherwise. /// </summary> bool CanReadToken(string securityToken); /// <summary> /// Returns true if a token can be validated. /// </summary> bool CanValidateToken { get; } /// <summary> /// Gets and sets the maximum size in bytes, that a will be processed. /// </summary> Int32 MaximumTokenSizeInBytes { get; set; } /// <summary> /// Validates a token passed as a string using <see cref="TokenValidationParameters"/> /// </summary> ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken); } }
using System.Collections.Generic; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.RegularExpressions; using System.Xml; using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.JsonWebTokens; using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; namespace System.IdentityModel.Tokens.Jwt { /// <summary> /// A <see cref="SecurityTokenHandler"/> designed for creating and validating Json Web Tokens. See: http://tools.ietf.org/html/rfc7519 and http://www.rfc-editor.org/info/rfc7515 /// </summary> public class JwtSecurityTokenHandler : SecurityTokenHandler { private delegate bool CertMatcher(X509Certificate2 cert); private ISet<string> _inboundClaimFilter; private IDictionary<string, string> _inboundClaimTypeMap; private static string _jsonClaimType = _namespace + "/json_type"; private const string _namespace = "http://schemas.xmlsoap.org/ws/2005/05/identity/claimproperties"; private IDictionary<string, string> _outboundClaimTypeMap; private IDictionary<string, string> _outboundAlgorithmMap = null; private static string _shortClaimType = _namespace + "/ShortTypeName"; private bool _mapInboundClaims = DefaultMapInboundClaims; /// <summary> /// Default claim type mapping for inbound claims. /// </summary> public static IDictionary<string, string> DefaultInboundClaimTypeMap = ClaimTypeMapping.InboundClaimTypeMap; /// <summary> /// Default value for the flag that determines whether or not the InboundClaimTypeMap is used. /// </summary> public static bool DefaultMapInboundClaims = true; /// <summary> /// Default claim type mapping for outbound claims. /// </summary> public static IDictionary<string, string> DefaultOutboundClaimTypeMap = ClaimTypeMapping.OutboundClaimTypeMap; /// <summary> /// Default claim type filter list. /// </summary> public static ISet<string> DefaultInboundClaimFilter = ClaimTypeMapping.InboundClaimFilter; /// <summary> /// Default JwtHeader algorithm mapping /// </summary> public static IDictionary<string, string> DefaultOutboundAlgorithmMap = new Dictionary<string, string> { { SecurityAlgorithms.EcdsaSha256Signature, SecurityAlgorithms.EcdsaSha256 }, { SecurityAlgorithms.EcdsaSha384Signature, SecurityAlgorithms.EcdsaSha384 }, { SecurityAlgorithms.EcdsaSha512Signature, SecurityAlgorithms.EcdsaSha512 }, { SecurityAlgorithms.HmacSha256Signature, SecurityAlgorithms.HmacSha256 }, { SecurityAlgorithms.HmacSha384Signature, SecurityAlgorithms.HmacSha384 }, { SecurityAlgorithms.HmacSha512Signature, SecurityAlgorithms.HmacSha512 }, { SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.RsaSha256 }, { SecurityAlgorithms.RsaSha384Signature, SecurityAlgorithms.RsaSha384 }, { SecurityAlgorithms.RsaSha512Signature, SecurityAlgorithms.RsaSha512 }, }; /// <summary> /// Static initializer for a new object. Static initializers run before the first instance of the type is created. /// </summary> static JwtSecurityTokenHandler() { LogHelper.LogVerbose("Assembly version info: " + typeof(JwtSecurityTokenHandler).AssemblyQualifiedName); } /// <summary> /// Initializes a new instance of the <see cref="JwtSecurityTokenHandler"/> class. /// </summary> public JwtSecurityTokenHandler() { if (_mapInboundClaims) _inboundClaimTypeMap = new Dictionary<string, string>(DefaultInboundClaimTypeMap); else _inboundClaimTypeMap = new Dictionary<string, string>(); _outboundClaimTypeMap = new Dictionary<string, string>(DefaultOutboundClaimTypeMap); _inboundClaimFilter = new HashSet<string>(DefaultInboundClaimFilter); _outboundAlgorithmMap = new Dictionary<string, string>(DefaultOutboundAlgorithmMap); } /// <summary> /// Gets or sets the <see cref="MapInboundClaims"/> property which is used when determining whether or not to map claim types that are extracted when validating a <see cref="JwtSecurityToken"/>. /// <para>If this is set to true, the <see cref="Claim.Type"/> is set to the JSON claim 'name' after translating using this mapping. Otherwise, no mapping occurs.</para> /// <para>The default value is true.</para> /// </summary> public bool MapInboundClaims { get { return _mapInboundClaims; } set { // If the inbound claim type mapping was turned off and is being turned on for the first time, make sure that the _inboundClaimTypeMap is populated with the default mappings. if (!_mapInboundClaims && value && _inboundClaimTypeMap.Count == 0) _inboundClaimTypeMap = new Dictionary<string, string>(DefaultInboundClaimTypeMap); _mapInboundClaims = value; } } /// <summary> /// Gets or sets the <see cref="InboundClaimTypeMap"/> which is used when setting the <see cref="Claim.Type"/> for claims in the <see cref="ClaimsPrincipal"/> extracted when validating a <see cref="JwtSecurityToken"/>. /// <para>The <see cref="Claim.Type"/> is set to the JSON claim 'name' after translating using this mapping.</para> /// <para>The default value is ClaimTypeMapping.InboundClaimTypeMap.</para> /// </summary> /// <exception cref="ArgumentNullException">'value' is null.</exception> public IDictionary<string, string> InboundClaimTypeMap { get { return _inboundClaimTypeMap; } set { _inboundClaimTypeMap = value ?? throw LogHelper.LogArgumentNullException(nameof(value)); } } /// <summary> /// <para>Gets or sets the <see cref="OutboundClaimTypeMap"/> which is used when creating a <see cref="JwtSecurityToken"/> from <see cref="Claim"/>(s).</para> /// <para>The JSON claim 'name' value is set to <see cref="Claim.Type"/> after translating using this mapping.</para> /// <para>The default value is ClaimTypeMapping.OutboundClaimTypeMap</para> /// </summary> /// <remarks>This mapping is applied only when using <see cref="JwtPayload.AddClaim"/> or <see cref="JwtPayload.AddClaims"/>. Adding values directly will not result in translation.</remarks> /// <exception cref="ArgumentNullException">'value' is null.</exception> public IDictionary<string, string> OutboundClaimTypeMap { get { return _outboundClaimTypeMap; } set { if (value == null) throw LogHelper.LogArgumentNullException(nameof(value)); _outboundClaimTypeMap = value; } } /// <summary> /// Gets the outbound algorithm map that is passed to the <see cref="JwtHeader"/> constructor. /// </summary> public IDictionary<string, string> OutboundAlgorithmMap { get { return _outboundAlgorithmMap; } } /// <summary>Gets or sets the <see cref="ISet{String}"/> used to filter claims when populating a <see cref="ClaimsIdentity"/> claims form a <see cref="JwtSecurityToken"/>. /// When a <see cref="JwtSecurityToken"/> is validated, claims with types found in this <see cref="ISet{String}"/> will not be added to the <see cref="ClaimsIdentity"/>. /// <para>The default value is ClaimTypeMapping.InboundClaimFilter.</para> /// </summary> /// <exception cref="ArgumentNullException">'value' is null.</exception> public ISet<string> InboundClaimFilter { get { return _inboundClaimFilter; } set { if (value == null) throw LogHelper.LogArgumentNullException(nameof(value)); _inboundClaimFilter = value; } } /// <summary> /// Gets or sets the property name of <see cref="Claim.Properties"/> the will contain the original JSON claim 'name' if a mapping occurred when the <see cref="Claim"/>(s) were created. /// <para>See <seealso cref="InboundClaimTypeMap"/> for more information.</para> /// </summary> /// <exception cref="ArgumentException">If <see cref="string"/>.IsNullOrWhiteSpace('value') is true.</exception> public static string ShortClaimTypeProperty { get { return _shortClaimType; } set { if (string.IsNullOrWhiteSpace(value)) throw LogHelper.LogArgumentNullException(nameof(value)); _shortClaimType = value; } } /// <summary> /// Gets or sets the property name of <see cref="Claim.Properties"/> the will contain .Net type that was recognized when JwtPayload.Claims serialized the value to JSON. /// <para>See <seealso cref="InboundClaimTypeMap"/> for more information.</para> /// </summary> /// <exception cref="ArgumentException">If <see cref="string"/>.IsNullOrWhiteSpace('value') is true.</exception> public static string JsonClaimTypeProperty { get { return _jsonClaimType; } set { if (string.IsNullOrWhiteSpace(value)) throw LogHelper.LogArgumentNullException(nameof(value)); _jsonClaimType = value; } } /// <summary> /// Returns a value that indicates if this handler can validate a <see cref="SecurityToken"/>. /// </summary> /// <returns>'true', indicating this instance can validate a <see cref="JwtSecurityToken"/>.</returns> public override bool CanValidateToken { get { return true; } } /// <summary> /// Gets the value that indicates if this instance can write a <see cref="SecurityToken"/>. /// </summary> /// <returns>'true', indicating this instance can write a <see cref="JwtSecurityToken"/>.</returns> public override bool CanWriteToken { get { return true; } } /// <summary> /// Gets the type of the <see cref="System.IdentityModel.Tokens.Jwt.JwtSecurityToken"/>. /// </summary> /// <return>The type of <see cref="System.IdentityModel.Tokens.Jwt.JwtSecurityToken"/></return> public override Type TokenType { get { return typeof(JwtSecurityToken); } } /// <summary> /// Determines if the string is a well formed Json Web Token (JWT). /// <para>see: http://tools.ietf.org/html/rfc7519 </para> /// </summary> /// <param name="token">String that should represent a valid JWT.</param> /// <remarks>Uses <see cref="Regex.IsMatch(string, string)"/> matching one of: /// <para>JWS: @"^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$"</para> /// <para>JWE: (dir): @"^[A-Za-z0-9-_]+\.\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$"</para> /// <para>JWE: (wrappedkey): @"^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]$"</para> /// </remarks> /// <returns> /// <para>'false' if the token is null or whitespace.</para> /// <para>'false' if token.Length is greater than <see cref="TokenHandler.MaximumTokenSizeInBytes"/>.</para> /// <para>'true' if the token is in JSON compact serialization format.</para> /// </returns> public override bool CanReadToken(string token) { if (string.IsNullOrWhiteSpace(token)) return false; if (token.Length > MaximumTokenSizeInBytes) { LogHelper.LogInformation(TokenLogMessages.IDX10209, token.Length, MaximumTokenSizeInBytes); return false; } // Set the maximum number of segments to MaxJwtSegmentCount + 1. This controls the number of splits and allows detecting the number of segments is too large. // For example: "a.b.c.d.e.f.g.h" => [a], [b], [c], [d], [e], [f.g.h]. 6 segments. // If just MaxJwtSegmentCount was used, then [a], [b], [c], [d], [e.f.g.h] would be returned. 5 segments. string[] tokenParts = token.Split(new char[] { '.' }, JwtConstants.MaxJwtSegmentCount + 1); if (tokenParts.Length == JwtConstants.JwsSegmentCount) { return JwtTokenUtilities.RegexJws.IsMatch(token); } else if (tokenParts.Length == JwtConstants.JweSegmentCount) { return JwtTokenUtilities.RegexJwe.IsMatch(token); } LogHelper.LogInformation(LogMessages.IDX12720); return false; } /// <summary> /// Returns a Json Web Token (JWT). /// </summary> /// <param name="tokenDescriptor">A <see cref="SecurityTokenDescriptor"/> that contains details of contents of the token.</param> /// <remarks>A JWS and JWE can be returned. /// <para>If <see cref="SecurityTokenDescriptor.EncryptingCredentials"/>is provided, then a JWE will be created.</para> /// <para>If <see cref="SecurityTokenDescriptor.SigningCredentials"/> is provided then a JWS will be created.</para> /// <para>If both are provided then a JWE with an embedded JWS will be created.</para> /// </remarks> public virtual string CreateEncodedJwt(SecurityTokenDescriptor tokenDescriptor) { if (tokenDescriptor == null) throw LogHelper.LogArgumentNullException(nameof(tokenDescriptor)); return CreateJwtSecurityToken(tokenDescriptor).RawData; } /// <summary> /// Creates a JWT in 'Compact Serialization Format'. /// </summary> /// <param name="issuer">The issuer of the token.</param> /// <param name="audience">The audience for this token.</param> /// <param name="subject">The source of the <see cref="Claim"/>(s) for this token.</param> /// <param name="notBefore">The notbefore time for this token.</param> /// <param name="expires">The expiration time for this token.</param> /// <param name="issuedAt">The issue time for this token.</param> /// <param name="signingCredentials">Contains cryptographic material for generating a signature.</param> /// <remarks>If <see cref="ClaimsIdentity.Actor"/> is not null, then a claim { actort, 'value' } will be added to the payload. See <see cref="CreateActorValue"/> for details on how the value is created. /// <para>See <seealso cref="JwtHeader"/> for details on how the HeaderParameters are added to the header.</para> /// <para>See <seealso cref="JwtPayload"/> for details on how the values are added to the payload.</para> /// <para>Each <see cref="Claim"/> in the <paramref name="subject"/> will map <see cref="Claim.Type"/> by applying <see cref="OutboundClaimTypeMap"/>. Modifying <see cref="OutboundClaimTypeMap"/> could change the outbound JWT.</para> /// <para>If <see cref="SigningCredentials"/> is provided, then a JWS will be created.</para> /// </remarks> /// <returns>A Base64UrlEncoded string in 'Compact Serialization Format'.</returns> public virtual string CreateEncodedJwt(string issuer, string audience, ClaimsIdentity subject, DateTime? notBefore, DateTime? expires, DateTime? issuedAt, SigningCredentials signingCredentials) { return CreateJwtSecurityTokenPrivate(issuer, audience, subject, notBefore, expires, issuedAt, signingCredentials, null, null, null).RawData; } /// <summary> /// Creates a JWT in 'Compact Serialization Format'. /// </summary> /// <param name="issuer">The issuer of the token.</param> /// <param name="audience">The audience for this token.</param> /// <param name="subject">The source of the <see cref="Claim"/>(s) for this token.</param> /// <param name="notBefore">Translated into 'epoch time' and assigned to 'nbf'.</param> /// <param name="expires">Translated into 'epoch time' and assigned to 'exp'.</param> /// <param name="issuedAt">Translated into 'epoch time' and assigned to 'iat'.</param> /// <param name="signingCredentials">Contains cryptographic material for signing.</param> /// <param name="encryptingCredentials">Contains cryptographic material for encrypting.</param> /// <remarks>If <see cref="ClaimsIdentity.Actor"/> is not null, then a claim { actort, 'value' } will be added to the payload. <see cref="CreateActorValue"/> for details on how the value is created. /// <para>See <seealso cref="JwtHeader"/> for details on how the HeaderParameters are added to the header.</para> /// <para>See <seealso cref="JwtPayload"/> for details on how the values are added to the payload.</para> /// <para>Each <see cref="Claim"/> in the <paramref name="subject"/> will map <see cref="Claim.Type"/> by applying <see cref="OutboundClaimTypeMap"/>. Modifying <see cref="OutboundClaimTypeMap"/> could change the outbound JWT.</para> /// </remarks> /// <returns>A Base64UrlEncoded string in 'Compact Serialization Format'.</returns> /// <exception cref="ArgumentException">If 'expires' <= 'notBefore'.</exception> public virtual string CreateEncodedJwt(string issuer, string audience, ClaimsIdentity subject, DateTime? notBefore, DateTime? expires, DateTime? issuedAt, SigningCredentials signingCredentials, EncryptingCredentials encryptingCredentials) { return CreateJwtSecurityTokenPrivate(issuer, audience, subject, notBefore, expires, issuedAt, signingCredentials, encryptingCredentials, null, null).RawData; } /// <summary> /// Creates a JWT in 'Compact Serialization Format'. /// </summary> /// <param name="issuer">The issuer of the token.</param> /// <param name="audience">The audience for this token.</param> /// <param name="subject">The source of the <see cref="Claim"/>(s) for this token.</param> /// <param name="notBefore">Translated into 'epoch time' and assigned to 'nbf'.</param> /// <param name="expires">Translated into 'epoch time' and assigned to 'exp'.</param> /// <param name="issuedAt">Translated into 'epoch time' and assigned to 'iat'.</param> /// <param name="signingCredentials">Contains cryptographic material for signing.</param> /// <param name="encryptingCredentials">Contains cryptographic material for encrypting.</param> /// <param name="claimCollection">A collection of (key,value) pairs representing <see cref="Claim"/>(s) for this token.</param> /// <remarks>If <see cref="ClaimsIdentity.Actor"/> is not null, then a claim { actort, 'value' } will be added to the payload. <see cref="CreateActorValue"/> for details on how the value is created. /// <para>See <seealso cref="JwtHeader"/> for details on how the HeaderParameters are added to the header.</para> /// <para>See <seealso cref="JwtPayload"/> for details on how the values are added to the payload.</para> /// <para>Each <see cref="Claim"/> in the <paramref name="subject"/> will map <see cref="Claim.Type"/> by applying <see cref="OutboundClaimTypeMap"/>. Modifying <see cref="OutboundClaimTypeMap"/> could change the outbound JWT.</para> /// </remarks> /// <returns>A Base64UrlEncoded string in 'Compact Serialization Format'.</returns> /// <exception cref="ArgumentException">If 'expires' <= 'notBefore'.</exception> public virtual string CreateEncodedJwt(string issuer, string audience, ClaimsIdentity subject, DateTime? notBefore, DateTime? expires, DateTime? issuedAt, SigningCredentials signingCredentials, EncryptingCredentials encryptingCredentials, IDictionary<string, object> claimCollection) { return CreateJwtSecurityTokenPrivate(issuer, audience, subject, notBefore, expires, issuedAt, signingCredentials, encryptingCredentials, claimCollection, null).RawData; } /// <summary> /// Creates a Json Web Token (JWT). /// </summary> /// <param name="tokenDescriptor"> A <see cref="SecurityTokenDescriptor"/> that contains details of contents of the token.</param> /// <remarks><see cref="SecurityTokenDescriptor.SigningCredentials"/> is used to sign <see cref="JwtSecurityToken.RawData"/>.</remarks> public virtual JwtSecurityToken CreateJwtSecurityToken(SecurityTokenDescriptor tokenDescriptor) { if (tokenDescriptor == null) throw LogHelper.LogArgumentNullException(nameof(tokenDescriptor)); return CreateJwtSecurityTokenPrivate( tokenDescriptor.Issuer, tokenDescriptor.Audience, tokenDescriptor.Subject, tokenDescriptor.NotBefore, tokenDescriptor.Expires, tokenDescriptor.IssuedAt, tokenDescriptor.SigningCredentials, tokenDescriptor.EncryptingCredentials, tokenDescriptor.Claims, tokenDescriptor.TokenType); } /// <summary> /// Creates a <see cref="JwtSecurityToken"/> /// </summary> /// <param name="issuer">The issuer of the token.</param> /// <param name="audience">The audience for this token.</param> /// <param name="subject">The source of the <see cref="Claim"/>(s) for this token.</param> /// <param name="notBefore">The notbefore time for this token.</param> /// <param name="expires">The expiration time for this token.</param> /// <param name="issuedAt">The issue time for this token.</param> /// <param name="signingCredentials">Contains cryptographic material for generating a signature.</param> /// <param name="encryptingCredentials">Contains cryptographic material for encrypting the token.</param> /// <remarks>If <see cref="ClaimsIdentity.Actor"/> is not null, then a claim { actort, 'value' } will be added to the payload. <see cref="CreateActorValue"/> for details on how the value is created. /// <para>See <seealso cref="JwtHeader"/> for details on how the HeaderParameters are added to the header.</para> /// <para>See <seealso cref="JwtPayload"/> for details on how the values are added to the payload.</para> /// <para>Each <see cref="Claim"/> on the <paramref name="subject"/> added will have <see cref="Claim.Type"/> translated according to the mapping found in /// <see cref="OutboundClaimTypeMap"/>. Adding and removing to <see cref="OutboundClaimTypeMap"/> will affect the name component of the Json claim.</para> /// <para><see cref="SigningCredentials.SigningCredentials(SecurityKey, string)"/> is used to sign <see cref="JwtSecurityToken.RawData"/>.</para> /// <para><see cref="EncryptingCredentials.EncryptingCredentials(SecurityKey, string, string)"/> is used to encrypt <see cref="JwtSecurityToken.RawData"/> or <see cref="JwtSecurityToken.RawPayload"/> .</para> /// </remarks> /// <returns>A <see cref="JwtSecurityToken"/>.</returns> /// <exception cref="ArgumentException">If 'expires' <= 'notBefore'.</exception> public virtual JwtSecurityToken CreateJwtSecurityToken(string issuer, string audience, ClaimsIdentity subject, DateTime? notBefore, DateTime? expires, DateTime? issuedAt, SigningCredentials signingCredentials, EncryptingCredentials encryptingCredentials) { return CreateJwtSecurityTokenPrivate(issuer, audience, subject, notBefore, expires, issuedAt, signingCredentials, encryptingCredentials, null, null); } /// <summary> /// Creates a <see cref="JwtSecurityToken"/> /// </summary> /// <param name="issuer">The issuer of the token.</param> /// <param name="audience">The audience for this token.</param> /// <param name="subject">The source of the <see cref="Claim"/>(s) for this token.</param> /// <param name="notBefore">The notbefore time for this token.</param> /// <param name="expires">The expiration time for this token.</param> /// <param name="issuedAt">The issue time for this token.</param> /// <param name="signingCredentials">Contains cryptographic material for generating a signature.</param> /// <param name="encryptingCredentials">Contains cryptographic material for encrypting the token.</param> /// <param name="claimCollection">A collection of (key,value) pairs representing <see cref="Claim"/>(s) for this token.</param> /// <remarks>If <see cref="ClaimsIdentity.Actor"/> is not null, then a claim { actort, 'value' } will be added to the payload. <see cref="CreateActorValue"/> for details on how the value is created. /// <para>See <seealso cref="JwtHeader"/> for details on how the HeaderParameters are added to the header.</para> /// <para>See <seealso cref="JwtPayload"/> for details on how the values are added to the payload.</para> /// <para>Each <see cref="Claim"/> on the <paramref name="subject"/> added will have <see cref="Claim.Type"/> translated according to the mapping found in /// <see cref="OutboundClaimTypeMap"/>. Adding and removing to <see cref="OutboundClaimTypeMap"/> will affect the name component of the Json claim.</para> /// <para><see cref="SigningCredentials.SigningCredentials(SecurityKey, string)"/> is used to sign <see cref="JwtSecurityToken.RawData"/>.</para> /// <para><see cref="EncryptingCredentials.EncryptingCredentials(SecurityKey, string, string)"/> is used to encrypt <see cref="JwtSecurityToken.RawData"/> or <see cref="JwtSecurityToken.RawPayload"/> .</para> /// </remarks> /// <returns>A <see cref="JwtSecurityToken"/>.</returns> /// <exception cref="ArgumentException">If 'expires' <= 'notBefore'.</exception> public virtual JwtSecurityToken CreateJwtSecurityToken(string issuer, string audience, ClaimsIdentity subject, DateTime? notBefore, DateTime? expires, DateTime? issuedAt, SigningCredentials signingCredentials, EncryptingCredentials encryptingCredentials, IDictionary<string, object> claimCollection) { return CreateJwtSecurityTokenPrivate(issuer, audience, subject, notBefore, expires, issuedAt, signingCredentials, encryptingCredentials, claimCollection, null); } /// <summary> /// Creates a <see cref="JwtSecurityToken"/> /// </summary> /// <param name="issuer">The issuer of the token.</param> /// <param name="audience">The audience for this token.</param> /// <param name="subject">The source of the <see cref="Claim"/>(s) for this token.</param> /// <param name="notBefore">The notbefore time for this token.</param> /// <param name="expires">The expiration time for this token.</param> /// <param name="issuedAt">The issue time for this token.</param> /// <param name="signingCredentials">Contains cryptographic material for generating a signature.</param> /// <remarks>If <see cref="ClaimsIdentity.Actor"/> is not null, then a claim { actort, 'value' } will be added to the payload. <see cref="CreateActorValue"/> for details on how the value is created. /// <para>See <seealso cref="JwtHeader"/> for details on how the HeaderParameters are added to the header.</para> /// <para>See <seealso cref="JwtPayload"/> for details on how the values are added to the payload.</para> /// <para>Each <see cref="Claim"/> on the <paramref name="subject"/> added will have <see cref="Claim.Type"/> translated according to the mapping found in /// <see cref="OutboundClaimTypeMap"/>. Adding and removing to <see cref="OutboundClaimTypeMap"/> will affect the name component of the Json claim.</para> /// <para><see cref="SigningCredentials.SigningCredentials(SecurityKey, string)"/> is used to sign <see cref="JwtSecurityToken.RawData"/>.</para> /// </remarks> /// <returns>A <see cref="JwtSecurityToken"/>.</returns> /// <exception cref="ArgumentException">If 'expires' <= 'notBefore'.</exception> public virtual JwtSecurityToken CreateJwtSecurityToken(string issuer = null, string audience = null, ClaimsIdentity subject = null, DateTime? notBefore = null, DateTime? expires = null, DateTime? issuedAt = null, SigningCredentials signingCredentials = null) { return CreateJwtSecurityTokenPrivate(issuer, audience, subject, notBefore, expires, issuedAt, signingCredentials, null, null, null); } /// <summary> /// Creates a Json Web Token (JWT). /// </summary> /// <param name="tokenDescriptor"> A <see cref="SecurityTokenDescriptor"/> that contains details of contents of the token.</param> /// <remarks><see cref="SecurityTokenDescriptor.SigningCredentials"/> is used to sign <see cref="JwtSecurityToken.RawData"/>.</remarks> public override SecurityToken CreateToken(SecurityTokenDescriptor tokenDescriptor) { if (tokenDescriptor == null) throw LogHelper.LogArgumentNullException(nameof(tokenDescriptor)); return CreateJwtSecurityTokenPrivate( tokenDescriptor.Issuer, tokenDescriptor.Audience, tokenDescriptor.Subject, tokenDescriptor.NotBefore, tokenDescriptor.Expires, tokenDescriptor.IssuedAt, tokenDescriptor.SigningCredentials, tokenDescriptor.EncryptingCredentials, tokenDescriptor.Claims, tokenDescriptor.TokenType); } private JwtSecurityToken CreateJwtSecurityTokenPrivate(string issuer, string audience, ClaimsIdentity subject, DateTime? notBefore, DateTime? expires, DateTime? issuedAt, SigningCredentials signingCredentials, EncryptingCredentials encryptingCredentials, IDictionary<string, object> claimCollection, string tokenType) { if (SetDefaultTimesOnTokenCreation && (!expires.HasValue || !issuedAt.HasValue || !notBefore.HasValue)) { DateTime now = DateTime.UtcNow; if (!expires.HasValue) expires = now + TimeSpan.FromMinutes(TokenLifetimeInMinutes); if (!issuedAt.HasValue) issuedAt = now; if (!notBefore.HasValue) notBefore = now; } LogHelper.LogVerbose(LogMessages.IDX12721, (audience ?? "null"), (issuer ?? "null")); JwtPayload payload = new JwtPayload(issuer, audience, (subject == null ? null : OutboundClaimTypeTransform(subject.Claims)), (claimCollection == null ? null : OutboundClaimTypeTransform(claimCollection)), notBefore, expires, issuedAt); JwtHeader header = new JwtHeader(signingCredentials, OutboundAlgorithmMap, tokenType); if (subject?.Actor != null) payload.AddClaim(new Claim(JwtRegisteredClaimNames.Actort, CreateActorValue(subject.Actor))); string rawHeader = header.Base64UrlEncode(); string rawPayload = payload.Base64UrlEncode(); string rawSignature = signingCredentials == null ? string.Empty : JwtTokenUtilities.CreateEncodedSignature(string.Concat(rawHeader, ".", rawPayload), signingCredentials); LogHelper.LogInformation(LogMessages.IDX12722, rawHeader, rawPayload, rawSignature); if (encryptingCredentials != null) return EncryptToken(new JwtSecurityToken(header, payload, rawHeader, rawPayload, rawSignature), encryptingCredentials, tokenType); return new JwtSecurityToken(header, payload, rawHeader, rawPayload, rawSignature); } private JwtSecurityToken EncryptToken(JwtSecurityToken innerJwt, EncryptingCredentials encryptingCredentials, string tokenType) { if (encryptingCredentials == null) throw LogHelper.LogArgumentNullException(nameof(encryptingCredentials)); var cryptoProviderFactory = encryptingCredentials.CryptoProviderFactory ?? encryptingCredentials.Key.CryptoProviderFactory; if (cryptoProviderFactory == null) throw LogHelper.LogExceptionMessage(new ArgumentException(TokenLogMessages.IDX10620)); byte[] wrappedKey = null; SecurityKey securityKey = JwtTokenUtilities.GetSecurityKey(encryptingCredentials, cryptoProviderFactory, out wrappedKey); using (var encryptionProvider = cryptoProviderFactory.CreateAuthenticatedEncryptionProvider(securityKey, encryptingCredentials.Enc)) { if (encryptionProvider == null) throw LogHelper.LogExceptionMessage(new SecurityTokenEncryptionFailedException(LogMessages.IDX12730)); try { var header = new JwtHeader(encryptingCredentials, OutboundAlgorithmMap, tokenType); var encryptionResult = encryptionProvider.Encrypt(Encoding.UTF8.GetBytes(innerJwt.RawData), Encoding.ASCII.GetBytes(header.Base64UrlEncode())); return JwtConstants.DirectKeyUseAlg.Equals(encryptingCredentials.Alg, StringComparison.Ordinal) ? new JwtSecurityToken( header, innerJwt, header.Base64UrlEncode(), string.Empty, Base64UrlEncoder.Encode(encryptionResult.IV), Base64UrlEncoder.Encode(encryptionResult.Ciphertext), Base64UrlEncoder.Encode(encryptionResult.AuthenticationTag)) : new JwtSecurityToken( header, innerJwt, header.Base64UrlEncode(), Base64UrlEncoder.Encode(wrappedKey), Base64UrlEncoder.Encode(encryptionResult.IV), Base64UrlEncoder.Encode(encryptionResult.Ciphertext), Base64UrlEncoder.Encode(encryptionResult.AuthenticationTag)); } catch (Exception ex) { throw LogHelper.LogExceptionMessage(new SecurityTokenEncryptionFailedException(LogHelper.FormatInvariant(TokenLogMessages.IDX10616, encryptingCredentials.Enc, encryptingCredentials.Key), ex)); } } } private IEnumerable<Claim> OutboundClaimTypeTransform(IEnumerable<Claim> claims) { foreach (Claim claim in claims) { string type = null; if (_outboundClaimTypeMap.TryGetValue(claim.Type, out type)) { yield return new Claim(type, claim.Value, claim.ValueType, claim.Issuer, claim.OriginalIssuer, claim.Subject); } else { yield return claim; } } } private IDictionary<string, object> OutboundClaimTypeTransform(IDictionary<string, object> claimCollection) { var claims = new Dictionary<string, object>(); foreach (string claimType in claimCollection.Keys) { if (_outboundClaimTypeMap.TryGetValue(claimType, out string type)) claims[type] = claimCollection[claimType]; else claims[claimType] = claimCollection[claimType]; } return claims; } /// <summary> /// Converts a string into an instance of <see cref="JwtSecurityToken"/>. /// </summary> /// <param name="token">A 'JSON Web Token' (JWT) in JWS or JWE Compact Serialization Format.</param> /// <returns>A <see cref="JwtSecurityToken"/></returns> /// <exception cref="ArgumentNullException">'token' is null or empty.</exception> /// <exception cref="ArgumentException">'token.Length' is greater than <see cref="TokenHandler.MaximumTokenSizeInBytes"/>.</exception> /// <exception cref="ArgumentException"><see cref="CanReadToken(string)"/></exception> /// <remarks><para>If the 'token' is in JWE Compact Serialization format, only the protected header will be deserialized.</para> /// This method is unable to decrypt the payload. Use <see cref="ValidateToken(string, TokenValidationParameters, out SecurityToken)"/>to obtain the payload.</remarks> public JwtSecurityToken ReadJwtToken(string token) { if (string.IsNullOrEmpty(token)) throw LogHelper.LogArgumentNullException(nameof(token)); if (token.Length > MaximumTokenSizeInBytes) throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(TokenLogMessages.IDX10209, token.Length, MaximumTokenSizeInBytes))); if (!CanReadToken(token)) throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(LogMessages.IDX12709, token))); var jwtToken = new JwtSecurityToken(); jwtToken.Decode(token.Split('.'), token); return jwtToken; } /// <summary> /// Converts a string into an instance of <see cref="JwtSecurityToken"/>. /// </summary> /// <param name="token">A 'JSON Web Token' (JWT) in JWS or JWE Compact Serialization Format.</param> /// <returns>A <see cref="JwtSecurityToken"/></returns> /// <exception cref="ArgumentNullException">'token' is null or empty.</exception> /// <exception cref="ArgumentException">'token.Length' is greater than <see cref="TokenHandler.MaximumTokenSizeInBytes"/>.</exception> /// <exception cref="ArgumentException"><see cref="CanReadToken(string)"/></exception> /// <remarks><para>If the 'token' is in JWE Compact Serialization format, only the protected header will be deserialized.</para> /// This method is unable to decrypt the payload. Use <see cref="ValidateToken(string, TokenValidationParameters, out SecurityToken)"/>to obtain the payload.</remarks> public override SecurityToken ReadToken(string token) { return ReadJwtToken(token); } /// <summary> /// Deserializes token with the provided <see cref="TokenValidationParameters"/>. /// </summary> /// <param name="reader"><see cref="XmlReader"/>.</param> /// <param name="validationParameters">The current <see cref="TokenValidationParameters"/>.</param> /// <returns>The <see cref="SecurityToken"/></returns> /// <remarks>This method is not current supported.</remarks> public override SecurityToken ReadToken(XmlReader reader, TokenValidationParameters validationParameters) { throw new NotImplementedException(); } /// <summary> /// Reads and validates a 'JSON Web Token' (JWT) encoded as a JWS or JWE in Compact Serialized Format. /// </summary> /// <param name="token">the JWT encoded as JWE or JWS</param> /// <param name="validationParameters">Contains validation parameters for the <see cref="JwtSecurityToken"/>.</param> /// <param name="validatedToken">The <see cref="JwtSecurityToken"/> that was validated.</param> /// <exception cref="ArgumentNullException"><paramref name="token"/> is null or whitespace.</exception> /// <exception cref="ArgumentNullException"><paramref name="validationParameters"/> is null.</exception> /// <exception cref="ArgumentException"><paramref name="token"/>.Length is greater than <see cref="TokenHandler.MaximumTokenSizeInBytes"/>.</exception> /// <exception cref="ArgumentException"><paramref name="token"/> does not have 3 or 5 parts.</exception> /// <exception cref="ArgumentException"><see cref="CanReadToken(string)"/> returns false.</exception> /// <exception cref="SecurityTokenDecryptionFailedException"><paramref name="token"/> was a JWE was not able to be decrypted.</exception> /// <exception cref="SecurityTokenEncryptionKeyNotFoundException"><paramref name="token"/> 'kid' header claim is not null AND decryption fails.</exception> /// <exception cref="SecurityTokenException"><paramref name="token"/> 'enc' header claim is null or empty.</exception> /// <exception cref="SecurityTokenExpiredException"><paramref name="token"/> 'exp' claim is < DateTime.UtcNow.</exception> /// <exception cref="SecurityTokenInvalidAudienceException"><see cref="TokenValidationParameters.ValidAudience"/> is null or whitespace and <see cref="TokenValidationParameters.ValidAudiences"/> is null. Audience is not validated if <see cref="TokenValidationParameters.ValidateAudience"/> is set to false.</exception> /// <exception cref="SecurityTokenInvalidAudienceException"><paramref name="token"/> 'aud' claim did not match either <see cref="TokenValidationParameters.ValidAudience"/> or one of <see cref="TokenValidationParameters.ValidAudiences"/>.</exception> /// <exception cref="SecurityTokenInvalidLifetimeException"><paramref name="token"/> 'nbf' claim is > 'exp' claim.</exception> /// <exception cref="SecurityTokenInvalidSignatureException"><paramref name="token"/>.signature is not properly formatted.</exception> /// <exception cref="SecurityTokenNoExpirationException"><paramref name="token"/> 'exp' claim is missing and <see cref="TokenValidationParameters.RequireExpirationTime"/> is true.</exception> /// <exception cref="SecurityTokenNoExpirationException"><see cref="TokenValidationParameters.TokenReplayCache"/> is not null and expirationTime.HasValue is false. When a TokenReplayCache is set, tokens require an expiration time.</exception> /// <exception cref="SecurityTokenNotYetValidException"><paramref name="token"/> 'nbf' claim is > DateTime.UtcNow.</exception> /// <exception cref="SecurityTokenReplayAddFailedException"><paramref name="token"/> could not be added to the <see cref="TokenValidationParameters.TokenReplayCache"/>.</exception> /// <exception cref="SecurityTokenReplayDetectedException"><paramref name="token"/> is found in the cache.</exception> /// <returns> A <see cref="ClaimsPrincipal"/> from the JWT. Does not include claims found in the JWT header.</returns> /// <remarks> /// Many of the exceptions listed above are not thrown directly from this method. See <see cref="Validators"/> to examine the call graph. /// </remarks> public override ClaimsPrincipal ValidateToken(string token, TokenValidationParameters validationParameters, out SecurityToken validatedToken) { if (string.IsNullOrWhiteSpace(token)) throw LogHelper.LogArgumentNullException(nameof(token)); if (validationParameters == null) throw LogHelper.LogArgumentNullException(nameof(validationParameters)); if (token.Length > MaximumTokenSizeInBytes) throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(TokenLogMessages.IDX10209, token.Length, MaximumTokenSizeInBytes))); var tokenParts = token.Split(new char[] { '.' }, JwtConstants.MaxJwtSegmentCount + 1); if (tokenParts.Length != JwtConstants.JwsSegmentCount && tokenParts.Length != JwtConstants.JweSegmentCount) throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(LogMessages.IDX12741, token))); ClaimsPrincipal claimsPrincipal = null; SecurityToken signatureValidatedToken = null; if (tokenParts.Length == JwtConstants.JweSegmentCount) { var jwtToken = ReadJwtToken(token); var decryptedJwt = DecryptToken(jwtToken, validationParameters); var innerToken = ValidateSignature(decryptedJwt, validationParameters); jwtToken.InnerToken = innerToken; signatureValidatedToken = jwtToken; claimsPrincipal = ValidateTokenPayload(innerToken, validationParameters); } else { signatureValidatedToken = ValidateSignature(token, validationParameters); claimsPrincipal = ValidateTokenPayload(signatureValidatedToken as JwtSecurityToken, validationParameters); } validatedToken = signatureValidatedToken; return claimsPrincipal; } /// <summary> /// Validates the JSON payload of a <see cref="JwtSecurityToken"/>. /// </summary> /// <param name="jwtToken">The token to validate.</param> /// <param name="validationParameters">Contains validation parameters for the <see cref="JwtSecurityToken"/>.</param> /// <returns>A <see cref="ClaimsPrincipal"/> from the jwt. Does not include the header claims.</returns> protected ClaimsPrincipal ValidateTokenPayload(JwtSecurityToken jwtToken, TokenValidationParameters validationParameters) { if (jwtToken is null) throw LogHelper.LogArgumentNullException(nameof(jwtToken)); if (validationParameters is null) throw LogHelper.LogArgumentNullException(nameof(validationParameters)); DateTime? expires = (jwtToken.Payload.Exp == null) ? null : new DateTime?(jwtToken.ValidTo); DateTime? notBefore = (jwtToken.Payload.Nbf == null) ? null : new DateTime?(jwtToken.ValidFrom); ValidateLifetime(notBefore, expires, jwtToken, validationParameters); ValidateAudience(jwtToken.Audiences, jwtToken, validationParameters); string issuer = ValidateIssuer(jwtToken.Issuer, jwtToken, validationParameters); ValidateTokenReplay(expires, jwtToken.RawData, validationParameters); if (validationParameters.ValidateActor && !string.IsNullOrWhiteSpace(jwtToken.Actor)) { ValidateToken(jwtToken.Actor, validationParameters.ActorValidationParameters ?? validationParameters, out _); } ValidateIssuerSecurityKey(jwtToken.SigningKey, jwtToken, validationParameters); Validators.ValidateTokenType(jwtToken.Header.Typ, jwtToken, validationParameters); var identity = CreateClaimsIdentity(jwtToken, issuer, validationParameters); if (validationParameters.SaveSigninToken) identity.BootstrapContext = jwtToken.RawData; LogHelper.LogInformation(TokenLogMessages.IDX10241, jwtToken.RawData); return new ClaimsPrincipal(identity); } /// <summary> /// Serializes a <see cref="JwtSecurityToken"/> into a JWT in Compact Serialization Format. /// </summary> /// <param name="token"><see cref="JwtSecurityToken"/> to serialize.</param> /// <remarks> /// <para>The JWT will be serialized as a JWE or JWS.</para> /// <para><see cref="JwtSecurityToken.Payload"/> will be used to create the JWT. If there is an inner token, the inner token's payload will be used.</para> /// <para>If either <see cref="JwtSecurityToken.SigningCredentials"/> or <see cref="JwtSecurityToken.InnerToken"/>.SigningCredentials are set, the JWT will be signed.</para> /// <para>If <see cref="JwtSecurityToken.EncryptingCredentials"/> is set, a JWE will be created using the JWT above as the plaintext.</para> /// </remarks> /// <exception cref="ArgumentNullException">'token' is null.</exception> /// <exception cref="ArgumentException">'token' is not a not <see cref="JwtSecurityToken"/>.</exception> /// <exception cref="SecurityTokenEncryptionFailedException">both <see cref="JwtSecurityToken.SigningCredentials"/> and <see cref="JwtSecurityToken.InnerToken"/> are set.</exception> /// <exception cref="SecurityTokenEncryptionFailedException">both <see cref="JwtSecurityToken.InnerToken"/> and <see cref="JwtSecurityToken.InnerToken"/>.EncryptingCredentials are set.</exception> /// <exception cref="SecurityTokenEncryptionFailedException">if <see cref="JwtSecurityToken.InnerToken"/> is set and <see cref="JwtSecurityToken.EncryptingCredentials"/> is not set.</exception> /// <returns>A JWE or JWS in 'Compact Serialization Format'.</returns> public override string WriteToken(SecurityToken token) { if (token == null) throw LogHelper.LogArgumentNullException(nameof(token)); JwtSecurityToken jwtToken = token as JwtSecurityToken; if (jwtToken == null) throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(LogMessages.IDX12706, GetType(), typeof(JwtSecurityToken), token.GetType()), nameof(token))); var encodedPayload = jwtToken.EncodedPayload; var encodedSignature = string.Empty; var encodedHeader = string.Empty; if (jwtToken.InnerToken != null) { if (jwtToken.SigningCredentials != null) throw LogHelper.LogExceptionMessage(new SecurityTokenEncryptionFailedException(LogMessages.IDX12736)); if (jwtToken.InnerToken.Header.EncryptingCredentials != null) throw LogHelper.LogExceptionMessage(new SecurityTokenEncryptionFailedException(LogMessages.IDX12737)); if (jwtToken.Header.EncryptingCredentials == null) throw LogHelper.LogExceptionMessage(new SecurityTokenEncryptionFailedException(LogMessages.IDX12735)); if (jwtToken.InnerToken.SigningCredentials != null) encodedSignature = JwtTokenUtilities.CreateEncodedSignature(string.Concat(jwtToken.InnerToken.EncodedHeader, ".", jwtToken.EncodedPayload), jwtToken.InnerToken.SigningCredentials); return EncryptToken(new JwtSecurityToken(jwtToken.InnerToken.Header, jwtToken.InnerToken.Payload, jwtToken.InnerToken.EncodedHeader, encodedPayload, encodedSignature), jwtToken.EncryptingCredentials, jwtToken.InnerToken.Header.Typ).RawData; } // if EncryptingCredentials isn't set, then we need to create JWE // first create a new header with the SigningCredentials, Create a JWS then wrap it in a JWE var header = jwtToken.EncryptingCredentials == null ? jwtToken.Header : new JwtHeader(jwtToken.SigningCredentials); encodedHeader = header.Base64UrlEncode(); if (jwtToken.SigningCredentials != null) encodedSignature = JwtTokenUtilities.CreateEncodedSignature(string.Concat(encodedHeader, ".", encodedPayload), jwtToken.SigningCredentials); if (jwtToken.EncryptingCredentials != null) return EncryptToken(new JwtSecurityToken(header, jwtToken.Payload, encodedHeader, encodedPayload, encodedSignature), jwtToken.EncryptingCredentials, jwtToken.Header.Typ).RawData; else return string.Concat(encodedHeader, ".", encodedPayload, ".", encodedSignature); } /// <summary> /// Obtains a <see cref="SignatureProvider "/> and validates the signature. /// </summary> /// <param name="encodedBytes">Bytes to validate.</param> /// <param name="signature">Signature to compare against.</param> /// <param name="key"><See cref="SecurityKey"/> to use.</param> /// <param name="algorithm">Crypto algorithm to use.</param> /// <param name="securityToken">The <see cref="SecurityToken"/> being validated.</param> /// <param name="validationParameters">Priority will be given to <see cref="TokenValidationParameters.CryptoProviderFactory"/> over <see cref="SecurityKey.CryptoProviderFactory"/>.</param> /// <returns>'true' if signature is valid.</returns> private static bool ValidateSignature(byte[] encodedBytes, byte[] signature, SecurityKey key, string algorithm, SecurityToken securityToken, TokenValidationParameters validationParameters) { Validators.ValidateAlgorithm(algorithm, key, securityToken, validationParameters); var cryptoProviderFactory = validationParameters.CryptoProviderFactory ?? key.CryptoProviderFactory; var signatureProvider = cryptoProviderFactory.CreateForVerifying(key, algorithm); if (signatureProvider == null) throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant(TokenLogMessages.IDX10636, key == null ? "Null" : key.ToString(), algorithm ?? "Null"))); try { return signatureProvider.Verify(encodedBytes, signature); } finally { cryptoProviderFactory.ReleaseSignatureProvider(signatureProvider); } } /// <summary> /// Validates that the signature, if found or required, is valid. /// </summary> /// <param name="token">A JWS token.</param> /// <param name="validationParameters"><see cref="TokenValidationParameters"/> that contains signing keys.</param> /// <exception cref="ArgumentNullException">If 'jwt' is null or whitespace.</exception> /// <exception cref="ArgumentNullException">If 'validationParameters' is null.</exception> /// <exception cref="SecurityTokenValidationException">If a signature is not found and <see cref="TokenValidationParameters.RequireSignedTokens"/> is true.</exception> /// <exception cref="SecurityTokenSignatureKeyNotFoundException"> /// If the 'token' has a key identifier and none of the <see cref="SecurityKey"/>(s) provided result in a validated signature. /// This can indicate that a key refresh is required. /// </exception> /// <exception cref="SecurityTokenUnableToValidateException"> /// If the 'token' has a key identifier and none of the <see cref="SecurityKey"/>(s) provided result in a validated signature as well as the token /// had validation errors or lifetime or issuer. This is not intended to be a signal to refresh keys. /// </exception> /// <exception cref="SecurityTokenInvalidSignatureException">If after trying all the <see cref="SecurityKey"/>(s), none result in a validated signature AND the 'token' does not have a key identifier.</exception> /// <returns>A <see cref="JwtSecurityToken"/> that has the signature validated if token was signed.</returns> /// <remarks><para>If the 'token' is signed, the signature is validated even if <see cref="TokenValidationParameters.RequireSignedTokens"/> is false.</para> /// <para>If the 'token' signature is validated, then the <see cref="JwtSecurityToken.SigningKey"/> will be set to the key that signed the 'token'.It is the responsibility of <see cref="TokenValidationParameters.SignatureValidator"/> to set the <see cref="JwtSecurityToken.SigningKey"/></para></remarks> protected virtual JwtSecurityToken ValidateSignature(string token, TokenValidationParameters validationParameters) { if (string.IsNullOrWhiteSpace(token)) throw LogHelper.LogArgumentNullException(nameof(token)); if (validationParameters == null) throw LogHelper.LogArgumentNullException(nameof(validationParameters)); if (validationParameters.SignatureValidator != null) { var validatedJwtToken = validationParameters.SignatureValidator(token, validationParameters); if (validatedJwtToken == null) throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10505, token))); var validatedJwt = validatedJwtToken as JwtSecurityToken; if (validatedJwt == null) throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10506, typeof(JwtSecurityToken), validatedJwtToken.GetType(), token))); return validatedJwt; } JwtSecurityToken jwtToken = null; if (validationParameters.TokenReader != null) { var securityToken = validationParameters.TokenReader(token, validationParameters); if (securityToken == null) throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10510, token))); jwtToken = securityToken as JwtSecurityToken; if (jwtToken == null) throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10509, typeof(JwtSecurityToken), securityToken.GetType(), token))); } else { jwtToken = ReadJwtToken(token); } byte[] encodedBytes = Encoding.UTF8.GetBytes(jwtToken.RawHeader + "." + jwtToken.RawPayload); if (string.IsNullOrEmpty(jwtToken.RawSignature)) { if (validationParameters.RequireSignedTokens) throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10504, token))); else return jwtToken; } bool kidMatched = false; IEnumerable<SecurityKey> keys = null; if (validationParameters.IssuerSigningKeyResolver != null) { keys = validationParameters.IssuerSigningKeyResolver(token, jwtToken, jwtToken.Header.Kid, validationParameters); } else { var key = ResolveIssuerSigningKey(token, jwtToken, validationParameters); if (key != null) { kidMatched = true; keys = new List<SecurityKey> { key }; } } if (keys == null && validationParameters.TryAllIssuerSigningKeys) { // control gets here if: // 1. User specified delegate: IssuerSigningKeyResolver returned null // 2. ResolveIssuerSigningKey returned null // Try all the keys. This is the degenerate case, not concerned about perf. keys = TokenUtilities.GetAllSigningKeys(validationParameters); } // keep track of exceptions thrown, keys that were tried var exceptionStrings = new StringBuilder(); var keysAttempted = new StringBuilder(); bool kidExists = !string.IsNullOrEmpty(jwtToken.Header.Kid); byte[] signatureBytes; try { signatureBytes = Base64UrlEncoder.DecodeBytes(jwtToken.RawSignature); } catch (FormatException e) { throw new SecurityTokenInvalidSignatureException(TokenLogMessages.IDX10508, e); } if (keys != null) { foreach (var key in keys) { try { if (ValidateSignature(encodedBytes, signatureBytes, key, jwtToken.Header.Alg, jwtToken, validationParameters)) { LogHelper.LogInformation(TokenLogMessages.IDX10242, token); jwtToken.SigningKey = key; return jwtToken; } } catch (Exception ex) { exceptionStrings.AppendLine(ex.ToString()); } if (key != null) { keysAttempted.AppendLine(key.ToString() + " , KeyId: " + key.KeyId); if (kidExists && !kidMatched && key.KeyId != null) kidMatched = jwtToken.Header.Kid.Equals(key.KeyId, key is X509SecurityKey ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); } } } if (kidExists) { if (kidMatched) throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10511, keysAttempted, jwtToken.Header.Kid, exceptionStrings, jwtToken))); DateTime? expires = (jwtToken.Payload.Exp == null) ? null : new DateTime?(jwtToken.ValidTo); DateTime? notBefore = (jwtToken.Payload.Nbf == null) ? null : new DateTime?(jwtToken.ValidFrom); InternalValidators.ValidateLifetimeAndIssuerAfterSignatureNotValidatedJwt( jwtToken, notBefore, expires, jwtToken.Header.Kid, validationParameters, exceptionStrings); } if (keysAttempted.Length > 0) throw LogHelper.LogExceptionMessage(new SecurityTokenSignatureKeyNotFoundException(LogHelper.FormatInvariant(TokenLogMessages.IDX10503, keysAttempted, exceptionStrings, jwtToken))); throw LogHelper.LogExceptionMessage(new SecurityTokenSignatureKeyNotFoundException(TokenLogMessages.IDX10500)); } private static IEnumerable<SecurityKey> GetAllDecryptionKeys(TokenValidationParameters validationParameters) { if (validationParameters.TokenDecryptionKey != null) yield return validationParameters.TokenDecryptionKey; if (validationParameters.TokenDecryptionKeys != null) foreach (SecurityKey key in validationParameters.TokenDecryptionKeys) yield return key; } /// <summary> /// Creates a <see cref="ClaimsIdentity"/> from a <see cref="JwtSecurityToken"/>. /// </summary> /// <param name="jwtToken">The <see cref="JwtSecurityToken"/> to use as a <see cref="Claim"/> source.</param> /// <param name="issuer">The value to set <see cref="Claim.Issuer"/></param> /// <param name="validationParameters"> Contains parameters for validating the token.</param> /// <returns>A <see cref="ClaimsIdentity"/> containing the <see cref="JwtSecurityToken.Claims"/>.</returns> protected virtual ClaimsIdentity CreateClaimsIdentity(JwtSecurityToken jwtToken, string issuer, TokenValidationParameters validationParameters) { if (jwtToken == null) throw LogHelper.LogArgumentNullException(nameof(jwtToken)); if (validationParameters == null) throw LogHelper.LogArgumentNullException(nameof(validationParameters)); var actualIssuer = issuer; if (string.IsNullOrWhiteSpace(issuer)) { LogHelper.LogVerbose(TokenLogMessages.IDX10244, ClaimsIdentity.DefaultIssuer); actualIssuer = ClaimsIdentity.DefaultIssuer; } return MapInboundClaims ? CreateClaimsIdentityWithMapping(jwtToken, actualIssuer, validationParameters) : CreateClaimsIdentityWithoutMapping(jwtToken, actualIssuer, validationParameters); } private ClaimsIdentity CreateClaimsIdentityWithMapping(JwtSecurityToken jwtToken, string actualIssuer, TokenValidationParameters validationParameters) { ClaimsIdentity identity = validationParameters.CreateClaimsIdentity(jwtToken, actualIssuer); foreach (Claim jwtClaim in jwtToken.Claims) { if (_inboundClaimFilter.Contains(jwtClaim.Type)) continue; string claimType; bool wasMapped = true; if (!_inboundClaimTypeMap.TryGetValue(jwtClaim.Type, out claimType)) { claimType = jwtClaim.Type; wasMapped = false; } if (claimType == ClaimTypes.Actor) { if (identity.Actor != null) throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant(LogMessages.IDX12710, JwtRegisteredClaimNames.Actort, jwtClaim.Value))); if (CanReadToken(jwtClaim.Value)) { JwtSecurityToken actor = ReadToken(jwtClaim.Value) as JwtSecurityToken; identity.Actor = CreateClaimsIdentity(actor, actualIssuer, validationParameters); } } Claim claim = new Claim(claimType, jwtClaim.Value, jwtClaim.ValueType, actualIssuer, actualIssuer, identity); if (jwtClaim.Properties.Count > 0) { foreach (var kv in jwtClaim.Properties) { claim.Properties[kv.Key] = kv.Value; } } if (wasMapped) claim.Properties[ShortClaimTypeProperty] = jwtClaim.Type; identity.AddClaim(claim); } return identity; } private ClaimsIdentity CreateClaimsIdentityWithoutMapping(JwtSecurityToken jwtToken, string actualIssuer, TokenValidationParameters validationParameters) { ClaimsIdentity identity = validationParameters.CreateClaimsIdentity(jwtToken, actualIssuer); foreach (Claim jwtClaim in jwtToken.Claims) { if (_inboundClaimFilter.Contains(jwtClaim.Type)) continue; string claimType = jwtClaim.Type; if (claimType == ClaimTypes.Actor) { if (identity.Actor != null) throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant(LogMessages.IDX12710, JwtRegisteredClaimNames.Actort, jwtClaim.Value))); if (CanReadToken(jwtClaim.Value)) { JwtSecurityToken actor = ReadToken(jwtClaim.Value) as JwtSecurityToken; identity.Actor = CreateClaimsIdentity(actor, actualIssuer, validationParameters); } } Claim claim = new Claim(claimType, jwtClaim.Value, jwtClaim.ValueType, actualIssuer, actualIssuer, identity); if (jwtClaim.Properties.Count > 0) { foreach (var kv in jwtClaim.Properties) claim.Properties[kv.Key] = kv.Value; } identity.AddClaim(claim); } return identity; } /// <summary> /// Creates the 'value' for the actor claim: { actort, 'value' } /// </summary> /// <param name="actor"><see cref="ClaimsIdentity"/> as actor.</param> /// <returns><see cref="string"/> representing the actor.</returns> /// <remarks>If <see cref="ClaimsIdentity.BootstrapContext"/> is not null: /// <para>  If 'type' is 'string', return as string.</para> /// <para>  if 'type' is 'BootstrapContext' and 'BootstrapContext.SecurityToken' is 'JwtSecurityToken'</para> /// <para>    if 'JwtSecurityToken.RawData' != null, return RawData.</para> /// <para>    else return <see cref="JwtSecurityTokenHandler.WriteToken( SecurityToken )"/>.</para> /// <para>  if 'BootstrapContext.Token' != null, return 'Token'.</para> /// <para>default: <see cref="JwtSecurityTokenHandler.WriteToken(SecurityToken)"/> new ( <see cref="JwtSecurityToken"/>( actor.Claims ).</para> /// </remarks> /// <exception cref="ArgumentNullException">'actor' is null.</exception> protected virtual string CreateActorValue(ClaimsIdentity actor) { if (actor == null) throw LogHelper.LogArgumentNullException(nameof(actor)); if (actor.BootstrapContext != null) { string encodedJwt = actor.BootstrapContext as string; if (encodedJwt != null) { LogHelper.LogVerbose(LogMessages.IDX12713); return encodedJwt; } JwtSecurityToken jwtToken = actor.BootstrapContext as JwtSecurityToken; if (jwtToken != null) { if (jwtToken.RawData != null) { LogHelper.LogVerbose(LogMessages.IDX12714); return jwtToken.RawData; } else { LogHelper.LogVerbose(LogMessages.IDX12715); return this.WriteToken(jwtToken); } } LogHelper.LogVerbose(LogMessages.IDX12711); } LogHelper.LogVerbose(LogMessages.IDX12712); return WriteToken(new JwtSecurityToken(claims: actor.Claims)); } /// <summary> /// Determines if the audiences found in a <see cref="JwtSecurityToken"/> are valid. /// </summary> /// <param name="audiences">The audiences found in the <see cref="JwtSecurityToken"/>.</param> /// <param name="jwtToken">The <see cref="JwtSecurityToken"/> being validated.</param> /// <param name="validationParameters"><see cref="TokenValidationParameters"/> required for validation.</param> /// <remarks>See <see cref="Validators.ValidateAudience"/> for additional details.</remarks> protected virtual void ValidateAudience(IEnumerable<string> audiences, JwtSecurityToken jwtToken, TokenValidationParameters validationParameters) { Validators.ValidateAudience(audiences, jwtToken, validationParameters); } /// <summary> /// Validates the lifetime of a <see cref="JwtSecurityToken"/>. /// </summary> /// <param name="notBefore">The <see cref="DateTime"/> value of the 'nbf' claim if it exists in the 'jwtToken'.</param> /// <param name="expires">The <see cref="DateTime"/> value of the 'exp' claim if it exists in the 'jwtToken'.</param> /// <param name="jwtToken">The <see cref="JwtSecurityToken"/> being validated.</param> /// <param name="validationParameters"><see cref="TokenValidationParameters"/> required for validation.</param> /// <remarks><see cref="Validators.ValidateLifetime"/> for additional details.</remarks> protected virtual void ValidateLifetime(DateTime? notBefore, DateTime? expires, JwtSecurityToken jwtToken, TokenValidationParameters validationParameters) { Validators.ValidateLifetime(notBefore, expires, jwtToken, validationParameters); } /// <summary> /// Determines if the issuer found in a <see cref="JwtSecurityToken"/> is valid. /// </summary> /// <param name="issuer">The issuer to validate</param> /// <param name="jwtToken">The <see cref="JwtSecurityToken"/> that is being validated.</param> /// <param name="validationParameters"><see cref="TokenValidationParameters"/> required for validation.</param> /// <returns>The issuer to use when creating the <see cref="Claim"/>(s) in the <see cref="ClaimsIdentity"/>.</returns> /// <remarks><see cref="Validators.ValidateIssuer"/> for additional details.</remarks> protected virtual string ValidateIssuer(string issuer, JwtSecurityToken jwtToken, TokenValidationParameters validationParameters) { return Validators.ValidateIssuer(issuer, jwtToken, validationParameters); } /// <summary> /// Determines if a <see cref="JwtSecurityToken"/> is already validated. /// </summary> /// <param name="expires">The <see cref="DateTime"/> value of the 'exp' claim if it exists in the <see cref="JwtSecurityToken"/>'.</param> /// <param name="securityToken">The <see cref="JwtSecurityToken"/> that is being validated.</param> /// <param name="validationParameters"><see cref="TokenValidationParameters"/> required for validation.</param> protected virtual void ValidateTokenReplay(DateTime? expires, string securityToken, TokenValidationParameters validationParameters) { Validators.ValidateTokenReplay(expires, securityToken, validationParameters); } /// <summary> /// Returns a <see cref="SecurityKey"/> to use when validating the signature of a token. /// </summary> /// <param name="token">The <see cref="string"/> representation of the token that is being validated.</param> /// <param name="jwtToken">The <see cref="JwtSecurityToken"/> that is being validated.</param> /// <param name="validationParameters">A <see cref="TokenValidationParameters"/> required for validation.</param> /// <returns>Returns a <see cref="SecurityKey"/> to use for signature validation.</returns> /// <remarks>If key fails to resolve, then null is returned</remarks> protected virtual SecurityKey ResolveIssuerSigningKey(string token, JwtSecurityToken jwtToken, TokenValidationParameters validationParameters) { if (validationParameters == null) throw LogHelper.LogArgumentNullException(nameof(validationParameters)); if (jwtToken == null) throw LogHelper.LogArgumentNullException(nameof(jwtToken)); return JwtTokenUtilities.ResolveTokenSigningKey(jwtToken.Header.Kid, jwtToken.Header.X5t, validationParameters); } /// <summary> /// Returns a <see cref="SecurityKey"/> to use when decryption a JWE. /// </summary> /// <param name="token">The <see cref="string"/> the token that is being decrypted.</param> /// <param name="jwtToken">The <see cref="JwtSecurityToken"/> that is being decrypted.</param> /// <param name="validationParameters">A <see cref="TokenValidationParameters"/> required for validation.</param> /// <returns>Returns a <see cref="SecurityKey"/> to use for signature validation.</returns> /// <remarks>If key fails to resolve, then null is returned</remarks> protected virtual SecurityKey ResolveTokenDecryptionKey(string token, JwtSecurityToken jwtToken, TokenValidationParameters validationParameters) { if (jwtToken == null) throw LogHelper.LogArgumentNullException(nameof(jwtToken)); if (validationParameters == null) throw LogHelper.LogArgumentNullException(nameof(validationParameters)); if (!string.IsNullOrEmpty(jwtToken.Header.Kid)) { if (validationParameters.TokenDecryptionKey != null && string.Equals(validationParameters.TokenDecryptionKey.KeyId, jwtToken.Header.Kid, validationParameters.TokenDecryptionKey is X509SecurityKey ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)) return validationParameters.TokenDecryptionKey; if (validationParameters.TokenDecryptionKeys != null) { foreach (var key in validationParameters.TokenDecryptionKeys) { if (key != null && string.Equals(key.KeyId, jwtToken.Header.Kid, key is X509SecurityKey ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)) return key; } } } if (!string.IsNullOrEmpty(jwtToken.Header.X5t)) { if (validationParameters.TokenDecryptionKey != null) { if (string.Equals(validationParameters.TokenDecryptionKey.KeyId, jwtToken.Header.X5t, validationParameters.TokenDecryptionKey is X509SecurityKey ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)) return validationParameters.TokenDecryptionKey; X509SecurityKey x509Key = validationParameters.TokenDecryptionKey as X509SecurityKey; if (x509Key != null && string.Equals(x509Key.X5t, jwtToken.Header.X5t, StringComparison.OrdinalIgnoreCase)) return validationParameters.TokenDecryptionKey; } if (validationParameters.TokenDecryptionKeys != null) { foreach (var key in validationParameters.TokenDecryptionKeys) { if (key != null && string.Equals(key.KeyId, jwtToken.Header.X5t, key is X509SecurityKey ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)) return key; X509SecurityKey x509Key = key as X509SecurityKey; if (x509Key != null && string.Equals(x509Key.X5t, jwtToken.Header.X5t, StringComparison.OrdinalIgnoreCase)) return key; } } } return null; } /// <summary> /// Decrypts a JWE and returns the clear text /// </summary> /// <param name="jwtToken">the JWE that contains the cypher text.</param> /// <param name="validationParameters">contains crypto material.</param> /// <returns>the decoded / cleartext contents of the JWE.</returns> /// <exception cref="ArgumentNullException">if 'jwtToken' is null.</exception> /// <exception cref="ArgumentNullException">if 'validationParameters' is null.</exception> /// <exception cref="SecurityTokenException">if 'jwtToken.Header.enc' is null or empty.</exception> /// <exception cref="SecurityTokenEncryptionKeyNotFoundException">if 'jwtToken.Header.kid' is not null AND decryption fails.</exception> /// <exception cref="SecurityTokenDecryptionFailedException">if the JWE was not able to be decrypted.</exception> protected string DecryptToken(JwtSecurityToken jwtToken, TokenValidationParameters validationParameters) { if (jwtToken == null) throw LogHelper.LogArgumentNullException(nameof(jwtToken)); if (validationParameters == null) throw LogHelper.LogArgumentNullException(nameof(validationParameters)); if (string.IsNullOrEmpty(jwtToken.Header.Enc)) throw LogHelper.LogExceptionMessage(new SecurityTokenException(LogHelper.FormatInvariant(TokenLogMessages.IDX10612))); var keys = GetContentEncryptionKeys(jwtToken, validationParameters); return JwtTokenUtilities.DecryptJwtToken(jwtToken, validationParameters, new JwtTokenDecryptionParameters { Alg = jwtToken.Header.Alg, AuthenticationTag = jwtToken.RawAuthenticationTag, Ciphertext = jwtToken.RawCiphertext, DecompressionFunction = JwtTokenUtilities.DecompressToken, Enc = jwtToken.Header.Enc, EncodedHeader = jwtToken.EncodedHeader, EncodedToken = jwtToken.RawData, InitializationVector= jwtToken.RawInitializationVector, Keys = keys, Zip = jwtToken.Header.Zip, }); } internal IEnumerable<SecurityKey> GetContentEncryptionKeys(JwtSecurityToken jwtToken, TokenValidationParameters validationParameters) { IEnumerable<SecurityKey> keys = null; if (validationParameters.TokenDecryptionKeyResolver != null) keys = validationParameters.TokenDecryptionKeyResolver(jwtToken.RawData, jwtToken, jwtToken.Header.Kid, validationParameters); else { var key = ResolveTokenDecryptionKey(jwtToken.RawData, jwtToken, validationParameters); if (key != null) keys = new List<SecurityKey> { key }; } // control gets here if: // 1. User specified delegate: TokenDecryptionKeyResolver returned null // 2. ResolveTokenDecryptionKey returned null // Try all the keys. This is the degenerate case, not concerned about perf. if (keys == null) keys = GetAllDecryptionKeys(validationParameters); if (jwtToken.Header.Alg.Equals(JwtConstants.DirectKeyUseAlg, StringComparison.Ordinal)) return keys; var unwrappedKeys = new List<SecurityKey>(); // keep track of exceptions thrown, keys that were tried var exceptionStrings = new StringBuilder(); var keysAttempted = new StringBuilder(); foreach (var key in keys) { try { if (key.CryptoProviderFactory.IsSupportedAlgorithm(jwtToken.Header.Alg, key)) { var kwp = key.CryptoProviderFactory.CreateKeyWrapProviderForUnwrap(key, jwtToken.Header.Alg); var unwrappedKey = kwp.UnwrapKey(Base64UrlEncoder.DecodeBytes(jwtToken.RawEncryptedKey)); unwrappedKeys.Add(new SymmetricSecurityKey(unwrappedKey)); } } catch (Exception ex) { exceptionStrings.AppendLine(ex.ToString()); } keysAttempted.AppendLine(key.ToString()); } if (unwrappedKeys.Count > 0 || exceptionStrings.Length == 0) return unwrappedKeys; else throw LogHelper.LogExceptionMessage(new SecurityTokenKeyWrapException(LogHelper.FormatInvariant(TokenLogMessages.IDX10618, keysAttempted, exceptionStrings, jwtToken))); } private static byte[] GetSymmetricSecurityKey(SecurityKey key) { if (key == null) throw LogHelper.LogArgumentNullException(nameof(key)); // try to use the provided key directly. SymmetricSecurityKey symmetricSecurityKey = key as SymmetricSecurityKey; if (symmetricSecurityKey != null) return symmetricSecurityKey.Key; else { JsonWebKey jsonWebKey = key as JsonWebKey; if (jsonWebKey != null && jsonWebKey.K != null) return Base64UrlEncoder.DecodeBytes(jsonWebKey.K); } return null; } /// <summary> /// Validates the <see cref="JwtSecurityToken.SigningKey"/> is an expected value. /// </summary> /// <param name="key">The <see cref="SecurityKey"/> that signed the <see cref="SecurityToken"/>.</param> /// <param name="securityToken">The <see cref="JwtSecurityToken"/> to validate.</param> /// <param name="validationParameters">The current <see cref="TokenValidationParameters"/>.</param> /// <remarks>If the <see cref="JwtSecurityToken.SigningKey"/> is a <see cref="X509SecurityKey"/> then the X509Certificate2 will be validated using the CertificateValidator.</remarks> protected virtual void ValidateIssuerSecurityKey(SecurityKey key, JwtSecurityToken securityToken, TokenValidationParameters validationParameters) { Validators.ValidateIssuerSecurityKey(key, securityToken, validationParameters); } /// <summary> /// Serializes to XML a token of the type handled by this instance. /// </summary> /// <param name="writer">The XML writer.</param> /// <param name="token">A token of type <see cref="TokenType"/>.</param> public override void WriteToken(XmlWriter writer, SecurityToken token) { throw new NotImplementedException(); } } }