From 54a712bef8eafc17c98162715f664c61965763dc Mon Sep 17 00:00:00 2001 From: Jenny Ferries Date: Thu, 13 Jun 2024 17:22:13 -0700 Subject: [PATCH 01/22] update dev to 8x version and builds --- .github/workflows/codeql-analysis.yml | 4 ++-- build/common.props | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 439ebd93b2..5e5d5b4bdb 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,14 +2,14 @@ name: "CodeQL" on: push: - branches: [ "dev", "dev6x" ] + branches: [ "dev", "dev6x", "dev7x"] pull_request: types: - opened - synchronize - reopened - ready_for_review - branches: [ "dev", "dev6x" ] + branches: [ "dev", "dev6x", "dev7x"] jobs: analyze: diff --git a/build/common.props b/build/common.props index 0fbddb3d63..efd7e90e2f 100644 --- a/build/common.props +++ b/build/common.props @@ -32,7 +32,7 @@ - 7.6.1 + 8.0.0 preview-$([System.DateTime]::Now.AddYears(-2019).Year)$([System.DateTime]::Now.ToString("MMddHHmmss")) @@ -46,8 +46,8 @@ - true - 7.0.0 + false + 8.0.0 From cbfb4b328c28447a633685d95792ef3587aac6c8 Mon Sep 17 00:00:00 2001 From: id4s Date: Sun, 5 May 2024 13:32:51 -0700 Subject: [PATCH 02/22] Add framework for reducing exceptions and logging. --- Wilson.sln | 2 + buildPack.bat | 1 + buildTestPack.bat | 1 + .../JsonWebTokenHandler.ValidateToken.cs | 771 ++++++++++++++++++ .../JsonWebTokenHandler.cs | 545 ------------- .../Exceptions/SecurityTokenException.cs | 49 +- .../LogMessages.cs | 4 +- .../TokenValidationParameters.cs | 22 +- .../Validation/AsyncValidate.cd | 81 ++ .../Validation/ExceptionDetail.cs | 73 ++ .../Validation/Exceptions.cd | 35 + .../Validation/IssuerValidationResult.cs | 69 ++ .../Validation/LogDetail.cs | 34 + .../Validation/MessageDetail.cs | 46 ++ ...tionParameters.IssuerValidationDelegate.cs | 16 + .../{ => Validation}/TokenValidationResult.cs | 43 +- .../Validation/ValidationDelegates.cd | 21 + .../Validation/ValidationFailureType.cs | 49 ++ .../Validation/ValidationResult.cs | 110 +++ .../Validation/Validators.Audience.cs | 154 ++++ .../Validation/Validators.Issuer.cs | 293 +++++++ .../Validation/Validators.Lifetime.cs | 50 ++ .../ValidatorUtilities.cs | 4 +- .../Validators.cs | 317 +------ .../JsonWebTokenHandlerTests.cs | 2 +- .../IdentityComparer.cs | 116 ++- .../ValidationDelegates.cs | 2 +- .../IdentityComparerTests.cs | 6 +- .../Json/JsonUtilities.cs | 6 + .../TokenValidationParametersTests.cs | 6 +- .../Validation/AsyncValidatorsTests.cs | 58 ++ .../Validation/IssuerValidationResultTests.cs | 120 +++ .../{ => Validation}/ValidatorsTests.cs | 4 - 33 files changed, 2206 insertions(+), 904 deletions(-) create mode 100644 src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/AsyncValidate.cd create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/ExceptionDetail.cs create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/Exceptions.cd create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/IssuerValidationResult.cs create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/LogDetail.cs create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/MessageDetail.cs create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/TokenValidationParameters.IssuerValidationDelegate.cs rename src/Microsoft.IdentityModel.Tokens/{ => Validation}/TokenValidationResult.cs (85%) create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/ValidationDelegates.cd create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/ValidationResult.cs create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/Validators.Audience.cs create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/Validators.Lifetime.cs create mode 100644 test/Microsoft.IdentityModel.Tokens.Tests/Validation/AsyncValidatorsTests.cs create mode 100644 test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs rename test/Microsoft.IdentityModel.Tokens.Tests/{ => Validation}/ValidatorsTests.cs (99%) diff --git a/Wilson.sln b/Wilson.sln index 9c671e6168..8dd5dcbce2 100644 --- a/Wilson.sln +++ b/Wilson.sln @@ -85,6 +85,8 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4655DBB4-70C6-475D-8971-FE6619B85F70}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + buildPack.bat = buildPack.bat + buildTestPack.bat = buildTestPack.bat EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.IdentityModel.Validators", "src\Microsoft.IdentityModel.Validators\Microsoft.IdentityModel.Validators.csproj", "{DA585910-0E6C-45A5-AABD-30917130FD63}" diff --git a/buildPack.bat b/buildPack.bat index 7e782b647f..6a4c89623c 100644 --- a/buildPack.bat +++ b/buildPack.bat @@ -1,2 +1,3 @@ +dotnet clean Product.proj > clean.log dotnet build /r Product.proj dotnet pack --no-restore -o artifacts --no-build Product.proj diff --git a/buildTestPack.bat b/buildTestPack.bat index 40e59fda52..b5dba1b86f 100644 --- a/buildTestPack.bat +++ b/buildTestPack.bat @@ -1,3 +1,4 @@ +dotnet clean Product.proj > clean.log dotnet build /r Product.proj dotnet test --no-restore --no-build Product.proj dotnet pack --no-restore -o artifacts --no-build Product.proj diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs new file mode 100644 index 0000000000..524559e198 --- /dev/null +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs @@ -0,0 +1,771 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Abstractions; +using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.Tokens; +using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; + +namespace Microsoft.IdentityModel.JsonWebTokens +{ + /// + /// A designed for creating and validating Json Web Tokens. + /// See: https://datatracker.ietf.org/doc/html/rfc7519 and http://www.rfc-editor.org/info/rfc7515. + /// + public partial class JsonWebTokenHandler : TokenHandler + { + /// + /// Returns a value that indicates if this handler can validate a . + /// + /// 'true', indicating this instance can validate a . + public virtual bool CanValidateToken + { + get { return true; } + } + + internal async ValueTask ValidateJWEAsync( + JsonWebToken jwtToken, + TokenValidationParameters validationParameters, + BaseConfiguration configuration) + { + try + { + TokenValidationResult tokenValidationResult = ReadToken(DecryptToken(jwtToken, validationParameters), validationParameters); + if (!tokenValidationResult.IsValid) + return tokenValidationResult; + + + tokenValidationResult = await ValidateJWSAsync( + tokenValidationResult.SecurityToken as JsonWebToken, + validationParameters, + configuration).ConfigureAwait(false); + + if (!tokenValidationResult.IsValid) + return tokenValidationResult; + + jwtToken.InnerToken = tokenValidationResult.SecurityToken as JsonWebToken; + jwtToken.Payload = (tokenValidationResult.SecurityToken as JsonWebToken).Payload; + return new TokenValidationResult + { + SecurityToken = jwtToken, + ClaimsIdentityNoLocking = tokenValidationResult.ClaimsIdentityNoLocking, + IsValid = true, + TokenType = tokenValidationResult.TokenType + }; + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + return new TokenValidationResult + { + Exception = ex, + IsValid = false, + TokenOnFailedValidation = validationParameters.IncludeTokenOnFailedValidation ? jwtToken : null + }; + } + } + + internal async ValueTask ValidateJWEAsync( + JsonWebToken jwtToken, + TokenValidationParameters validationParameters, + CallContext callContext, + CancellationToken cancellationToken) + { + try + { + TokenValidationResult tokenValidationResult = ReadToken(DecryptToken(jwtToken, validationParameters), validationParameters); + if (!tokenValidationResult.IsValid) + return tokenValidationResult; + + tokenValidationResult = await ValidateJWSAsync( + tokenValidationResult.SecurityToken as JsonWebToken, + validationParameters, + callContext, + cancellationToken).ConfigureAwait(false); + + if (!tokenValidationResult.IsValid) + return tokenValidationResult; + + jwtToken.InnerToken = tokenValidationResult.SecurityToken as JsonWebToken; + jwtToken.Payload = (tokenValidationResult.SecurityToken as JsonWebToken).Payload; + return new TokenValidationResult + { + SecurityToken = jwtToken, + ClaimsIdentityNoLocking = tokenValidationResult.ClaimsIdentityNoLocking, + IsValid = true, + TokenType = tokenValidationResult.TokenType + }; + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + return new TokenValidationResult + { + Exception = ex, + IsValid = false, + TokenOnFailedValidation = validationParameters.IncludeTokenOnFailedValidation ? jwtToken : null + }; + } + } + + internal async ValueTask ValidateJWSAsync( + JsonWebToken jsonWebToken, + TokenValidationParameters validationParameters, + BaseConfiguration configuration) + { + try + { + TokenValidationResult tokenValidationResult; + if (validationParameters.TransformBeforeSignatureValidation != null) + jsonWebToken = validationParameters.TransformBeforeSignatureValidation(jsonWebToken, validationParameters) as JsonWebToken; + + if (validationParameters.SignatureValidator != null || validationParameters.SignatureValidatorUsingConfiguration != null) + { + var validatedToken = ValidateSignatureUsingDelegates(jsonWebToken, validationParameters); + tokenValidationResult = await ValidateTokenPayloadAsync( + validatedToken, + validationParameters, + configuration).ConfigureAwait(false); + + Validators.ValidateIssuerSecurityKey(validatedToken.SigningKey, validatedToken, validationParameters); + } + else + { + if (validationParameters.ValidateSignatureLast) + { + tokenValidationResult = await ValidateTokenPayloadAsync( + jsonWebToken, + validationParameters, + configuration).ConfigureAwait(false); + + if (tokenValidationResult.IsValid) + tokenValidationResult.SecurityToken = ValidateSignatureAndIssuerSecurityKey(jsonWebToken, validationParameters, configuration); + } + else + { + var validatedToken = ValidateSignatureAndIssuerSecurityKey(jsonWebToken, validationParameters, configuration); + tokenValidationResult = await ValidateTokenPayloadAsync( + validatedToken, + validationParameters, + configuration).ConfigureAwait(false); + } + } + + return tokenValidationResult; + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + return new TokenValidationResult + { + Exception = ex, + IsValid = false, + TokenOnFailedValidation = validationParameters.IncludeTokenOnFailedValidation ? jsonWebToken : null + }; + } + } + + internal async ValueTask ValidateJWSAsync( + JsonWebToken jsonWebToken, + TokenValidationParameters validationParameters, + CallContext callContext, + CancellationToken cancellationToken) + { + try + { + BaseConfiguration currentConfiguration = null; + if (validationParameters.ConfigurationManager != null) + { + try + { + currentConfiguration = await validationParameters.ConfigurationManager.GetBaseConfigurationAsync(CancellationToken.None).ConfigureAwait(false); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + // The exception is not re-thrown as the TokenValidationParameters may have the issuer and signing key set + // directly on them, allowing the library to continue with token validation. + if (LogHelper.IsEnabled(EventLogLevel.Warning)) + LogHelper.LogWarning(LogHelper.FormatInvariant(TokenLogMessages.IDX10261, validationParameters.ConfigurationManager.MetadataAddress, ex.ToString())); + } + } + + TokenValidationResult tokenValidationResult; + if (validationParameters.TransformBeforeSignatureValidation != null) + jsonWebToken = validationParameters.TransformBeforeSignatureValidation(jsonWebToken, validationParameters) as JsonWebToken; + + if (validationParameters.SignatureValidator != null || validationParameters.SignatureValidatorUsingConfiguration != null) + { + var validatedToken = ValidateSignatureUsingDelegates(jsonWebToken, validationParameters); + tokenValidationResult = await ValidateTokenPayloadAsync( + validatedToken, + validationParameters, + callContext, + cancellationToken).ConfigureAwait(false); + + Validators.ValidateIssuerSecurityKey(validatedToken.SigningKey, validatedToken, validationParameters); + } + else + { + if (validationParameters.ValidateSignatureLast) + { + tokenValidationResult = await ValidateTokenPayloadAsync( + jsonWebToken, + validationParameters, + callContext, + cancellationToken).ConfigureAwait(false); + + if (tokenValidationResult.IsValid) + tokenValidationResult.SecurityToken = ValidateSignatureAndIssuerSecurityKey(jsonWebToken, validationParameters, currentConfiguration); + } + else + { + var validatedToken = ValidateSignatureAndIssuerSecurityKey(jsonWebToken, validationParameters, currentConfiguration); + tokenValidationResult = await ValidateTokenPayloadAsync( + validatedToken, + validationParameters, + callContext, + cancellationToken).ConfigureAwait(false); + } + } + + return tokenValidationResult; + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + return new TokenValidationResult + { + Exception = ex, + IsValid = false, + TokenOnFailedValidation = validationParameters.IncludeTokenOnFailedValidation ? jsonWebToken : null + }; + } + } + + private static JsonWebToken ValidateSignatureAndIssuerSecurityKey(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) + { + JsonWebToken validatedToken = ValidateSignature(jsonWebToken, validationParameters, configuration); + Validators.ValidateIssuerSecurityKey(validatedToken.SigningKey, jsonWebToken, validationParameters, configuration); + return validatedToken; + } + + /// + /// Validates the JWT signature. + /// + private static JsonWebToken ValidateSignature(JsonWebToken jwtToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) + { + bool kidMatched = false; + IEnumerable keys = null; + + if (!jwtToken.IsSigned) + { + if (validationParameters.RequireSignedTokens) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10504, jwtToken))); + else + return jwtToken; + } + + if (validationParameters.IssuerSigningKeyResolverUsingConfiguration != null) + { + keys = validationParameters.IssuerSigningKeyResolverUsingConfiguration(jwtToken.EncodedToken, jwtToken, jwtToken.Kid, validationParameters, configuration); + } + else if (validationParameters.IssuerSigningKeyResolver != null) + { + keys = validationParameters.IssuerSigningKeyResolver(jwtToken.EncodedToken, jwtToken, jwtToken.Kid, validationParameters); + } + else + { + var key = JwtTokenUtilities.ResolveTokenSigningKey(jwtToken.Kid, jwtToken.X5t, validationParameters, configuration); + if (key != null) + { + kidMatched = true; + keys = [key]; + } + } + + if (validationParameters.TryAllIssuerSigningKeys && keys.IsNullOrEmpty()) + { + // 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(configuration, validationParameters); + } + + // keep track of exceptions thrown, keys that were tried + StringBuilder exceptionStrings = null; + StringBuilder keysAttempted = null; + var kidExists = !string.IsNullOrEmpty(jwtToken.Kid); + + if (keys != null) + { + foreach (var key in keys) + { +#pragma warning disable CA1031 // Do not catch general exception types + try + { + if (ValidateSignature(jwtToken, key, validationParameters)) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(TokenLogMessages.IDX10242, jwtToken); + + jwtToken.SigningKey = key; + return jwtToken; + } + } + catch (Exception ex) + { + (exceptionStrings ??= new StringBuilder()).AppendLine(ex.ToString()); + } +#pragma warning restore CA1031 // Do not catch general exception types + + if (key != null) + { + (keysAttempted ??= new StringBuilder()).Append(key.ToString()).Append(" , KeyId: ").AppendLine(key.KeyId); + if (kidExists && !kidMatched && key.KeyId != null) + kidMatched = jwtToken.Kid.Equals(key.KeyId, key is X509SecurityKey ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); + } + } + } + + // Get information on where keys used during token validation came from for debugging purposes. + var keysInTokenValidationParameters = TokenUtilities.GetAllSigningKeys(validationParameters: validationParameters); + + var keysInConfiguration = TokenUtilities.GetAllSigningKeys(configuration); + var numKeysInTokenValidationParameters = keysInTokenValidationParameters.Count(); + var numKeysInConfiguration = keysInConfiguration.Count(); + + if (kidExists) + { + if (kidMatched) + { + JsonWebToken localJwtToken = jwtToken; // avoid closure on non-exceptional path + var isKidInTVP = keysInTokenValidationParameters.Any(x => x.KeyId.Equals(localJwtToken.Kid)); + var keyLocation = isKidInTVP ? "TokenValidationParameters" : "Configuration"; + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10511, + LogHelper.MarkAsNonPII((object)keysAttempted ?? ""), + LogHelper.MarkAsNonPII(numKeysInTokenValidationParameters), + LogHelper.MarkAsNonPII(numKeysInConfiguration), + LogHelper.MarkAsNonPII(keyLocation), + LogHelper.MarkAsNonPII(jwtToken.Kid), + (object)exceptionStrings ?? "", + jwtToken))); + } + + if (!validationParameters.ValidateSignatureLast) + { + InternalValidators.ValidateAfterSignatureFailed( + jwtToken, + jwtToken.ValidFromNullable, + jwtToken.ValidToNullable, + jwtToken.Audiences, + validationParameters, + configuration); + } + } + + if (keysAttempted is not null) + { + if (kidExists) + { + throw LogHelper.LogExceptionMessage(new SecurityTokenSignatureKeyNotFoundException(LogHelper.FormatInvariant(TokenLogMessages.IDX10503, + LogHelper.MarkAsNonPII(jwtToken.Kid), + LogHelper.MarkAsNonPII((object)keysAttempted ?? ""), + LogHelper.MarkAsNonPII(numKeysInTokenValidationParameters), + LogHelper.MarkAsNonPII(numKeysInConfiguration), + (object)exceptionStrings ?? "", + jwtToken))); + } + else + { + throw LogHelper.LogExceptionMessage(new SecurityTokenSignatureKeyNotFoundException(LogHelper.FormatInvariant(TokenLogMessages.IDX10517, + LogHelper.MarkAsNonPII((object)keysAttempted ?? ""), + LogHelper.MarkAsNonPII(numKeysInTokenValidationParameters), + LogHelper.MarkAsNonPII(numKeysInConfiguration), + (object)exceptionStrings ?? "", + jwtToken))); + } + } + + throw LogHelper.LogExceptionMessage(new SecurityTokenSignatureKeyNotFoundException(TokenLogMessages.IDX10500)); + } + + internal static bool IsSignatureValid(byte[] signatureBytes, int signatureBytesLength, SignatureProvider signatureProvider, byte[] dataToVerify, int dataToVerifyLength) + { + if (signatureProvider is SymmetricSignatureProvider) + { + return signatureProvider.Verify(dataToVerify, 0, dataToVerifyLength, signatureBytes, 0, signatureBytesLength); + } + else + { + if (signatureBytes.Length == signatureBytesLength) + { + return signatureProvider.Verify(dataToVerify, 0, dataToVerifyLength, signatureBytes, 0, signatureBytesLength); + } + else + { + byte[] sigBytes = new byte[signatureBytesLength]; + Array.Copy(signatureBytes, 0, sigBytes, 0, signatureBytesLength); + return signatureProvider.Verify(dataToVerify, 0, dataToVerifyLength, sigBytes, 0, signatureBytesLength); + } + } + } + + internal static bool ValidateSignature(byte[] bytes, int len, string stringWithSignature, int signatureStartIndex, SignatureProvider signatureProvider) + { + return Base64UrlEncoding.Decode( + stringWithSignature, + signatureStartIndex + 1, + stringWithSignature.Length - signatureStartIndex - 1, + signatureProvider, + bytes, + len, + IsSignatureValid); + } + + internal static bool ValidateSignature(JsonWebToken jsonWebToken, SecurityKey key, TokenValidationParameters validationParameters) + { + var cryptoProviderFactory = validationParameters.CryptoProviderFactory ?? key.CryptoProviderFactory; + if (!cryptoProviderFactory.IsSupportedAlgorithm(jsonWebToken.Alg, key)) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX14000, LogHelper.MarkAsNonPII(jsonWebToken.Alg), key); + + return false; + } + + Validators.ValidateAlgorithm(jsonWebToken.Alg, key, jsonWebToken, validationParameters); + var signatureProvider = cryptoProviderFactory.CreateForVerifying(key, jsonWebToken.Alg); + try + { + if (signatureProvider == null) + throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant(TokenLogMessages.IDX10636, key == null ? "Null" : key.ToString(), LogHelper.MarkAsNonPII(jsonWebToken.Alg)))); + + return EncodingUtils.PerformEncodingDependentOperation( + jsonWebToken.EncodedToken, + 0, + jsonWebToken.Dot2, + Encoding.UTF8, + jsonWebToken.EncodedToken, + jsonWebToken.Dot2, + signatureProvider, + ValidateSignature); + } + finally + { + cryptoProviderFactory.ReleaseSignatureProvider(signatureProvider); + } + } + + private static JsonWebToken ValidateSignatureUsingDelegates(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters) + { + if (validationParameters.SignatureValidatorUsingConfiguration != null) + { + // TODO - get configuration from validationParameters + BaseConfiguration configuration = null; + var validatedToken = validationParameters.SignatureValidatorUsingConfiguration(jsonWebToken.EncodedToken, validationParameters, configuration); + if (validatedToken == null) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10505, jsonWebToken))); + + if (!(validatedToken is JsonWebToken validatedJsonWebToken)) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10506, LogHelper.MarkAsNonPII(typeof(JsonWebToken)), LogHelper.MarkAsNonPII(validatedToken.GetType()), jsonWebToken))); + + return validatedJsonWebToken; + } + else if (validationParameters.SignatureValidator != null) + { + var validatedToken = validationParameters.SignatureValidator(jsonWebToken.EncodedToken, validationParameters); + if (validatedToken == null) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10505, jsonWebToken))); + + if (!(validatedToken is JsonWebToken validatedJsonWebToken)) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10506, LogHelper.MarkAsNonPII(typeof(JsonWebToken)), LogHelper.MarkAsNonPII(validatedToken.GetType()), jsonWebToken))); + + return validatedJsonWebToken; + } + + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10505, jsonWebToken))); + } + + /// + /// Validates a JWS or a JWE. + /// + /// A 'JSON Web Token' (JWT) in JWS or JWE Compact Serialization Format. + /// A required for validation. + /// A + [Obsolete("`JsonWebTokens.ValidateToken(string, TokenValidationParameters)` has been deprecated and will be removed in a future release. Use `JsonWebTokens.ValidateTokenAsync(string, TokenValidationParameters)` instead. For more information, see https://aka.ms/IdentityModel/7-breaking-changes", false)] + public virtual TokenValidationResult ValidateToken(string token, TokenValidationParameters validationParameters) + { + return ValidateTokenAsync(token, validationParameters).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + /// + /// Validates a token. + /// On a validation failure, no exception will be thrown; instead, the exception will be set in the returned TokenValidationResult.Exception property. + /// Callers should always check the TokenValidationResult.IsValid property to verify the validity of the result. + /// + /// The token to be validated. + /// A required for validation. + /// A + /// + /// TokenValidationResult.Exception will be set to one of the following exceptions if the is invalid. + /// if is null or empty. + /// if is null. + /// 'token.Length' is greater than . + /// if is not a valid , + /// if the validationParameters.TokenReader delegate is not able to parse/read the token as a valid , + /// + public override async Task ValidateTokenAsync(string token, TokenValidationParameters validationParameters) + { + if (string.IsNullOrEmpty(token)) + return new TokenValidationResult { Exception = LogHelper.LogArgumentNullException(nameof(token)), IsValid = false }; + + if (validationParameters == null) + return new TokenValidationResult { Exception = LogHelper.LogArgumentNullException(nameof(validationParameters)), IsValid = false }; + + if (token.Length > MaximumTokenSizeInBytes) + return new TokenValidationResult { Exception = LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(TokenLogMessages.IDX10209, LogHelper.MarkAsNonPII(token.Length), LogHelper.MarkAsNonPII(MaximumTokenSizeInBytes)))), IsValid = false }; + + try + { + TokenValidationResult result = ReadToken(token, validationParameters); + if (result.IsValid) + return await ValidateTokenAsync(result.SecurityToken, validationParameters).ConfigureAwait(false); + + return result; + } + catch (Exception ex) + { + return new TokenValidationResult + { + Exception = ex, + IsValid = false + }; + } + } + + /// + public override async Task ValidateTokenAsync(SecurityToken token, TokenValidationParameters validationParameters) + { + if (token == null) + throw LogHelper.LogArgumentNullException(nameof(token)); + + if (validationParameters == null) + return new TokenValidationResult { Exception = LogHelper.LogArgumentNullException(nameof(validationParameters)), IsValid = false }; + + var jwt = token as JsonWebToken; + if (jwt == null) + return new TokenValidationResult { Exception = LogHelper.LogArgumentException(nameof(token), $"{nameof(token)} must be a {nameof(JsonWebToken)}."), IsValid = false }; + + try + { + return await ValidateTokenAsync(jwt, validationParameters).ConfigureAwait(false); + } + catch (Exception ex) + { + return new TokenValidationResult + { + Exception = ex, + IsValid = false + }; + } + } + + /// + /// Private method for token validation, responsible for: + /// (1) Obtaining a configuration from the . + /// (2) Revalidating using the Last Known Good Configuration (if present), and obtaining a refreshed configuration (if necessary) and revalidating using it. + /// + /// The JWT token + /// The to be used for validation. + /// + internal async ValueTask ValidateTokenAsync( + JsonWebToken jsonWebToken, + TokenValidationParameters validationParameters) + { + BaseConfiguration currentConfiguration = null; + if (validationParameters.ConfigurationManager != null) + { + try + { + currentConfiguration = await validationParameters.ConfigurationManager.GetBaseConfigurationAsync(CancellationToken.None).ConfigureAwait(false); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + // The exception is not re-thrown as the TokenValidationParameters may have the issuer and signing key set + // directly on them, allowing the library to continue with token validation. + if (LogHelper.IsEnabled(EventLogLevel.Warning)) + LogHelper.LogWarning(LogHelper.FormatInvariant(TokenLogMessages.IDX10261, validationParameters.ConfigurationManager.MetadataAddress, ex.ToString())); + } + } + + TokenValidationResult tokenValidationResult = jsonWebToken.IsEncrypted ? + await ValidateJWEAsync(jsonWebToken, validationParameters, currentConfiguration).ConfigureAwait(false) : + await ValidateJWSAsync(jsonWebToken, validationParameters, currentConfiguration).ConfigureAwait(false); + + if (validationParameters.ConfigurationManager != null) + { + if (tokenValidationResult.IsValid) + { + // Set current configuration as LKG if it exists. + if (currentConfiguration != null) + validationParameters.ConfigurationManager.LastKnownGoodConfiguration = currentConfiguration; + + return tokenValidationResult; + } + else if (TokenUtilities.IsRecoverableException(tokenValidationResult.Exception)) + { + // If we were still unable to validate, attempt to refresh the configuration and validate using it + // but ONLY if the currentConfiguration is not null. We want to avoid refreshing the configuration on + // retrieval error as this case should have already been hit before. This refresh handles the case + // where a new valid configuration was somehow published during validation time. + if (currentConfiguration != null) + { + validationParameters.ConfigurationManager.RequestRefresh(); + validationParameters.RefreshBeforeValidation = true; + var lastConfig = currentConfiguration; + currentConfiguration = await validationParameters.ConfigurationManager.GetBaseConfigurationAsync(CancellationToken.None).ConfigureAwait(false); + + // Only try to re-validate using the newly obtained config if it doesn't reference equal the previously used configuration. + if (lastConfig != currentConfiguration) + { + tokenValidationResult = jsonWebToken.IsEncrypted ? + await ValidateJWEAsync(jsonWebToken, validationParameters, currentConfiguration).ConfigureAwait(false) : + await ValidateJWSAsync(jsonWebToken, validationParameters, currentConfiguration).ConfigureAwait(false); + + if (tokenValidationResult.IsValid) + { + validationParameters.ConfigurationManager.LastKnownGoodConfiguration = currentConfiguration; + return tokenValidationResult; + } + } + } + + if (validationParameters.ConfigurationManager.UseLastKnownGoodConfiguration) + { + validationParameters.RefreshBeforeValidation = false; + validationParameters.ValidateWithLKG = true; + var recoverableException = tokenValidationResult.Exception; + + foreach (BaseConfiguration lkgConfiguration in validationParameters.ConfigurationManager.GetValidLkgConfigurations()) + { + if (!lkgConfiguration.Equals(currentConfiguration) && TokenUtilities.IsRecoverableConfiguration(jsonWebToken.Kid, currentConfiguration, lkgConfiguration, recoverableException)) + { + tokenValidationResult = jsonWebToken.IsEncrypted ? + await ValidateJWEAsync(jsonWebToken, validationParameters, lkgConfiguration).ConfigureAwait(false) : + await ValidateJWSAsync(jsonWebToken, validationParameters, lkgConfiguration).ConfigureAwait(false); + + if (tokenValidationResult.IsValid) + return tokenValidationResult; + } + } + } + } + } + + return tokenValidationResult; + } + + internal async ValueTask ValidateTokenPayloadAsync( + JsonWebToken jsonWebToken, + TokenValidationParameters validationParameters, + BaseConfiguration configuration) + { + var expires = jsonWebToken.HasPayloadClaim(JwtRegisteredClaimNames.Exp) ? (DateTime?)jsonWebToken.ValidTo : null; + var notBefore = jsonWebToken.HasPayloadClaim(JwtRegisteredClaimNames.Nbf) ? (DateTime?)jsonWebToken.ValidFrom : null; + + Validators.ValidateLifetime(notBefore, expires, jsonWebToken, validationParameters); + Validators.ValidateAudience(jsonWebToken.Audiences, jsonWebToken, validationParameters); + string issuer = await Validators.ValidateIssuerAsync(jsonWebToken.Issuer, jsonWebToken, validationParameters, configuration).ConfigureAwait(false); + + Validators.ValidateTokenReplay(expires, jsonWebToken.EncodedToken, validationParameters); + if (validationParameters.ValidateActor && !string.IsNullOrWhiteSpace(jsonWebToken.Actor)) + { + // Infinite recursion should not occur here, as the JsonWebToken passed into this method is (1) constructed from a string + // AND (2) the signature is successfully validated on it. (1) implies that even if there are nested actor tokens, + // they must end at some point since they cannot reference one another. (2) means that the token has a valid signature + // and (since issuer validation occurs first) came from a trusted authority. + // NOTE: More than one nested actor token should not be considered a valid token, but if we somehow encounter one, + // this code will still work properly. + TokenValidationResult tokenValidationResult = + await ValidateTokenAsync(jsonWebToken.Actor, validationParameters.ActorValidationParameters ?? validationParameters).ConfigureAwait(false); + + if (!tokenValidationResult.IsValid) + return tokenValidationResult; + } + + string tokenType = Validators.ValidateTokenType(jsonWebToken.Typ, jsonWebToken, validationParameters); + return new TokenValidationResult(jsonWebToken, this, validationParameters.Clone(), issuer) + { + IsValid = true, + TokenType = tokenType + }; + } + + internal async ValueTask ValidateTokenPayloadAsync( + JsonWebToken jsonWebToken, + TokenValidationParameters validationParameters, + CallContext callContext, + CancellationToken cancellationToken) + { + var expires = jsonWebToken.HasPayloadClaim(JwtRegisteredClaimNames.Exp) ? (DateTime?)jsonWebToken.ValidTo : null; + var notBefore = jsonWebToken.HasPayloadClaim(JwtRegisteredClaimNames.Nbf) ? (DateTime?)jsonWebToken.ValidFrom : null; + + Validators.ValidateLifetime(notBefore, expires, jsonWebToken, validationParameters); + Validators.ValidateAudience(jsonWebToken.Audiences, jsonWebToken, validationParameters); + + IssuerValidationResult issuerValidationResult = await Validators.ValidateIssuerAsync( + jsonWebToken.Issuer, + jsonWebToken, + validationParameters, + callContext, + cancellationToken).ConfigureAwait(false); + + if (!issuerValidationResult.IsValid) + { + return new TokenValidationResult(jsonWebToken, this, validationParameters, issuerValidationResult.Issuer) + { + IsValid = false, + Exception = issuerValidationResult.Exception + }; + } + + Validators.ValidateTokenReplay(expires, jsonWebToken.EncodedToken, validationParameters); + if (validationParameters.ValidateActor && !string.IsNullOrWhiteSpace(jsonWebToken.Actor)) + { + // Infinite recursion should not occur here, as the JsonWebToken passed into this method is (1) constructed from a string + // AND (2) the signature is successfully validated on it. (1) implies that even if there are nested actor tokens, + // they must end at some point since they cannot reference one another. (2) means that the token has a valid signature + // and (since issuer validation occurs first) came from a trusted authority. + // NOTE: More than one nested actor token should not be considered a valid token, but if we somehow encounter one, + // this code will still work properly. + TokenValidationResult tokenValidationResult = + await ValidateTokenAsync(jsonWebToken.Actor, validationParameters.ActorValidationParameters ?? validationParameters).ConfigureAwait(false); + + if (!tokenValidationResult.IsValid) + return tokenValidationResult; + } + + string tokenType = Validators.ValidateTokenType(jsonWebToken.Typ, jsonWebToken, validationParameters); + return new TokenValidationResult(jsonWebToken, this, validationParameters.Clone(), issuerValidationResult.Issuer) + { + IsValid = true, + TokenType = tokenType + }; + } + } +} diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs index cf4ad2e264..43d1999a3c 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs @@ -3,12 +3,8 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Security.Claims; -using System.Text; using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; using Microsoft.IdentityModel.Abstractions; using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; @@ -174,15 +170,6 @@ public virtual bool CanReadToken(string token) } } - /// - /// Returns a value that indicates if this handler can validate a . - /// - /// 'true', indicating this instance can validate a . - public virtual bool CanValidateToken - { - get { return true; } - } - private static StringComparison GetStringComparisonRuleIf509(SecurityKey securityKey) => (securityKey is X509SecurityKey) ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; @@ -498,90 +485,6 @@ public override SecurityToken ReadToken(string token) return ReadJsonWebToken(token); } - /// - /// Validates a JWS or a JWE. - /// - /// A 'JSON Web Token' (JWT) in JWS or JWE Compact Serialization Format. - /// A required for validation. - /// A - [Obsolete("`JsonWebTokens.ValidateToken(string, TokenValidationParameters)` has been deprecated and will be removed in a future release. Use `JsonWebTokens.ValidateTokenAsync(string, TokenValidationParameters)` instead. For more information, see https://aka.ms/IdentityModel/7-breaking-changes", false)] - public virtual TokenValidationResult ValidateToken(string token, TokenValidationParameters validationParameters) - { - return ValidateTokenAsync(token, validationParameters).ConfigureAwait(false).GetAwaiter().GetResult(); - } - - /// - /// Validates a token. - /// On a validation failure, no exception will be thrown; instead, the exception will be set in the returned TokenValidationResult.Exception property. - /// Callers should always check the TokenValidationResult.IsValid property to verify the validity of the result. - /// - /// The token to be validated. - /// A required for validation. - /// A - /// - /// TokenValidationResult.Exception will be set to one of the following exceptions if the is invalid. - /// if is null or empty. - /// if is null. - /// 'token.Length' is greater than . - /// if is not a valid , - /// if the validationParameters.TokenReader delegate is not able to parse/read the token as a valid , - /// - public override async Task ValidateTokenAsync(string token, TokenValidationParameters validationParameters) - { - if (string.IsNullOrEmpty(token)) - return new TokenValidationResult { Exception = LogHelper.LogArgumentNullException(nameof(token)), IsValid = false }; - - if (validationParameters == null) - return new TokenValidationResult { Exception = LogHelper.LogArgumentNullException(nameof(validationParameters)), IsValid = false }; - - if (token.Length > MaximumTokenSizeInBytes) - return new TokenValidationResult { Exception = LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(TokenLogMessages.IDX10209, LogHelper.MarkAsNonPII(token.Length), LogHelper.MarkAsNonPII(MaximumTokenSizeInBytes)))), IsValid = false }; - - try - { - TokenValidationResult result = ReadToken(token, validationParameters); - if (result.IsValid) - return await ValidateTokenAsync(result.SecurityToken, validationParameters).ConfigureAwait(false); - - return result; - } - catch (Exception ex) - { - return new TokenValidationResult - { - Exception = ex, - IsValid = false - }; - } - } - - /// - public override async Task ValidateTokenAsync(SecurityToken token, TokenValidationParameters validationParameters) - { - if (token == null) - throw LogHelper.LogArgumentNullException(nameof(token)); - - if (validationParameters == null) - return new TokenValidationResult { Exception = LogHelper.LogArgumentNullException(nameof(validationParameters)), IsValid = false }; - - var jwt = token as JsonWebToken; - if (jwt == null) - return new TokenValidationResult { Exception = LogHelper.LogArgumentException(nameof(token), $"{nameof(token)} must be a {nameof(JsonWebToken)}."), IsValid = false }; - - try - { - return await ValidateTokenAsync(jwt, validationParameters).ConfigureAwait(false); - } - catch (Exception ex) - { - return new TokenValidationResult - { - Exception = ex, - IsValid = false - }; - } - } - /// /// Converts a string into an instance of . /// @@ -627,453 +530,5 @@ private static TokenValidationResult ReadToken(string token, TokenValidationPara IsValid = true }; } - - /// - /// Private method for token validation, responsible for: - /// (1) Obtaining a configuration from the . - /// (2) Revalidating using the Last Known Good Configuration (if present), and obtaining a refreshed configuration (if necessary) and revalidating using it. - /// - /// The JWT token - /// The to be used for validation. - /// - private async ValueTask ValidateTokenAsync(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters) - { - BaseConfiguration currentConfiguration = null; - if (validationParameters.ConfigurationManager != null) - { - try - { - currentConfiguration = await validationParameters.ConfigurationManager.GetBaseConfigurationAsync(CancellationToken.None).ConfigureAwait(false); - } -#pragma warning disable CA1031 // Do not catch general exception types - catch (Exception ex) -#pragma warning restore CA1031 // Do not catch general exception types - { - // The exception is not re-thrown as the TokenValidationParameters may have the issuer and signing key set - // directly on them, allowing the library to continue with token validation. - if (LogHelper.IsEnabled(EventLogLevel.Warning)) - LogHelper.LogWarning(LogHelper.FormatInvariant(TokenLogMessages.IDX10261, validationParameters.ConfigurationManager.MetadataAddress, ex.ToString())); - } - } - - TokenValidationResult tokenValidationResult = await ValidateTokenAsync(jsonWebToken, validationParameters, currentConfiguration).ConfigureAwait(false); - if (validationParameters.ConfigurationManager != null) - { - if (tokenValidationResult.IsValid) - { - // Set current configuration as LKG if it exists. - if (currentConfiguration != null) - validationParameters.ConfigurationManager.LastKnownGoodConfiguration = currentConfiguration; - - return tokenValidationResult; - } - else if (TokenUtilities.IsRecoverableException(tokenValidationResult.Exception)) - { - // If we were still unable to validate, attempt to refresh the configuration and validate using it - // but ONLY if the currentConfiguration is not null. We want to avoid refreshing the configuration on - // retrieval error as this case should have already been hit before. This refresh handles the case - // where a new valid configuration was somehow published during validation time. - if (currentConfiguration != null) - { - validationParameters.ConfigurationManager.RequestRefresh(); - validationParameters.RefreshBeforeValidation = true; - var lastConfig = currentConfiguration; - currentConfiguration = await validationParameters.ConfigurationManager.GetBaseConfigurationAsync(CancellationToken.None).ConfigureAwait(false); - - // Only try to re-validate using the newly obtained config if it doesn't reference equal the previously used configuration. - if (lastConfig != currentConfiguration) - { - tokenValidationResult = await ValidateTokenAsync(jsonWebToken, validationParameters, currentConfiguration).ConfigureAwait(false); - - if (tokenValidationResult.IsValid) - { - validationParameters.ConfigurationManager.LastKnownGoodConfiguration = currentConfiguration; - return tokenValidationResult; - } - } - } - - if (validationParameters.ConfigurationManager.UseLastKnownGoodConfiguration) - { - validationParameters.RefreshBeforeValidation = false; - validationParameters.ValidateWithLKG = true; - var recoverableException = tokenValidationResult.Exception; - - foreach (BaseConfiguration lkgConfiguration in validationParameters.ConfigurationManager.GetValidLkgConfigurations()) - { - if (!lkgConfiguration.Equals(currentConfiguration) && TokenUtilities.IsRecoverableConfiguration(jsonWebToken.Kid, currentConfiguration, lkgConfiguration, recoverableException)) - { - tokenValidationResult = await ValidateTokenAsync(jsonWebToken, validationParameters, lkgConfiguration).ConfigureAwait(false); - - if (tokenValidationResult.IsValid) - return tokenValidationResult; - } - } - } - } - } - - return tokenValidationResult; - } - - private ValueTask ValidateTokenAsync(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) - { - return jsonWebToken.IsEncrypted ? - ValidateJWEAsync(jsonWebToken, validationParameters, configuration) : - ValidateJWSAsync(jsonWebToken, validationParameters, configuration); - } - - private async ValueTask ValidateJWSAsync(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) - { - try - { - TokenValidationResult tokenValidationResult; - if (validationParameters.TransformBeforeSignatureValidation != null) - jsonWebToken = validationParameters.TransformBeforeSignatureValidation(jsonWebToken, validationParameters) as JsonWebToken; - - if (validationParameters.SignatureValidator != null || validationParameters.SignatureValidatorUsingConfiguration != null) - { - var validatedToken = ValidateSignatureUsingDelegates(jsonWebToken, validationParameters, configuration); - tokenValidationResult = await ValidateTokenPayloadAsync(validatedToken, validationParameters, configuration).ConfigureAwait(false); - Validators.ValidateIssuerSecurityKey(validatedToken.SigningKey, validatedToken, validationParameters, configuration); - } - else - { - if (validationParameters.ValidateSignatureLast) - { - tokenValidationResult = await ValidateTokenPayloadAsync(jsonWebToken, validationParameters, configuration).ConfigureAwait(false); - if (tokenValidationResult.IsValid) - tokenValidationResult.SecurityToken = ValidateSignatureAndIssuerSecurityKey(jsonWebToken, validationParameters, configuration); - } - else - { - var validatedToken = ValidateSignatureAndIssuerSecurityKey(jsonWebToken, validationParameters, configuration); - tokenValidationResult = await ValidateTokenPayloadAsync(validatedToken, validationParameters, configuration).ConfigureAwait(false); - } - } - - return tokenValidationResult; - } -#pragma warning disable CA1031 // Do not catch general exception types - catch (Exception ex) -#pragma warning restore CA1031 // Do not catch general exception types - { - return new TokenValidationResult - { - Exception = ex, - IsValid = false, - TokenOnFailedValidation = validationParameters.IncludeTokenOnFailedValidation ? jsonWebToken : null - }; - } - } - - private async ValueTask ValidateJWEAsync(JsonWebToken jwtToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) - { - try - { - TokenValidationResult tokenValidationResult = ReadToken(DecryptToken(jwtToken, validationParameters, configuration), validationParameters); - if (!tokenValidationResult.IsValid) - return tokenValidationResult; - - tokenValidationResult = await ValidateJWSAsync(tokenValidationResult.SecurityToken as JsonWebToken, validationParameters, configuration).ConfigureAwait(false); - if (!tokenValidationResult.IsValid) - return tokenValidationResult; - - jwtToken.InnerToken = tokenValidationResult.SecurityToken as JsonWebToken; - jwtToken.Payload = (tokenValidationResult.SecurityToken as JsonWebToken).Payload; - return new TokenValidationResult - { - SecurityToken = jwtToken, - ClaimsIdentityNoLocking = tokenValidationResult.ClaimsIdentityNoLocking, - IsValid = true, - TokenType = tokenValidationResult.TokenType - }; - } -#pragma warning disable CA1031 // Do not catch general exception types - catch (Exception ex) -#pragma warning restore CA1031 // Do not catch general exception types - { - return new TokenValidationResult - { - Exception = ex, - IsValid = false, - TokenOnFailedValidation = validationParameters.IncludeTokenOnFailedValidation ? jwtToken : null - }; - } - } - - private static JsonWebToken ValidateSignatureUsingDelegates(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) - { - if (validationParameters.SignatureValidatorUsingConfiguration != null) - { - var validatedToken = validationParameters.SignatureValidatorUsingConfiguration(jsonWebToken.EncodedToken, validationParameters, configuration); - if (validatedToken == null) - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10505, jsonWebToken))); - - if (!(validatedToken is JsonWebToken validatedJsonWebToken)) - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10506, LogHelper.MarkAsNonPII(typeof(JsonWebToken)), LogHelper.MarkAsNonPII(validatedToken.GetType()), jsonWebToken))); - - return validatedJsonWebToken; - } - else if (validationParameters.SignatureValidator != null) - { - var validatedToken = validationParameters.SignatureValidator(jsonWebToken.EncodedToken, validationParameters); - if (validatedToken == null) - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10505, jsonWebToken))); - - if (!(validatedToken is JsonWebToken validatedJsonWebToken)) - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10506, LogHelper.MarkAsNonPII(typeof(JsonWebToken)), LogHelper.MarkAsNonPII(validatedToken.GetType()), jsonWebToken))); - - return validatedJsonWebToken; - } - - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10505, jsonWebToken))); - } - - private static JsonWebToken ValidateSignatureAndIssuerSecurityKey(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) - { - JsonWebToken validatedToken = ValidateSignature(jsonWebToken, validationParameters, configuration); - Validators.ValidateIssuerSecurityKey(validatedToken.SigningKey, jsonWebToken, validationParameters, configuration); - - return validatedToken; - } - - private async ValueTask ValidateTokenPayloadAsync(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) - { - var expires = jsonWebToken.HasPayloadClaim(JwtRegisteredClaimNames.Exp) ? (DateTime?)jsonWebToken.ValidTo : null; - var notBefore = jsonWebToken.HasPayloadClaim(JwtRegisteredClaimNames.Nbf) ? (DateTime?)jsonWebToken.ValidFrom : null; - - Validators.ValidateLifetime(notBefore, expires, jsonWebToken, validationParameters); - Validators.ValidateAudience(jsonWebToken.Audiences, jsonWebToken, validationParameters); - string issuer = await Validators.ValidateIssuerAsync(jsonWebToken.Issuer, jsonWebToken, validationParameters, configuration).ConfigureAwait(false); - - Validators.ValidateTokenReplay(expires, jsonWebToken.EncodedToken, validationParameters); - if (validationParameters.ValidateActor && !string.IsNullOrWhiteSpace(jsonWebToken.Actor)) - { - // Infinite recursion should not occur here, as the JsonWebToken passed into this method is (1) constructed from a string - // AND (2) the signature is successfully validated on it. (1) implies that even if there are nested actor tokens, - // they must end at some point since they cannot reference one another. (2) means that the token has a valid signature - // and (since issuer validation occurs first) came from a trusted authority. - // NOTE: More than one nested actor token should not be considered a valid token, but if we somehow encounter one, - // this code will still work properly. - TokenValidationResult tokenValidationResult = - await ValidateTokenAsync(jsonWebToken.Actor, validationParameters.ActorValidationParameters ?? validationParameters).ConfigureAwait(false); - - if (!tokenValidationResult.IsValid) - return tokenValidationResult; - } - - string tokenType = Validators.ValidateTokenType(jsonWebToken.Typ, jsonWebToken, validationParameters); - return new TokenValidationResult(jsonWebToken, this, validationParameters.Clone(), issuer) - { - IsValid = true, - TokenType = tokenType - }; - } - - /// - /// Validates the JWT signature. - /// - private static JsonWebToken ValidateSignature(JsonWebToken jwtToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) - { - bool kidMatched = false; - IEnumerable keys = null; - - if (!jwtToken.IsSigned) - { - if (validationParameters.RequireSignedTokens) - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10504, jwtToken))); - else - return jwtToken; - } - - if (validationParameters.IssuerSigningKeyResolverUsingConfiguration != null) - { - keys = validationParameters.IssuerSigningKeyResolverUsingConfiguration(jwtToken.EncodedToken, jwtToken, jwtToken.Kid, validationParameters, configuration); - } - else if (validationParameters.IssuerSigningKeyResolver != null) - { - keys = validationParameters.IssuerSigningKeyResolver(jwtToken.EncodedToken, jwtToken, jwtToken.Kid, validationParameters); - } - else - { - var key = JwtTokenUtilities.ResolveTokenSigningKey(jwtToken.Kid, jwtToken.X5t, validationParameters, configuration); - if (key != null) - { - kidMatched = true; - keys = [key]; - } - } - - if (validationParameters.TryAllIssuerSigningKeys && keys.IsNullOrEmpty()) - { - // 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(configuration, validationParameters); - } - - // keep track of exceptions thrown, keys that were tried - StringBuilder exceptionStrings = null; - StringBuilder keysAttempted = null; - var kidExists = !string.IsNullOrEmpty(jwtToken.Kid); - - if (keys != null) - { - foreach (var key in keys) - { - try - { - if (ValidateSignature(jwtToken, key, validationParameters)) - { - if (LogHelper.IsEnabled(EventLogLevel.Informational)) - LogHelper.LogInformation(TokenLogMessages.IDX10242, jwtToken); - - jwtToken.SigningKey = key; - return jwtToken; - } - } - catch (Exception ex) - { - (exceptionStrings ??= new StringBuilder()).AppendLine(ex.ToString()); - } - - if (key != null) - { - (keysAttempted ??= new StringBuilder()).Append(key.ToString()).Append(" , KeyId: ").AppendLine(key.KeyId); - if (kidExists && !kidMatched && key.KeyId != null) - kidMatched = jwtToken.Kid.Equals(key.KeyId, key is X509SecurityKey ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); - } - } - } - - // Get information on where keys used during token validation came from for debugging purposes. - var keysInTokenValidationParameters = TokenUtilities.GetAllSigningKeys(validationParameters: validationParameters); - var keysInConfiguration = TokenUtilities.GetAllSigningKeys(configuration); - var numKeysInTokenValidationParameters = keysInTokenValidationParameters.Count(); - var numKeysInConfiguration = keysInConfiguration.Count(); - - if (kidExists) - { - if (kidMatched) - { - JsonWebToken localJwtToken = jwtToken; // avoid closure on non-exceptional path - var isKidInTVP = keysInTokenValidationParameters.Any(x => x.KeyId.Equals(localJwtToken.Kid)); - var keyLocation = isKidInTVP ? "TokenValidationParameters" : "Configuration"; - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10511, - LogHelper.MarkAsNonPII((object)keysAttempted ?? ""), - LogHelper.MarkAsNonPII(numKeysInTokenValidationParameters), - LogHelper.MarkAsNonPII(numKeysInConfiguration), - LogHelper.MarkAsNonPII(keyLocation), - LogHelper.MarkAsNonPII(jwtToken.Kid), - (object)exceptionStrings ?? "", - jwtToken))); - } - - if (!validationParameters.ValidateSignatureLast) - { - InternalValidators.ValidateAfterSignatureFailed( - jwtToken, - jwtToken.ValidFromNullable, - jwtToken.ValidToNullable, - jwtToken.Audiences, - validationParameters, - configuration); - } - } - - if (keysAttempted is not null) - { - if (kidExists) - { - throw LogHelper.LogExceptionMessage(new SecurityTokenSignatureKeyNotFoundException(LogHelper.FormatInvariant(TokenLogMessages.IDX10503, - LogHelper.MarkAsNonPII(jwtToken.Kid), - LogHelper.MarkAsNonPII((object)keysAttempted ?? ""), - LogHelper.MarkAsNonPII(numKeysInTokenValidationParameters), - LogHelper.MarkAsNonPII(numKeysInConfiguration), - (object)exceptionStrings ?? "", - jwtToken))); - } - else - { - throw LogHelper.LogExceptionMessage(new SecurityTokenSignatureKeyNotFoundException(LogHelper.FormatInvariant(TokenLogMessages.IDX10517, - LogHelper.MarkAsNonPII((object)keysAttempted ?? ""), - LogHelper.MarkAsNonPII(numKeysInTokenValidationParameters), - LogHelper.MarkAsNonPII(numKeysInConfiguration), - (object)exceptionStrings ?? "", - jwtToken))); - } - } - - throw LogHelper.LogExceptionMessage(new SecurityTokenSignatureKeyNotFoundException(TokenLogMessages.IDX10500)); - } - - internal static bool IsSignatureValid(byte[] signatureBytes, int signatureBytesLength, SignatureProvider signatureProvider, byte[] dataToVerify, int dataToVerifyLength) - { - if (signatureProvider is SymmetricSignatureProvider) - { - return signatureProvider.Verify(dataToVerify, 0, dataToVerifyLength, signatureBytes, 0, signatureBytesLength); - } - else - { - if (signatureBytes.Length == signatureBytesLength) - { - return signatureProvider.Verify(dataToVerify, 0, dataToVerifyLength, signatureBytes, 0, signatureBytesLength); - } - else - { - byte[] sigBytes = new byte[signatureBytesLength]; - Array.Copy(signatureBytes, 0, sigBytes, 0, signatureBytesLength); - return signatureProvider.Verify(dataToVerify, 0, dataToVerifyLength, sigBytes, 0, signatureBytesLength); - } - } - } - - internal static bool ValidateSignature(byte[] bytes, int len, string stringWithSignature, int signatureStartIndex, SignatureProvider signatureProvider) - { - return Base64UrlEncoding.Decode( - stringWithSignature, - signatureStartIndex + 1, - stringWithSignature.Length - signatureStartIndex - 1, - signatureProvider, - bytes, - len, - IsSignatureValid); - } - - internal static bool ValidateSignature(JsonWebToken jsonWebToken, SecurityKey key, TokenValidationParameters validationParameters) - { - var cryptoProviderFactory = validationParameters.CryptoProviderFactory ?? key.CryptoProviderFactory; - if (!cryptoProviderFactory.IsSupportedAlgorithm(jsonWebToken.Alg, key)) - { - if (LogHelper.IsEnabled(EventLogLevel.Informational)) - LogHelper.LogInformation(LogMessages.IDX14000, LogHelper.MarkAsNonPII(jsonWebToken.Alg), key); - - return false; - } - - Validators.ValidateAlgorithm(jsonWebToken.Alg, key, jsonWebToken, validationParameters); - var signatureProvider = cryptoProviderFactory.CreateForVerifying(key, jsonWebToken.Alg); - try - { - if (signatureProvider == null) - throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant(TokenLogMessages.IDX10636, key == null ? "Null" : key.ToString(), LogHelper.MarkAsNonPII(jsonWebToken.Alg)))); - - return EncodingUtils.PerformEncodingDependentOperation( - jsonWebToken.EncodedToken, - 0, - jsonWebToken.Dot2, - Encoding.UTF8, - jsonWebToken.EncodedToken, - jsonWebToken.Dot2, - signatureProvider, - ValidateSignature); - } - finally - { - cryptoProviderFactory.ReleaseSignatureProvider(signatureProvider); - } - } } } diff --git a/src/Microsoft.IdentityModel.Tokens/Exceptions/SecurityTokenException.cs b/src/Microsoft.IdentityModel.Tokens/Exceptions/SecurityTokenException.cs index f544c99e93..e346c7cf73 100644 --- a/src/Microsoft.IdentityModel.Tokens/Exceptions/SecurityTokenException.cs +++ b/src/Microsoft.IdentityModel.Tokens/Exceptions/SecurityTokenException.cs @@ -2,18 +2,22 @@ // Licensed under the MIT License. using System; +using System.Diagnostics; using System.Runtime.Serialization; +using System.Text; using Microsoft.IdentityModel.Logging; namespace Microsoft.IdentityModel.Tokens { - /// /// Represents a security token exception. /// [Serializable] public class SecurityTokenException : Exception { + [NonSerialized] + private string _stackTrace; + /// /// Initializes a new instance of the class. /// @@ -55,6 +59,49 @@ protected SecurityTokenException(SerializationInfo info, StreamingContext contex { } + /// + /// Gets the stack trace that is captured when the exception is created. + /// + public override string StackTrace + { + get + { + if (_stackTrace == null) + { + if (ExceptionDetail == null) + return base.StackTrace; +#if NET8_0_OR_GREATER + _stackTrace = new StackTrace(ExceptionDetail.StackFrames).ToString(); +#else + StringBuilder sb = new(); + foreach (StackFrame frame in ExceptionDetail.StackFrames) + { + sb.Append(frame.ToString()); + sb.Append(Environment.NewLine); + } + + _stackTrace = sb.ToString(); +#endif + } + + return _stackTrace; + } + } + + /// + /// Gets or sets the source of the exception. + /// + public override string Source + { + get => base.Source; + set => base.Source = value; + } + + internal ExceptionDetail ExceptionDetail + { + get; set; + } + #if NET472 || NETSTANDARD2_0 || NET6_0_OR_GREATER /// /// When overridden in a derived class, sets the System.Runtime.Serialization.SerializationInfo diff --git a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs index b9fe6c2996..679fbf2b8f 100644 --- a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs +++ b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs @@ -35,7 +35,7 @@ internal static class LogMessages public const string IDX10207 = "IDX10207: Unable to validate audience. The 'audiences' parameter is null."; public const string IDX10208 = "IDX10208: Unable to validate audience. validationParameters.ValidAudience is null or whitespace and validationParameters.ValidAudiences is null."; public const string IDX10209 = "IDX10209: Token has length: '{0}' which is larger than the MaximumTokenSizeInBytes: '{1}'."; - public const string IDX10211 = "IDX10211: Unable to validate issuer. The 'issuer' parameter is null or whitespace"; + public const string IDX10211 = "IDX10211: Unable to validate issuer. The 'issuer' parameter is null or whitespace."; public const string IDX10214 = "IDX10214: Audience validation failed. Audiences: '{0}'. Did not match: validationParameters.ValidAudience: '{1}' or validationParameters.ValidAudiences: '{2}'."; public const string IDX10222 = "IDX10222: Lifetime validation failed. The token is not yet valid. ValidFrom (UTC): '{0}', Current time (UTC): '{1}'."; public const string IDX10223 = "IDX10223: Lifetime validation failed. The token is expired. ValidTo (UTC): '{0}', Current time (UTC): '{1}'."; @@ -79,6 +79,8 @@ internal static class LogMessages //public const string IDX10263 = "IDX10263: Unable to re-validate with ConfigurationManager.LastKnownGoodConfiguration as it is expired."; public const string IDX10264 = "IDX10264: Reading issuer signing keys from validation parameters and configuration."; public const string IDX10265 = "IDX10265: Reading issuer signing keys from configuration."; + //public const string IDX10266 = "IDX10266: Unable to validate issuer. validationParameters.ValidIssuer is null or whitespace, validationParameters.ValidIssuers is null or empty and ConfigurationManager is null."; + // 10500 - SignatureValidation public const string IDX10500 = "IDX10500: Signature validation failed. No security keys were provided to validate the signature."; diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index bb68764a6d..2112772699 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs @@ -13,7 +13,7 @@ namespace Microsoft.IdentityModel.Tokens /// /// Contains a set of parameters that are used by a when validating a . /// - public class TokenValidationParameters + public partial class TokenValidationParameters { private string _authenticationType; private TimeSpan _clockSkew = DefaultClockSkew; @@ -38,7 +38,7 @@ public class TokenValidationParameters /// Default for the maximum token size. /// /// 250 KB (kilobytes). - public const Int32 DefaultMaximumTokenSizeInBytes = 1024 * 250; + public const int DefaultMaximumTokenSizeInBytes = 1024 * 250; /// /// Copy constructor for . @@ -66,6 +66,7 @@ protected TokenValidationParameters(TokenValidationParameters other) IssuerSigningKeyValidatorUsingConfiguration = other.IssuerSigningKeyValidatorUsingConfiguration; IssuerValidator = other.IssuerValidator; IssuerValidatorAsync = other.IssuerValidatorAsync; + IssuerValidationDelegateAsync = other.IssuerValidationDelegateAsync; IssuerValidatorUsingConfiguration = other.IssuerValidatorUsingConfiguration; LifetimeValidator = other.LifetimeValidator; LogTokenId = other.LogTokenId; @@ -172,22 +173,6 @@ public string AuthenticationType } } - ///// - ///// Gets or sets the for validating X509Certificate2(s). - ///// - //public X509CertificateValidator CertificateValidator - //{ - // get - // { - // return _certificateValidator; - // } - - // set - // { - // _certificateValidator = value; - // } - //} - /// /// Gets or sets the clock skew to apply when validating a time. /// @@ -368,7 +353,6 @@ public virtual ClaimsIdentity CreateClaimsIdentity(SecurityToken securityToken, /// public IssuerValidator IssuerValidator { get; set; } - /// /// Gets or sets a delegate that will be used to validate the issuer of the token. /// diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/AsyncValidate.cd b/src/Microsoft.IdentityModel.Tokens/Validation/AsyncValidate.cd new file mode 100644 index 0000000000..ebefb90859 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/AsyncValidate.cd @@ -0,0 +1,81 @@ + + + + + + ABEAIAABEEAAEAIAAAAAAAABEQAAAEEACABAAAAkIoA= + Validation\TokenValidationResult.cs + + + + + + AAEAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAA= + Validation\IssuerValidationResult.cs + + + + + + AAAEAAAAAAAAAAAAAAAAEAAEAAAAAAAAAEAABAAAAAA= + Validation\ExceptionDetail.cs + + + + + + + + + AIAAAAJAAAAAAAAAAAgAIAABAAgAAAAABEBBAAAAAAA= + Validation\ValidationResult.cs + + + + + + + + + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEAAA= + Validation\LogDetail.cs + + + + + + + + + + + + AAAIAAAAAAAAAAAAAAIAAAQAAABAQAAAAAAAAAAAAAA= + Validation\ValidationFailureType.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAEIAAIAAAAAA= + Validation\MessageDetail.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + CallContext.cs + + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ExceptionDetail.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ExceptionDetail.cs new file mode 100644 index 0000000000..1ab311c9e8 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ExceptionDetail.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// Contains information so that Exceptions can be logged or thrown written as required. + /// + internal class ExceptionDetail + { + /// + /// Creates an instance of + /// + /// contains information about the exception that is used to generate the exception message. + /// is the type of exception that occurred. + /// contains information about the stack frame where the exception occurred. + public ExceptionDetail(MessageDetail messageDetail, Type exceptionType, StackFrame stackFrame) + : this(messageDetail, exceptionType, stackFrame, null) + { + } + + /// + /// Creates an instance of + /// + /// contains information about the exception that is used to generate the exception message. + /// is the type of exception that occurred. + /// contains information about the stack frame where the exception occurred. + /// is the inner exception that occurred. + public ExceptionDetail(MessageDetail messageDetail, Type exceptionType, StackFrame stackFrame, Exception innerException) + { + ExceptionType = exceptionType; + InnerException = innerException; + MessageDetail = messageDetail; + StackFrames.Add(stackFrame); + } + + /// + /// Creates an instance of an using + /// + /// An instantance of an Exception. + public Exception GetException() + { + if (InnerException != null) + return Activator.CreateInstance(ExceptionType, MessageDetail.Message, InnerException) as Exception; + + return Activator.CreateInstance(ExceptionType, MessageDetail.Message) as Exception; + } + + /// + /// Gets the type of exception that occurred. + /// + public Type ExceptionType { get; } + + /// + /// Gets the inner exception that occurred. + /// + public Exception InnerException { get; } + + /// + /// Gets the message details that are used to generate the exception message. + /// + public MessageDetail MessageDetail { get; } + + /// + /// Gets the stack frames where the exception occurred. + /// + public IList StackFrames { get; } = []; + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Exceptions.cd b/src/Microsoft.IdentityModel.Tokens/Validation/Exceptions.cd new file mode 100644 index 0000000000..cd69bb66db --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Exceptions.cd @@ -0,0 +1,35 @@ + + + + + + AIAAAAAAAgAAAgAAAAQAAAAAAAAAAAAAAAAAAAAAAAA= + Exceptions\SecurityTokenException.cs + + + + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Exceptions\SecurityTokenValidationException.cs + + + + + + AAgAAEAAAAAAAAAAAAACAAAgAAAAAAAAAAAAAAAAAAA= + Exceptions\SecurityTokenInvalidIssuerException.cs + + + + + + AAAEAAAAAAAAAAAAAAAAEAAEAAAAAAAAAEAABAAAAAA= + Validation\ExceptionDetail.cs + + + + \ No newline at end of file diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/IssuerValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/IssuerValidationResult.cs new file mode 100644 index 0000000000..35c53dc561 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/IssuerValidationResult.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// Contains the result of validating a issuer. + /// The contains a collection of for each step in the token validation. + /// + internal class IssuerValidationResult : ValidationResult + { + private Exception _exception; + + /// + /// Creates an instance of + /// + /// is the issuer that was validated successfully. + public IssuerValidationResult(string issuer) + : base(ValidationFailureType.ValidationSucceeded) + { + Issuer = issuer; + IsValid = true; + } + + /// + /// Creates an instance of + /// + /// is the issuer that was intended to be validated. + /// is the that occurred during validation. + /// is the that occurred during validation. + public IssuerValidationResult(string issuer, ValidationFailureType validationFailure, ExceptionDetail exceptionDetail) + : base(validationFailure, exceptionDetail) + { + Issuer = issuer; + IsValid = false; + } + + /// + /// Gets the that occurred during validation. + /// + public override Exception Exception + { + get + { + if (_exception != null || ExceptionDetail == null) + return _exception; + + HasValidOrExceptionWasRead = true; + _exception = ExceptionDetail.GetException(); + SecurityTokenInvalidIssuerException securityTokenInvalidIssuerException = _exception as SecurityTokenInvalidIssuerException; + if (securityTokenInvalidIssuerException != null) + { + securityTokenInvalidIssuerException.InvalidIssuer = Issuer; + securityTokenInvalidIssuerException.ExceptionDetail = ExceptionDetail; + securityTokenInvalidIssuerException.Source = "Microsoft.IdentityModel.Tokens"; + } + + return _exception; + } + } + + /// + /// Gets the issuer that was validated or intended to be validated. + /// + public string Issuer { get; } + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/LogDetail.cs b/src/Microsoft.IdentityModel.Tokens/Validation/LogDetail.cs new file mode 100644 index 0000000000..6b91a67c1e --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/LogDetail.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.IdentityModel.Abstractions; + +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// Contains information so that logs can be written when needed. + /// + internal class LogDetail + { + /// + /// Creates an instance of + /// + /// contains information about the exception that is used to generate the exception message. + /// is the level of the event log. + public LogDetail(MessageDetail messageDetail, EventLogLevel eventLogLevel) + { + EventLogLevel = eventLogLevel; + MessageDetail = messageDetail; + } + + /// + /// Gets the level of the event log. + /// + public EventLogLevel EventLogLevel { get; } + + /// + /// Gets the message detail. + /// + public MessageDetail MessageDetail { get; } + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/MessageDetail.cs b/src/Microsoft.IdentityModel.Tokens/Validation/MessageDetail.cs new file mode 100644 index 0000000000..1579ab14da --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/MessageDetail.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.IdentityModel.Logging; + +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// Contains information about a message that is used to generate a message for logging or exceptions. + /// + internal class MessageDetail + { + private string _message; + + // TODO - remove the need to create NonPII objects, we could use tuples where bool == true => object is PII. + // TODO - does this need to be ReadOnlyMemory? + /// + /// Creates an instance of + /// + /// The message to be formated. + /// The parameters for formatting. + public MessageDetail(ReadOnlyMemory formatString, params object[] parameters) + { + // TODO - paramter validation. + FormatString = formatString; + Parameters = parameters; + } + + /// + /// Gets the formatted message. + /// + public string Message + { + get + { + _message ??= LogHelper.FormatInvariant(FormatString.ToString(), Parameters); + return _message; + } + } + + private ReadOnlyMemory FormatString { get; } + + private object[] Parameters { get; } + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/TokenValidationParameters.IssuerValidationDelegate.cs b/src/Microsoft.IdentityModel.Tokens/Validation/TokenValidationParameters.IssuerValidationDelegate.cs new file mode 100644 index 0000000000..cab11a6bbc --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/TokenValidationParameters.IssuerValidationDelegate.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// partial class for the IssuerValidation delegate. + /// + public partial class TokenValidationParameters + { + /// + /// Gets or sets a delegate that will be used to validate the issuer of a . + /// + internal IssuerValidationDelegateAsync IssuerValidationDelegateAsync { get; set; } + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/TokenValidationResult.cs similarity index 85% rename from src/Microsoft.IdentityModel.Tokens/TokenValidationResult.cs rename to src/Microsoft.IdentityModel.Tokens/Validation/TokenValidationResult.cs index 6714c71b9e..46c90738e0 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationResult.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/TokenValidationResult.cs @@ -19,10 +19,6 @@ public class TokenValidationResult private readonly TokenValidationParameters _validationParameters; private readonly TokenHandler _tokenHandler; - private Exception _exception; - private bool _hasIsValidOrExceptionBeenRead = false; - private bool _isValid = false; - // Fields lazily initialized in a thread-safe manner. _claimsIdentity is protected by the _claimsIdentitySyncObj // lock, and since null is a valid initialized value, _claimsIdentityInitialized tracks whether or not it's valid. // _claims is constructed by reading the data from the ClaimsIdentity and is synchronized using Interlockeds @@ -37,6 +33,11 @@ public class TokenValidationResult private ClaimsIdentity _claimsIdentity; private Dictionary _claims; private Dictionary _propertyBag; + // TODO - lazy creation of _validationResults + private List _validationResults = []; + + private Exception _exception; + private bool _isValid; /// /// Creates an instance of @@ -60,6 +61,18 @@ internal TokenValidationResult(SecurityToken securityToken, TokenHandler tokenHa SecurityToken = securityToken; } + /// + /// Adds a to the list of . + /// + /// the associated with one of the validation steps. For example . + internal void AddValidationResult(ValidationResult validationResult) + { + if (validationResult is null) + throw LogHelper.LogArgumentNullException(nameof(validationResult)); + + _validationResults.Add(validationResult); + } + /// /// The created from the validated security token. /// @@ -67,7 +80,7 @@ public IDictionary Claims { get { - if (!_hasIsValidOrExceptionBeenRead) + if (!HasValidOrExceptionWasRead) LogHelper.LogWarning(LogMessages.IDX10109); if (_claims is null && ClaimsIdentity is { } ci) @@ -162,7 +175,7 @@ public Exception Exception { get { - _hasIsValidOrExceptionBeenRead = true; + HasValidOrExceptionWasRead = true; return _exception; } set @@ -171,6 +184,8 @@ public Exception Exception } } + internal bool HasValidOrExceptionWasRead { get; set; } + /// /// Gets or sets the issuer that was found in the token. /// @@ -183,7 +198,7 @@ public bool IsValid { get { - _hasIsValidOrExceptionBeenRead = true; + HasValidOrExceptionWasRead = true; return _isValid; } set @@ -228,5 +243,19 @@ public bool IsValid /// (e.g for a JSON Web Token, from the "typ" header). /// public string TokenType { get; set; } + + /// + /// Gets the list of that contains the result of validating the token. + /// + internal IReadOnlyList ValidationResults + { + get + { + if (_validationResults is null) + Interlocked.CompareExchange(ref _validationResults, new List(), null); + + return _validationResults; + } + } } } diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationDelegates.cd b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationDelegates.cd new file mode 100644 index 0000000000..a6af5bfb82 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationDelegates.cd @@ -0,0 +1,21 @@ + + + + + + YAgEEAQAKMQck0AAi5R6AACRWgBkBQIAAQgYQsaIkxA= + Validation\TokenValidationParameters.IssuerValidationDelegate.cs + + + + + + + + + AAAAAAAAAACAAAAAACAQBAAAAAAAAAAAgAAAAAAAAAA= + Validation\Validators.Issuer.cs + + + + \ No newline at end of file diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs new file mode 100644 index 0000000000..3d872bb63a --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// The type of the failure that occurred when validating a . + /// + internal abstract class ValidationFailureType + { + /// + /// Creates an instance of + /// + protected ValidationFailureType(string name) + { + Name = name; + } + + /// + /// Gets the name of the . + /// + public string Name { get; } + + /// + /// Defines a type that represents a required parameter was null. + /// + public static readonly ValidationFailureType NullArgument = new NullArgumentFailure("NullArgument"); + private class NullArgumentFailure : ValidationFailureType { internal NullArgumentFailure(string name) : base(name) { } } + + /// + /// Defines a type that represents that issuer validation failed. + /// + public static readonly ValidationFailureType IssuerValidationFailed = new IssuerValidationFailure("IssuerValidationFailed"); + private class IssuerValidationFailure : ValidationFailureType { internal IssuerValidationFailure(string name) : base(name) { } } + + /// + /// Defines a type that represents that no evaluation has taken place. + /// + public static readonly ValidationFailureType ValidationNotEvaluated = new NotEvaluated("NotEvaluated"); + private class NotEvaluated : ValidationFailureType { internal NotEvaluated(string name) : base(name) { } } + + /// + /// Defines a type that represents that no evaluation has taken place. + /// + public static readonly ValidationFailureType ValidationSucceeded = new Succeeded("Succeeded"); + private class Succeeded : ValidationFailureType { internal Succeeded(string name) : base(name) { } } + + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationResult.cs new file mode 100644 index 0000000000..f4d8ce0dc0 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationResult.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// Contains results of a single step in validating a . + /// A maintains a list of for each step in the token validation. + /// + internal abstract class ValidationResult + { + private bool _isValid = false; + + /// + /// Creates an instance of + /// + protected ValidationResult() + { + ValidationFailureType = ValidationFailureType.ValidationNotEvaluated; + } + + /// + /// Creates an instance of + /// + /// The that occurred during validation. + protected ValidationResult(ValidationFailureType validationFailureType) + { + ValidationFailureType = validationFailureType; + } + + /// + /// Creates an instance of + /// + /// The that occurred during validation. + /// The representing the that occurred during validation. + protected ValidationResult(ValidationFailureType validationFailureType, ExceptionDetail exceptionDetail) + { + ValidationFailureType = validationFailureType; + ExceptionDetail = exceptionDetail; + } + + /// + /// Adds a new stack frame to the exception details. + /// + /// + public void AddStackFrame(StackFrame stackFrame) + { + ExceptionDetail.StackFrames.Add(stackFrame); + } + + /// + /// Gets the that occurred during validation. + /// + public abstract Exception Exception { get; } + + /// + /// Gets the that occurred during validation. + /// + public ExceptionDetail ExceptionDetail { get; } + + /// + /// True if the token was successfully validated, false otherwise. + /// + public bool IsValid + { + get + { + HasValidOrExceptionWasRead = true; + return _isValid; + } + set + { + _isValid = value; + } + } + + // TODO - HasValidOrExceptionWasRead, IsValid, Exception are temporary and will be removed when TokenValidationResult derives from ValidationResult. + /// + /// Gets or sets a boolean recording if IsValid or Exception was called. + /// + protected bool HasValidOrExceptionWasRead { get; set; } + + /// + /// Logs the validation result. + /// +#pragma warning disable CA1822 // Mark members as static + public void Log() +#pragma warning restore CA1822 // Mark members as static + { + // TODO - Do we need this, how will it work? + } + + /// + /// Contains any logs that would have been written. + /// + public IList LogDetails { get; } = new List(); + + /// + /// Gets the indicating why the validation was not satisfied. + /// + public ValidationFailureType ValidationFailureType + { + get; + } = ValidationFailureType.ValidationNotEvaluated; + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Audience.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Audience.cs new file mode 100644 index 0000000000..9c41082c53 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Audience.cs @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.IdentityModel.Abstractions; +using Microsoft.IdentityModel.Logging; + +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// Partial class for Audience Validation. + /// + public static partial class Validators + { + /// + /// Determines if the audiences found in a are valid. + /// + /// The audiences found in the . + /// The being validated. + /// required for validation. + /// If 'validationParameters' is null. + /// If 'audiences' is null and is true. + /// If is null or whitespace and is null. + /// If none of the 'audiences' matched either or one of . + /// An EXACT match is required. + public static void ValidateAudience(IEnumerable audiences, SecurityToken securityToken, TokenValidationParameters validationParameters) + { + if (validationParameters == null) + throw LogHelper.LogArgumentNullException(nameof(validationParameters)); + + if (validationParameters.AudienceValidator != null) + { + if (!validationParameters.AudienceValidator(audiences, securityToken, validationParameters)) + throw LogHelper.LogExceptionMessage( + new SecurityTokenInvalidAudienceException( + LogHelper.FormatInvariant( + LogMessages.IDX10231, + LogHelper.MarkAsUnsafeSecurityArtifact(securityToken, t => t.ToString()))) + { + InvalidAudience = Utility.SerializeAsSingleCommaDelimitedString(audiences) + }); + + return; + } + + if (!validationParameters.ValidateAudience) + { + LogHelper.LogWarning(LogMessages.IDX10233); + return; + } + + if (audiences == null) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidAudienceException(LogMessages.IDX10207) { InvalidAudience = null }); + + if (string.IsNullOrWhiteSpace(validationParameters.ValidAudience) && (validationParameters.ValidAudiences == null)) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidAudienceException(LogMessages.IDX10208) { InvalidAudience = Utility.SerializeAsSingleCommaDelimitedString(audiences) }); + + if (!audiences.Any()) + throw LogHelper.LogExceptionMessage( + new SecurityTokenInvalidAudienceException(LogHelper.FormatInvariant(LogMessages.IDX10206)) + { InvalidAudience = Utility.SerializeAsSingleCommaDelimitedString(audiences) }); + + // create enumeration of all valid audiences from validationParameters + IEnumerable validationParametersAudiences; + + if (validationParameters.ValidAudiences == null) + validationParametersAudiences = new[] { validationParameters.ValidAudience }; + else if (string.IsNullOrWhiteSpace(validationParameters.ValidAudience)) + validationParametersAudiences = validationParameters.ValidAudiences; + else + validationParametersAudiences = validationParameters.ValidAudiences.Concat(new[] { validationParameters.ValidAudience }); + + if (AudienceIsValid(audiences, validationParameters, validationParametersAudiences)) + return; + + SecurityTokenInvalidAudienceException ex = new SecurityTokenInvalidAudienceException( + LogHelper.FormatInvariant(LogMessages.IDX10214, + LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(audiences)), + LogHelper.MarkAsNonPII(validationParameters.ValidAudience ?? "null"), + LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(validationParameters.ValidAudiences)))) + { InvalidAudience = Utility.SerializeAsSingleCommaDelimitedString(audiences) }; + + if (!validationParameters.LogValidationExceptions) + throw ex; + + throw LogHelper.LogExceptionMessage(ex); + } + + private static bool AudienceIsValid(IEnumerable audiences, TokenValidationParameters validationParameters, IEnumerable validationParametersAudiences) + { + foreach (string tokenAudience in audiences) + { + if (string.IsNullOrWhiteSpace(tokenAudience)) + continue; + + foreach (string validAudience in validationParametersAudiences) + { + if (string.IsNullOrWhiteSpace(validAudience)) + continue; + + if (AudiencesMatch(validationParameters, tokenAudience, validAudience)) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX10234, LogHelper.MarkAsNonPII(tokenAudience)); + + return true; + } + } + } + + return false; + } + + private static bool AudiencesMatch(TokenValidationParameters validationParameters, string tokenAudience, string validAudience) + { + if (validAudience.Length == tokenAudience.Length) + { + if (string.Equals(validAudience, tokenAudience)) + return true; + } + else if (validationParameters.IgnoreTrailingSlashWhenValidatingAudience && AudiencesMatchIgnoringTrailingSlash(tokenAudience, validAudience)) + return true; + + return false; + } + + private static bool AudiencesMatchIgnoringTrailingSlash(string tokenAudience, string validAudience) + { + int length = -1; + + if (validAudience.Length == tokenAudience.Length + 1 && validAudience.EndsWith("/", StringComparison.InvariantCulture)) + length = validAudience.Length - 1; + else if (tokenAudience.Length == validAudience.Length + 1 && tokenAudience.EndsWith("/", StringComparison.InvariantCulture)) + length = tokenAudience.Length - 1; + + // the length of the audiences is different by more than 1 and neither ends in a "/" + if (length == -1) + return false; + + if (string.CompareOrdinal(validAudience, 0, tokenAudience, 0, length) == 0) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX10234, LogHelper.MarkAsNonPII(tokenAudience)); + + return true; + } + + return false; + } + + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs new file mode 100644 index 0000000000..32e6742c30 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs @@ -0,0 +1,293 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Abstractions; +using Microsoft.IdentityModel.Logging; + +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// Definition for delegate that will validate the issuer value in a token. + /// + /// The issuer to validate. + /// The that is being validated. + /// required for validation. + /// + /// + /// A that contains the results of validating the issuer. + /// This delegate is not expected to throw. + internal delegate Task IssuerValidationDelegateAsync( + string issuer, + SecurityToken securityToken, + TokenValidationParameters validationParameters, + CallContext callContext, + CancellationToken cancellationToken); + + /// + /// IssuerValidation + /// + public static partial class Validators + { + /// + /// Determines if an issuer found in a is valid. + /// + /// The issuer to validate + /// The that is being validated. + /// required for validation. + /// The issuer to use when creating the "Claim"(s) in a "ClaimsIdentity". + /// If 'validationParameters' is null. + /// If 'issuer' is null or whitespace and is true. + /// If is null or whitespace and is null. + /// If 'issuer' failed to matched either or one of . + /// An EXACT match is required. + public static string ValidateIssuer(string issuer, SecurityToken securityToken, TokenValidationParameters validationParameters) + { + return ValidateIssuer(issuer, securityToken, validationParameters, null); + } + + /// + /// Determines if an issuer found in a is valid. + /// + /// The issuer to validate + /// The that is being validated. + /// required for validation. + /// The required for issuer and signing key validation. + /// The issuer to use when creating the "Claim"(s) in a "ClaimsIdentity". + /// If 'validationParameters' is null. + /// If 'issuer' is null or whitespace and is true. + /// If ' configuration' is null. + /// If is null or whitespace and is null and is null. + /// If 'issuer' failed to matched either or one of or . + /// An EXACT match is required. + internal static string ValidateIssuer(string issuer, SecurityToken securityToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) + { + ValueTask vt = ValidateIssuerAsync(issuer, securityToken, validationParameters, configuration); + return vt.IsCompletedSuccessfully ? + vt.Result : + vt.AsTask().ConfigureAwait(false).GetAwaiter().GetResult(); + } + + /// + /// Determines if an issuer found in a is valid. + /// + /// The issuer to validate + /// The that is being validated. + /// required for validation. + /// The required for issuer and signing key validation. + /// The issuer to use when creating the "Claim"(s) in a "ClaimsIdentity". + /// If 'validationParameters' is null. + /// If 'issuer' is null or whitespace and is true. + /// If ' configuration' is null. + /// If is null or whitespace and is null and is null. + /// If 'issuer' failed to matched either or one of or . + /// An EXACT match is required. + internal static async ValueTask ValidateIssuerAsync( + string issuer, + SecurityToken securityToken, + TokenValidationParameters validationParameters, + BaseConfiguration configuration) + { + if (validationParameters == null) + throw LogHelper.LogArgumentNullException(nameof(validationParameters)); + + if (validationParameters.IssuerValidatorAsync != null) + return await validationParameters.IssuerValidatorAsync(issuer, securityToken, validationParameters).ConfigureAwait(false); + + if (validationParameters.IssuerValidatorUsingConfiguration != null) + return validationParameters.IssuerValidatorUsingConfiguration(issuer, securityToken, validationParameters, configuration); + + if (validationParameters.IssuerValidator != null) + return validationParameters.IssuerValidator(issuer, securityToken, validationParameters); + + if (!validationParameters.ValidateIssuer) + { + LogHelper.LogWarning(LogMessages.IDX10235); + return issuer; + } + + if (string.IsNullOrWhiteSpace(issuer)) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidIssuerException(LogMessages.IDX10211) + { InvalidIssuer = issuer }); + + // Throw if all possible places to validate against are null or empty + if (string.IsNullOrWhiteSpace(validationParameters.ValidIssuer) + && validationParameters.ValidIssuers.IsNullOrEmpty() + && string.IsNullOrWhiteSpace(configuration?.Issuer)) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidIssuerException(LogMessages.IDX10204) + { InvalidIssuer = issuer }); + + if (configuration != null) + { + if (string.Equals(configuration.Issuer, issuer)) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX10236, LogHelper.MarkAsNonPII(issuer)); + + return issuer; + } + } + + if (string.Equals(validationParameters.ValidIssuer, issuer)) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX10236, LogHelper.MarkAsNonPII(issuer)); + + return issuer; + } + + if (validationParameters.ValidIssuers != null) + { + foreach (string str in validationParameters.ValidIssuers) + { + if (string.IsNullOrEmpty(str)) + { + LogHelper.LogInformation(LogMessages.IDX10262); + continue; + } + + if (string.Equals(str, issuer)) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX10236, LogHelper.MarkAsNonPII(issuer)); + + return issuer; + } + } + } + + SecurityTokenInvalidIssuerException ex = new SecurityTokenInvalidIssuerException( + LogHelper.FormatInvariant(LogMessages.IDX10205, + LogHelper.MarkAsNonPII(issuer), + LogHelper.MarkAsNonPII(validationParameters.ValidIssuer ?? "null"), + LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(validationParameters.ValidIssuers)), + LogHelper.MarkAsNonPII(configuration?.Issuer))) + { InvalidIssuer = issuer }; + + if (!validationParameters.LogValidationExceptions) + throw ex; + + throw LogHelper.LogExceptionMessage(ex); + } + + /// + /// Determines if an issuer found in a is valid. + /// + /// The issuer to validate + /// The that is being validated. + /// required for validation. + /// + /// + /// The issuer to use when creating the "Claim"(s) in a "ClaimsIdentity". + /// If 'validationParameters' is null. + /// If 'issuer' is null or whitespace and is true. + /// If is null or whitespace and is null. + /// If 'issuer' failed to matched either or one of . + /// An EXACT match is required. + internal static async Task ValidateIssuerAsync( + string issuer, + SecurityToken securityToken, + TokenValidationParameters validationParameters, + CallContext callContext, + CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(issuer)) + { + return new IssuerValidationResult( + issuer, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10211.AsMemory(), + null), + typeof(SecurityTokenInvalidIssuerException), + new StackFrame(true), + null)); + } + + if (validationParameters == null) + throw LogHelper.LogArgumentNullException(nameof(validationParameters)); + + if (securityToken == null) + throw LogHelper.LogArgumentNullException(nameof(securityToken)); + + BaseConfiguration configuration = null; + if (validationParameters.ConfigurationManager != null) + configuration = await validationParameters.ConfigurationManager.GetBaseConfigurationAsync(cancellationToken).ConfigureAwait(false); + + // Throw if all possible places to validate against are null or empty + if (string.IsNullOrWhiteSpace(validationParameters.ValidIssuer) + && validationParameters.ValidIssuers.IsNullOrEmpty() + && string.IsNullOrWhiteSpace(configuration?.Issuer)) + { + return new IssuerValidationResult( + issuer, + ValidationFailureType.IssuerValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10211.AsMemory(), + null), + typeof(SecurityTokenInvalidIssuerException), + new StackFrame(true))); + } + + // TODO - we should distinguish if configuration, TVP.ValidIssuer or TVP.ValidIssuers was used to validate the issuer. + if (configuration != null) + { + if (string.Equals(configuration.Issuer, issuer)) + { + // TODO - how and when to log + // Logs will have to be passed back to Wilson + // so that they can be written to the correct place and in the correct format respecting PII. + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX10236, LogHelper.MarkAsNonPII(issuer), callContext); + + return new IssuerValidationResult(issuer); + } + } + + if (string.Equals(validationParameters.ValidIssuer, issuer)) + { + return new IssuerValidationResult(issuer); + } + + if (validationParameters.ValidIssuers != null) + { + foreach (string str in validationParameters.ValidIssuers) + { + if (string.IsNullOrEmpty(str)) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX10262); + + continue; + } + + if (string.Equals(str, issuer)) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX10236, LogHelper.MarkAsNonPII(issuer)); + + return new IssuerValidationResult(issuer); + } + } + } + + return new IssuerValidationResult( + issuer, + ValidationFailureType.IssuerValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10205.AsMemory(), + LogHelper.MarkAsNonPII(issuer), + LogHelper.MarkAsNonPII(validationParameters.ValidIssuer ?? "null"), + LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(validationParameters.ValidIssuers)), + LogHelper.MarkAsNonPII(configuration?.Issuer)), + typeof(SecurityTokenInvalidIssuerException), + new StackFrame(true))); + } + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Lifetime.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Lifetime.cs new file mode 100644 index 0000000000..77c553c4db --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Lifetime.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.IdentityModel.Logging; + +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// IssuerValidation + /// + public static partial class Validators + { + /// + /// Validates the lifetime of a . + /// + /// The 'notBefore' time found in the . + /// The 'expiration' time found in the . + /// The being validated. + /// required for validation. + /// If 'validationParameters' is null. + /// If 'expires.HasValue' is false and is true. + /// If 'notBefore' is > 'expires'. + /// If 'notBefore' is > DateTime.UtcNow. + /// If 'expires' is < DateTime.UtcNow. + /// All time comparisons apply . + public static void ValidateLifetime(DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters) + { + if (validationParameters == null) + throw LogHelper.LogArgumentNullException(nameof(validationParameters)); + + if (validationParameters.LifetimeValidator != null) + { + if (!validationParameters.LifetimeValidator(notBefore, expires, securityToken, validationParameters)) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidLifetimeException(LogHelper.FormatInvariant(LogMessages.IDX10230, securityToken)) + { NotBefore = notBefore, Expires = expires }); + + return; + } + + if (!validationParameters.ValidateLifetime) + { + LogHelper.LogInformation(LogMessages.IDX10238); + return; + } + + ValidatorUtilities.ValidateLifetime(notBefore, expires, securityToken, validationParameters); + } + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/ValidatorUtilities.cs b/src/Microsoft.IdentityModel.Tokens/ValidatorUtilities.cs index 482f2f3d37..6c652f6a3b 100644 --- a/src/Microsoft.IdentityModel.Tokens/ValidatorUtilities.cs +++ b/src/Microsoft.IdentityModel.Tokens/ValidatorUtilities.cs @@ -3,6 +3,7 @@ using System; using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.Abstractions; namespace Microsoft.IdentityModel.Tokens { @@ -49,7 +50,8 @@ internal static void ValidateLifetime(DateTime? notBefore, DateTime? expires, Se }); // if it reaches here, that means lifetime of the token is valid - LogHelper.LogInformation(LogMessages.IDX10239); + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX10239); } } } diff --git a/src/Microsoft.IdentityModel.Tokens/Validators.cs b/src/Microsoft.IdentityModel.Tokens/Validators.cs index 2c9f9e6ca1..000ba4c617 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validators.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validators.cs @@ -2,10 +2,8 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; using System.Linq; using System.Security.Cryptography.X509Certificates; -using System.Threading.Tasks; using Microsoft.IdentityModel.Abstractions; using Microsoft.IdentityModel.Logging; @@ -14,7 +12,7 @@ namespace Microsoft.IdentityModel.Tokens /// /// AudienceValidator /// - public static class Validators + public static partial class Validators { /// /// Validates if a given algorithm for a is valid. @@ -50,283 +48,6 @@ public static void ValidateAlgorithm(string algorithm, SecurityKey securityKey, } } - /// - /// Determines if the audiences found in a are valid. - /// - /// The audiences found in the . - /// The being validated. - /// required for validation. - /// If 'validationParameters' is null. - /// If 'audiences' is null and is true. - /// If is null or whitespace and is null. - /// If none of the 'audiences' matched either or one of . - /// An EXACT match is required. - public static void ValidateAudience(IEnumerable audiences, SecurityToken securityToken, TokenValidationParameters validationParameters) - { - if (validationParameters == null) - throw LogHelper.LogArgumentNullException(nameof(validationParameters)); - - if (validationParameters.AudienceValidator != null) - { - if (!validationParameters.AudienceValidator(audiences, securityToken, validationParameters)) - throw LogHelper.LogExceptionMessage( - new SecurityTokenInvalidAudienceException( - LogHelper.FormatInvariant( - LogMessages.IDX10231, - LogHelper.MarkAsUnsafeSecurityArtifact(securityToken, t => t.ToString()))) - { - InvalidAudience = Utility.SerializeAsSingleCommaDelimitedString(audiences) - }); - - return; - } - - if (!validationParameters.ValidateAudience) - { - LogHelper.LogWarning(LogMessages.IDX10233); - return; - } - - if (audiences == null) - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidAudienceException(LogMessages.IDX10207) { InvalidAudience = null }); - - if (string.IsNullOrWhiteSpace(validationParameters.ValidAudience) && (validationParameters.ValidAudiences == null)) - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidAudienceException(LogMessages.IDX10208) { InvalidAudience = Utility.SerializeAsSingleCommaDelimitedString(audiences) }); - - if (!audiences.Any()) - throw LogHelper.LogExceptionMessage( - new SecurityTokenInvalidAudienceException(LogHelper.FormatInvariant(LogMessages.IDX10206)) - { InvalidAudience = Utility.SerializeAsSingleCommaDelimitedString(audiences) }); - - // create enumeration of all valid audiences from validationParameters - IEnumerable validationParametersAudiences; - - if (validationParameters.ValidAudiences == null) - validationParametersAudiences = new[] { validationParameters.ValidAudience }; - else if (string.IsNullOrWhiteSpace(validationParameters.ValidAudience)) - validationParametersAudiences = validationParameters.ValidAudiences; - else - validationParametersAudiences = validationParameters.ValidAudiences.Concat(new[] { validationParameters.ValidAudience }); - - if (AudienceIsValid(audiences, validationParameters, validationParametersAudiences)) - return; - - SecurityTokenInvalidAudienceException ex = new SecurityTokenInvalidAudienceException( - LogHelper.FormatInvariant(LogMessages.IDX10214, - LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(audiences)), - LogHelper.MarkAsNonPII(validationParameters.ValidAudience ?? "null"), - LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(validationParameters.ValidAudiences)))) - { InvalidAudience = Utility.SerializeAsSingleCommaDelimitedString(audiences) }; - - if (!validationParameters.LogValidationExceptions) - throw ex; - - throw LogHelper.LogExceptionMessage(ex); - } - - private static bool AudienceIsValid(IEnumerable audiences, TokenValidationParameters validationParameters, IEnumerable validationParametersAudiences) - { - foreach (string tokenAudience in audiences) - { - if (string.IsNullOrWhiteSpace(tokenAudience)) - continue; - - foreach (string validAudience in validationParametersAudiences) - { - if (string.IsNullOrWhiteSpace(validAudience)) - continue; - - if (AudiencesMatch(validationParameters, tokenAudience, validAudience)) - { - if (LogHelper.IsEnabled(EventLogLevel.Informational)) - LogHelper.LogInformation(LogMessages.IDX10234, LogHelper.MarkAsNonPII(tokenAudience)); - - return true; - } - } - } - - return false; - } - - private static bool AudiencesMatch(TokenValidationParameters validationParameters, string tokenAudience, string validAudience) - { - if (validAudience.Length == tokenAudience.Length) - { - if (string.Equals(validAudience, tokenAudience)) - return true; - } - else if (validationParameters.IgnoreTrailingSlashWhenValidatingAudience && AudiencesMatchIgnoringTrailingSlash(tokenAudience, validAudience)) - return true; - - return false; - } - - private static bool AudiencesMatchIgnoringTrailingSlash(string tokenAudience, string validAudience) - { - int length = -1; - - if (validAudience.Length == tokenAudience.Length + 1 && validAudience.EndsWith("/", StringComparison.InvariantCulture)) - length = validAudience.Length - 1; - else if (tokenAudience.Length == validAudience.Length + 1 && tokenAudience.EndsWith("/", StringComparison.InvariantCulture)) - length = tokenAudience.Length - 1; - - // the length of the audiences is different by more than 1 and neither ends in a "/" - if (length == -1) - return false; - - if (string.CompareOrdinal(validAudience, 0, tokenAudience, 0, length) == 0) - { - if (LogHelper.IsEnabled(EventLogLevel.Informational)) - LogHelper.LogInformation(LogMessages.IDX10234, LogHelper.MarkAsNonPII(tokenAudience)); - - return true; - } - - return false; - } - - /// - /// Determines if an issuer found in a is valid. - /// - /// The issuer to validate - /// The that is being validated. - /// required for validation. - /// The issuer to use when creating the "Claim"(s) in a "ClaimsIdentity". - /// If 'validationParameters' is null. - /// If 'issuer' is null or whitespace and is true. - /// If is null or whitespace and is null. - /// If 'issuer' failed to matched either or one of . - /// An EXACT match is required. - public static string ValidateIssuer(string issuer, SecurityToken securityToken, TokenValidationParameters validationParameters) - { - return ValidateIssuer(issuer, securityToken, validationParameters, null); - } - - /// - /// Determines if an issuer found in a is valid. - /// - /// The issuer to validate - /// The that is being validated. - /// required for validation. - /// The required for issuer and signing key validation. - /// The issuer to use when creating the "Claim"(s) in a "ClaimsIdentity". - /// If 'validationParameters' is null. - /// If 'issuer' is null or whitespace and is true. - /// If ' configuration' is null. - /// If is null or whitespace and is null and is null. - /// If 'issuer' failed to matched either or one of or . - /// An EXACT match is required. - internal static string ValidateIssuer(string issuer, SecurityToken securityToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) - { - ValueTask vt = ValidateIssuerAsync(issuer, securityToken, validationParameters, configuration); - return vt.IsCompletedSuccessfully ? - vt.Result : - vt.AsTask().ConfigureAwait(false).GetAwaiter().GetResult(); - } - - /// - /// Determines if an issuer found in a is valid. - /// - /// The issuer to validate - /// The that is being validated. - /// required for validation. - /// The required for issuer and signing key validation. - /// The issuer to use when creating the "Claim"(s) in a "ClaimsIdentity". - /// If 'validationParameters' is null. - /// If 'issuer' is null or whitespace and is true. - /// If ' configuration' is null. - /// If is null or whitespace and is null and is null. - /// If 'issuer' failed to matched either or one of or . - /// An EXACT match is required. - internal static async ValueTask ValidateIssuerAsync( - string issuer, - SecurityToken securityToken, - TokenValidationParameters validationParameters, - BaseConfiguration configuration) - { - if (validationParameters == null) - throw LogHelper.LogArgumentNullException(nameof(validationParameters)); - - if (validationParameters.IssuerValidatorAsync != null) - return await validationParameters.IssuerValidatorAsync(issuer, securityToken, validationParameters).ConfigureAwait(false); - - if (validationParameters.IssuerValidatorUsingConfiguration != null) - return validationParameters.IssuerValidatorUsingConfiguration(issuer, securityToken, validationParameters, configuration); - - if (validationParameters.IssuerValidator != null) - return validationParameters.IssuerValidator(issuer, securityToken, validationParameters); - - if (!validationParameters.ValidateIssuer) - { - LogHelper.LogWarning(LogMessages.IDX10235); - return issuer; - } - - if (string.IsNullOrWhiteSpace(issuer)) - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidIssuerException(LogMessages.IDX10211) - { InvalidIssuer = issuer }); - - // Throw if all possible places to validate against are null or empty - if ( string.IsNullOrWhiteSpace(validationParameters.ValidIssuer) - && validationParameters.ValidIssuers.IsNullOrEmpty() - && string.IsNullOrWhiteSpace(configuration?.Issuer)) - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidIssuerException(LogMessages.IDX10204) - { InvalidIssuer = issuer }); - - if (configuration != null) - { - if (string.Equals(configuration.Issuer, issuer)) - { - if (LogHelper.IsEnabled(EventLogLevel.Informational)) - LogHelper.LogInformation(LogMessages.IDX10236, LogHelper.MarkAsNonPII(issuer)); - - return issuer; - } - } - - if (string.Equals(validationParameters.ValidIssuer, issuer)) - { - if (LogHelper.IsEnabled(EventLogLevel.Informational)) - LogHelper.LogInformation(LogMessages.IDX10236, LogHelper.MarkAsNonPII(issuer)); - - return issuer; - } - - if (validationParameters.ValidIssuers != null) - { - foreach (string str in validationParameters.ValidIssuers) - { - if (string.IsNullOrEmpty(str)) - { - LogHelper.LogInformation(LogMessages.IDX10262); - continue; - } - - if (string.Equals(str, issuer)) - { - if (LogHelper.IsEnabled(EventLogLevel.Informational)) - LogHelper.LogInformation(LogMessages.IDX10236, LogHelper.MarkAsNonPII(issuer)); - - return issuer; - } - } - } - - SecurityTokenInvalidIssuerException ex = new SecurityTokenInvalidIssuerException( - LogHelper.FormatInvariant(LogMessages.IDX10205, - LogHelper.MarkAsNonPII(issuer), - LogHelper.MarkAsNonPII(validationParameters.ValidIssuer ?? "null"), - LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(validationParameters.ValidIssuers)), - LogHelper.MarkAsNonPII(configuration?.Issuer))) - { InvalidIssuer = issuer }; - - if (!validationParameters.LogValidationExceptions) - throw ex; - - throw LogHelper.LogExceptionMessage(ex); - } - /// /// Validates the that signed a . /// @@ -422,42 +143,6 @@ internal static void ValidateIssuerSigningKeyLifeTime(SecurityKey securityKey, T } } - /// - /// Validates the lifetime of a . - /// - /// The 'notBefore' time found in the . - /// The 'expiration' time found in the . - /// The being validated. - /// required for validation. - /// If 'validationParameters' is null. - /// If 'expires.HasValue' is false and is true. - /// If 'notBefore' is > 'expires'. - /// If 'notBefore' is > DateTime.UtcNow. - /// If 'expires' is < DateTime.UtcNow. - /// All time comparisons apply . - public static void ValidateLifetime(DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters) - { - if (validationParameters == null) - throw LogHelper.LogArgumentNullException(nameof(validationParameters)); - - if (validationParameters.LifetimeValidator != null) - { - if (!validationParameters.LifetimeValidator(notBefore, expires, securityToken, validationParameters)) - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidLifetimeException(LogHelper.FormatInvariant(LogMessages.IDX10230, securityToken)) - { NotBefore = notBefore, Expires = expires }); - - return; - } - - if (!validationParameters.ValidateLifetime) - { - LogHelper.LogInformation(LogMessages.IDX10238); - return; - } - - ValidatorUtilities.ValidateLifetime(notBefore, expires, securityToken, validationParameters); - } - /// /// Validates if a token has been replayed. /// diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs index f7b1d7b843..bd041cac2f 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs @@ -3523,7 +3523,7 @@ public static TheoryData ValidateJwsWithConfigTheoryData incorrectSigningKeysConfig.SigningKeys.Add(KeyingMaterial.X509SecurityKey2); theoryData.Add(new JwtTheoryData { - TestId = nameof(Default.AsymmetricJws) + "_" + "TVPInvalid" + "_" + "ConfigSigningKeysInvalid" + "_SignatureValidatorReturnsValidToken", + TestId = nameof(Default.AsymmetricJws) + "_TVPInvalid_ConfigSigningKeysInvalid_SignatureValidatorReturnsValidToken", Token = Default.AsymmetricJws, ValidationParameters = new TokenValidationParameters { diff --git a/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs b/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs index 753b97efad..872a9e8cea 100644 --- a/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs +++ b/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs @@ -19,6 +19,7 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text.Json; +using System.Xml.Linq; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; @@ -60,6 +61,7 @@ public class IdentityComparer { typeof(IEnumerable).ToString(), AreX509DataEnumsEqual }, { typeof(int).ToString(), AreIntsEqual }, { typeof(IssuerSerial).ToString(), CompareAllPublicProperties }, + { typeof(IssuerValidationResult).ToString(), AreIssuerValidationResultsEqual }, { typeof(JArray).ToString(), AreJArraysEqual }, { typeof(JObject).ToString(), AreJObjectsEqual }, { typeof(JsonElement).ToString(), AreJsonElementsEqual }, @@ -542,6 +544,66 @@ public static bool AreEqual(object object1, object object2, CompareContext conte return context.Merge(localContext); } + public static bool AreIssuerValidationResultsEqual(object object1, object object2, CompareContext context) + { + var localContext = new CompareContext(context); + if (!ContinueCheckingEquality(object1, object2, context)) + return context.Merge(localContext); + + return AreIssuerValidationResultsEqual( + object1 as IssuerValidationResult, + object2 as IssuerValidationResult, + "IssuerValidationResult1", + "IssuerValidationResult2", + null, + context); + } + + internal static bool AreIssuerValidationResultsEqual( + IssuerValidationResult issuerValidationResult1, + IssuerValidationResult issuerValidationResult2, + string name1, + string name2, + string stackPrefix, + CompareContext context) + { + var localContext = new CompareContext(context); + if (!ContinueCheckingEquality(issuerValidationResult1, issuerValidationResult2, localContext)) + return context.Merge(localContext); + + if (issuerValidationResult1.Issuer != issuerValidationResult2.Issuer) + localContext.Diffs.Add($"IssuerValidationResult1.Issuer: {issuerValidationResult1.Issuer} != IssuerValidationResult2.Issuer: {issuerValidationResult2.Issuer}"); + + // true => both are not null. + if (ContinueCheckingEquality(issuerValidationResult1.Exception, issuerValidationResult2.Exception, localContext)) + { + AreStringsEqual( + issuerValidationResult1.Exception.Message, + issuerValidationResult2.Exception.Message, + $"({name1})issuerValidationResult1.Exception.Message", + $"({name2})issuerValidationResult1.Exception.Message", + localContext); + + AreStringsEqual( + issuerValidationResult1.Exception.Source, + issuerValidationResult2.Exception.Source, + $"({name1})issuerValidationResult1.Exception.Source", + $"({name2})issuerValidationResult2.Exception.Source", + localContext); + + if (!string.IsNullOrEmpty(stackPrefix)) + AreStringPrefixesEqual( + issuerValidationResult1.Exception.StackTrace.Trim(), + issuerValidationResult2.Exception.StackTrace.Trim(), + $"({name1})issuerValidationResult1.Exception.StackTrace", + $"({name2})issuerValidationResult2.Exception.StackTrace", + stackPrefix.Trim(), + localContext); + } + + return context.Merge(localContext); + } + public static bool AreJArraysEqual(object object1, object object2, CompareContext context) { var localContext = new CompareContext(context); @@ -1089,15 +1151,43 @@ public static bool AreStringsEqual(object object1, object object2, string name1, if (!string.Equals(str1, str2, context.StringComparison)) { - localContext.Diffs.Add($"{name1} != {name2}, StringComparison: '{context.StringComparison}'"); - localContext.Diffs.Add(str1); + localContext.Diffs.Add($"'{name1}' != '{name2}', StringComparison: '{context.StringComparison}'"); + localContext.Diffs.Add($"'{str1}'"); localContext.Diffs.Add($"!="); - localContext.Diffs.Add(str2); + localContext.Diffs.Add($"'{str2}'"); + } + + return context.Merge(localContext); + } + + public static bool AreStringPrefixesEqual( + string string1, + string string2, + string name1, + string name2, + string prefix, + CompareContext context) + { + var localContext = new CompareContext(context); + if (!ContinueCheckingEquality(string1, string2, localContext)) + return context.Merge(localContext); + + if (!string1.StartsWith(prefix, context.StringComparison)) + { + localContext.Diffs.Add($"'{name1}': does not start with prefix: '{prefix}', StringComparison: '{context.StringComparison}'"); + localContext.Diffs.Add($"'{string1}'"); + } + + if (!string2.StartsWith(prefix, context.StringComparison)) + { + localContext.Diffs.Add($"'{name2}': does not start with prefix: '{prefix}', StringComparison: '{context.StringComparison}'"); + localContext.Diffs.Add($"'{string2}'"); } return context.Merge(localContext); } + public static bool AreStringEnumDictionariesEqual(IDictionary> dictionary1, IDictionary> dictionary2, CompareContext context) { var localContext = new CompareContext(context); @@ -1316,6 +1406,26 @@ public static bool CompareAllPublicProperties(object obj1, object obj2, CompareC return context.Merge($"CompareAllPublicProperties: {type}", localContext); } + public static bool IsOnlyOneObjectNull(object object1, object object2, CompareContext context) + { + if (object1 == null && object2 == null) + return false; + + if (object1 == null) + { + context.Diffs.Add(BuildStringDiff(object2.GetType().ToString(), object1, object2)); + return true; + } + + if (object2 == null) + { + context.Diffs.Add(BuildStringDiff(object1.GetType().ToString(), object1, object2)); + return true; + } + + return false; + } + public static bool ContinueCheckingEquality(object obj1, object obj2, CompareContext context) { if (obj1 == null && obj2 == null) diff --git a/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs b/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs index f0663cc997..1170d40822 100644 --- a/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs +++ b/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs @@ -87,7 +87,7 @@ public static string IssuerValidatorUsingConfigEcho(string issuer, SecurityToken return issuer; } - public static ValueTask IssuerValidatorAsync(string issuer, SecurityToken token, TokenValidationParameters validationParameters) + public static ValueTask IssuerValidatorInternalAsync(string issuer, SecurityToken token, TokenValidationParameters validationParameters) { return new ValueTask(issuer); } diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/IdentityComparerTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/IdentityComparerTests.cs index e78a0391b6..f293582ed3 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/IdentityComparerTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/IdentityComparerTests.cs @@ -575,9 +575,9 @@ public void CompareStrings() var string2 = "goodbye"; IdentityComparer.AreEqual(string1, string2, context); - Assert.True(context.Diffs.Count(s => s == "str1 != str2, StringComparison: 'Ordinal'") == 1); - Assert.True(context.Diffs[1] == string1); - Assert.True(context.Diffs[3] == string2); + Assert.True(context.Diffs.Count(s => s == "'str1' != 'str2', StringComparison: 'Ordinal'") == 1); + Assert.True(context.Diffs[1] == $"'{string1}'"); + Assert.True(context.Diffs[3] == $"'{string2}'"); } [Fact] diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonUtilities.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonUtilities.cs index dfbc058216..8dd4f023f1 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonUtilities.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonUtilities.cs @@ -7,6 +7,7 @@ using System.Text; using System.Text.Json; using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.JsonWebTokens; using Newtonsoft.Json.Linq; namespace Microsoft.IdentityModel.Tokens.Json.Tests @@ -169,6 +170,11 @@ public static void SetAdditionalDataValues(IDictionary dictionar dictionary["true"] = true; } + public static JsonWebToken CreateUnsignedJsonWebToken(string key, object value) + { + return new JsonWebToken(CreateUnsignedToken(key, value)); + } + public static string CreateUnsignedToken(string key, object value) { return EmptyHeader + "." + CreateEncodedJson(key, value) + "."; diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs index 041de6d40d..a44e4c7152 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs @@ -15,7 +15,7 @@ namespace Microsoft.IdentityModel.Tokens.Tests { public class TokenValidationParametersTests { - int ExpectedPropertyCount = 59; + int ExpectedPropertyCount = 60; [Fact] public void Publics() @@ -71,6 +71,7 @@ public void Publics() IssuerSigningKey = issuerSigningKey, IssuerSigningKeyResolver = (token, securityToken, keyIdentifier, tvp) => { return new List { issuerSigningKey }; }, IssuerSigningKeys = issuerSigningKeys, + IssuerValidationDelegateAsync = Validators.ValidateIssuerAsync, IssuerValidator = ValidationDelegates.IssuerValidatorEcho, LifetimeValidator = ValidationDelegates.LifetimeValidatorReturnsTrue, LogTokenId = true, @@ -290,8 +291,9 @@ private TokenValidationParameters CreateTokenValidationParameters() validationParameters.IssuerSigningKeyResolverUsingConfiguration = ValidationDelegates.IssuerSigningKeyResolverUsingConfiguration; validationParameters.IssuerSigningKeyValidator = ValidationDelegates.IssuerSigningKeyValidator; validationParameters.IssuerSigningKeyValidatorUsingConfiguration = ValidationDelegates.IssuerSigningKeyValidatorUsingConfiguration; + validationParameters.IssuerValidationDelegateAsync = Validators.ValidateIssuerAsync; validationParameters.IssuerValidator = ValidationDelegates.IssuerValidatorEcho; - validationParameters.IssuerValidatorAsync = ValidationDelegates.IssuerValidatorAsync; + validationParameters.IssuerValidatorAsync = ValidationDelegates.IssuerValidatorInternalAsync; validationParameters.IssuerValidatorUsingConfiguration = ValidationDelegates.IssuerValidatorUsingConfigEcho; validationParameters.LifetimeValidator = ValidationDelegates.LifetimeValidatorReturnsTrue; validationParameters.NameClaimTypeRetriever = ValidationDelegates.NameClaimTypeRetriever; diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/AsyncValidatorsTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/AsyncValidatorsTests.cs new file mode 100644 index 0000000000..f5bacfc0b7 --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/AsyncValidatorsTests.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.IdentityModel.TestUtils; +using Xunit; + +namespace Microsoft.IdentityModel.Tokens.Validation.Tests +{ + public class AsyncValidatorTests + { + [Theory, MemberData(nameof(AsyncIssuerValidatorTestCases))] + public async Task AsyncIssuerValidatorTests(IssuerValidatorTheoryData theoryData) + { + CompareContext context = TestUtilities.WriteHeader($"{this}.AsyncIssuerValidatorTests", theoryData); + try + { + IssuerValidationResult result = await Validators.ValidateIssuerAsync( + theoryData.Issuer, + theoryData.SecurityToken, + theoryData.ValidationParameters, + null, + CancellationToken.None).ConfigureAwait(false); + Exception exception = result.Exception; + context.Diffs.Add("Exception: " + exception.ToString()); + } + catch (Exception ex) + { + context.Diffs.Add("Exception: " + ex.ToString()); + } + } + + public static TheoryData AsyncIssuerValidatorTestCases + { + get + { + TheoryData theoryData = new TheoryData(); + + theoryData.Add(new IssuerValidatorTheoryData + { + Issuer = null, + ValidationParameters = new TokenValidationParameters(), + }); + + return theoryData; + } + } + } + + public class IssuerValidatorTheoryData : TheoryDataBase + { + public string Issuer { get; set; } + public TokenValidationParameters ValidationParameters { get; set; } + public SecurityToken SecurityToken { get; set; } + } +} diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs new file mode 100644 index 0000000000..3edb202206 --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.TestUtils; +using Microsoft.IdentityModel.Tokens.Json.Tests; +using Xunit; + +namespace Microsoft.IdentityModel.Tokens.Validation.Tests +{ + public class IssuerValidationResultTests + { + [Theory, MemberData(nameof(IssuerValdationResultsTestCases), DisableDiscoveryEnumeration = true)] + public async Task IssuerValidatorAsyncTests(IssuerValidationResultsTheoryData theoryData) + { + CompareContext context = TestUtilities.WriteHeader($"{this}.IssuerValidatorAsyncTests", theoryData); + + try + { + IssuerValidationResult issuerValidationResult = await Validators.ValidateIssuerAsync( + theoryData.Issuer, + theoryData.SecurityToken, + theoryData.ValidationParameters, + new CallContext(), + CancellationToken.None).ConfigureAwait(false); + + theoryData.ExpectedException.ProcessException(issuerValidationResult.Exception, context); + IdentityComparer.AreIssuerValidationResultsEqual( + issuerValidationResult, + theoryData.IssuerValidationResult, + context); + } + catch (SecurityTokenInvalidIssuerException ex) + { + theoryData.ExpectedException.ProcessException(ex, context); + } + + TestUtilities.AssertFailIfErrors(context); + } + + public static TheoryData IssuerValdationResultsTestCases + { + get + { + TheoryData theoryData = new(); + + string validIssuer = Guid.NewGuid().ToString(); + string issClaim = Guid.NewGuid().ToString(); + theoryData.Add(new IssuerValidationResultsTheoryData("Invalid_Issuer") + { + ExpectedException = ExpectedException.SecurityTokenInvalidIssuerException("IDX10205:"), + Issuer = issClaim, + IssuerValidationResult = new IssuerValidationResult( + issClaim, + ValidationFailureType.IssuerValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10205.AsMemory(), + LogHelper.MarkAsNonPII(issClaim), + LogHelper.MarkAsNonPII(validIssuer), + LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(null)), + LogHelper.MarkAsNonPII(null)), + typeof(SecurityTokenInvalidIssuerException), + new StackFrame(true))), + IsValid = false, + SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Iss, issClaim), + ValidationParameters = new TokenValidationParameters { ValidIssuer = validIssuer } + }); + + theoryData.Add(new IssuerValidationResultsTheoryData("NULL_Issuer") + { + ExpectedException = ExpectedException.SecurityTokenInvalidIssuerException("IDX10211:"), + IssuerValidationResult = new IssuerValidationResult( + null, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10211.AsMemory(), + LogHelper.MarkAsNonPII(null), + LogHelper.MarkAsNonPII(validIssuer), + LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(null)), + LogHelper.MarkAsNonPII(null)), + typeof(SecurityTokenInvalidIssuerException), + new StackFrame(true))), + IsValid = false, + SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Iss, issClaim), + ValidationParameters = new TokenValidationParameters(), + }); + + return theoryData; + } + } + } + + public class IssuerValidationResultsTheoryData : TheoryDataBase + { + public IssuerValidationResultsTheoryData(string testId) : base(testId) + { + } + + public BaseConfiguration Configuration { get; set; } + + public string Issuer { get; set; } + + internal IssuerValidationResult IssuerValidationResult { get; set; } + + public bool IsValid { get; set; } + + public SecurityToken SecurityToken { get; set; } + + public TokenValidationParameters ValidationParameters { get; set; } + + internal ValidationFailureType ValidationFailureType { get; set; } + } +} diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/ValidatorsTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/ValidatorsTests.cs similarity index 99% rename from test/Microsoft.IdentityModel.Tokens.Tests/ValidatorsTests.cs rename to test/Microsoft.IdentityModel.Tokens.Tests/Validation/ValidatorsTests.cs index d7af19095e..181c05e926 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/ValidatorsTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/ValidatorsTests.cs @@ -8,8 +8,6 @@ using Microsoft.IdentityModel.TestUtils; using Xunit; -#pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant - namespace Microsoft.IdentityModel.Tokens.Tests { public class ValidatorsTests @@ -510,5 +508,3 @@ public class AudienceValidationTheoryData : TheoryDataBase } } } - -#pragma warning restore CS3016 // Arrays as attribute arguments is not CLS-compliant From 78236a00a303b54e1a576d7799865561b2d2997c Mon Sep 17 00:00:00 2001 From: id4s Date: Wed, 12 Jun 2024 21:14:21 -0700 Subject: [PATCH 03/22] Addressed PR comments. --- .../JsonWebTokenHandler.ValidateToken.cs | 2 +- .../Validation/MessageDetail.cs | 6 +++--- .../Validation/Validators.Issuer.cs | 6 +++--- .../Validation/IssuerValidationResultTests.cs | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs index 524559e198..3242311e9a 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs @@ -584,7 +584,7 @@ public override async Task ValidateTokenAsync(SecurityTok } /// - /// Private method for token validation, responsible for: + /// Internal method for token validation, responsible for: /// (1) Obtaining a configuration from the . /// (2) Revalidating using the Last Known Good Configuration (if present), and obtaining a refreshed configuration (if necessary) and revalidating using it. /// diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/MessageDetail.cs b/src/Microsoft.IdentityModel.Tokens/Validation/MessageDetail.cs index 1579ab14da..800126a725 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/MessageDetail.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/MessageDetail.cs @@ -20,7 +20,7 @@ internal class MessageDetail /// /// The message to be formated. /// The parameters for formatting. - public MessageDetail(ReadOnlyMemory formatString, params object[] parameters) + public MessageDetail(string formatString, params object[] parameters) { // TODO - paramter validation. FormatString = formatString; @@ -34,12 +34,12 @@ public string Message { get { - _message ??= LogHelper.FormatInvariant(FormatString.ToString(), Parameters); + _message ??= LogHelper.FormatInvariant(FormatString, Parameters); return _message; } } - private ReadOnlyMemory FormatString { get; } + private string FormatString { get; } private object[] Parameters { get; } } diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs index 32e6742c30..faca7d71e5 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs @@ -201,7 +201,7 @@ internal static async Task ValidateIssuerAsync( ValidationFailureType.NullArgument, new ExceptionDetail( new MessageDetail( - LogMessages.IDX10211.AsMemory(), + LogMessages.IDX10211, null), typeof(SecurityTokenInvalidIssuerException), new StackFrame(true), @@ -228,7 +228,7 @@ internal static async Task ValidateIssuerAsync( ValidationFailureType.IssuerValidationFailed, new ExceptionDetail( new MessageDetail( - LogMessages.IDX10211.AsMemory(), + LogMessages.IDX10211, null), typeof(SecurityTokenInvalidIssuerException), new StackFrame(true))); @@ -281,7 +281,7 @@ internal static async Task ValidateIssuerAsync( ValidationFailureType.IssuerValidationFailed, new ExceptionDetail( new MessageDetail( - LogMessages.IDX10205.AsMemory(), + LogMessages.IDX10205, LogHelper.MarkAsNonPII(issuer), LogHelper.MarkAsNonPII(validationParameters.ValidIssuer ?? "null"), LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(validationParameters.ValidIssuers)), diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs index 3edb202206..3c3f403fa2 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs @@ -60,7 +60,7 @@ public static TheoryData IssuerValdationResul ValidationFailureType.IssuerValidationFailed, new ExceptionDetail( new MessageDetail( - LogMessages.IDX10205.AsMemory(), + LogMessages.IDX10205, LogHelper.MarkAsNonPII(issClaim), LogHelper.MarkAsNonPII(validIssuer), LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(null)), @@ -80,7 +80,7 @@ public static TheoryData IssuerValdationResul ValidationFailureType.NullArgument, new ExceptionDetail( new MessageDetail( - LogMessages.IDX10211.AsMemory(), + LogMessages.IDX10211, LogHelper.MarkAsNonPII(null), LogHelper.MarkAsNonPII(validIssuer), LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(null)), From d0131d5ef15fffb5aa0217d7b13ba3b0e9507cce Mon Sep 17 00:00:00 2001 From: joegoldman2 <147369450+joegoldman@users.noreply.github.com> Date: Sat, 15 Jun 2024 20:14:24 +0000 Subject: [PATCH 04/22] Add additional metadata parameters to OpenIdConnectConfiguration --- .../OpenIdConnectConfiguration.cs | 83 +++++- .../OpenIdConnectConfigurationSerializer.cs | 66 ++++- .../OpenIdProviderMetadataNames.cs | 10 + .../OpenIdConfigData.cs | 252 +++++++++--------- .../OpenIdConnectConfigurationTests.cs | 17 +- .../OpenIdConnectMetadata.json | 21 +- .../OpenIdConnectSerializationTests.cs | 4 +- 7 files changed, 308 insertions(+), 145 deletions(-) diff --git a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Configuration/OpenIdConnectConfiguration.cs b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Configuration/OpenIdConnectConfiguration.cs index 6ddc1b613d..36fb436a61 100644 --- a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Configuration/OpenIdConnectConfiguration.cs +++ b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Configuration/OpenIdConnectConfiguration.cs @@ -28,6 +28,7 @@ public class OpenIdConnectConfiguration : BaseConfiguration private ICollection _claimsSupported; private ICollection _claimsLocalesSupported; private ICollection _claimTypesSupported; + private ICollection _codeChallengeMethodsSupported; private ICollection _displayValuesSupported; private ICollection _dPoPSigningAlgValuesSupported; private ICollection _grantTypesSupported; @@ -42,6 +43,8 @@ public class OpenIdConnectConfiguration : BaseConfiguration private ICollection _requestObjectSigningAlgValuesSupported; private ICollection _responseModesSupported; private ICollection _responseTypesSupported; + private ICollection _revocationEndpointAuthMethodsSupported; + private ICollection _revocationEndpointAuthSigningAlgValuesSupported; private ICollection _scopesSupported; private ICollection _subjectTypesSupported; private ICollection _tokenEndpointAuthMethodsSupported; @@ -233,6 +236,24 @@ public OpenIdConnectConfiguration(string json) Interlocked.CompareExchange(ref _claimTypesSupported, new Collection(), null) ?? _claimTypesSupported; + /// + /// Gets the collection of 'code_challenge_methods_supported' + /// + [JsonPropertyName(OpenIdProviderMetadataNames.CodeChallengeMethodsSupported)] + public ICollection CodeChallengeMethodsSupported => + _codeChallengeMethodsSupported ?? + Interlocked.CompareExchange(ref _codeChallengeMethodsSupported, new Collection(), null) ?? + _codeChallengeMethodsSupported; + + /// + /// Gets or sets the 'device_authorization_endpoint'. + /// + [JsonPropertyName(OpenIdProviderMetadataNames.DeviceAuthorizationEndpoint)] +#if NET6_0_OR_GREATER + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] +#endif + public string DeviceAuthorizationEndpoint { get; set; } + /// /// Gets the collection of 'display_values_supported' /// @@ -374,7 +395,7 @@ public OpenIdConnectConfiguration(string json) /// Gets or sets the /// [JsonIgnore] - public JsonWebKeySet JsonWebKeySet {get; set;} + public JsonWebKeySet JsonWebKeySet { get; set; } /// /// Boolean value specifying whether the OP can pass a sid (session ID) query parameter to identify the RP session at the OP when the logout_uri is used. Dafault Value is false. @@ -508,6 +529,33 @@ public OpenIdConnectConfiguration(string json) Interlocked.CompareExchange(ref _responseTypesSupported, new Collection(), null) ?? _responseTypesSupported; + /// + /// Gets or sets the 'revocation_endpoint' + /// + [JsonPropertyName(OpenIdProviderMetadataNames.RevocationEndpoint)] +#if NET6_0_OR_GREATER + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] +#endif + public string RevocationEndpoint { get; set; } + + /// + /// Gets the collection of 'revocation_endpoint_auth_methods_supported'. + /// + [JsonPropertyName(OpenIdProviderMetadataNames.RevocationEndpointAuthMethodsSupported)] + public ICollection RevocationEndpointAuthMethodsSupported => + _revocationEndpointAuthMethodsSupported ?? + Interlocked.CompareExchange(ref _revocationEndpointAuthMethodsSupported, new Collection(), null) ?? + _revocationEndpointAuthMethodsSupported; + + /// + /// Gets the collection of 'revocation_endpoint_auth_signing_alg_values_supported'. + /// + [JsonPropertyName(OpenIdProviderMetadataNames.RevocationEndpointAuthSigningAlgValuesSupported)] + public ICollection RevocationEndpointAuthSigningAlgValuesSupported => + _revocationEndpointAuthSigningAlgValuesSupported ?? + Interlocked.CompareExchange(ref _revocationEndpointAuthSigningAlgValuesSupported, new Collection(), null) ?? + _revocationEndpointAuthSigningAlgValuesSupported; + /// /// Gets or sets the 'service_documentation' /// @@ -688,6 +736,17 @@ public bool ShouldSerializeClaimTypesSupported() return ClaimTypesSupported.Count > 0; } + /// + /// Gets a bool that determines if the 'code_challenge_methods_supported' (CodeChallengeMethodsSupported) property should be serialized. + /// This is used by Json.NET in order to conditionally serialize properties. + /// + /// true if 'code_challenge_methods_supported' (CodeChallengeMethodsSupported) is not empty; otherwise, false. + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeCodeChallengeMethodsSupported() + { + return CodeChallengeMethodsSupported.Count > 0; + } + /// /// Gets a bool that determines if the 'display_values_supported' (DisplayValuesSupported) property should be serialized. /// This is used by Json.NET in order to conditionally serialize properties. @@ -842,6 +901,28 @@ public bool ShouldSerializeResponseTypesSupported() return ResponseTypesSupported.Count > 0; } + /// + /// Gets a bool that determines if the 'revocation_endpoint_auth_methods_supported' (RevocationEndpointAuthMethodsSupported) property should be serialized. + /// This is used by Json.NET in order to conditionally serialize properties. + /// + /// true if 'revocation_endpoint_auth_methods_supported' (RevocationEndpointAuthMethodsSupported) is not empty; otherwise, false. + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeRevocationEndpointAuthMethodsSupported() + { + return RevocationEndpointAuthMethodsSupported.Count > 0; + } + + /// + /// Gets a bool that determines if the 'revocation_endpoint_auth_signing_alg_values_supported' (RevocationEndpointAuthSigningAlgValuesSupported) property should be serialized. + /// This is used by Json.NET in order to conditionally serialize properties. + /// + /// true if 'revocation_endpoint_auth_signing_alg_values_supported' (RevocationEndpointAuthSigningAlgValuesSupported) is not empty; otherwise, false. + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeRevocationEndpointAuthSigningAlgValuesSupported() + { + return RevocationEndpointAuthSigningAlgValuesSupported.Count > 0; + } + /// /// Gets a bool that determines if the 'SigningKeys' property should be serialized. /// This is used by Json.NET in order to conditionally serialize properties. diff --git a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Json/OpenIdConnectConfigurationSerializer.cs b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Json/OpenIdConnectConfigurationSerializer.cs index fc877effad..73ace9f25f 100644 --- a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Json/OpenIdConnectConfigurationSerializer.cs +++ b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Json/OpenIdConnectConfigurationSerializer.cs @@ -40,7 +40,9 @@ internal static class OpenIdConnectConfigurationSerializer "CLAIMS_PARAMETER_SUPPORTED", "CLAIMS_SUPPORTED", "CLAIM_TYPES_SUPPORTED", + "CODE_CHALLENGE_METHODS_SUPPORTED", ".WELL-KNOWN/OPENID-CONFIGURATION", + "DEVICE_AUTHORIZATION_ENDPOINT", "DISPLAY_VALUES_SUPPORTED", "DPOP_SIGNING_ALG_VALUES_SUPPORTED", "END_SESSION_ENDPOINT", @@ -71,6 +73,9 @@ internal static class OpenIdConnectConfigurationSerializer "REQUIRE_REQUEST_URI_REGISTRATION", "RESPONSE_MODES_SUPPORTED", "RESPONSE_TYPES_SUPPORTED", + "REVOCATION_ENDPOINT", + "REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED", + "REVOCATION_ENDPOINT_AUTH_SIGNING_ALG_VALUES_SUPPORTED", "SERVICE_DOCUMENTATION", "SCOPES_SUPPORTED", "SUBJECT_TYPES_SUPPORTED", @@ -97,7 +102,7 @@ public static OpenIdConnectConfiguration Read(string json, OpenIdConnectConfigur { return Read(ref reader, config); } - catch(JsonException ex) + catch (JsonException ex) { if (ex.GetType() == typeof(JsonException)) throw; @@ -131,7 +136,7 @@ public static OpenIdConnectConfiguration Read(ref Utf8JsonReader reader, OpenIdC LogHelper.MarkAsNonPII(reader.CurrentDepth), LogHelper.MarkAsNonPII(reader.BytesConsumed)))); - while(true) + while (true) { #region Check property name using ValueTextEquals // https://datatracker.ietf.org/doc/html/rfc7517#section-4, does not require that we reject JSON with duplicate member names. @@ -174,6 +179,12 @@ public static OpenIdConnectConfiguration Read(ref Utf8JsonReader reader, OpenIdC else if (reader.ValueTextEquals(Utf8Bytes.ClaimTypesSupported)) JsonPrimitives.ReadStrings(ref reader, config.ClaimTypesSupported, MetadataName.ClaimTypesSupported, ClassName, true); + else if (reader.ValueTextEquals(Utf8Bytes.CodeChallengeMethodsSupported)) + JsonPrimitives.ReadStrings(ref reader, config.CodeChallengeMethodsSupported, MetadataName.CodeChallengeMethodsSupported, ClassName, true); + + else if (reader.ValueTextEquals(Utf8Bytes.DeviceAuthorizationEndpoint)) + config.DeviceAuthorizationEndpoint = JsonPrimitives.ReadString(ref reader, MetadataName.DeviceAuthorizationEndpoint, ClassName, true); + else if (reader.ValueTextEquals(Utf8Bytes.DisplayValuesSupported)) JsonPrimitives.ReadStrings(ref reader, config.DisplayValuesSupported, MetadataName.DisplayValuesSupported, ClassName, true); @@ -277,14 +288,20 @@ public static OpenIdConnectConfiguration Read(ref Utf8JsonReader reader, OpenIdC else if (reader.ValueTextEquals(Utf8Bytes.ResponseTypesSupported)) JsonPrimitives.ReadStrings(ref reader, config.ResponseTypesSupported, MetadataName.ResponseTypesSupported, ClassName, true); - else if (reader.ValueTextEquals(Utf8Bytes.ScopesSupported)) - JsonPrimitives.ReadStrings(ref reader, config.ScopesSupported, MetadataName.ScopesSupported, ClassName, true); + else if (reader.ValueTextEquals(Utf8Bytes.RevocationEndpoint)) + config.RevocationEndpoint = JsonPrimitives.ReadString(ref reader, MetadataName.RevocationEndpoint, ClassName, true); + + else if (reader.ValueTextEquals(Utf8Bytes.RevocationEndpointAuthMethodsSupported)) + JsonPrimitives.ReadStrings(ref reader, config.RevocationEndpointAuthMethodsSupported, MetadataName.RevocationEndpointAuthMethodsSupported, ClassName, true); + + else if (reader.ValueTextEquals(Utf8Bytes.RevocationEndpointAuthSigningAlgValuesSupported)) + JsonPrimitives.ReadStrings(ref reader, config.RevocationEndpointAuthSigningAlgValuesSupported, MetadataName.RevocationEndpointAuthSigningAlgValuesSupported, ClassName, true); else if (reader.ValueTextEquals(Utf8Bytes.ServiceDocumentation)) - config.ServiceDocumentation = JsonPrimitives.ReadString(ref reader, MetadataName.ScopesSupported, ClassName, true); + config.ServiceDocumentation = JsonPrimitives.ReadString(ref reader, MetadataName.ServiceDocumentation, ClassName, true); - else if (reader.ValueTextEquals(Utf8Bytes.SubjectTypesSupported)) - JsonPrimitives.ReadStrings(ref reader, config.SubjectTypesSupported, MetadataName.SubjectTypesSupported, ClassName, true); + else if (reader.ValueTextEquals(Utf8Bytes.ScopesSupported)) + JsonPrimitives.ReadStrings(ref reader, config.ScopesSupported, MetadataName.ScopesSupported, ClassName, true); else if (reader.ValueTextEquals(Utf8Bytes.SubjectTypesSupported)) JsonPrimitives.ReadStrings(ref reader, config.SubjectTypesSupported, MetadataName.SubjectTypesSupported, ClassName, true); @@ -366,6 +383,12 @@ public static OpenIdConnectConfiguration Read(ref Utf8JsonReader reader, OpenIdC else if (propertyName.Equals(MetadataName.ClaimTypesSupported, StringComparison.OrdinalIgnoreCase)) JsonPrimitives.ReadStrings(ref reader, config.ClaimTypesSupported, propertyName, ClassName); + else if (propertyName.Equals(MetadataName.CodeChallengeMethodsSupported, StringComparison.OrdinalIgnoreCase)) + JsonPrimitives.ReadStrings(ref reader, config.CodeChallengeMethodsSupported, propertyName, ClassName); + + else if (propertyName.Equals(MetadataName.DeviceAuthorizationEndpoint, StringComparison.OrdinalIgnoreCase)) + config.DeviceAuthorizationEndpoint = JsonPrimitives.ReadString(ref reader, propertyName, ClassName); + else if (propertyName.Equals(MetadataName.DisplayValuesSupported, StringComparison.OrdinalIgnoreCase)) JsonPrimitives.ReadStrings(ref reader, config.DisplayValuesSupported, propertyName, ClassName); @@ -407,7 +430,7 @@ public static OpenIdConnectConfiguration Read(ref Utf8JsonReader reader, OpenIdC else if (propertyName.Equals(MetadataName.IdTokenEncryptionEncValuesSupported, StringComparison.OrdinalIgnoreCase)) JsonPrimitives.ReadStrings(ref reader, config.IdTokenEncryptionEncValuesSupported, propertyName, ClassName); - else if (propertyName.Equals(MetadataName.IdTokenSigningAlgValuesSupported , StringComparison.OrdinalIgnoreCase)) + else if (propertyName.Equals(MetadataName.IdTokenSigningAlgValuesSupported, StringComparison.OrdinalIgnoreCase)) JsonPrimitives.ReadStrings(ref reader, config.IdTokenSigningAlgValuesSupported, propertyName, ClassName); else if (propertyName.Equals(MetadataName.IntrospectionEndpoint, StringComparison.OrdinalIgnoreCase)) @@ -470,6 +493,15 @@ public static OpenIdConnectConfiguration Read(ref Utf8JsonReader reader, OpenIdC else if (propertyName.Equals(MetadataName.ResponseTypesSupported, StringComparison.OrdinalIgnoreCase)) JsonPrimitives.ReadStrings(ref reader, config.ResponseTypesSupported, propertyName, ClassName); + else if (propertyName.Equals(MetadataName.RevocationEndpoint, StringComparison.OrdinalIgnoreCase)) + config.RevocationEndpoint = JsonPrimitives.ReadString(ref reader, propertyName, ClassName); + + else if (propertyName.Equals(MetadataName.RevocationEndpointAuthMethodsSupported, StringComparison.OrdinalIgnoreCase)) + JsonPrimitives.ReadStrings(ref reader, config.RevocationEndpointAuthMethodsSupported, propertyName, ClassName); + + else if (propertyName.Equals(MetadataName.RevocationEndpointAuthSigningAlgValuesSupported, StringComparison.OrdinalIgnoreCase)) + JsonPrimitives.ReadStrings(ref reader, config.RevocationEndpointAuthSigningAlgValuesSupported, propertyName, ClassName); + else if (propertyName.Equals(MetadataName.ScopesSupported, StringComparison.OrdinalIgnoreCase)) JsonPrimitives.ReadStrings(ref reader, config.ScopesSupported, propertyName, ClassName); @@ -577,6 +609,12 @@ public static void Write(ref Utf8JsonWriter writer, OpenIdConnectConfiguration c if (config.ClaimTypesSupported.Count > 0) JsonPrimitives.WriteStrings(ref writer, Utf8Bytes.ClaimTypesSupported, config.ClaimTypesSupported); + if (config.CodeChallengeMethodsSupported.Count > 0) + JsonPrimitives.WriteStrings(ref writer, Utf8Bytes.CodeChallengeMethodsSupported, config.CodeChallengeMethodsSupported); + + if (!string.IsNullOrEmpty(config.DeviceAuthorizationEndpoint)) + writer.WriteString(Utf8Bytes.DeviceAuthorizationEndpoint, config.DeviceAuthorizationEndpoint); + if (config.DisplayValuesSupported.Count > 0) JsonPrimitives.WriteStrings(ref writer, Utf8Bytes.DisplayValuesSupported, config.DisplayValuesSupported); @@ -638,7 +676,7 @@ public static void Write(ref Utf8JsonWriter writer, OpenIdConnectConfiguration c writer.WriteString(Utf8Bytes.PushedAuthorizationRequestEndpoint, config.PushedAuthorizationRequestEndpoint); if (!string.IsNullOrEmpty(config.RegistrationEndpoint)) - writer.WriteString(Utf8Bytes.RegistrationEndpoint, config.RegistrationEndpoint); + writer.WriteString(Utf8Bytes.RegistrationEndpoint, config.RegistrationEndpoint); if (config.RequestObjectEncryptionAlgValuesSupported.Count > 0) JsonPrimitives.WriteStrings(ref writer, Utf8Bytes.RequestObjectEncryptionAlgValuesSupported, config.RequestObjectEncryptionAlgValuesSupported); @@ -670,6 +708,15 @@ public static void Write(ref Utf8JsonWriter writer, OpenIdConnectConfiguration c if (config.ScopesSupported.Count > 0) JsonPrimitives.WriteStrings(ref writer, Utf8Bytes.ScopesSupported, config.ScopesSupported); + if (!string.IsNullOrEmpty(config.RevocationEndpoint)) + writer.WriteString(Utf8Bytes.RevocationEndpoint, config.RevocationEndpoint); + + if (config.RevocationEndpointAuthMethodsSupported.Count > 0) + JsonPrimitives.WriteStrings(ref writer, Utf8Bytes.RevocationEndpointAuthMethodsSupported, config.RevocationEndpointAuthMethodsSupported); + + if (config.RevocationEndpointAuthSigningAlgValuesSupported.Count > 0) + JsonPrimitives.WriteStrings(ref writer, Utf8Bytes.RevocationEndpointAuthSigningAlgValuesSupported, config.RevocationEndpointAuthSigningAlgValuesSupported); + if (!string.IsNullOrEmpty(config.ServiceDocumentation)) writer.WriteString(Utf8Bytes.ServiceDocumentation, config.ServiceDocumentation); @@ -708,4 +755,3 @@ public static void Write(ref Utf8JsonWriter writer, OpenIdConnectConfiguration c #endregion } } - diff --git a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdProviderMetadataNames.cs b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdProviderMetadataNames.cs index 554f81a0f7..62f15637c6 100644 --- a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdProviderMetadataNames.cs +++ b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdProviderMetadataNames.cs @@ -24,6 +24,8 @@ public static class OpenIdProviderMetadataNames public const string ClaimsParameterSupported = "claims_parameter_supported"; public const string ClaimsSupported = "claims_supported"; public const string ClaimTypesSupported = "claim_types_supported"; + public const string CodeChallengeMethodsSupported = "code_challenge_methods_supported"; + public const string DeviceAuthorizationEndpoint = "device_authorization_endpoint"; public const string Discovery = ".well-known/openid-configuration"; public const string DisplayValuesSupported = "display_values_supported"; public const string DPoPSigningAlgValuesSupported = "dpop_signing_alg_values_supported"; @@ -56,6 +58,9 @@ public static class OpenIdProviderMetadataNames public const string RequireRequestUriRegistration = "require_request_uri_registration"; public const string ResponseModesSupported = "response_modes_supported"; public const string ResponseTypesSupported = "response_types_supported"; + public const string RevocationEndpoint = "revocation_endpoint"; + public const string RevocationEndpointAuthMethodsSupported = "revocation_endpoint_auth_methods_supported"; + public const string RevocationEndpointAuthSigningAlgValuesSupported = "revocation_endpoint_auth_signing_alg_values_supported"; public const string ServiceDocumentation = "service_documentation"; public const string ScopesSupported = "scopes_supported"; public const string SubjectTypesSupported = "subject_types_supported"; @@ -89,6 +94,8 @@ internal static class OpenIdProviderMetadataUtf8Bytes public static ReadOnlySpan ClaimsParameterSupported => "claims_parameter_supported"u8; public static ReadOnlySpan ClaimsSupported => "claims_supported"u8; public static ReadOnlySpan ClaimTypesSupported => "claim_types_supported"u8; + public static ReadOnlySpan CodeChallengeMethodsSupported => "code_challenge_methods_supported"u8; + public static ReadOnlySpan DeviceAuthorizationEndpoint => "device_authorization_endpoint"u8; public static ReadOnlySpan Discovery => ".well-known/openid-configuration"u8; public static ReadOnlySpan DisplayValuesSupported => "display_values_supported"u8; public static ReadOnlySpan DPoPSigningAlgValuesSupported => "dpop_signing_alg_values_supported"u8; @@ -121,6 +128,9 @@ internal static class OpenIdProviderMetadataUtf8Bytes public static ReadOnlySpan RequireRequestUriRegistration => "require_request_uri_registration"u8; public static ReadOnlySpan ResponseModesSupported => "response_modes_supported"u8; public static ReadOnlySpan ResponseTypesSupported => "response_types_supported"u8; + public static ReadOnlySpan RevocationEndpoint => "revocation_endpoint"u8; + public static ReadOnlySpan RevocationEndpointAuthMethodsSupported => "revocation_endpoint_auth_methods_supported"u8; + public static ReadOnlySpan RevocationEndpointAuthSigningAlgValuesSupported => "revocation_endpoint_auth_signing_alg_values_supported"u8; public static ReadOnlySpan ServiceDocumentation => "service_documentation"u8; public static ReadOnlySpan ScopesSupported => "scopes_supported"u8; public static ReadOnlySpan SubjectTypesSupported => "subject_types_supported"u8; diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConfigData.cs b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConfigData.cs index 56495e2331..1f60d43365 100644 --- a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConfigData.cs +++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConfigData.cs @@ -42,123 +42,128 @@ public static OpenIdConnectConfiguration FullyPopulatedWithKeys public static string HttpsBadUri = "https://_____NoSuchfile____"; #region Configuration Strings - public static string OpenIdConnectMetadataPingString = @"{""authorization_endpoint"":""https:\/\/connect-interop.pinglabs.org:9031\/as\/authorization.oauth2"", - ""issuer"":""https:\/\/connect-interop.pinglabs.org:9031"", - ""id_token_signing_alg_values_supported"":[""none"",""HS256"",""HS384"",""HS512"",""RS256"",""RS384"",""RS512"",""ES256"",""ES384"",""ES512""], - ""claim_types_supported"":[""normal""], - ""claims_parameter_supported"":false, - ""ping_end_session_endpoint"":""https:\/\/connect-interop.pinglabs.org:9031\/idp\/startSLO.ping"", - ""ping_revoked_sris_endpoint"":""https:\/\/connect-interop.pinglabs.org:9031\/pf-ws\/rest\/sessionMgmt\/revokedSris"", - ""request_parameter_supported"":false, - ""request_uri_parameter_supported"":false, - ""response_modes_supported"":[""fragment"",""query"",""form_post""], - ""response_types_supported"":[""code"",""token"",""id_token"",""code token"",""code id_token"",""token id_token"",""code token id_token""], - ""revocation_endpoint"":""https:\/\/connect-interop.pinglabs.org:9031\/as\/revoke_token.oauth2"", - ""scopes_supported"":[""phone"",""address"",""email"",""openid"",""profile""], - ""subject_types_supported"":[""public""], - ""token_endpoint"":""https:\/\/connect-interop.pinglabs.org:9031\/as\/token.oauth2"", - ""token_endpoint_auth_methods_supported"":[""client_secret_basic"",""client_secret_post""], - ""userinfo_endpoint"":""https:\/\/connect-interop.pinglabs.org:9031\/idp\/userinfo.openid"", - ""version"":""3.0""}"; + public static string OpenIdConnectMetadataPingString = @"{""authorization_endpoint"": ""https:\/\/connect-interop.pinglabs.org:9031\/as\/authorization.oauth2"", + ""issuer"": ""https:\/\/connect-interop.pinglabs.org:9031"", + ""id_token_signing_alg_values_supported"": [""none"", ""HS256"", ""HS384"", ""HS512"", ""RS256"", ""RS384"", ""RS512"", ""ES256"", ""ES384"", ""ES512""], + ""claim_types_supported"": [""normal""], + ""claims_parameter_supported"": false, + ""ping_end_session_endpoint"": ""https:\/\/connect-interop.pinglabs.org:9031\/idp\/startSLO.ping"", + ""ping_revoked_sris_endpoint"": ""https:\/\/connect-interop.pinglabs.org:9031\/pf-ws\/rest\/sessionMgmt\/revokedSris"", + ""request_parameter_supported"": false, + ""request_uri_parameter_supported"": false, + ""response_modes_supported"": [""fragment"", ""query"", ""form_post""], + ""response_types_supported"": [""code"", ""token"", ""id_token"", ""code token"", ""code id_token"", ""token id_token"", ""code token id_token""], + ""revocation_endpoint"": ""https:\/\/connect-interop.pinglabs.org:9031\/as\/revoke_token.oauth2"", + ""scopes_supported"": [""phone"", ""address"", ""email"", ""openid"", ""profile""], + ""subject_types_supported"": [""public""], + ""token_endpoint"": ""https:\/\/connect-interop.pinglabs.org:9031\/as\/token.oauth2"", + ""token_endpoint_auth_methods_supported"": [""client_secret_basic"", ""client_secret_post""], + ""userinfo_endpoint"": ""https:\/\/connect-interop.pinglabs.org:9031\/idp\/userinfo.openid"", + ""version"": ""3.0""}"; public static string JsonFile = @"OpenIdConnectMetadata.json"; public static string OpenIdConnectMetadataFileEnd2End = @"OpenIdConnectMetadataEnd2End.json"; public static string OpenIdConnectMetadataFileEnd2EndEC = @"OpenIdConnectMetadataEnd2EndEC.json"; public static string JsonWebKeySetBadUriFile = @"OpenIdConnectMetadataJsonWebKeySetBadUri.json"; public static string JsonAllValues = - @"{ ""acr_values_supported"" : [""acr_value1"", ""acr_value2"", ""acr_value3""], - ""authorization_endpoint"" : ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/authorize"", - ""authorization_response_iss_parameter_supported"" : false, - ""backchannel_authentication_endpoint"" : ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/bc-authorize"", - ""backchannel_authentication_request_signing_alg_values_supported"" : [""ES384"", ""ES512""], - ""backchannel_token_delivery_modes_supported"" : [""poll"", ""ping""], - ""backchannel_user_code_parameter_supported"" : false, + @"{ ""acr_values_supported"": [""acr_value1"", ""acr_value2"", ""acr_value3""], + ""authorization_endpoint"": ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/authorize"", + ""authorization_response_iss_parameter_supported"": false, + ""backchannel_authentication_endpoint"": ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/bc-authorize"", + ""backchannel_authentication_request_signing_alg_values_supported"": [""ES384"", ""ES512""], + ""backchannel_token_delivery_modes_supported"": [""poll"", ""ping""], + ""backchannel_user_code_parameter_supported"": false, + ""code_challenge_methods_supported"": [""plain"", ""S256""], + ""device_authorization_endpoint"": ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/devicecode"", ""frontchannel_logout_session_supported"": ""true"", ""frontchannel_logout_supported"": ""true"", - ""check_session_iframe"":""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/checksession"", - ""claims_locales_supported"" : [ ""claim_local1"", ""claim_local2"", ""claim_local3"" ], - ""claims_parameter_supported"" : true, - ""claims_supported"": [ ""sub"", ""iss"", ""aud"", ""exp"", ""iat"", ""auth_time"", ""acr"", ""amr"", ""nonce"", ""email"", ""given_name"", ""family_name"", ""nickname"" ], - ""claim_types_supported"" : [ ""Normal Claims"", ""Aggregated Claims"", ""Distributed Claims"" ], - ""display_values_supported"" : [ ""displayValue1"", ""displayValue2"", ""displayValue3"" ], - ""dpop_signing_alg_values_supported"" : [""ES384"", ""ES512""], - ""end_session_endpoint"" : ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/logout"", - ""grant_types_supported"" : [""authorization_code"",""implicit""], - ""http_logout_supported"" : true, - ""id_token_encryption_alg_values_supported"" : [""RSA1_5"", ""A256KW""], - ""id_token_encryption_enc_values_supported"" : [""A128CBC-HS256"",""A256CBC-HS512""], - ""id_token_signing_alg_values_supported"" : [""RS256""], - ""introspection_endpoint"" : ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/introspect"", - ""introspection_endpoint_auth_methods_supported"" : [""client_secret_post"",""private_key_jwt""], - ""introspection_endpoint_auth_signing_alg_values_supported"" : [""ES192"", ""ES256""], - ""issuer"" : ""https://sts.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/"", - ""jwks_uri"" : ""JsonWebKeySet.json"", - ""logout_session_supported"" : true, - ""microsoft_multi_refresh_token"" : true, - ""op_policy_uri"" : ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/op_policy_uri"", - ""op_tos_uri"" : ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/op_tos_uri"", - ""prompt_values_supported"" : [""none"", ""login"", ""consent""], - ""pushed_authorization_request_endpoint"" : ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/par"", - ""request_object_encryption_alg_values_supported"" : [""A192KW"", ""A256KW""], - ""request_object_encryption_enc_values_supported"" : [""A192GCM"",""A256GCM""], - ""request_object_signing_alg_values_supported"" : [""PS256"", ""PS512""], - ""request_parameter_supported"" : true, - ""request_uri_parameter_supported"" : true, - ""require_pushed_authorization_requests"" : false, - ""require_request_uri_registration"" : true, - ""response_modes_supported"" : [""query"", ""fragment"",""form_post""], - ""response_types_supported"" : [""code"",""id_token"",""code id_token""], - ""service_documentation"" : ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/service_documentation"", - ""scopes_supported"" : [""openid""], - ""subject_types_supported"" : [""pairwise""], - ""token_endpoint"" : ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/token"", - ""token_endpoint_auth_methods_supported"" : [""client_secret_post"",""private_key_jwt""], - ""token_endpoint_auth_signing_alg_values_supported"" : [""ES192"", ""ES256""], - ""ui_locales_supported"" : [""hak-CN"", ""en-us""], - ""userinfo_endpoint"" : ""https://login.microsoftonline.com/add29489-7269-41f4-8841-b63c95564420/openid/userinfo"", - ""userinfo_encryption_alg_values_supported"" : [""ECDH-ES+A128KW"",""ECDH-ES+A192KW""], - ""userinfo_encryption_enc_values_supported"" : [""A256CBC-HS512"", ""A128CBC-HS256""], - ""userinfo_signing_alg_values_supported"" : [""ES384"", ""ES512""] + ""check_session_iframe"": ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/checksession"", + ""claims_locales_supported"": [""claim_local1"", ""claim_local2"", ""claim_local3"" ], + ""claims_parameter_supported"": true, + ""claims_supported"": [""sub"", ""iss"", ""aud"", ""exp"", ""iat"", ""auth_time"", ""acr"", ""amr"", ""nonce"", ""email"", ""given_name"", ""family_name"", ""nickname"" ], + ""claim_types_supported"": [""Normal Claims"", ""Aggregated Claims"", ""Distributed Claims"" ], + ""display_values_supported"": [""displayValue1"", ""displayValue2"", ""displayValue3"" ], + ""dpop_signing_alg_values_supported"": [""ES384"", ""ES512""], + ""end_session_endpoint"": ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/logout"", + ""grant_types_supported"": [""authorization_code"", ""implicit""], + ""http_logout_supported"": true, + ""id_token_encryption_alg_values_supported"": [""RSA1_5"", ""A256KW""], + ""id_token_encryption_enc_values_supported"": [""A128CBC-HS256"", ""A256CBC-HS512""], + ""id_token_signing_alg_values_supported"": [""RS256""], + ""introspection_endpoint"": ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/introspect"", + ""introspection_endpoint_auth_methods_supported"": [""client_secret_post"", ""private_key_jwt""], + ""introspection_endpoint_auth_signing_alg_values_supported"": [""ES192"", ""ES256""], + ""issuer"": ""https://sts.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/"", + ""jwks_uri"": ""JsonWebKeySet.json"", + ""logout_session_supported"": true, + ""microsoft_multi_refresh_token"": true, + ""op_policy_uri"": ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/op_policy_uri"", + ""op_tos_uri"": ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/op_tos_uri"", + ""prompt_values_supported"": [""none"", ""login"", ""consent""], + ""pushed_authorization_request_endpoint"": ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/par"", + ""request_object_encryption_alg_values_supported"": [""A192KW"", ""A256KW""], + ""request_object_encryption_enc_values_supported"": [""A192GCM"", ""A256GCM""], + ""request_object_signing_alg_values_supported"": [""PS256"", ""PS512""], + ""request_parameter_supported"": true, + ""request_uri_parameter_supported"": true, + ""require_pushed_authorization_requests"": false, + ""require_request_uri_registration"": true, + ""response_modes_supported"": [""query"", ""fragment"", ""form_post""], + ""response_types_supported"": [""code"", ""id_token"", ""code id_token""], + ""revocation_endpoint"": ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/revocation"", + ""revocation_endpoint_auth_methods_supported"": [""client_secret_post"", ""client_secret_basic""], + ""revocation_endpoint_auth_signing_alg_values_supported"": [""ES192"", ""ES256""], + ""service_documentation"": ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/service_documentation"", + ""scopes_supported"": [""openid""], + ""subject_types_supported"": [""pairwise""], + ""token_endpoint"": ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/token"", + ""token_endpoint_auth_methods_supported"": [""client_secret_post"", ""private_key_jwt""], + ""token_endpoint_auth_signing_alg_values_supported"": [""ES192"", ""ES256""], + ""ui_locales_supported"": [""hak-CN"", ""en-us""], + ""userinfo_endpoint"": ""https://login.microsoftonline.com/add29489-7269-41f4-8841-b63c95564420/openid/userinfo"", + ""userinfo_encryption_alg_values_supported"": [""ECDH-ES+A128KW"", ""ECDH-ES+A192KW""], + ""userinfo_encryption_enc_values_supported"": [""A256CBC-HS512"", ""A128CBC-HS256""], + ""userinfo_signing_alg_values_supported"": [""ES384"", ""ES512""] }"; public static string OpenIdConnectMetadataSingleX509DataString = - @"{ ""authorization_endpoint"":""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/authorize"", - ""check_session_iframe"":""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/checksession"", - ""end_session_endpoint"":""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/logout"", - ""id_token_signing_alg_values_supported"":[""RS256""], - ""issuer"":""https://sts.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/"", - ""jwks_uri"":""JsonWebKeySetSingleX509Data.json"", + @"{ ""authorization_endpoint"": ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/authorize"", + ""check_session_iframe"": ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/checksession"", + ""end_session_endpoint"": ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/logout"", + ""id_token_signing_alg_values_supported"": [""RS256""], + ""issuer"": ""https://sts.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/"", + ""jwks_uri"": ""JsonWebKeySetSingleX509Data.json"", ""microsoft_multi_refresh_token"":true, - ""response_types_supported"":[""code"",""id_token"",""code id_token""], - ""response_modes_supported"":[""query"",""fragment"",""form_post""], - ""scopes_supported"":[""openid""], - ""subject_types_supported"":[""pairwise""], - ""token_endpoint"":""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/token"", - ""token_endpoint_auth_methods_supported"":[""client_secret_post"",""private_key_jwt""] + ""response_types_supported"": [""code"", ""id_token"", ""code id_token""], + ""response_modes_supported"": [""query"", ""fragment"", ""form_post""], + ""scopes_supported"": [""openid""], + ""subject_types_supported"": [""pairwise""], + ""token_endpoint"": ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/token"", + ""token_endpoint_auth_methods_supported"": [""client_secret_post"", ""private_key_jwt""] }"; public static string JsonWithSigningKeys = - @"{ ""authorization_endpoint"":""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/authorize"", - ""check_session_iframe"":""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/checksession"", - ""end_session_endpoint"":""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/logout"", - ""id_token_signing_alg_values_supported"":[""RS256""], - ""issuer"":""https://sts.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/"", - ""jwks_uri"":""JsonWebKeySetSingleX509Data.json"", + @"{ ""authorization_endpoint"": ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/authorize"", + ""check_session_iframe"": ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/checksession"", + ""end_session_endpoint"": ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/logout"", + ""id_token_signing_alg_values_supported"": [""RS256""], + ""issuer"": ""https://sts.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/"", + ""jwks_uri"": ""JsonWebKeySetSingleX509Data.json"", ""microsoft_multi_refresh_token"":true, - ""response_types_supported"":[""code"",""id_token"",""code id_token""], - ""response_modes_supported"":[""query"",""fragment"",""form_post""], - ""scopes_supported"":[""openid""], - ""subject_types_supported"":[""pairwise""], - ""token_endpoint"":""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/token"", - ""token_endpoint_auth_methods_supported"":[""client_secret_post"",""private_key_jwt""], - ""SigningKeys"":[""key1"",""key2""] + ""response_types_supported"": [""code"", ""id_token"", ""code id_token""], + ""response_modes_supported"": [""query"", ""fragment"", ""form_post""], + ""scopes_supported"": [""openid""], + ""subject_types_supported"": [""pairwise""], + ""token_endpoint"": ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/token"", + ""token_endpoint_auth_methods_supported"": [""client_secret_post"", ""private_key_jwt""], + ""SigningKeys"": [""key1"", ""key2""] }"; - public static string OpenIdConnectMetadataBadX509DataString = @"{""jwks_uri"":""JsonWebKeySetBadX509Data.json""}"; - public static string OpenIdConnectMetadataBadBase64DataString = @"{""jwks_uri"":""JsonWebKeySetBadBase64Data.json""}"; - public static string OpenIdConnectMetadataBadUriKeysString = @"{""jwks_uri"":""___NoSuchFile___""}"; + public static string OpenIdConnectMetadataBadX509DataString = @"{""jwks_uri"": ""JsonWebKeySetBadX509Data.json""}"; + public static string OpenIdConnectMetadataBadBase64DataString = @"{""jwks_uri"": ""JsonWebKeySetBadBase64Data.json""}"; + public static string OpenIdConnectMetadataBadUriKeysString = @"{""jwks_uri"": ""___NoSuchFile___""}"; public static string OpenIdConnectMetadataBadFormatString = @"{""issuer""::""https://sts.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/""}"; - public static string OpenIdConnectMetadataPingLabsJWKSString = @"{""jwks_uri"":""PingLabsJWKS.json""}"; + public static string OpenIdConnectMetadataPingLabsJWKSString = @"{""jwks_uri"": ""PingLabsJWKS.json""}"; public static string OpenIdConnectMetatadataBadJson = @"{..."; #endregion @@ -191,14 +196,14 @@ public static OpenIdConnectConfiguration FullyPopulatedWithKeys "userinfo_endpoint": "https://openidconnect.googleapis.com/v1/userinfo", "revocation_endpoint": "https://oauth2.googleapis.com/revoke", "jwks_uri": "https://www.googleapis.com/oauth2/v3/certs", - "response_types_supported": ["code","id_token","code id_token"], + "response_types_supported": ["code", "id_token", "code id_token"], "subject_types_supported": ["public"], "id_token_signing_alg_values_supported": ["RS256"], - "scopes_supported": ["openid","email","profile"], - "token_endpoint_auth_methods_supported": ["client_secret_post","client_secret_basic"], - "claims_supported": ["aud","email","email_verified","exp","family_name","given_name","iat","iss","locale","name","picture","sub"], - "code_challenge_methods_supported": ["plain","S256"], - "grant_types_supported": ["authorization_code","refresh_token","urn:ietf:params:oauth:grant-type:device_code","urn:ietf:params:oauth:grant-type:jwt-bearer"] + "scopes_supported": ["openid", "email", "profile"], + "token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"], + "claims_supported": ["aud", "email", "email_verified", "exp", "family_name", "given_name", "iat", "iss", "locale", "name", "picture", "sub"], + "code_challenge_methods_supported": ["plain", "S256"], + "grant_types_supported": ["authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code", "urn:ietf:params:oauth:grant-type:jwt-bearer"] } """; public static OpenIdConnectConfiguration AccountsGoogleComConfig @@ -209,12 +214,15 @@ public static OpenIdConnectConfiguration AccountsGoogleComConfig OpenIdConnectConfiguration config = new OpenIdConnectConfiguration { AuthorizationEndpoint = "https://accounts.google.com/o/oauth2/v2/auth", + DeviceAuthorizationEndpoint = "https://oauth2.googleapis.com/device/code", Issuer = "https://accounts.google.com", JwksUri = "https://www.googleapis.com/oauth2/v3/certs", + RevocationEndpoint = "https://oauth2.googleapis.com/revoke", TokenEndpoint = "https://oauth2.googleapis.com/token", UserInfoEndpoint = "https://openidconnect.googleapis.com/v1/userinfo", }; + AddToCollection(config.CodeChallengeMethodsSupported, "plain", "S256"); AddToCollection(config.ResponseTypesSupported, "code", "id_token", "code id_token"); config.SubjectTypesSupported.Add("public"); config.IdTokenSigningAlgValuesSupported.Add("RS256"); @@ -223,27 +231,22 @@ public static OpenIdConnectConfiguration AccountsGoogleComConfig AddToCollection(config.ClaimsSupported, "aud", "email", "email_verified", "exp", "family_name", "given_name", "iat", "iss", "locale", "name", "picture", "sub"); AddToCollection(config.GrantTypesSupported, "authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code", "urn:ietf:params:oauth:grant-type:jwt-bearer"); - // Adjust if Google changes their config or https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/2456 is implemented. - config.AdditionalData.Add("device_authorization_endpoint", "https://oauth2.googleapis.com/device/code"); - config.AdditionalData.Add("code_challenge_methods_supported", JsonUtilities.CreateJsonElement(""" ["plain","S256"] """)); - config.AdditionalData.Add("revocation_endpoint", "https://oauth2.googleapis.com/revoke"); - return config; } } #endregion - #region AADCommonV1 2/2/2024 https://login.microsoftonline.com/common/.well-known/openid-configuration + #region AADCommonV1 2/2/2024 https://login.microsoftonline.com/common/.well-known/openid-configuration public static string AADCommonV1Json => """ { "token_endpoint": "https://login.microsoftonline.com/common/oauth2/token", - "token_endpoint_auth_methods_supported": ["client_secret_post","private_key_jwt","client_secret_basic"], + "token_endpoint_auth_methods_supported": ["client_secret_post", "private_key_jwt", "client_secret_basic"], "jwks_uri": "https://login.microsoftonline.com/common/discovery/keys", - "response_modes_supported": ["query","fragment","form_post"], + "response_modes_supported": ["query", "fragment", "form_post"], "subject_types_supported": ["pairwise"], "id_token_signing_alg_values_supported": ["RS256"], - "response_types_supported": ["code","id_token","code id_token","token id_token","token"], + "response_types_supported": ["code", "id_token", "code id_token", "token id_token", "token"], "scopes_supported": ["openid"], "issuer": "https://sts.windows.net/{tenantid}/", "microsoft_multi_refresh_token": true, @@ -252,7 +255,7 @@ public static OpenIdConnectConfiguration AccountsGoogleComConfig "http_logout_supported": true, "frontchannel_logout_supported": true, "end_session_endpoint": "https://login.microsoftonline.com/common/oauth2/logout", - "claims_supported": ["sub","iss","cloud_instance_name","cloud_instance_host_name","cloud_graph_host_name","msgraph_host","aud","exp","iat","auth_time","acr","amr","nonce","email","given_name","family_name","nickname"], + "claims_supported": ["sub", "iss", "cloud_instance_name", "cloud_instance_host_name", "cloud_graph_host_name", "msgraph_host", "aud", "exp", "iat", "auth_time", "acr", "amr", "nonce", "email", "given_name", "family_name", "nickname"], "check_session_iframe": "https://login.microsoftonline.com/common/oauth2/checksession", "userinfo_endpoint": "https://login.microsoftonline.com/common/openid/userinfo", "kerberos_endpoint": "https://login.microsoftonline.com/common/kerberos", @@ -271,6 +274,7 @@ public static OpenIdConnectConfiguration AADCommonV1Config OpenIdConnectConfiguration config = new OpenIdConnectConfiguration { AuthorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/authorize", + DeviceAuthorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/devicecode", CheckSessionIframe = "https://login.microsoftonline.com/common/oauth2/checksession", HttpLogoutSupported = true, Issuer = "https://sts.windows.net/{tenantid}/", @@ -289,7 +293,6 @@ public static OpenIdConnectConfiguration AADCommonV1Config AddToCollection(config.TokenEndpointAuthMethodsSupported, "client_secret_post", "private_key_jwt", "client_secret_basic"); AddToCollection(config.ClaimsSupported, "sub", "iss", "cloud_instance_name", "cloud_instance_host_name", "cloud_graph_host_name", "msgraph_host", "aud", "exp", "iat", "auth_time", "acr", "amr", "nonce", "email", "given_name", "family_name", "nickname"); config.AdditionalData.Add("microsoft_multi_refresh_token", true); - config.AdditionalData.Add("device_authorization_endpoint", "https://login.microsoftonline.com/common/oauth2/devicecode"); config.AdditionalData.Add("kerberos_endpoint", "https://login.microsoftonline.com/common/kerberos"); config.AdditionalData.Add("tenant_region_scope", null); config.AdditionalData.Add("cloud_instance_name", "microsoftonline.com"); @@ -307,13 +310,13 @@ public static OpenIdConnectConfiguration AADCommonV1Config """ { "token_endpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/token", - "token_endpoint_auth_methods_supported": ["client_secret_post","private_key_jwt","client_secret_basic"], + "token_endpoint_auth_methods_supported": ["client_secret_post", "private_key_jwt", "client_secret_basic"], "jwks_uri": "https://login.microsoftonline.com/common/discovery/v2.0/keys", - "response_modes_supported": ["query","fragment","form_post"], + "response_modes_supported": ["query", "fragment", "form_post"], "subject_types_supported": ["pairwise"], "id_token_signing_alg_values_supported": ["RS256"], - "response_types_supported": ["code","id_token","code id_token","id_token token"], - "scopes_supported": ["openid","profile","email","offline_access"], + "response_types_supported": ["code", "id_token", "code id_token", "id_token token"], + "scopes_supported": ["openid", "profile", "email", "offline_access"], "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0", "request_uri_parameter_supported": false, "userinfo_endpoint": "https://graph.microsoft.com/oidc/userinfo", @@ -322,7 +325,7 @@ public static OpenIdConnectConfiguration AADCommonV1Config "http_logout_supported": true, "frontchannel_logout_supported": true, "end_session_endpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/logout", - "claims_supported": ["sub","iss","cloud_instance_name","cloud_instance_host_name","cloud_graph_host_name","msgraph_host","aud","exp","iat","auth_time","acr","nonce","preferred_username","name","tid","ver","at_hash","c_hash","email"], + "claims_supported": ["sub", "iss", "cloud_instance_name", "cloud_instance_host_name", "cloud_graph_host_name", "msgraph_host", "aud", "exp", "iat", "auth_time", "acr", "nonce", "preferred_username", "name", "tid", "ver", "at_hash", "c_hash", "email"], "kerberos_endpoint": "https://login.microsoftonline.com/common/kerberos", "tenant_region_scope": null, "cloud_instance_name": "microsoftonline.com", @@ -339,6 +342,7 @@ public static OpenIdConnectConfiguration AADCommonV2Config OpenIdConnectConfiguration config = new OpenIdConnectConfiguration { AuthorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", + DeviceAuthorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/devicecode", HttpLogoutSupported = true, Issuer = "https://login.microsoftonline.com/{tenantid}/v2.0", JwksUri = "https://login.microsoftonline.com/common/discovery/v2.0/keys", @@ -355,7 +359,6 @@ public static OpenIdConnectConfiguration AADCommonV2Config AddToCollection(config.ScopesSupported, "openid", "profile", "email", "offline_access"); AddToCollection(config.TokenEndpointAuthMethodsSupported, "client_secret_post", "private_key_jwt", "client_secret_basic"); AddToCollection(config.ClaimsSupported, "sub", "iss", "cloud_instance_name", "cloud_instance_host_name", "cloud_graph_host_name", "msgraph_host", "aud", "exp", "iat", "auth_time", "acr", "nonce", "preferred_username", "name", "tid", "ver", "at_hash", "c_hash", "email"); - config.AdditionalData.Add("device_authorization_endpoint", "https://login.microsoftonline.com/common/oauth2/v2.0/devicecode"); config.AdditionalData.Add("kerberos_endpoint", "https://login.microsoftonline.com/common/kerberos"); config.AdditionalData.Add("tenant_region_scope", null); config.AdditionalData.Add("cloud_instance_name", "microsoftonline.com"); @@ -617,6 +620,8 @@ private static OpenIdConnectConfiguration SetDefaultConfiguration(OpenIdConnectC config.ClaimsParameterSupported = true; AddToCollection(config.ClaimsSupported, "sub", "iss", "aud", "exp", "iat", "auth_time", "acr", "amr", "nonce", "email", "given_name", "family_name", "nickname"); AddToCollection(config.ClaimTypesSupported, "Normal Claims", "Aggregated Claims", "Distributed Claims"); + AddToCollection(config.CodeChallengeMethodsSupported, "plain", "S256"); + config.DeviceAuthorizationEndpoint = "https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/devicecode"; AddToCollection(config.DisplayValuesSupported, "displayValue1", "displayValue2", "displayValue3"); AddToCollection(config.DPoPSigningAlgValuesSupported, "ES384", "ES512"); config.EndSessionEndpoint = "https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/logout"; @@ -646,8 +651,11 @@ private static OpenIdConnectConfiguration SetDefaultConfiguration(OpenIdConnectC config.RequireRequestUriRegistration = true; AddToCollection(config.ResponseModesSupported, "query", "fragment", "form_post"); AddToCollection(config.ResponseTypesSupported, "code", "id_token", "code id_token"); - config.ScopesSupported.Add("openid"); + config.RevocationEndpoint = "https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/revocation"; + AddToCollection(config.RevocationEndpointAuthMethodsSupported, "client_secret_post", "client_secret_basic"); + AddToCollection(config.RevocationEndpointAuthSigningAlgValuesSupported, "ES192", "ES256"); config.ServiceDocumentation = "https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/service_documentation"; + config.ScopesSupported.Add("openid"); config.SubjectTypesSupported.Add("pairwise"); config.TokenEndpoint = "https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/token"; AddToCollection(config.TokenEndpointAuthMethodsSupported, "client_secret_post", "private_key_jwt"); diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectConfigurationTests.cs b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectConfigurationTests.cs index 8baedf49a2..9ee0bb7cdb 100644 --- a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectConfigurationTests.cs +++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectConfigurationTests.cs @@ -83,6 +83,7 @@ public void Defaults() Assert.NotNull(configuration.ClaimsLocalesSupported); Assert.False(configuration.ClaimsParameterSupported); Assert.NotNull(configuration.ClaimTypesSupported); + Assert.NotNull(configuration.CodeChallengeMethodsSupported); Assert.NotNull(configuration.DisplayValuesSupported); Assert.NotNull(configuration.DPoPSigningAlgValuesSupported); Assert.NotNull(configuration.GrantTypesSupported); @@ -102,6 +103,8 @@ public void Defaults() Assert.False(configuration.RequireRequestUriRegistration); Assert.NotNull(configuration.ResponseModesSupported); Assert.NotNull(configuration.ResponseTypesSupported); + Assert.NotNull(configuration.RevocationEndpointAuthMethodsSupported); + Assert.NotNull(configuration.RevocationEndpointAuthSigningAlgValuesSupported); Assert.NotNull(configuration.ScopesSupported); Assert.NotNull(configuration.SigningKeys); Assert.NotNull(configuration.SubjectTypesSupported); @@ -138,8 +141,8 @@ public void GetSets() OpenIdConnectConfiguration configuration = new OpenIdConnectConfiguration(); Type type = typeof(OpenIdConnectConfiguration); PropertyInfo[] properties = type.GetProperties(); - if (properties.Length != 58) - Assert.True(false, "Number of properties has changed from 58 to: " + properties.Length + ", adjust tests"); + if (properties.Length != 63) + Assert.True(false, "Number of properties has changed from 63 to: " + properties.Length + ", adjust tests"); TestUtilities.CallAllPublicInstanceAndStaticPropertyGets(configuration, "OpenIdConnectConfiguration_GetSets"); @@ -154,6 +157,8 @@ public void GetSets() new KeyValuePair>("BackchannelUserCodeParameterSupported", new List{ false, true, true }), new KeyValuePair>("CheckSessionIframe", new List{ (string)null, Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }), new KeyValuePair>("ClaimsParameterSupported", new List{ false, true, false }), + new KeyValuePair>("CodeChallengeMethodsSupported", new List{ false, true, true }), + new KeyValuePair>("DeviceAuthorizationEndpoint", new List{ (string)null, Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }), new KeyValuePair>("EndSessionEndpoint", new List{ (string)null, Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }), new KeyValuePair>("HttpLogoutSupported", new List{ false, true, true }), new KeyValuePair>("IntrospectionEndpoint", new List{ (string)null, Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }), @@ -169,6 +174,9 @@ public void GetSets() new KeyValuePair>("RequestUriParameterSupported", new List{ false, true, true }), new KeyValuePair>("RequirePushedAuthorizationRequests", new List{ false, true, true }), new KeyValuePair>("RequireRequestUriRegistration", new List{ false, true, true }), + new KeyValuePair>("RevocationEndpoint", new List{ (string)null, Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }), + new KeyValuePair>("RevocationEndpointAuthMethodsSupported", new List{ false, true, true }), + new KeyValuePair>("RevocationEndpointAuthSigningAlgValuesSupported", new List{ false, true, true }), new KeyValuePair>("ServiceDocumentation", new List{ (string)null, Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }), new KeyValuePair>("TokenEndpoint", new List{ (string)null, Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }), new KeyValuePair>("UserInfoEndpoint", new List{ (string)null, Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }), @@ -286,6 +294,8 @@ public void NonemptyCollectionSerialization() "claims_supported", "claims_locales_supported", "claim_types_supported", + "code_challenge_methods_supported", + "device_authorization_endpoint", "display_values_supported", "dpop_signing_alg_values_supported", "grant_types_supported", @@ -300,6 +310,9 @@ public void NonemptyCollectionSerialization() "request_object_signing_alg_values_supported", "response_modes_supported", "response_types_supported", + "revocation_endpoint", + "revocation_endpoint_auth_methods_supported", + "revocation_endpoint_auth_signing_alg_values_supported", "scopes_supported", "subject_types_supported", "token_endpoint_auth_methods_supported", diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectMetadata.json b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectMetadata.json index e71ed8aa53..bf10bb1f2c 100644 --- a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectMetadata.json +++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectMetadata.json @@ -7,22 +7,24 @@ "backchannel_token_delivery_modes_supported": ["poll", "ping"], "backchannel_user_code_parameter_supported": false, "dpop_signing_alg_values_supported": ["ES384", "ES512"], - "frontchannel_logout_session_supported": "true", - "frontchannel_logout_supported": "true", + "frontchannel_logout_session_supported": "true", + "frontchannel_logout_supported": "true", "check_session_iframe": "https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/checksession", "claims_locales_supported": ["claim_local1", "claim_local2", "claim_local3"], "claims_parameter_supported": true, "claims_supported": ["sub", "iss", "aud", "exp", "iat", "auth_time", "acr", "amr", "nonce", "email", "given_name", "family_name", "nickname"], "claim_types_supported": ["Normal Claims", "Aggregated Claims", "Distributed Claims"], + "code_challenge_methods_supported": ["plain", "S256"], + "device_authorization_endpoint": "https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/devicecode", "display_values_supported": ["displayValue1", "displayValue2", "displayValue3"], "end_session_endpoint": "https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/logout", - "grant_types_supported": ["authorization_code","implicit"], + "grant_types_supported": ["authorization_code", "implicit"], "http_logout_supported": true, "id_token_encryption_alg_values_supported": ["RSA1_5", "A256KW"], - "id_token_encryption_enc_values_supported": ["A128CBC-HS256","A256CBC-HS512"], + "id_token_encryption_enc_values_supported": ["A128CBC-HS256", "A256CBC-HS512"], "id_token_signing_alg_values_supported": ["RS256"], "introspection_endpoint": "https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/introspect", - "introspection_endpoint_auth_methods_supported": ["client_secret_post","private_key_jwt"], + "introspection_endpoint_auth_methods_supported": ["client_secret_post", "private_key_jwt"], "introspection_endpoint_auth_signing_alg_values_supported": ["ES192", "ES256"], "issuer": "https://sts.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/", "jwks_uri": "JsonWebKeySet.json", @@ -33,7 +35,7 @@ "prompt_values_supported": ["none", "login", "consent"], "pushed_authorization_request_endpoint": "https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/par", "request_object_encryption_alg_values_supported": ["A192KW", "A256KW"], - "request_object_encryption_enc_values_supported": ["A192GCM","A256GCM"], + "request_object_encryption_enc_values_supported": ["A192GCM", "A256GCM"], "request_object_signing_alg_values_supported": ["PS256", "PS512"], "request_parameter_supported": true, "request_uri_parameter_supported": true, @@ -41,6 +43,9 @@ "require_request_uri_registration": true, "response_types_supported": ["code", "id_token", "code id_token"], "response_modes_supported": ["query", "fragment", "form_post"], + "revocation_endpoint": "https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/revocation", + "revocation_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"], + "revocation_endpoint_auth_signing_alg_values_supported": ["ES192", "ES256"], "service_documentation": "https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/service_documentation", "scopes_supported": ["openid"], "subject_types_supported": ["pairwise"], @@ -49,7 +54,7 @@ "token_endpoint_auth_signing_alg_values_supported": ["ES192", "ES256"], "ui_locales_supported": ["hak-CN", "en-us"], "userinfo_endpoint": "https://login.microsoftonline.com/add29489-7269-41f4-8841-b63c95564420/openid/userinfo", - "userinfo_encryption_alg_values_supported": ["ECDH-ES+A128KW","ECDH-ES+A192KW"], + "userinfo_encryption_alg_values_supported": ["ECDH-ES+A128KW", "ECDH-ES+A192KW"], "userinfo_encryption_enc_values_supported": ["A256CBC-HS512", "A128CBC-HS256"], "userinfo_signing_alg_values_supported": ["ES384", "ES512"] -} \ No newline at end of file +} diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectSerializationTests.cs b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectSerializationTests.cs index 9d28137c02..22454614f5 100644 --- a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectSerializationTests.cs +++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectSerializationTests.cs @@ -57,8 +57,8 @@ public static TheoryData DesrializeTheoryData theoryData.Add(new OpenIdConnectTheoryData("AccountsGoogleCom") { - CompareTo = JsonUtilities.SetAdditionalDataKeysToUpperCase(OpenIdConfigData.AccountsGoogleComConfig), - Json = JsonUtilities.SetAdditionalDataKeysToUpperCase(OpenIdConfigData.AccountsGoogleComJson, OpenIdConfigData.AccountsGoogleComConfig) + CompareTo = OpenIdConfigData.AccountsGoogleComConfig, + Json = OpenIdConfigData.AccountsGoogleComJson }); theoryData.Add(new OpenIdConnectTheoryData("FrontChannelFalse") From bf18516ee5852fdac75535dc3b151e11c62ef2d9 Mon Sep 17 00:00:00 2001 From: joegoldman2 <147369450+joegoldman2@users.noreply.github.com> Date: Mon, 17 Jun 2024 23:28:19 +0000 Subject: [PATCH 05/22] Use FrozenSet in two appropriate places (#2640) * Use FrozenSet in two appropriate places * Use FrozenSet only for .NET 8 and above --------- Co-authored-by: joegoldman2 <147369450+joegoldman@users.noreply.github.com> --- .../OpenIdConnectConfigurationRetriever.cs | 2 -- .../OpenIdConnectProtocolException.cs | 1 - .../OpenIdConnectConfigurationSerializer.cs | 17 +++++++++++++++-- .../Json/JsonWebKeySerializer.cs | 18 +++++++++++++++--- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Configuration/OpenIdConnectConfigurationRetriever.cs b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Configuration/OpenIdConnectConfigurationRetriever.cs index 17b354b390..d8d0c9d37b 100644 --- a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Configuration/OpenIdConnectConfigurationRetriever.cs +++ b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Configuration/OpenIdConnectConfigurationRetriever.cs @@ -10,13 +10,11 @@ namespace Microsoft.IdentityModel.Protocols.OpenIdConnect { - /// /// Retrieves a populated given an address. /// public class OpenIdConnectConfigurationRetriever : IConfigurationRetriever { - /// /// Retrieves a populated given an address. /// diff --git a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Exceptions/OpenIdConnectProtocolException.cs b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Exceptions/OpenIdConnectProtocolException.cs index 1cc301ed01..91585f9573 100644 --- a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Exceptions/OpenIdConnectProtocolException.cs +++ b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Exceptions/OpenIdConnectProtocolException.cs @@ -6,7 +6,6 @@ namespace Microsoft.IdentityModel.Protocols.OpenIdConnect { - /// /// This exception is thrown when an OpenIdConnect protocol handler encounters a protocol error. /// diff --git a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Json/OpenIdConnectConfigurationSerializer.cs b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Json/OpenIdConnectConfigurationSerializer.cs index 73ace9f25f..9e99a0899d 100644 --- a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Json/OpenIdConnectConfigurationSerializer.cs +++ b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Json/OpenIdConnectConfigurationSerializer.cs @@ -3,6 +3,9 @@ // See the LICENSE file in the project root for more information. using System; +#if NET8_0_OR_GREATER +using System.Collections.Frozen; +#endif using System.Collections.Generic; using System.IO; using System.Text; @@ -26,7 +29,13 @@ internal static class OpenIdConnectConfigurationSerializer // If not found, then we assume we should put the value into AdditionalData. // If we didn't do that, we would pay a performance penalty for those cases where there is AdditionalData // but otherwise the JSON properties are all lower case. - public static HashSet OpenIdProviderMetadataNamesUpperCase = new HashSet + public static readonly +#if NET8_0_OR_GREATER + FrozenSet +#else + HashSet +#endif + OpenIdProviderMetadataNamesUpperCase = new HashSet { "ACR_VALUES_SUPPORTED", "AUTHORIZATION_ENDPOINT", @@ -87,7 +96,11 @@ internal static class OpenIdConnectConfigurationSerializer "USERINFO_ENCRYPTION_ALG_VALUES_SUPPORTED", "USERINFO_ENCRYPTION_ENC_VALUES_SUPPORTED", "USERINFO_SIGNING_ALG_VALUES_SUPPORTED", - }; + } +#if NET8_0_OR_GREATER + .ToFrozenSet() +#endif + ; #region Read public static OpenIdConnectConfiguration Read(string json) diff --git a/src/Microsoft.IdentityModel.Tokens/Json/JsonWebKeySerializer.cs b/src/Microsoft.IdentityModel.Tokens/Json/JsonWebKeySerializer.cs index ef28e1cf5c..89defe3734 100644 --- a/src/Microsoft.IdentityModel.Tokens/Json/JsonWebKeySerializer.cs +++ b/src/Microsoft.IdentityModel.Tokens/Json/JsonWebKeySerializer.cs @@ -3,6 +3,9 @@ // See the LICENSE file in the project root for more information. using System; +#if NET8_0_OR_GREATER +using System.Collections.Frozen; +#endif using System.Collections.Generic; using System.IO; using System.Text; @@ -21,7 +24,13 @@ internal static class JsonWebKeySerializer // If not found, then we assume we should put the value into AdditionalData. // If we didn't do that, we would pay a performance penalty for those cases where there is AdditionalData // but otherwise the JSON properties are all lower case. - public static HashSet JsonWebKeyParameterNamesUpperCase = new HashSet + public static readonly +#if NET8_0_OR_GREATER + FrozenSet +#else + HashSet +# endif + JsonWebKeyParameterNamesUpperCase = new HashSet { "ALG", "CRV", @@ -46,7 +55,11 @@ internal static class JsonWebKeySerializer "X5T#S256", "X5U", "Y" - }; + } +#if NET8_0_OR_GREATER + .ToFrozenSet() +#endif + ; #region Read public static JsonWebKey Read(string json) @@ -399,4 +412,3 @@ public static void Write(ref Utf8JsonWriter writer, JsonWebKey jsonWebKey) #endregion } } - From d01b8ea4c7a7a72c4bce448f152c4d785267c975 Mon Sep 17 00:00:00 2001 From: jennyf19 Date: Mon, 17 Jun 2024 17:50:57 -0700 Subject: [PATCH 06/22] remove non-compliant packages (#2635) * remove non-compliant packages * fix issues * try to fix codeql * a few more updates to remove the projects * revert codeql changes * re-add codeql * fix space * fix test * added m.im.TestUtils as friend. --------- Co-authored-by: id4s --- .github/workflows/codeql-analysis.yml | 12 +- Wilson.sln | 31 - build/dependencies.props | 2 - buildConfiguration.xml | 4 - .../InternalsVisibleTo.cs | 4 - .../KeyVaultCryptoProvider.cs | 102 --- .../KeyVaultKeyWrapProvider.cs | 140 ---- .../KeyVaultSecurityKey.cs | 109 --- .../KeyVaultSignatureProvider.cs | 157 ----- ...ft.IdentityModel.KeyVaultExtensions.csproj | 33 - .../Properties/AssemblyInfo.cs | 10 - .../GlobalSuppression.cs | 8 - .../InternalsVisibleTo.cs | 4 - .../ManagedKeyVaultSecurityKey.cs | 65 -- ...ityModel.ManagedKeyVaultSecurityKey.csproj | 34 - .../Properties/AssemblyInfo.cs | 10 - .../InternalsVisibleTo.cs | 3 +- .../Properties/AssemblyInfo.cs | 1 - ...t.IdentityModel.JsonWebTokens.Tests.csproj | 4 + .../KeyVaultCryptoProviderTests.cs | 43 -- .../KeyVaultKeyWrapProviderTests.cs | 135 ---- ...rityKeyAuthenticationCallbackTheoryData.cs | 12 - ...SecurityKeyConfidentialClientTheoryData.cs | 13 - ...rityKeyManagedServiceIdentityTheoryData.cs | 9 - .../KeyVaultSecurityKeyTests.cs | 76 --- .../KeyVaultSecurityKeyTheoryData.cs | 14 - .../KeyVaultSignatureProviderTests.cs | 170 ----- .../KeyVaultUtilities.cs | 17 - .../KeyWrapProviderTheoryData.cs | 12 - ...ntityModel.KeyVaultExtensions.Tests.csproj | 24 - .../MockKeyVaultClient.cs | 622 ------------------ .../Properties/AssemblyInfo.cs | 11 - .../SignatureProviderTheoryData.cs | 12 - .../KeyVaultSecurityKeyTests.cs | 160 ----- ...el.ManagedKeyVaultSecurityKey.Tests.csproj | 25 - .../Properties/AssemblyInfo.cs | 11 - .../DerivedTypes.cs | 2 +- ...icrosoft.IdentityModel.Tokens.Tests.csproj | 1 - .../SecurityKeyTests.cs | 5 +- .../JwtSecurityTokenHandlerTests.cs | 1 + .../JwtTestDatasets.cs | 1 + ...stem.IdentityModel.Tokens.Jwt.Tests.csproj | 4 + 42 files changed, 24 insertions(+), 2089 deletions(-) delete mode 100644 src/Microsoft.IdentityModel.KeyVaultExtensions/InternalsVisibleTo.cs delete mode 100644 src/Microsoft.IdentityModel.KeyVaultExtensions/KeyVaultCryptoProvider.cs delete mode 100644 src/Microsoft.IdentityModel.KeyVaultExtensions/KeyVaultKeyWrapProvider.cs delete mode 100644 src/Microsoft.IdentityModel.KeyVaultExtensions/KeyVaultSecurityKey.cs delete mode 100644 src/Microsoft.IdentityModel.KeyVaultExtensions/KeyVaultSignatureProvider.cs delete mode 100644 src/Microsoft.IdentityModel.KeyVaultExtensions/Microsoft.IdentityModel.KeyVaultExtensions.csproj delete mode 100644 src/Microsoft.IdentityModel.KeyVaultExtensions/Properties/AssemblyInfo.cs delete mode 100644 src/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey/GlobalSuppression.cs delete mode 100644 src/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey/InternalsVisibleTo.cs delete mode 100644 src/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey/ManagedKeyVaultSecurityKey.cs delete mode 100644 src/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.csproj delete mode 100644 src/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey/Properties/AssemblyInfo.cs delete mode 100644 test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultCryptoProviderTests.cs delete mode 100644 test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultKeyWrapProviderTests.cs delete mode 100644 test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultSecurityKeyAuthenticationCallbackTheoryData.cs delete mode 100644 test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultSecurityKeyConfidentialClientTheoryData.cs delete mode 100644 test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultSecurityKeyManagedServiceIdentityTheoryData.cs delete mode 100644 test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultSecurityKeyTests.cs delete mode 100644 test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultSecurityKeyTheoryData.cs delete mode 100644 test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultSignatureProviderTests.cs delete mode 100644 test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultUtilities.cs delete mode 100644 test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyWrapProviderTheoryData.cs delete mode 100644 test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/Microsoft.IdentityModel.KeyVaultExtensions.Tests.csproj delete mode 100644 test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/MockKeyVaultClient.cs delete mode 100644 test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/Properties/AssemblyInfo.cs delete mode 100644 test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/SignatureProviderTheoryData.cs delete mode 100644 test/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.Tests/KeyVaultSecurityKeyTests.cs delete mode 100644 test/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.Tests/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.Tests.csproj delete mode 100644 test/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.Tests/Properties/AssemblyInfo.cs diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 5e5d5b4bdb..2f29ac340c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,9 +1,19 @@ name: "CodeQL" on: - push: + push: + paths-ignore: + - 'test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/**' + - 'test/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.Tests/**' + - '/src/Microsoft.IdentityModel.KeyVaultExtensions/**' + - '/src/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey/**' branches: [ "dev", "dev6x", "dev7x"] pull_request: + paths-ignore: + - 'test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/**' + - 'test/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.Tests/**' + - '/src/Microsoft.IdentityModel.KeyVaultExtensions/**' + - '/src/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey/**' types: - opened - synchronize diff --git a/Wilson.sln b/Wilson.sln index 8dd5dcbce2..18274278e0 100644 --- a/Wilson.sln +++ b/Wilson.sln @@ -66,18 +66,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.IdentityModel.Jso EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{EB14B99B-2255-45BC-BF14-E488DCD4A4BA}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{B961CF69-0DE6-4B9F-9473-9F669365BD62}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.IdentityModel.KeyVaultExtensions.Tests", "test\Microsoft.IdentityModel.KeyVaultExtensions.Tests\Microsoft.IdentityModel.KeyVaultExtensions.Tests.csproj", "{987772FA-BA24-4EF4-9B58-3DA78FFD61DD}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.Tests", "test\Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.Tests\Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.Tests.csproj", "{97315A25-B694-4BD0-8DF5-C339884A6D26}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.IdentityModel.KeyVaultExtensions", "src\Microsoft.IdentityModel.KeyVaultExtensions\Microsoft.IdentityModel.KeyVaultExtensions.csproj", "{F5636C24-D6D5-4F6A-8A21-7C78FC1FC6C6}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.IdentityModel.TestExtensions", "src\Microsoft.IdentityModel.TestExtensions\Microsoft.IdentityModel.TestExtensions.csproj", "{AF787AA8-DE6E-4B74-816E-E8F3203A2FA0}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.IdentityModel.ManagedKeyVaultSecurityKey", "src\Microsoft.IdentityModel.ManagedKeyVaultSecurityKey\Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.csproj", "{8DFF1DEA-F01F-4CE4-9471-5D2CEFB7E59F}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.IdentityModel.Protocols.SignedHttpRequest", "src\Microsoft.IdentityModel.Protocols.SignedHttpRequest\Microsoft.IdentityModel.Protocols.SignedHttpRequest.csproj", "{C768FBB5-DE0D-4970-918C-96B37485E34C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests", "test\Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests\Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests.csproj", "{15944563-F7DA-4150-B5F1-6144EBF2CE23}" @@ -200,26 +190,10 @@ Global {DBF58792-25DF-4B6E-866C-77A0BC5AB81B}.Debug|Any CPU.Build.0 = Debug|Any CPU {DBF58792-25DF-4B6E-866C-77A0BC5AB81B}.Release|Any CPU.ActiveCfg = Release|Any CPU {DBF58792-25DF-4B6E-866C-77A0BC5AB81B}.Release|Any CPU.Build.0 = Release|Any CPU - {987772FA-BA24-4EF4-9B58-3DA78FFD61DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {987772FA-BA24-4EF4-9B58-3DA78FFD61DD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {987772FA-BA24-4EF4-9B58-3DA78FFD61DD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {987772FA-BA24-4EF4-9B58-3DA78FFD61DD}.Release|Any CPU.Build.0 = Release|Any CPU - {97315A25-B694-4BD0-8DF5-C339884A6D26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {97315A25-B694-4BD0-8DF5-C339884A6D26}.Debug|Any CPU.Build.0 = Debug|Any CPU - {97315A25-B694-4BD0-8DF5-C339884A6D26}.Release|Any CPU.ActiveCfg = Release|Any CPU - {97315A25-B694-4BD0-8DF5-C339884A6D26}.Release|Any CPU.Build.0 = Release|Any CPU - {F5636C24-D6D5-4F6A-8A21-7C78FC1FC6C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F5636C24-D6D5-4F6A-8A21-7C78FC1FC6C6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F5636C24-D6D5-4F6A-8A21-7C78FC1FC6C6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F5636C24-D6D5-4F6A-8A21-7C78FC1FC6C6}.Release|Any CPU.Build.0 = Release|Any CPU {AF787AA8-DE6E-4B74-816E-E8F3203A2FA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AF787AA8-DE6E-4B74-816E-E8F3203A2FA0}.Debug|Any CPU.Build.0 = Debug|Any CPU {AF787AA8-DE6E-4B74-816E-E8F3203A2FA0}.Release|Any CPU.ActiveCfg = Release|Any CPU {AF787AA8-DE6E-4B74-816E-E8F3203A2FA0}.Release|Any CPU.Build.0 = Release|Any CPU - {8DFF1DEA-F01F-4CE4-9471-5D2CEFB7E59F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8DFF1DEA-F01F-4CE4-9471-5D2CEFB7E59F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8DFF1DEA-F01F-4CE4-9471-5D2CEFB7E59F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8DFF1DEA-F01F-4CE4-9471-5D2CEFB7E59F}.Release|Any CPU.Build.0 = Release|Any CPU {C768FBB5-DE0D-4970-918C-96B37485E34C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C768FBB5-DE0D-4970-918C-96B37485E34C}.Debug|Any CPU.Build.0 = Debug|Any CPU {C768FBB5-DE0D-4970-918C-96B37485E34C}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -289,12 +263,7 @@ Global {E4E6D0ED-12CB-4C01-A4C1-4F60D10E2304} = {BD2706C5-6C57-484D-89C8-A0CF5F8E3D19} {DBF58792-25DF-4B6E-866C-77A0BC5AB81B} = {BD2706C5-6C57-484D-89C8-A0CF5F8E3D19} {EB14B99B-2255-45BC-BF14-E488DCD4A4BA} = {BD2706C5-6C57-484D-89C8-A0CF5F8E3D19} - {B961CF69-0DE6-4B9F-9473-9F669365BD62} = {8905D2E3-4499-4A86-BF3E-F098F228DD59} - {987772FA-BA24-4EF4-9B58-3DA78FFD61DD} = {B961CF69-0DE6-4B9F-9473-9F669365BD62} - {97315A25-B694-4BD0-8DF5-C339884A6D26} = {B961CF69-0DE6-4B9F-9473-9F669365BD62} - {F5636C24-D6D5-4F6A-8A21-7C78FC1FC6C6} = {EB14B99B-2255-45BC-BF14-E488DCD4A4BA} {AF787AA8-DE6E-4B74-816E-E8F3203A2FA0} = {EB14B99B-2255-45BC-BF14-E488DCD4A4BA} - {8DFF1DEA-F01F-4CE4-9471-5D2CEFB7E59F} = {EB14B99B-2255-45BC-BF14-E488DCD4A4BA} {C768FBB5-DE0D-4970-918C-96B37485E34C} = {BD2706C5-6C57-484D-89C8-A0CF5F8E3D19} {15944563-F7DA-4150-B5F1-6144EBF2CE23} = {8905D2E3-4499-4A86-BF3E-F098F228DD59} {DA585910-0E6C-45A5-AABD-30917130FD63} = {BD2706C5-6C57-484D-89C8-A0CF5F8E3D19} diff --git a/build/dependencies.props b/build/dependencies.props index 23e1b91065..8203da572c 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -2,8 +2,6 @@ 2.1.1 - 3.0.5 - 1.0.3 4.5.0 1.0.0 2.0.3 diff --git a/buildConfiguration.xml b/buildConfiguration.xml index 06e064dafa..0abda703bf 100644 --- a/buildConfiguration.xml +++ b/buildConfiguration.xml @@ -14,8 +14,6 @@ - - @@ -33,8 +31,6 @@ - - diff --git a/src/Microsoft.IdentityModel.KeyVaultExtensions/InternalsVisibleTo.cs b/src/Microsoft.IdentityModel.KeyVaultExtensions/InternalsVisibleTo.cs deleted file mode 100644 index 5e625e7a84..0000000000 --- a/src/Microsoft.IdentityModel.KeyVaultExtensions/InternalsVisibleTo.cs +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Microsoft.IdentityModel.KeyVaultExtensions.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] diff --git a/src/Microsoft.IdentityModel.KeyVaultExtensions/KeyVaultCryptoProvider.cs b/src/Microsoft.IdentityModel.KeyVaultExtensions/KeyVaultCryptoProvider.cs deleted file mode 100644 index 7eecfe8d82..0000000000 --- a/src/Microsoft.IdentityModel.KeyVaultExtensions/KeyVaultCryptoProvider.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Linq; -using Microsoft.Azure.KeyVault.WebKey; -using Microsoft.IdentityModel.Logging; -using Microsoft.IdentityModel.Tokens; - -namespace Microsoft.IdentityModel.KeyVaultExtensions -{ - /// - /// Provides cryptographic operators based on Azure Key Vault. - /// - public class KeyVaultCryptoProvider : ICryptoProvider - { - private readonly CryptoProviderCache _cache; - - /// - /// Initializes a new instance of the class. - /// - public KeyVaultCryptoProvider() - { - _cache = new InMemoryCryptoProviderCache(); - } - - /// - /// Gets the - /// - internal CryptoProviderCache CryptoProviderCache => _cache; - - /// - /// Returns a cryptographic operator that supports the algorithm. - /// - /// the algorithm that defines the cryptographic operator. - /// the arguments required by the cryptographic operator. May be null. - /// if is null or empty. - /// if is null. - /// if does not contain a . - /// call when finished with the object. - public object Create(string algorithm, params object[] args) - { - if (string.IsNullOrEmpty(algorithm)) - throw LogHelper.LogArgumentNullException(nameof(algorithm)); - - if (args == null) - throw LogHelper.LogArgumentNullException(nameof(args)); - - if (args.FirstOrDefault() is KeyVaultSecurityKey key) - { - if (JsonWebKeyEncryptionAlgorithm.AllAlgorithms.Contains(algorithm, StringComparer.Ordinal)) - return new KeyVaultKeyWrapProvider(key, algorithm); - else if (JsonWebKeySignatureAlgorithm.AllAlgorithms.Contains(algorithm, StringComparer.Ordinal)) - { - var willCreateSignatures = (bool)(args.Skip(1).FirstOrDefault() ?? false); - - if (_cache.TryGetSignatureProvider(key, algorithm, typeofProvider: key.GetType().ToString(), willCreateSignatures, out var cachedProvider)) - return cachedProvider; - - var signatureProvider = new KeyVaultSignatureProvider(key, algorithm, willCreateSignatures); - if (CryptoProviderFactory.ShouldCacheSignatureProvider(signatureProvider)) - _cache.TryAdd(signatureProvider); - - return signatureProvider; - } - } - - throw LogHelper.LogExceptionMessage(new NotSupportedException(LogHelper.FormatInvariant(LogMessages.IDX10652, LogHelper.MarkAsNonPII(algorithm)))); - } - - /// - /// Called to determine if a cryptographic operation is supported. - /// - /// the algorithm that defines the cryptographic operator. - /// the arguments required by the cryptographic operator. May be null. - /// true if supported - public bool IsSupportedAlgorithm(string algorithm, params object[] args) - { - if (string.IsNullOrEmpty(algorithm)) - throw LogHelper.LogArgumentNullException(nameof(algorithm)); - - if (args == null) - throw LogHelper.LogArgumentNullException(nameof(args)); - - return args.FirstOrDefault() is KeyVaultSecurityKey - && (JsonWebKeyEncryptionAlgorithm.AllAlgorithms.Contains(algorithm, StringComparer.Ordinal) || JsonWebKeySignatureAlgorithm.AllAlgorithms.Contains(algorithm, StringComparer.Ordinal)); - } - - /// - /// Called to release the object returned from - /// - /// the object returned from . - public void Release(object cryptoInstance) - { - if (cryptoInstance is SignatureProvider signatureProvider) - _cache.TryRemove(signatureProvider); - - if (cryptoInstance is IDisposable obj) - obj.Dispose(); - } - } -} diff --git a/src/Microsoft.IdentityModel.KeyVaultExtensions/KeyVaultKeyWrapProvider.cs b/src/Microsoft.IdentityModel.KeyVaultExtensions/KeyVaultKeyWrapProvider.cs deleted file mode 100644 index 63c625fe03..0000000000 --- a/src/Microsoft.IdentityModel.KeyVaultExtensions/KeyVaultKeyWrapProvider.cs +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Azure.KeyVault; -using Microsoft.IdentityModel.Logging; -using Microsoft.IdentityModel.Tokens; - -namespace Microsoft.IdentityModel.KeyVaultExtensions -{ - /// - /// Provides wrap and unwrap operations using Azure Key Vault. - /// - public class KeyVaultKeyWrapProvider : KeyWrapProvider - { - private readonly IKeyVaultClient _client; - private readonly KeyVaultSecurityKey _key; - private readonly string _algorithm; - private bool _disposed = false; - - /// - /// Initializes a new instance of the class. - /// - /// The that will be used for key wrap operations. - /// The key wrap algorithm to apply. - /// if is null. - /// if is not a . - /// if is null or empty. - public KeyVaultKeyWrapProvider(SecurityKey key, string algorithm) - : this(key, algorithm, null) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The that will be used for key wrap operations. - /// The key wrap algorithm to apply. - /// A mock used for testing purposes. - internal KeyVaultKeyWrapProvider(SecurityKey key, string algorithm, IKeyVaultClient? client) - { - _algorithm = string.IsNullOrEmpty(algorithm) ? throw LogHelper.LogArgumentNullException(nameof(algorithm)) : algorithm; - if (key == null) - throw LogHelper.LogArgumentNullException(nameof(key)); - - _key = key as KeyVaultSecurityKey ?? throw LogHelper.LogExceptionMessage(new NotSupportedException(key.GetType().ToString())); - _client = client ?? new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(_key.Callback!)); - } - - /// - /// Gets the KeyWrap algorithm that is being used. - /// - public override string Algorithm => _algorithm; - - /// - /// Gets or sets a user context for a . - /// - /// This is null by default. This can be used by runtimes or for extensibility scenarios. - public override string? Context { get; set; } - - /// - /// Gets the that is being used. - /// - public override SecurityKey Key => _key; - - /// - /// Unwrap a key. - /// - /// key to unwrap. - /// if is null. - /// if .Length == 0. - /// Unwrapped key. - public override byte[] UnwrapKey(byte[] keyBytes) - { - return UnwrapKeyAsync(keyBytes, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); - } - - /// - /// Wrap a key. - /// - /// the key to be wrapped - /// if is null. - /// if .Length == 0. - /// wrapped key. - public override byte[] WrapKey(byte[] keyBytes) - { - return WrapKeyAsync(keyBytes, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - /// true, if called from Dispose(), false, if invoked inside a finalizer - protected override void Dispose(bool disposing) - { - if (!_disposed) - { - if (disposing) - { - _disposed = true; - _client.Dispose(); - } - } - } - - /// - /// Unwraps a symmetric key using Azure Key Vault. - /// - /// key to unwrap. - /// Propagates notification that operations should be canceled. - /// if is null. - /// if .Length == 0. - /// Unwrapped key. - private async Task UnwrapKeyAsync(byte[] keyBytes, CancellationToken cancellation) - { - if (keyBytes == null || keyBytes.Length == 0) - throw LogHelper.LogArgumentNullException(nameof(keyBytes)); - - return (await _client.UnwrapKeyAsync(_key.KeyId, Algorithm, keyBytes, cancellation).ConfigureAwait(false)).Result; - } - - /// - /// Wraps a symmetric key using Azure Key Vault. - /// - /// the key to be wrapped - /// Propagates notification that operations should be canceled. - /// if is null. - /// if .Length == 0. - /// wrapped key. - private async Task WrapKeyAsync(byte[] keyBytes, CancellationToken cancellation) - { - if (keyBytes == null || keyBytes.Length == 0) - throw LogHelper.LogArgumentNullException(nameof(keyBytes)); - - return (await _client.WrapKeyAsync(_key.KeyId, Algorithm, keyBytes, cancellation).ConfigureAwait(false)).Result; - } - } -} diff --git a/src/Microsoft.IdentityModel.KeyVaultExtensions/KeyVaultSecurityKey.cs b/src/Microsoft.IdentityModel.KeyVaultExtensions/KeyVaultSecurityKey.cs deleted file mode 100644 index 9a56ce27e8..0000000000 --- a/src/Microsoft.IdentityModel.KeyVaultExtensions/KeyVaultSecurityKey.cs +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Azure.KeyVault; -using Microsoft.IdentityModel.Logging; -using Microsoft.IdentityModel.Tokens; - -namespace Microsoft.IdentityModel.KeyVaultExtensions -{ - /// - /// Provides signing and verifying operations using Azure Key Vault. - /// - public class KeyVaultSecurityKey : SecurityKey - { - private int? _keySize; - private string? _keyId; - - /// - /// The authentication callback delegate which is to be implemented by the client code. - /// - /// Identifier of the authority, a URL. - /// Identifier of the target resource that is the recipient of the requested token, a URL. - /// The scope of the authentication request. - /// An access token for Azure Key Vault. - public delegate Task AuthenticationCallback(string authority, string resource, string scope); - - /// - /// Initializes a new instance of the class. - /// - protected KeyVaultSecurityKey() - { - - } - - /// - /// Initializes a new instance of the class. - /// - /// The key identifier that is recognized by KeyVault. - /// The authentication callback that will obtain the access_token for KeyVault. - /// if is null or empty. - /// if is null. - public KeyVaultSecurityKey(string keyIdentifier, AuthenticationCallback callback) - { - Callback = callback ?? throw LogHelper.LogArgumentNullException(nameof(callback)); - KeyId = keyIdentifier; - } - - internal KeyVaultSecurityKey(string keyIdentifier, int keySize) - { - _keyId = keyIdentifier; - _keySize = keySize; - } - - /// - /// The authentication callback delegate that retrieves an access token for the KeyVault. - /// - public AuthenticationCallback? Callback { get; protected set; } - - /// - /// The uniform resource identifier of the security key. - /// - public override string KeyId - { - get => _keyId!; - set - { - if (string.IsNullOrEmpty(value)) - throw LogHelper.LogArgumentNullException(nameof(value)); - else if (StringComparer.Ordinal.Equals(_keyId, value)) - return; - - _keyId = value; - - // Reset the properties so they can be retrieved from Azure KeyVault the next time they are accessed. - _keySize = null; - } - } - - /// - /// The size of the security key. - /// - public override int KeySize - { - get - { - if (!_keySize.HasValue) - Initialize(); - - return _keySize!.Value; - } - } - - /// - /// Retrieve the properties from Azure Key Vault. - /// - private void Initialize() - { - using (var client = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(Callback!))) - { - var bundle = client.GetKeyAsync(_keyId, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); - _keySize = new BitArray(bundle.Key.N).Length; - } - } - } -} diff --git a/src/Microsoft.IdentityModel.KeyVaultExtensions/KeyVaultSignatureProvider.cs b/src/Microsoft.IdentityModel.KeyVaultExtensions/KeyVaultSignatureProvider.cs deleted file mode 100644 index 38478482fd..0000000000 --- a/src/Microsoft.IdentityModel.KeyVaultExtensions/KeyVaultSignatureProvider.cs +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Security.Cryptography; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Azure.KeyVault; -using Microsoft.IdentityModel.Logging; -using Microsoft.IdentityModel.Tokens; - -namespace Microsoft.IdentityModel.KeyVaultExtensions -{ - /// - /// Provides signing and verifying operations using Azure Key Vault. - /// - public class KeyVaultSignatureProvider : SignatureProvider - { - private readonly HashAlgorithm _hash; - private readonly IKeyVaultClient _client; - private readonly KeyVaultSecurityKey _key; - private bool _disposed = false; - - /// - /// Initializes a new instance of the class. - /// - /// The that will be used for signature operations. - /// The signature algorithm to apply. - /// Whether this is required to create signatures then set this to true. - /// is null. - /// is null or empty. - public KeyVaultSignatureProvider(SecurityKey key, string algorithm, bool willCreateSignatures) - : this(key, algorithm, willCreateSignatures, null) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The that will be used for signature operations. - /// The signature algorithm to apply. - /// Whether this is required to create signatures then set this to true. - /// A mock used for testing purposes. - internal KeyVaultSignatureProvider(SecurityKey key, string algorithm, bool willCreateSignatures, IKeyVaultClient? client) - : base(key, algorithm) - { - _key = key as KeyVaultSecurityKey ?? throw LogHelper.LogArgumentNullException(nameof(key)); - _client = client ?? new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(_key.Callback!)); - WillCreateSignatures = willCreateSignatures; - - switch (algorithm) - { - case SecurityAlgorithms.RsaSha256: - _hash = SHA256.Create(); - break; - case SecurityAlgorithms.RsaSha384: - _hash = SHA384.Create(); - break; - case SecurityAlgorithms.RsaSha512: - _hash = SHA512.Create(); - break; - default: - throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(LogMessages.IDX10652, LogHelper.MarkAsNonPII(algorithm)), nameof(algorithm))); - } - } - - /// - /// Produces a signature over the 'input' using Azure Key Vault. - /// - /// The bytes to sign. - /// A signature over the input. - /// if is null. - /// if .Length == 0. - /// If Dispose has been called. - public override byte[] Sign(byte[] input) - { - return SignAsync(input, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); - } - - /// - /// Verifies that the is over using Azure Key Vault. - /// - /// bytes to verify. - /// signature to compare against. - /// true if the computed signature matches the signature parameter, false otherwise. - /// is null or has length == 0. - /// is null or has length == 0. - /// If Dispose has been called. - public override bool Verify(byte[] input, byte[] signature) - { - return VerifyAsync(input, signature, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); - } - - /// - public override bool Verify(byte[] input, int inputOffset, int lengthOffset, byte[] signature, int signatureOffset, int signatureLength) => throw new NotImplementedException(); - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - /// true, if called from Dispose(), false, if invoked inside a finalizer - protected override void Dispose(bool disposing) - { - if (!_disposed) - { - if (disposing) - { - _disposed = true; - _hash.Dispose(); - _client.Dispose(); - } - } - } - - /// - /// Creates a digital signature using Azure Key Vault. - /// - /// bytes to sign. - /// Propagates notification that operations should be canceled. - /// A signature over the input. - /// is null or has length == 0. - /// If Dispose has been called. - private async Task SignAsync(byte[] input, CancellationToken cancellation) - { - if (input == null || input.Length == 0) - throw LogHelper.LogArgumentNullException(nameof(input)); - - if (_disposed) - throw LogHelper.LogExceptionMessage(new ObjectDisposedException(GetType().ToString())); - - return (await _client.SignAsync(_key.KeyId, Algorithm, _hash.ComputeHash(input), cancellation).ConfigureAwait(false)).Result; - } - - /// - /// Verifies a digital signature using Azure Key Vault. - /// - /// bytes to verify. - /// signature to compare against. - /// Propagates notification that operations should be canceled. - /// true if the computed signature matches the signature parameter, false otherwise. - /// is null or has length == 0. - /// is null or has length == 0. - /// If Dispose has been called. - private async Task VerifyAsync(byte[] input, byte[] signature, CancellationToken cancellation) - { - if (input == null || input.Length == 0) - throw LogHelper.LogArgumentNullException(nameof(input)); - - if (signature == null || signature.Length == 0) - throw LogHelper.LogArgumentNullException(nameof(signature)); - - if (_disposed) - throw LogHelper.LogExceptionMessage(new ObjectDisposedException(GetType().ToString())); - - return await _client.VerifyAsync(_key.KeyId, Algorithm, _hash.ComputeHash(input), signature, cancellation).ConfigureAwait(false); - } - } -} diff --git a/src/Microsoft.IdentityModel.KeyVaultExtensions/Microsoft.IdentityModel.KeyVaultExtensions.csproj b/src/Microsoft.IdentityModel.KeyVaultExtensions/Microsoft.IdentityModel.KeyVaultExtensions.csproj deleted file mode 100644 index 2825f85730..0000000000 --- a/src/Microsoft.IdentityModel.KeyVaultExtensions/Microsoft.IdentityModel.KeyVaultExtensions.csproj +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - Microsoft.IdentityModel.KeyVaultExtensions - Includes types that provide support for signing and encrypting tokens with Azure Key Vault. - true - Microsoft.IdentityModel.KeyVaultExtensions - netstandard2.0;net6.0;net8.0 - $(TargetFrameworks);net9.0 - .NET;Windows;Authentication;Identity;Azure;Key;Vault;Extensions - enable - - - - full - true - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - diff --git a/src/Microsoft.IdentityModel.KeyVaultExtensions/Properties/AssemblyInfo.cs b/src/Microsoft.IdentityModel.KeyVaultExtensions/Properties/AssemblyInfo.cs deleted file mode 100644 index 1f06ec3782..0000000000 --- a/src/Microsoft.IdentityModel.KeyVaultExtensions/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Reflection; -using System.Runtime.InteropServices; - -[assembly: AssemblyMetadata("Serviceable", "True")] -[assembly: CLSCompliant(true)] -[assembly: ComVisible(false)] diff --git a/src/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey/GlobalSuppression.cs b/src/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey/GlobalSuppression.cs deleted file mode 100644 index 04498d0dc3..0000000000 --- a/src/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey/GlobalSuppression.cs +++ /dev/null @@ -1,8 +0,0 @@ -// This file is used by Code Analysis to maintain SuppressMessage -// attributes that are applied to this project. -// Project-level suppressions either have no target or are given -// a specific target and scoped to a namespace, type, member, etc. - -using System.Diagnostics.CodeAnalysis; - -[assembly: SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Previously released with this name", Scope ="Type", Target = "Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.ManagedKeyVaultSecurityKey")] diff --git a/src/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey/InternalsVisibleTo.cs b/src/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey/InternalsVisibleTo.cs deleted file mode 100644 index cae70d8a10..0000000000 --- a/src/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey/InternalsVisibleTo.cs +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Microsoft.IdentityModel.Tokens.Extensions.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] diff --git a/src/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey/ManagedKeyVaultSecurityKey.cs b/src/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey/ManagedKeyVaultSecurityKey.cs deleted file mode 100644 index 78ce01f9ee..0000000000 --- a/src/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey/ManagedKeyVaultSecurityKey.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.Azure.Services.AppAuthentication; -using Microsoft.IdentityModel.Clients.ActiveDirectory; -using Microsoft.IdentityModel.KeyVaultExtensions; -using Microsoft.IdentityModel.Logging; -using System; - -namespace Microsoft.IdentityModel.ManagedKeyVaultSecurityKey -{ - /// - /// Provides signing and verifying operations using Azure Key Vault - /// for resources that are using Managed identities for Azure resources. - /// - public class ManagedKeyVaultSecurityKey : KeyVaultSecurityKey - { - /// - /// Initializes a new instance of the class. - /// - /// The key identifier that is recognized by KeyVault. - /// if is null or empty. - public ManagedKeyVaultSecurityKey(string keyIdentifier) - : base(keyIdentifier, new AuthenticationCallback((new AzureServiceTokenProvider()).KeyVaultTokenCallback)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The key identifier that is recognized by KeyVault. - /// The authentication callback. - /// if is null or empty. - /// if is null. - public ManagedKeyVaultSecurityKey(string keyIdentifier, AuthenticationCallback callback) - : base(keyIdentifier, callback) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The key identifier. - /// Identifier of the client. - /// Secret of the client identity. - /// if is null or empty. - /// if is null or empty. - /// if is null or clientSecret. - public ManagedKeyVaultSecurityKey(string keyIdentifier, string clientId, string clientSecret) - { - if (string.IsNullOrEmpty(keyIdentifier)) - throw LogHelper.LogArgumentNullException(nameof(keyIdentifier)); - - if (string.IsNullOrEmpty(clientId)) - throw LogHelper.LogArgumentNullException(nameof(clientId)); - - if (string.IsNullOrEmpty(clientSecret)) - throw LogHelper.LogArgumentNullException(nameof(clientSecret)); - - KeyId = keyIdentifier; - Callback = new AuthenticationCallback(async (string authority, string resource, string scope) => - (await (new AuthenticationContext(authority, TokenCache.DefaultShared)).AcquireTokenAsync(resource, new ClientCredential(clientId, clientSecret)).ConfigureAwait(false)).AccessToken); - } - } -} diff --git a/src/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.csproj b/src/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.csproj deleted file mode 100644 index aa342135ca..0000000000 --- a/src/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.csproj +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - Microsoft.IdentityModel.ManagedKeyVaultSecurityKey - Includes types that provide support for signing and encrypting tokens with Azure Key Vault for - Applications that are using Managed identities for Azure resources. - true - Microsoft.IdentityModel.ManagedKeyVaultSecurityKey - netstandard2.0 - .NET;Windows;Authentication;Identity;Azure;Key;Vault;Extensions - enable - - - - full - true - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - diff --git a/src/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey/Properties/AssemblyInfo.cs b/src/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey/Properties/AssemblyInfo.cs deleted file mode 100644 index 1f06ec3782..0000000000 --- a/src/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Reflection; -using System.Runtime.InteropServices; - -[assembly: AssemblyMetadata("Serviceable", "True")] -[assembly: CLSCompliant(true)] -[assembly: ComVisible(false)] diff --git a/src/Microsoft.IdentityModel.Tokens/InternalsVisibleTo.cs b/src/Microsoft.IdentityModel.Tokens/InternalsVisibleTo.cs index ba34f3462b..dd03b90595 100644 --- a/src/Microsoft.IdentityModel.Tokens/InternalsVisibleTo.cs +++ b/src/Microsoft.IdentityModel.Tokens/InternalsVisibleTo.cs @@ -1,14 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Microsoft.IdentityModel.KeyVaultExtensions, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] -[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Microsoft.IdentityModel.TestUtils, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CrossVersionTokenValidation.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Microsoft.IdentityModel.Tokens.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Microsoft.IdentityModel.JsonWebTokens, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Microsoft.IdentityModel.JsonWebTokens.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Microsoft.IdentityModel.Protocols.OpenIdConnect, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Microsoft.IdentityModel.TestUtils, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("System.IdentityModel.Tokens.Jwt, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("System.IdentityModel.Tokens.Jwt.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Microsoft.IdentityModel.S2S, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] diff --git a/src/Microsoft.IdentityModel.Tokens/Properties/AssemblyInfo.cs b/src/Microsoft.IdentityModel.Tokens/Properties/AssemblyInfo.cs index f22a0f4b5e..12d7bcb55f 100644 --- a/src/Microsoft.IdentityModel.Tokens/Properties/AssemblyInfo.cs +++ b/src/Microsoft.IdentityModel.Tokens/Properties/AssemblyInfo.cs @@ -17,7 +17,6 @@ [assembly: InternalsVisibleTo("Microsoft.IdentityModel.Tokens.Saml, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] [assembly: InternalsVisibleTo("Microsoft.IdentityModel.Xml, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] [assembly: InternalsVisibleTo("Microsoft.AzureAD.SmartSessionEvaluator, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] -[assembly: InternalsVisibleTo("Microsoft.IdentityModel.KeyVaultExtensions.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] [assembly: InternalsVisibleTo("Microsoft.IdentityModel.Protocols, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] [assembly: InternalsVisibleTo("Microsoft.IdentityModel.Protocols.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] [assembly: InternalsVisibleTo("Microsoft.IdentityModel.Validators, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/Microsoft.IdentityModel.JsonWebTokens.Tests.csproj b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/Microsoft.IdentityModel.JsonWebTokens.Tests.csproj index e333ac1f95..523c78d729 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/Microsoft.IdentityModel.JsonWebTokens.Tests.csproj +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/Microsoft.IdentityModel.JsonWebTokens.Tests.csproj @@ -20,6 +20,10 @@ + + + + diff --git a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultCryptoProviderTests.cs b/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultCryptoProviderTests.cs deleted file mode 100644 index 39e83e852c..0000000000 --- a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultCryptoProviderTests.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Threading.Tasks; -using Microsoft.Azure.KeyVault.WebKey; -using Microsoft.IdentityModel.TestUtils; -using Microsoft.IdentityModel.Tokens; -using Xunit; -using static Microsoft.IdentityModel.KeyVaultExtensions.KeyVaultSecurityKey; - -namespace Microsoft.IdentityModel.KeyVaultExtensions.Tests -{ - public class KeyVaultCryptoProviderTests - { - [Fact] - public void ShouldCacheSignatureProvider() - { - TestUtilities.WriteHeader($"{this}.ShouldCacheSignatureProvider"); - var context = new CompareContext($"{this}.ShouldCacheSignatureProvider"); - var keyVaultKeyWithEmptyKid = new CustomKeyVaultSecurityKey("test", new AuthenticationCallback((string authority, string resource, string scope) => Task.FromResult(string.Empty))); - var keyVaultCryptoProvider = new KeyVaultCryptoProvider(); - var signatureProvider = keyVaultCryptoProvider.Create(JsonWebKeySignatureAlgorithm.RS256, keyVaultKeyWithEmptyKid, true); - if (keyVaultCryptoProvider.CryptoProviderCache.TryGetSignatureProvider(keyVaultKeyWithEmptyKid, SecurityAlgorithms.RsaSha256Signature, typeof(KeyVaultSignatureProvider).ToString(), true, out var _)) - context.Diffs.Add("A SignatureProvider was added to keyVaultCryptoProvider.CryptoProviderCache.CryptoProviderCache, but ShouldCacheSignatureProvider() should return false as the key has an empty key id."); - - CryptoProviderFactory.Default.ReleaseSignatureProvider(signatureProvider as KeyVaultSignatureProvider); - - TestUtilities.AssertFailIfErrors(context); - } - - public class CustomKeyVaultSecurityKey : KeyVaultSecurityKey - { - /// - /// Initializes a new instance of the class. - /// - public CustomKeyVaultSecurityKey(string keyIdentifier, AuthenticationCallback callback) : base(keyIdentifier, callback) - { - } - - internal override string InternalId => ""; - } - } -} diff --git a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultKeyWrapProviderTests.cs b/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultKeyWrapProviderTests.cs deleted file mode 100644 index c16f6cdefa..0000000000 --- a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultKeyWrapProviderTests.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using Microsoft.Azure.KeyVault; -using Microsoft.IdentityModel.TestUtils; -using Microsoft.IdentityModel.Tokens; -using Xunit; - -#pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant - -namespace Microsoft.IdentityModel.KeyVaultExtensions.Tests -{ - public class KeyVaultKeyWrapProviderTests - { - private readonly MockKeyVaultClient _client; - private readonly SecurityKey _key; - - public KeyVaultKeyWrapProviderTests() - { - _client = new MockKeyVaultClient(); - _key = new KeyVaultSecurityKey(KeyVaultUtilities.CreateKeyIdentifier(), keySize: default); - } - - [Theory, MemberData(nameof(DisposeProviderTheoryData))] - public void DisposeProviderTest(KeyWrapProviderTheoryData theoryData) - { - var context = TestUtilities.WriteHeader($"{this}.DisposeProviderTest", theoryData); - - try - { - var provider = new KeyVaultKeyWrapProvider(_key, theoryData.Algorithm, _client); - _key.CryptoProviderFactory.ReleaseKeyWrapProvider(provider); - - theoryData.ExpectedException.ProcessNoException(context); - } - catch (Exception exception) - { - theoryData.ExpectedException.ProcessException(exception, context); - } - - TestUtilities.AssertFailIfErrors(context); - } - - public static TheoryData DisposeProviderTheoryData - { - get => new TheoryData - { - new KeyWrapProviderTheoryData - { - Algorithm = SecurityAlgorithms.RsaPKCS1, - ExpectedException = ExpectedException.NoExceptionExpected, - First = true, - TestId = nameof(SecurityAlgorithms.RsaPKCS1), - }, - new KeyWrapProviderTheoryData - { - Algorithm = SecurityAlgorithms.RsaOAEP, - ExpectedException = ExpectedException.NoExceptionExpected, - TestId = nameof(SecurityAlgorithms.RsaOAEP), - }, - }; - } - - [Theory, MemberData(nameof(KeyWrapProviderTheoryData))] - public void WrapUnwrapKeyTest(KeyWrapProviderTheoryData theoryData) - { - var context = TestUtilities.WriteHeader($"{this}.WrapUnwrapKeyTest", theoryData); - - try - { - var provider = new KeyVaultKeyWrapProvider(_key, theoryData.Algorithm, _client); - if (provider == null) - context.AddDiff("(provider == null)"); - - var keyBytes = Guid.NewGuid().ToByteArray(); - var wrappedKey = provider.WrapKey(keyBytes); - if (wrappedKey == null) - context.AddDiff("(wrappedKey == null)"); - - if (_client.ExpectedKeyWrapLength != wrappedKey.Length) - context.AddDiff($"_client.ExpectedKeyWrapLength != wrappedKey.Length. {_client.ExpectedKeyWrapLength} != {wrappedKey.Length}"); - - if (Utility.AreEqual(keyBytes, wrappedKey)) - context.AddDiff("Utility.AreEqual(keyBytes, wrappedKey)"); - - var unwrappedKey = provider.UnwrapKey(wrappedKey); - if (unwrappedKey == null) - context.AddDiff("(unwrappedKey == null)"); - - IdentityComparer.AreBytesEqual(keyBytes, unwrappedKey, context); - - theoryData.ExpectedException.ProcessNoException(context); - } - catch (Exception exception) - { - theoryData.ExpectedException.ProcessException(exception, context); - } - - TestUtilities.AssertFailIfErrors(context); - } - - public static TheoryData KeyWrapProviderTheoryData - { - get => new TheoryData - { - new KeyWrapProviderTheoryData - { - Algorithm = null, - ExpectedException = ExpectedException.ArgumentNullException(), - First = true, - TestId = "NullAlgorithm", - }, - new KeyWrapProviderTheoryData - { - Algorithm = string.Empty, - ExpectedException = ExpectedException.ArgumentNullException(), - TestId = "EmptyAlgorithm", - }, - new KeyWrapProviderTheoryData - { - Algorithm = SecurityAlgorithms.RsaPKCS1, - TestId = nameof(SecurityAlgorithms.RsaPKCS1), - }, - new KeyWrapProviderTheoryData - { - Algorithm = SecurityAlgorithms.RsaOAEP, - TestId = nameof(SecurityAlgorithms.RsaOAEP), - }, - }; - } - } -} - -#pragma warning restore CS3016 // Arrays as attribute arguments is not CLS-compliant diff --git a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultSecurityKeyAuthenticationCallbackTheoryData.cs b/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultSecurityKeyAuthenticationCallbackTheoryData.cs deleted file mode 100644 index 39747206c6..0000000000 --- a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultSecurityKeyAuthenticationCallbackTheoryData.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Threading.Tasks; - -namespace Microsoft.IdentityModel.KeyVaultExtensions.Tests -{ - public class KeyVaultSecurityKeyAuthenticationCallbackTheoryData : KeyVaultSecurityKeyTheoryData - { - public KeyVaultSecurityKey.AuthenticationCallback Callback { get; set; } = new KeyVaultSecurityKey.AuthenticationCallback((string authority, string resource, string scope) => Task.FromResult(string.Empty)); - } -} diff --git a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultSecurityKeyConfidentialClientTheoryData.cs b/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultSecurityKeyConfidentialClientTheoryData.cs deleted file mode 100644 index 75e3f1bca9..0000000000 --- a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultSecurityKeyConfidentialClientTheoryData.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -namespace Microsoft.IdentityModel.KeyVaultExtensions.Tests -{ - public class KeyVaultSecurityKeyConfidentialClientTheoryData : KeyVaultSecurityKeyTheoryData - { - public string ClientId { get; set; } = $"{Guid.NewGuid():D}"; - public string ClientSecret { get; set; } = Guid.NewGuid().ToString(); - } -} diff --git a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultSecurityKeyManagedServiceIdentityTheoryData.cs b/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultSecurityKeyManagedServiceIdentityTheoryData.cs deleted file mode 100644 index f5c096fa16..0000000000 --- a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultSecurityKeyManagedServiceIdentityTheoryData.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Microsoft.IdentityModel.KeyVaultExtensions.Tests -{ - public class KeyVaultSecurityKeyManagedServiceIdentityTheoryData : KeyVaultSecurityKeyTheoryData - { - } -} diff --git a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultSecurityKeyTests.cs b/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultSecurityKeyTests.cs deleted file mode 100644 index 9badfe1f52..0000000000 --- a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultSecurityKeyTests.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Reflection; -using Microsoft.Azure.KeyVault.Models; -using Microsoft.IdentityModel.TestUtils; -using Xunit; - -#pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant - -namespace Microsoft.IdentityModel.KeyVaultExtensions.Tests -{ - public class KeyVaultSecurityKeyTests - { - private static ExpectedException ArgumentNullExceptionExpected = new ExpectedException(typeExpected: typeof(TargetInvocationException), substringExpected: "Exception has been thrown by the target of an invocation.", innerTypeExpected: typeof(ArgumentNullException)); - private static ExpectedException KeyVaultErrorExceptionExpected = new ExpectedException(typeExpected: typeof(TargetInvocationException), substringExpected: "Exception has been thrown by the target of an invocation.", innerTypeExpected: typeof(KeyVaultErrorException)); - - [Theory, MemberData(nameof(KeyVaultSecurityKeyAuthenticationCallbackTheoryData))] - public void AuthenticationCallbackConstructorParams(KeyVaultSecurityKeyAuthenticationCallbackTheoryData theoryData) - { - var context = TestUtilities.WriteHeader($"{this}.AuthenticationCallbackConstructorParams", theoryData); - - try - { - _ = Activator.CreateInstance(theoryData.Type, new object[] { theoryData.KeyIdentifier, theoryData.Callback }); - } - catch (Exception ex) - { - theoryData.ExpectedException.ProcessException(ex, context); - } - } - - public static TheoryData KeyVaultSecurityKeyAuthenticationCallbackTheoryData - { - get => new TheoryData - { - new KeyVaultSecurityKeyAuthenticationCallbackTheoryData - { - // Callback = default, - ExpectedException = ExpectedException.ArgumentNullException(), - First = true, - KeyIdentifier = null, - TestId = typeof(KeyVaultSecurityKey).FullName, - Type = typeof(KeyVaultSecurityKey), - }, - new KeyVaultSecurityKeyAuthenticationCallbackTheoryData - { - // Callback = default, - ExpectedException = ExpectedException.ArgumentNullException(), - KeyIdentifier = string.Empty, - TestId = typeof(KeyVaultSecurityKey).FullName, - Type = typeof(KeyVaultSecurityKey), - }, - new KeyVaultSecurityKeyAuthenticationCallbackTheoryData - { - Callback = null, - ExpectedException = ExpectedException.ArgumentNullException(), - // KeyIdentifier = default, - TestId = typeof(KeyVaultSecurityKey).FullName, - Type = typeof(KeyVaultSecurityKey), - }, - new KeyVaultSecurityKeyAuthenticationCallbackTheoryData - { - // Callback = default, - ExpectedException = KeyVaultErrorExceptionExpected, - // KeyIdentifier = default, - TestId = typeof(KeyVaultSecurityKey).FullName, - Type = typeof(KeyVaultSecurityKey), - }, - }; - } - } -} - -#pragma warning restore CS3016 // Arrays as attribute arguments is not CLS-compliant diff --git a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultSecurityKeyTheoryData.cs b/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultSecurityKeyTheoryData.cs deleted file mode 100644 index 73dd0a20c8..0000000000 --- a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultSecurityKeyTheoryData.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using Microsoft.IdentityModel.TestUtils; - -namespace Microsoft.IdentityModel.KeyVaultExtensions.Tests -{ - public abstract class KeyVaultSecurityKeyTheoryData : TheoryDataBase - { - public string KeyIdentifier { get; set; } = KeyVaultUtilities.CreateKeyIdentifier(); - public Type Type { get; set; } - } -} diff --git a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultSignatureProviderTests.cs b/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultSignatureProviderTests.cs deleted file mode 100644 index 7ab512625c..0000000000 --- a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultSignatureProviderTests.cs +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Linq; -using Microsoft.Azure.KeyVault; -using Microsoft.IdentityModel.TestUtils; -using Microsoft.IdentityModel.Tokens; -using Microsoft.IdentityModel.KeyVaultExtensions; -using Xunit; - -#pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant - -namespace Microsoft.IdentityModel.KeyVaultExtensions.Tests -{ - public class KeyVaultSignatureProviderTests - { - private readonly MockKeyVaultClient _client; - private readonly SecurityKey _key; - - public KeyVaultSignatureProviderTests() - { - _client = new MockKeyVaultClient(); - _key = new KeyVaultSecurityKey(KeyVaultUtilities.CreateKeyIdentifier(), keySize: default); - } - - [Theory, MemberData(nameof(DisposeProviderTheoryData))] - public void DisposeProviderTest(SignatureProviderTheoryData theoryData) - { - var context = TestUtilities.WriteHeader($"{this}.DisposeProviderTest", theoryData); - - try - { - var provider = new KeyVaultSignatureProvider(_key, theoryData.Algorithm, willCreateSignatures: true, _client); - _key.CryptoProviderFactory.ReleaseSignatureProvider(provider); - - theoryData.ExpectedException.ProcessNoException(context); - } - catch (Exception exception) - { - theoryData.ExpectedException.ProcessException(exception, context); - } - - TestUtilities.AssertFailIfErrors(context); - } - - public static TheoryData DisposeProviderTheoryData - { - get => new TheoryData - { - new SignatureProviderTheoryData - { - Algorithm = SecurityAlgorithms.RsaSha256, - ExpectedException = ExpectedException.NoExceptionExpected, - First = true, - TestId = nameof(SecurityAlgorithms.RsaSha256), - }, - new SignatureProviderTheoryData - { - Algorithm = SecurityAlgorithms.RsaSha384, - ExpectedException = ExpectedException.NoExceptionExpected, - TestId = nameof(SecurityAlgorithms.RsaSha384), - }, - new SignatureProviderTheoryData - { - Algorithm = SecurityAlgorithms.RsaSha512, - ExpectedException = ExpectedException.NoExceptionExpected, - TestId = nameof(SecurityAlgorithms.RsaSha512), - }, - }; - } - - [Theory, MemberData(nameof(SignatureProviderTheoryData))] - public void SignatureTest(SignatureProviderTheoryData theoryData) - { - var context = TestUtilities.WriteHeader($"{this}.SignatureTest", theoryData); - - try - { - var provider = new KeyVaultSignatureProvider(_key, theoryData.Algorithm, willCreateSignatures: true, _client); - if (provider == null) - context.AddDiff("(provider == null)"); - - var input = Guid.NewGuid().ToByteArray(); - var signature = provider.Sign(input); - - if (signature == null) - context.AddDiff("(signature == null)"); - - if (_client.ExpectedSignatureLength != signature.Length) - context.AddDiff($"_client.ExpectedSignatureLength != signature.Length. == {_client.ExpectedSignatureLength}, {signature.Length}."); - - if (!provider.Verify(input, signature)) - context.AddDiff("!provider.Verify(input, signature)"); - - var tamperedInput = new byte[input.Length]; - input.CopyTo(tamperedInput, 0); - if (tamperedInput[0] == byte.MaxValue) - tamperedInput[0]--; - else - tamperedInput[0]++; - - if (provider.Verify(tamperedInput, signature)) - context.AddDiff("provider.Verify(tamperedInput, signature)"); - - foreach (var data in SignatureProviderTheoryData) - { - var newAlgorithm = (data.Single() as SignatureProviderTheoryData)?.Algorithm; - if (string.IsNullOrEmpty(newAlgorithm)) - continue; // Skip invalid input - - // Check that a given Security Key will only validate a signature using the same hash algorithm. - var isValidSignature = new KeyVaultSignatureProvider(_key, newAlgorithm, willCreateSignatures: false, _client).Verify(input, signature); - if (StringComparer.Ordinal.Equals(theoryData.Algorithm, newAlgorithm)) - { - if (!isValidSignature) - context.AddDiff("Signature should have been valid, isValidSignature == false"); - } - else if (isValidSignature) - context.AddDiff("Signature should NOT have been valid, isValidSignature == true"); - } - - theoryData.ExpectedException.ProcessNoException(context); - } - catch (Exception exception) - { - theoryData.ExpectedException.ProcessException(exception, context); - } - - TestUtilities.AssertFailIfErrors(context); - } - - public static TheoryData SignatureProviderTheoryData - { - get => new TheoryData - { - new SignatureProviderTheoryData - { - Algorithm = null, - ExpectedException = ExpectedException.ArgumentNullException(), - First = true, - TestId = "NullAlgorithm", - }, - new SignatureProviderTheoryData - { - Algorithm = string.Empty, - ExpectedException = ExpectedException.ArgumentNullException(), - TestId = "EmptyAlgorithm", - }, - new SignatureProviderTheoryData - { - Algorithm = SecurityAlgorithms.RsaSha256, - TestId = nameof(SecurityAlgorithms.RsaSha256), - }, - new SignatureProviderTheoryData - { - Algorithm = SecurityAlgorithms.RsaSha384, - TestId = nameof(SecurityAlgorithms.RsaSha384), - }, - new SignatureProviderTheoryData - { - Algorithm = SecurityAlgorithms.RsaSha512, - TestId = nameof(SecurityAlgorithms.RsaSha512), - }, - }; - } - } -} - -#pragma warning restore CS3016 // Arrays as attribute arguments is not CLS-compliant diff --git a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultUtilities.cs b/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultUtilities.cs deleted file mode 100644 index 072a868068..0000000000 --- a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyVaultUtilities.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -namespace Microsoft.IdentityModel.KeyVaultExtensions.Tests -{ - public static class KeyVaultUtilities - { - public static string CreateKeyIdentifier() => CreateKeyIdentifier("contoso.vault.azure.net", nameof(KeyVaultUtilities), $"{Guid.NewGuid():N}"); - - public static string CreateKeyIdentifier(string vaultBaseUrl, string vaultKeyName, string vaultKeyVersion) - { - return new UriBuilder(Uri.UriSchemeHttps, vaultBaseUrl, -1, $"/keys/{vaultKeyName}/{vaultKeyVersion}").Uri.ToString(); - } - } -} diff --git a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyWrapProviderTheoryData.cs b/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyWrapProviderTheoryData.cs deleted file mode 100644 index 5e8ab0f194..0000000000 --- a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/KeyWrapProviderTheoryData.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.IdentityModel.TestUtils; - -namespace Microsoft.IdentityModel.KeyVaultExtensions.Tests -{ - public class KeyWrapProviderTheoryData : TheoryDataBase - { - public string Algorithm { get; set; } - } -} diff --git a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/Microsoft.IdentityModel.KeyVaultExtensions.Tests.csproj b/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/Microsoft.IdentityModel.KeyVaultExtensions.Tests.csproj deleted file mode 100644 index 3c797af7a1..0000000000 --- a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/Microsoft.IdentityModel.KeyVaultExtensions.Tests.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - Microsoft.IdentityModel.KeyVaultExtensions.Tests - $(MSBuildThisFileDirectory)..\..\build\35MSSharedLib1024.snk - true - Tests for Microsoft.IdentityModel.KeyVaultExtensions - true - Microsoft.IdentityModel.KeyVaultExtensions.Tests - true - - - - - - - - - - - - diff --git a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/MockKeyVaultClient.cs b/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/MockKeyVaultClient.cs deleted file mode 100644 index 8fcbc31691..0000000000 --- a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/MockKeyVaultClient.cs +++ /dev/null @@ -1,622 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Security.Cryptography; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Azure.KeyVault; -using Microsoft.Azure.KeyVault.Models; -using Microsoft.IdentityModel.TestUtils; -using Microsoft.IdentityModel.Tokens; -using Microsoft.Rest; -using Microsoft.Rest.Azure; -using Newtonsoft.Json; - -namespace Microsoft.IdentityModel.KeyVaultExtensions.Tests -{ - [CLSCompliant(false)] - public class MockKeyVaultClient : IKeyVaultClient - { - private readonly Microsoft.Azure.KeyVault.WebKey.JsonWebKey _key; - private readonly RSACryptoServiceProvider _rsa; - private bool _disposed = false; - - public MockKeyVaultClient() - { - _rsa = new RSACryptoServiceProvider(); - _rsa.ImportParameters(KeyingMaterial.RsaParameters_2048); - _key = new Microsoft.Azure.KeyVault.WebKey.JsonWebKey(_rsa, includePrivateParameters: false); - } - - public int ExpectedKeyWrapLength => 256; - - public int ExpectedSignatureLength => 256; - - public JsonSerializerSettings SerializationSettings => throw new NotImplementedException(); - - public JsonSerializerSettings DeserializationSettings => throw new NotImplementedException(); - - public ServiceClientCredentials Credentials => throw new NotImplementedException(); - - public string ApiVersion => throw new NotImplementedException(); - - public string AcceptLanguage { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - - public int? LongRunningOperationRetryTimeout { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - - public bool? GenerateClientRequestId { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - - public Task> BackupCertificateWithHttpMessagesAsync(string vaultBaseUrl, string certificateName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> BackupKeyWithHttpMessagesAsync(string vaultBaseUrl, string keyName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> BackupSecretWithHttpMessagesAsync(string vaultBaseUrl, string secretName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> BackupStorageAccountWithHttpMessagesAsync(string vaultBaseUrl, string storageAccountName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> CreateCertificateWithHttpMessagesAsync(string vaultBaseUrl, string certificateName, CertificatePolicy certificatePolicy = null, CertificateAttributes certificateAttributes = null, IDictionary tags = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> CreateKeyWithHttpMessagesAsync(string vaultBaseUrl, string keyName, string kty, int? keySize = null, IList keyOps = null, KeyAttributes keyAttributes = null, IDictionary tags = null, string curve = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> DecryptWithHttpMessagesAsync(string vaultBaseUrl, string keyName, string keyVersion, string algorithm, byte[] value, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> DeleteCertificateContactsWithHttpMessagesAsync(string vaultBaseUrl, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> DeleteCertificateIssuerWithHttpMessagesAsync(string vaultBaseUrl, string issuerName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> DeleteCertificateOperationWithHttpMessagesAsync(string vaultBaseUrl, string certificateName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> DeleteCertificateWithHttpMessagesAsync(string vaultBaseUrl, string certificateName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> DeleteKeyWithHttpMessagesAsync(string vaultBaseUrl, string keyName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> DeleteSasDefinitionWithHttpMessagesAsync(string vaultBaseUrl, string storageAccountName, string sasDefinitionName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> DeleteSecretWithHttpMessagesAsync(string vaultBaseUrl, string secretName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> DeleteStorageAccountWithHttpMessagesAsync(string vaultBaseUrl, string storageAccountName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - /// - /// Calls and - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (!_disposed) - { - _disposed = true; - if (disposing) - { - _rsa.Dispose(); - } - } - } - - public Task> EncryptWithHttpMessagesAsync(string vaultBaseUrl, string keyName, string keyVersion, string algorithm, byte[] value, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> GetCertificateContactsWithHttpMessagesAsync(string vaultBaseUrl, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetCertificateIssuersNextWithHttpMessagesAsync(string nextPageLink, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetCertificateIssuersWithHttpMessagesAsync(string vaultBaseUrl, int? maxresults = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> GetCertificateIssuerWithHttpMessagesAsync(string vaultBaseUrl, string issuerName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> GetCertificateOperationWithHttpMessagesAsync(string vaultBaseUrl, string certificateName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> GetCertificatePolicyWithHttpMessagesAsync(string vaultBaseUrl, string certificateName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetCertificatesNextWithHttpMessagesAsync(string nextPageLink, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetCertificatesWithHttpMessagesAsync(string vaultBaseUrl, int? maxresults = null, bool? includePending = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetCertificateVersionsNextWithHttpMessagesAsync(string nextPageLink, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetCertificateVersionsWithHttpMessagesAsync(string vaultBaseUrl, string certificateName, int? maxresults = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> GetCertificateWithHttpMessagesAsync(string vaultBaseUrl, string certificateName, string certificateVersion, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetDeletedCertificatesNextWithHttpMessagesAsync(string nextPageLink, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetDeletedCertificatesWithHttpMessagesAsync(string vaultBaseUrl, int? maxresults = null, bool? includePending = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> GetDeletedCertificateWithHttpMessagesAsync(string vaultBaseUrl, string certificateName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetDeletedKeysNextWithHttpMessagesAsync(string nextPageLink, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetDeletedKeysWithHttpMessagesAsync(string vaultBaseUrl, int? maxresults = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> GetDeletedKeyWithHttpMessagesAsync(string vaultBaseUrl, string keyName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetDeletedSasDefinitionsNextWithHttpMessagesAsync(string nextPageLink, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetDeletedSasDefinitionsWithHttpMessagesAsync(string vaultBaseUrl, string storageAccountName, int? maxresults = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> GetDeletedSasDefinitionWithHttpMessagesAsync(string vaultBaseUrl, string storageAccountName, string sasDefinitionName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetDeletedSecretsNextWithHttpMessagesAsync(string nextPageLink, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetDeletedSecretsWithHttpMessagesAsync(string vaultBaseUrl, int? maxresults = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> GetDeletedSecretWithHttpMessagesAsync(string vaultBaseUrl, string secretName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetDeletedStorageAccountsNextWithHttpMessagesAsync(string nextPageLink, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetDeletedStorageAccountsWithHttpMessagesAsync(string vaultBaseUrl, int? maxresults = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> GetDeletedStorageAccountWithHttpMessagesAsync(string vaultBaseUrl, string storageAccountName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetKeysNextWithHttpMessagesAsync(string nextPageLink, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetKeysWithHttpMessagesAsync(string vaultBaseUrl, int? maxresults = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetKeyVersionsNextWithHttpMessagesAsync(string nextPageLink, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetKeyVersionsWithHttpMessagesAsync(string vaultBaseUrl, string keyName, int? maxresults = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> GetKeyWithHttpMessagesAsync(string vaultBaseUrl, string keyName, string keyVersion, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - _key.Kid = GetKeyIdentifier(vaultBaseUrl, keyName, keyVersion); - KeyAttributes attributes = new KeyAttributes(enabled: true); - var response = new AzureOperationResponse - { - Body = new KeyBundle(_key, attributes), - }; - - return Task.FromResult(response); - } - - public Task> GetPendingCertificateSigningRequestWithHttpMessagesAsync(string vault, string certificateName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetSasDefinitionsNextWithHttpMessagesAsync(string nextPageLink, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetSasDefinitionsWithHttpMessagesAsync(string vaultBaseUrl, string storageAccountName, int? maxresults = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> GetSasDefinitionWithHttpMessagesAsync(string vaultBaseUrl, string storageAccountName, string sasDefinitionName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetSecretsNextWithHttpMessagesAsync(string nextPageLink, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetSecretsWithHttpMessagesAsync(string vaultBaseUrl, int? maxresults = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetSecretVersionsNextWithHttpMessagesAsync(string nextPageLink, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetSecretVersionsWithHttpMessagesAsync(string vaultBaseUrl, string secretName, int? maxresults = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> GetSecretWithHttpMessagesAsync(string vaultBaseUrl, string secretName, string secretVersion, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetStorageAccountsNextWithHttpMessagesAsync(string nextPageLink, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task>> GetStorageAccountsWithHttpMessagesAsync(string vaultBaseUrl, int? maxresults = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> GetStorageAccountWithHttpMessagesAsync(string vaultBaseUrl, string storageAccountName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> ImportCertificateWithHttpMessagesAsync(string vaultBaseUrl, string certificateName, string base64EncodedCertificate, string password = null, CertificatePolicy certificatePolicy = null, CertificateAttributes certificateAttributes = null, IDictionary tags = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> ImportKeyWithHttpMessagesAsync(string vaultBaseUrl, string keyName, Microsoft.Azure.KeyVault.WebKey.JsonWebKey key, bool? hsm = null, KeyAttributes keyAttributes = null, IDictionary tags = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> MergeCertificateWithHttpMessagesAsync(string vaultBaseUrl, string certificateName, IList x509Certificates, CertificateAttributes certificateAttributes = null, IDictionary tags = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task PurgeDeletedCertificateWithHttpMessagesAsync(string vaultBaseUrl, string certificateName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task PurgeDeletedKeyWithHttpMessagesAsync(string vaultBaseUrl, string keyName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task PurgeDeletedSecretWithHttpMessagesAsync(string vaultBaseUrl, string secretName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task PurgeDeletedStorageAccountWithHttpMessagesAsync(string vaultBaseUrl, string storageAccountName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> RecoverDeletedCertificateWithHttpMessagesAsync(string vaultBaseUrl, string certificateName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> RecoverDeletedKeyWithHttpMessagesAsync(string vaultBaseUrl, string keyName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> RecoverDeletedSasDefinitionWithHttpMessagesAsync(string vaultBaseUrl, string storageAccountName, string sasDefinitionName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> RecoverDeletedSecretWithHttpMessagesAsync(string vaultBaseUrl, string secretName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> RecoverDeletedStorageAccountWithHttpMessagesAsync(string vaultBaseUrl, string storageAccountName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> RegenerateStorageAccountKeyWithHttpMessagesAsync(string vaultBaseUrl, string storageAccountName, string keyName, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> RestoreCertificateWithHttpMessagesAsync(string vaultBaseUrl, byte[] certificateBundleBackup, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> RestoreKeyWithHttpMessagesAsync(string vaultBaseUrl, byte[] keyBundleBackup, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> RestoreSecretWithHttpMessagesAsync(string vaultBaseUrl, byte[] secretBundleBackup, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> RestoreStorageAccountWithHttpMessagesAsync(string vaultBaseUrl, byte[] storageBundleBackup, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> SetCertificateContactsWithHttpMessagesAsync(string vaultBaseUrl, Contacts contacts, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> SetCertificateIssuerWithHttpMessagesAsync(string vaultBaseUrl, string issuerName, string provider, IssuerCredentials credentials = null, OrganizationDetails organizationDetails = null, IssuerAttributes attributes = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> SetSasDefinitionWithHttpMessagesAsync(string vaultBaseUrl, string storageAccountName, string sasDefinitionName, string templateUri, string sasType, string validityPeriod, SasDefinitionAttributes sasDefinitionAttributes = null, IDictionary tags = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> SetSecretWithHttpMessagesAsync(string vaultBaseUrl, string secretName, string value, IDictionary tags = null, string contentType = null, SecretAttributes secretAttributes = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> SetStorageAccountWithHttpMessagesAsync(string vaultBaseUrl, string storageAccountName, string resourceId, string activeKeyName, bool autoRegenerateKey, string regenerationPeriod = null, StorageAccountAttributes storageAccountAttributes = null, IDictionary tags = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> SignWithHttpMessagesAsync(string vaultBaseUrl, string keyName, string keyVersion, string algorithm, byte[] value, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - string digestAlgorithm; - switch (algorithm) - { - case SecurityAlgorithms.RsaSha256: - digestAlgorithm = SecurityAlgorithms.Sha256; - break; - case SecurityAlgorithms.RsaSha384: - digestAlgorithm = SecurityAlgorithms.Sha384; - break; - case SecurityAlgorithms.RsaSha512: - digestAlgorithm = SecurityAlgorithms.Sha512; - break; - default: - throw new NotImplementedException(); - } - - var result = _rsa.SignHash(value, digestAlgorithm); - var response = new AzureOperationResponse - { - Body = new KeyOperationResult(GetKeyIdentifier(vaultBaseUrl, keyName, keyVersion), result), - }; - - return Task.FromResult(response); - } - - public Task> UnwrapKeyWithHttpMessagesAsync(string vaultBaseUrl, string keyName, string keyVersion, string algorithm, byte[] value, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - bool fOAEP; - if (StringComparer.OrdinalIgnoreCase.Equals(algorithm, SecurityAlgorithms.RsaOAEP)) - fOAEP = true; - else if (StringComparer.OrdinalIgnoreCase.Equals(algorithm, SecurityAlgorithms.RsaPKCS1)) - fOAEP = false; - else - throw new NotImplementedException($"The mock key vault is not configured to unwrap keys using the {algorithm} security algorithm."); - - var result = _rsa.Decrypt(value, fOAEP); - var response = new AzureOperationResponse - { - Body = new KeyOperationResult(GetKeyIdentifier(vaultBaseUrl, keyName, keyVersion), result), - }; - - return Task.FromResult(response); - } - - public Task> UpdateCertificateIssuerWithHttpMessagesAsync(string vaultBaseUrl, string issuerName, string provider = null, IssuerCredentials credentials = null, OrganizationDetails organizationDetails = null, IssuerAttributes attributes = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> UpdateCertificateOperationWithHttpMessagesAsync(string vaultBaseUrl, string certificateName, bool cancellationRequested, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> UpdateCertificatePolicyWithHttpMessagesAsync(string vaultBaseUrl, string certificateName, CertificatePolicy certificatePolicy, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> UpdateCertificateWithHttpMessagesAsync(string vaultBaseUrl, string certificateName, string certificateVersion, CertificatePolicy certificatePolicy = null, CertificateAttributes certificateAttributes = null, IDictionary tags = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> UpdateKeyWithHttpMessagesAsync(string vaultBaseUrl, string keyName, string keyVersion, IList keyOps = null, KeyAttributes keyAttributes = null, IDictionary tags = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> UpdateSasDefinitionWithHttpMessagesAsync(string vaultBaseUrl, string storageAccountName, string sasDefinitionName, string templateUri = null, string sasType = null, string validityPeriod = null, SasDefinitionAttributes sasDefinitionAttributes = null, IDictionary tags = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> UpdateSecretWithHttpMessagesAsync(string vaultBaseUrl, string secretName, string secretVersion, string contentType = null, SecretAttributes secretAttributes = null, IDictionary tags = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> UpdateStorageAccountWithHttpMessagesAsync(string vaultBaseUrl, string storageAccountName, string activeKeyName = null, bool? autoRegenerateKey = null, string regenerationPeriod = null, StorageAccountAttributes storageAccountAttributes = null, IDictionary tags = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> VerifyWithHttpMessagesAsync(string vaultBaseUrl, string keyName, string keyVersion, string algorithm, byte[] digest, byte[] signature, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - string digestAlgorithm; - switch (algorithm) - { - case SecurityAlgorithms.RsaSha256: - digestAlgorithm = SecurityAlgorithms.Sha256; - break; - case SecurityAlgorithms.RsaSha384: - digestAlgorithm = SecurityAlgorithms.Sha384; - break; - case SecurityAlgorithms.RsaSha512: - digestAlgorithm = SecurityAlgorithms.Sha512; - break; - default: - digestAlgorithm = null; - break; - } - - var result = string.IsNullOrEmpty(digestAlgorithm) ? null : (bool?)_rsa.VerifyHash(digest, digestAlgorithm, signature); - var response = new AzureOperationResponse - { - Body = new KeyVerifyResult(result), - }; - - return Task.FromResult(response); - } - - public Task> WrapKeyWithHttpMessagesAsync(string vaultBaseUrl, string keyName, string keyVersion, string algorithm, byte[] value, Dictionary> customHeaders = null, CancellationToken cancellationToken = default) - { - bool fOAEP; - if (StringComparer.OrdinalIgnoreCase.Equals(algorithm, SecurityAlgorithms.RsaOAEP)) - fOAEP = true; - else if (StringComparer.OrdinalIgnoreCase.Equals(algorithm, SecurityAlgorithms.RsaPKCS1)) - fOAEP = false; - else - throw new NotImplementedException($"The mock key vault is not configured to wrap keys using the {algorithm} security algorithm."); - - var response = new AzureOperationResponse - { - Body = new KeyOperationResult(GetKeyIdentifier(vaultBaseUrl, keyName, keyVersion), _rsa.Encrypt(value, fOAEP)), - }; - - return Task.FromResult(response); - } - - private string GetKeyIdentifier(string vaultBaseUrl, string keyName, string keyVersion) - { - return new Uri(new Uri(vaultBaseUrl), $"/keys/{keyName}/{keyVersion}").ToString(); - } - } -} - diff --git a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/Properties/AssemblyInfo.cs b/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/Properties/AssemblyInfo.cs deleted file mode 100644 index b6a9bad9cd..0000000000 --- a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Runtime.InteropServices; -using Xunit; - -[assembly: CLSCompliant(false)] -[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)] -[assembly: ComVisible(false)] - diff --git a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/SignatureProviderTheoryData.cs b/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/SignatureProviderTheoryData.cs deleted file mode 100644 index 212a8656ed..0000000000 --- a/test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/SignatureProviderTheoryData.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.IdentityModel.TestUtils; - -namespace Microsoft.IdentityModel.KeyVaultExtensions.Tests -{ - public class SignatureProviderTheoryData : TheoryDataBase - { - public string Algorithm { get; set; } - } -} diff --git a/test/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.Tests/KeyVaultSecurityKeyTests.cs b/test/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.Tests/KeyVaultSecurityKeyTests.cs deleted file mode 100644 index d7c3d41dc6..0000000000 --- a/test/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.Tests/KeyVaultSecurityKeyTests.cs +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.Azure.KeyVault.Models; -using Microsoft.IdentityModel.Clients.ActiveDirectory; -using Microsoft.IdentityModel.KeyVaultExtensions; -using Microsoft.IdentityModel.KeyVaultExtensions.Tests; -using Microsoft.IdentityModel.TestUtils; -using System; -using System.Reflection; -using Xunit; - -#pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant - -namespace Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.Tests -{ - public class KeyVaultSecurityKeyTests - { - private static ExpectedException AdalServiceExceptionExpected = new ExpectedException(typeExpected: typeof(TargetInvocationException), substringExpected: "Exception has been thrown by the target of an invocation.", innerTypeExpected: typeof(AdalServiceException)); - private static ExpectedException ArgumentNullExceptionExpected = new ExpectedException(typeExpected: typeof(TargetInvocationException), substringExpected: "Exception has been thrown by the target of an invocation.", innerTypeExpected: typeof(ArgumentNullException)); - private static ExpectedException KeyVaultErrorExceptionExpected = new ExpectedException(typeExpected: typeof(TargetInvocationException), substringExpected: "Exception has been thrown by the target of an invocation.", innerTypeExpected: typeof(KeyVaultErrorException)); - - [Theory, MemberData(nameof(KeyVaultSecurityKeyConfidentialClientTheoryData))] - public void ConfidentialClientConstructorParams(KeyVaultSecurityKeyConfidentialClientTheoryData theoryData) - { - var context = TestUtilities.WriteHeader($"{this}.ConfidentialClientConstructorParams", theoryData); - - try - { - _ = Activator.CreateInstance(theoryData.Type, new object[] { theoryData.KeyIdentifier, theoryData.ClientId, theoryData.ClientSecret }); - theoryData.ExpectedException.ProcessNoException(context); - } - catch (Exception ex) - { - theoryData.ExpectedException.ProcessException(ex, context); - } - - TestUtilities.AssertFailIfErrors(context); - } - - public static TheoryData KeyVaultSecurityKeyConfidentialClientTheoryData - { - get => new TheoryData - { - new KeyVaultSecurityKeyConfidentialClientTheoryData - { - // ClientId = default, - // ClientSecret = default, - ExpectedException = ArgumentNullExceptionExpected, - First = true, - KeyIdentifier = null, - TestId = "Test1", - Type = typeof(ManagedKeyVaultSecurityKey), - }, - new KeyVaultSecurityKeyConfidentialClientTheoryData - { - // ClientId = default, - // ClientSecret = default, - ExpectedException = ArgumentNullExceptionExpected, - KeyIdentifier = string.Empty, - TestId = "Test2", - Type = typeof(ManagedKeyVaultSecurityKey), - }, - new KeyVaultSecurityKeyConfidentialClientTheoryData - { - ClientId = null, - // ClientSecret = default, - ExpectedException = ArgumentNullExceptionExpected, - // KeyIdentifier = default, - TestId = "Test3", - Type = typeof(ManagedKeyVaultSecurityKey), - }, - new KeyVaultSecurityKeyConfidentialClientTheoryData - { - ClientId = string.Empty, - /* - ClientSecret = default, - */ - ExpectedException = ArgumentNullExceptionExpected, - // KeyIdentifier = default, - TestId = "Test4", - Type = typeof(ManagedKeyVaultSecurityKey), - }, - new KeyVaultSecurityKeyConfidentialClientTheoryData - { - // ClientId = default, - ClientSecret = null, - ExpectedException = ArgumentNullExceptionExpected, - // KeyIdentifier = default, - TestId = "Test5", - Type = typeof(ManagedKeyVaultSecurityKey), - }, - new KeyVaultSecurityKeyConfidentialClientTheoryData - { - // ClientId = default, - ClientSecret = string.Empty, - ExpectedException = ArgumentNullExceptionExpected, - // KeyIdentifier = default, - TestId = "Test6", - Type = typeof(ManagedKeyVaultSecurityKey), - }, - new KeyVaultSecurityKeyConfidentialClientTheoryData - { - // ClientId = default, - // ClientSecret = default, - // KeyIdentifier = default, - TestId = "Test7", - Type = typeof(ManagedKeyVaultSecurityKey), - } - }; - } - - [Theory, MemberData(nameof(KeyVaultSecurityKeyManagedServiceIdentityTheoryData))] - public void ManagedServiceIdentityConstructorParams(KeyVaultSecurityKeyTheoryData theoryData) - { - var context = TestUtilities.WriteHeader($"{this}.ManagedServiceIdentityConstructorParams", theoryData); - - try - { - _ = Activator.CreateInstance(theoryData.Type, new object[] { theoryData.KeyIdentifier }); - theoryData.ExpectedException.ProcessNoException(context); - } - catch (Exception ex) - { - theoryData.ExpectedException.ProcessException(ex, context); - } - } - - public static TheoryData KeyVaultSecurityKeyManagedServiceIdentityTheoryData - { - get => new TheoryData - { - new KeyVaultSecurityKeyManagedServiceIdentityTheoryData - { - ExpectedException = ExpectedException.ArgumentNullException(), - First = true, - KeyIdentifier = null, - TestId = "Test1", - Type = typeof(ManagedKeyVaultSecurityKey), - }, - new KeyVaultSecurityKeyManagedServiceIdentityTheoryData - { - ExpectedException = ExpectedException.ArgumentNullException(), - KeyIdentifier = string.Empty, - TestId = "Test2", - Type = typeof(ManagedKeyVaultSecurityKey), - }, - new KeyVaultSecurityKeyManagedServiceIdentityTheoryData - { - ExpectedException = KeyVaultErrorExceptionExpected, - // KeyIdentifier = default, - TestId = "Test3", - Type = typeof(ManagedKeyVaultSecurityKey), - }, - }; - } - } -} - -#pragma warning restore CS3016 // Arrays as attribute arguments is not CLS-compliant diff --git a/test/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.Tests/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.Tests.csproj b/test/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.Tests/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.Tests.csproj deleted file mode 100644 index d372f245e9..0000000000 --- a/test/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.Tests/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.Tests.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.Tests - $(MSBuildThisFileDirectory)..\..\build\35MSSharedLib1024.snk - true - Tests for Microsoft.IdentityModel.ManagedKeyVaultSecurityKey - true - Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.Tests - true - - - - - - - - - - - - - diff --git a/test/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.Tests/Properties/AssemblyInfo.cs b/test/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.Tests/Properties/AssemblyInfo.cs deleted file mode 100644 index b6a9bad9cd..0000000000 --- a/test/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.Tests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Runtime.InteropServices; -using Xunit; - -[assembly: CLSCompliant(false)] -[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)] -[assembly: ComVisible(false)] - diff --git a/test/Microsoft.IdentityModel.TestUtils/DerivedTypes.cs b/test/Microsoft.IdentityModel.TestUtils/DerivedTypes.cs index f4580dba31..1590981f36 100644 --- a/test/Microsoft.IdentityModel.TestUtils/DerivedTypes.cs +++ b/test/Microsoft.IdentityModel.TestUtils/DerivedTypes.cs @@ -303,7 +303,7 @@ public DerivedSecurityKey(string keyId, int keySize) _keySize = keySize; } - internal override string InternalId { get =>_keyId; } + internal override string InternalId { get => _keyId; } public Exception ThrowOnGetKeyId { get; set; } diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Microsoft.IdentityModel.Tokens.Tests.csproj b/test/Microsoft.IdentityModel.Tokens.Tests/Microsoft.IdentityModel.Tokens.Tests.csproj index 571c3cdde6..1457cc8bd1 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/Microsoft.IdentityModel.Tokens.Tests.csproj +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Microsoft.IdentityModel.Tokens.Tests.csproj @@ -13,7 +13,6 @@ - diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/SecurityKeyTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/SecurityKeyTests.cs index b2c606189f..d11ad7c75b 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/SecurityKeyTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/SecurityKeyTests.cs @@ -14,11 +14,8 @@ public class SecurityKeyTests [Fact] public void ComputeJwkThumbprint() { - var exception = Assert.Throws(() => new ManagedKeyVaultSecurityKey.ManagedKeyVaultSecurityKey("keyid").ComputeJwkThumbprint()); - Assert.Contains("IDX10710", exception.Message); - #if NET461 || NET462 - exception = Assert.Throws(() => new ECDsaSecurityKey(KeyingMaterial.JsonWebKeyP256, false).ComputeJwkThumbprint()); + var exception = Assert.Throws(() => new ECDsaSecurityKey(KeyingMaterial.JsonWebKeyP256, false).ComputeJwkThumbprint()); Assert.Contains("IDX10695", exception.Message); #else var ex = Record.Exception(() => new ECDsaSecurityKey(KeyingMaterial.JsonWebKeyP256, false).ComputeJwkThumbprint()); diff --git a/test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTests.cs b/test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTests.cs index ea2c58a738..9400e6770a 100644 --- a/test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTests.cs +++ b/test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net.Http; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.IdentityModel.JsonWebTokens; diff --git a/test/System.IdentityModel.Tokens.Jwt.Tests/JwtTestDatasets.cs b/test/System.IdentityModel.Tokens.Jwt.Tests/JwtTestDatasets.cs index 64344d495b..4de6f67bce 100644 --- a/test/System.IdentityModel.Tokens.Jwt.Tests/JwtTestDatasets.cs +++ b/test/System.IdentityModel.Tokens.Jwt.Tests/JwtTestDatasets.cs @@ -27,6 +27,7 @@ using System.Collections.Generic; using System.IO; using System.Net; +using System.Net.Http; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; diff --git a/test/System.IdentityModel.Tokens.Jwt.Tests/System.IdentityModel.Tokens.Jwt.Tests.csproj b/test/System.IdentityModel.Tokens.Jwt.Tests/System.IdentityModel.Tokens.Jwt.Tests.csproj index 5ff3cf0408..b0ec53c7af 100644 --- a/test/System.IdentityModel.Tokens.Jwt.Tests/System.IdentityModel.Tokens.Jwt.Tests.csproj +++ b/test/System.IdentityModel.Tokens.Jwt.Tests/System.IdentityModel.Tokens.Jwt.Tests.csproj @@ -19,6 +19,10 @@ + + + + From aad1e253acc5a46a3fbe65d5baef2ebc3ed0af8f Mon Sep 17 00:00:00 2001 From: jennyf19 Date: Tue, 18 Jun 2024 10:14:06 -0700 Subject: [PATCH 07/22] remove net 461 (#2642) * remove net 461 * fix spelling error * PR feedback round 1 * PR feedback --- build/targets.props | 4 +-- build/targetsTest.props | 4 +-- .../IdentityModelTelemetryUtil.cs | 4 +-- .../CryptoProviderFactory.cs | 2 -- .../ECDsaAdapter.cs | 34 ++++--------------- .../GlobalSuppressions.cs | 2 -- .../RsaCryptoServiceProviderProxy.cs | 2 +- .../RsaSecurityKey.cs | 2 +- .../SupportedAlgorithms.cs | 2 -- src/Microsoft.IdentityModel.Tokens/Utility.cs | 2 +- .../X509SecurityKey.cs | 9 +---- .../OpenIdConnectMessageTests.cs | 10 ++---- .../SignedHttpRequestTestUtils.cs | 2 +- .../KeyingMaterial.cs | 10 +++--- .../AsymmetricAdapterTests.cs | 7 +--- .../AsymmetricSignatureTests.cs | 12 +++---- .../CryptoProviderCacheTests.cs | 2 +- .../MultiThreadingTests.cs | 4 +-- .../RsaCryptoServiceProviderProxyTests.cs | 14 ++++---- .../RsaSecurityKeyTests.cs | 4 +-- .../SecurityKeyTests.cs | 2 +- .../SignatureProviderTests.cs | 2 +- .../CreateAndValidateTokens.cs | 2 +- 23 files changed, 45 insertions(+), 93 deletions(-) diff --git a/build/targets.props b/build/targets.props index b1af98f10a..6f92780d1f 100644 --- a/build/targets.props +++ b/build/targets.props @@ -1,7 +1,7 @@ - net461;net462;net472;netstandard2.0;net6.0;net8.0 - net461;netstandard2.0;net8.0 + net462;net472;netstandard2.0;net6.0;net8.0 + netstandard2.0;net8.0 $(SrcTargets);net9.0 netstandard2.0 diff --git a/build/targetsTest.props b/build/targetsTest.props index 3f26b8e2d9..e867c8b147 100644 --- a/build/targetsTest.props +++ b/build/targetsTest.props @@ -1,7 +1,7 @@ - net461;net462;net472;netcoreapp2.1;net6.0;net8.0 - net461;netcoreapp2.1;net8.0 + net462;net472;netcoreapp2.1;net6.0;net8.0 + netcoreapp2.1;net8.0 $(TestTargets);net9.0 netcoreapp2.1 diff --git a/src/Microsoft.IdentityModel.Logging/IdentityModelTelemetryUtil.cs b/src/Microsoft.IdentityModel.Logging/IdentityModelTelemetryUtil.cs index 0a29c91991..74a8a0e7ac 100644 --- a/src/Microsoft.IdentityModel.Logging/IdentityModelTelemetryUtil.cs +++ b/src/Microsoft.IdentityModel.Logging/IdentityModelTelemetryUtil.cs @@ -27,9 +27,7 @@ public static class IdentityModelTelemetryUtil /// Get the string that represents the client SKU. /// public static string ClientSku => -#if NET461 - "ID_NET461"; -#elif NET462 +#if NET462 "ID_NET462"; #elif NET472 "ID_NET472"; diff --git a/src/Microsoft.IdentityModel.Tokens/CryptoProviderFactory.cs b/src/Microsoft.IdentityModel.Tokens/CryptoProviderFactory.cs index 593bd33a19..3ce56064ad 100644 --- a/src/Microsoft.IdentityModel.Tokens/CryptoProviderFactory.cs +++ b/src/Microsoft.IdentityModel.Tokens/CryptoProviderFactory.cs @@ -318,7 +318,6 @@ public virtual SignatureProvider CreateForVerifying(SecurityKey key, string algo return CreateSignatureProvider(key, algorithm, false, cacheProvider); } -#if NET461 || NET462 || NET472 || NETSTANDARD2_0 || NET6_0_OR_GREATER /// /// Creates a for a specific algorithm. /// @@ -354,7 +353,6 @@ public virtual HashAlgorithm CreateHashAlgorithm(HashAlgorithmName algorithm) throw LogHelper.LogExceptionMessage(new NotSupportedException(LogHelper.FormatInvariant(LogMessages.IDX10640, LogHelper.MarkAsNonPII(algorithm)))); } -#endif /// /// Creates a for a specific algorithm. diff --git a/src/Microsoft.IdentityModel.Tokens/ECDsaAdapter.cs b/src/Microsoft.IdentityModel.Tokens/ECDsaAdapter.cs index 003d4e18ab..6986a9b0ee 100644 --- a/src/Microsoft.IdentityModel.Tokens/ECDsaAdapter.cs +++ b/src/Microsoft.IdentityModel.Tokens/ECDsaAdapter.cs @@ -32,14 +32,12 @@ internal ECDsaAdapter() CreateECDsaFunction = CreateECDsaUsingECParams; #elif NETSTANDARD2_0 // Although NETSTANDARD2_0 specifies that ECParameters are supported, we still need to call SupportsECParameters() - // as NET461 is listed as supporting NETSTANDARD2_0, but DOES NOT support ECParameters. + // as NET462 is listed as supporting NETSTANDARD2_0, but DOES NOT support ECParameters. // See: https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.ecparameters?view=netstandard-2.0 - if (SupportsECParameters()) - CreateECDsaFunction = CreateECDsaUsingECParams; - else - CreateECDsaFunction = CreateECDsaUsingCNGKey; + if (SupportsECParameters()) CreateECDsaFunction = CreateECDsaUsingECParams; + else CreateECDsaFunction = CreateECDsaUsingCNGKey; #else - CreateECDsaFunction = CreateECDsaUsingCNGKey; + CreateECDsaFunction = CreateECDsaUsingCNGKey; #endif } @@ -51,7 +49,7 @@ internal ECDsa CreateECDsa(JsonWebKey jsonWebKey, bool usePrivateKey) return CreateECDsaFunction(jsonWebKey, usePrivateKey); } -#if NET461 || NET462 || NETSTANDARD2_0 +#if NET462 || NETSTANDARD2_0 /// /// Creates an ECDsa object using the and . /// 'ECParameters' structure is available in .NET Framework 4.7+, .NET Standard 1.6+, and .NET Core 1.0+. @@ -230,32 +228,12 @@ private static uint GetMagicValue(string curveId, bool willCreateSignatures) return (uint)magicNumber; } - /// - /// Tests if user's runtime platform supports operations using . - /// - /// True if operations using are supported on user's runtime platform, false otherwise. - [MethodImpl(MethodImplOptions.NoOptimization)] - private static bool SupportsCNGKey() - { - try - { -#pragma warning disable CA1416 // Validate platform compatibility - _ = CngKeyBlobFormat.EccPrivateBlob; -#pragma warning restore CA1416 // Validate platform compatibility - return true; - } - catch - { - return false; - } - } - #if NET472 || NETSTANDARD2_0 || NET6_0_OR_GREATER /// /// Creates an ECDsa object using the and . /// 'ECParameters' structure is available in .NET Framework 4.7+, .NET Standard 1.6+, and .NET Core 1.0+. /// - private ECDsa CreateECDsaUsingECParams(JsonWebKey jsonWebKey, bool usePrivateKey) + private static ECDsa CreateECDsaUsingECParams(JsonWebKey jsonWebKey, bool usePrivateKey) { if (jsonWebKey == null) throw LogHelper.LogArgumentNullException(nameof(jsonWebKey)); diff --git a/src/Microsoft.IdentityModel.Tokens/GlobalSuppressions.cs b/src/Microsoft.IdentityModel.Tokens/GlobalSuppressions.cs index f8c5c81d5b..27c7f4e0c6 100644 --- a/src/Microsoft.IdentityModel.Tokens/GlobalSuppressions.cs +++ b/src/Microsoft.IdentityModel.Tokens/GlobalSuppressions.cs @@ -21,8 +21,6 @@ [assembly: SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "Breaking change", Scope = "member", Target = "~P:Microsoft.IdentityModel.Tokens.AuthenticatedEncryptionResult.AuthenticationTag")] [assembly: SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "Breaking change", Scope = "member", Target = "~P:Microsoft.IdentityModel.Tokens.SymmetricSecurityKey.Key")] [assembly: SuppressMessage("Usage", "CA2211:Non-constant fields should not be visible", Justification = "Breaking change", Scope = "member", Target = "~F:Microsoft.IdentityModel.Tokens.JsonWebKeySet.DefaultSkipUnresolvedJsonWebKeys")] -[assembly: SuppressMessage("Usage", "CA1801:Review unused parameters", Justification = "Used in net461", Scope = "member", Target = "~M:Microsoft.IdentityModel.Tokens.SupportedAlgorithms.IsSupportedRsaPss(Microsoft.IdentityModel.Tokens.SecurityKey)~System.Boolean")] -[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Used as platform test", Scope = "member", Target = "~M:Microsoft.IdentityModel.Tokens.ECDsaAdapter.SupportsCNGKey~System.Boolean")] [assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Used for try pattern", Scope = "member", Target = "~M:Microsoft.IdentityModel.Tokens.JsonWebKeyConverter.ConvertFromRSASecurityKey(Microsoft.IdentityModel.Tokens.RsaSecurityKey)~Microsoft.IdentityModel.Tokens.JsonWebKey")] [assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Used for try pattern", Scope = "member", Target = "~M:Microsoft.IdentityModel.Tokens.JsonWebKeyConverter.TryConvertToECDsaSecurityKey(Microsoft.IdentityModel.Tokens.JsonWebKey,Microsoft.IdentityModel.Tokens.SecurityKey@)~System.Boolean")] [assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Used for try pattern", Scope = "member", Target = "~M:Microsoft.IdentityModel.Tokens.JsonWebKeyConverter.TryConvertToSecurityKey(Microsoft.IdentityModel.Tokens.JsonWebKey,Microsoft.IdentityModel.Tokens.SecurityKey@)~System.Boolean")] diff --git a/src/Microsoft.IdentityModel.Tokens/RsaCryptoServiceProviderProxy.cs b/src/Microsoft.IdentityModel.Tokens/RsaCryptoServiceProviderProxy.cs index ea2adbee28..07b21c14d0 100644 --- a/src/Microsoft.IdentityModel.Tokens/RsaCryptoServiceProviderProxy.cs +++ b/src/Microsoft.IdentityModel.Tokens/RsaCryptoServiceProviderProxy.cs @@ -205,7 +205,7 @@ public bool VerifyData(byte[] input, object hash, byte[] signature) return _rsa.VerifyData(input, hash, signature); } -#if NET461 || NET462 || NET472 || NETSTANDARD2_0 +#if NET462 || NET472 || NETSTANDARD2_0 /// /// Verifies that a digital signature is valid by determining the hash value in the signature using the provided public key and comparing it to the hash value of the provided data. /// diff --git a/src/Microsoft.IdentityModel.Tokens/RsaSecurityKey.cs b/src/Microsoft.IdentityModel.Tokens/RsaSecurityKey.cs index 248f5cc947..7b3fa3ef05 100644 --- a/src/Microsoft.IdentityModel.Tokens/RsaSecurityKey.cs +++ b/src/Microsoft.IdentityModel.Tokens/RsaSecurityKey.cs @@ -76,7 +76,7 @@ public override bool HasPrivateKey { // imitate signing byte[] hash = new byte[20]; -#if NET461 || NET462 || NET472 || NETSTANDARD2_0 || NET6_0_OR_GREATER +#if NET462 || NET472 || NETSTANDARD2_0 || NET6_0_OR_GREATER Rsa.SignData(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); #else if (Rsa is RSACryptoServiceProvider rsaCryptoServiceProvider) diff --git a/src/Microsoft.IdentityModel.Tokens/SupportedAlgorithms.cs b/src/Microsoft.IdentityModel.Tokens/SupportedAlgorithms.cs index 086374360a..7be591da2c 100644 --- a/src/Microsoft.IdentityModel.Tokens/SupportedAlgorithms.cs +++ b/src/Microsoft.IdentityModel.Tokens/SupportedAlgorithms.cs @@ -104,7 +104,6 @@ internal static class SupportedAlgorithms SecurityAlgorithms.EcdhEsA256kw }; -#if NET461 || NET462 || NET472 || NETSTANDARD2_0 || NET6_0_OR_GREATER /// /// Creating a Signature requires the use of a . /// This method returns the @@ -148,7 +147,6 @@ internal static HashAlgorithmName GetHashAlgorithmName(string algorithm) throw LogHelper.LogExceptionMessage(new ArgumentOutOfRangeException(nameof(algorithm), LogHelper.FormatInvariant(LogMessages.IDX10652, LogHelper.MarkAsNonPII(algorithm)))); } -#endif /// /// Creating a Signature requires the use of a . diff --git a/src/Microsoft.IdentityModel.Tokens/Utility.cs b/src/Microsoft.IdentityModel.Tokens/Utility.cs index 49e2a3d059..4990981417 100644 --- a/src/Microsoft.IdentityModel.Tokens/Utility.cs +++ b/src/Microsoft.IdentityModel.Tokens/Utility.cs @@ -113,7 +113,7 @@ public static bool IsHttps(Uri uri) { return false; } -#if NET461 || NET462 +#if NET462 return uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase); //Uri.UriSchemeHttps is internal in dnxcore #else return uri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase); diff --git a/src/Microsoft.IdentityModel.Tokens/X509SecurityKey.cs b/src/Microsoft.IdentityModel.Tokens/X509SecurityKey.cs index 5b7687ec10..2c45122707 100644 --- a/src/Microsoft.IdentityModel.Tokens/X509SecurityKey.cs +++ b/src/Microsoft.IdentityModel.Tokens/X509SecurityKey.cs @@ -78,11 +78,8 @@ public AsymmetricAlgorithm PrivateKey { if (!_privateKeyAvailabilityDetermined) { -#if NET461 || NET462 || NET472 || NETSTANDARD2_0 || NET6_0_OR_GREATER _privateKey = RSACertificateExtensions.GetRSAPrivateKey(Certificate); -#else - _privateKey = Certificate.PrivateKey; -#endif + _privateKeyAvailabilityDetermined = true; } } @@ -105,11 +102,7 @@ public AsymmetricAlgorithm PublicKey { if (_publicKey == null) { -#if NET461 || NET462 || NET472 || NETSTANDARD2_0 || NET6_0_OR_GREATER _publicKey = RSACertificateExtensions.GetRSAPublicKey(Certificate); -#else - _publicKey = Certificate.PublicKey.Key; -#endif } } } diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectMessageTests.cs b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectMessageTests.cs index 5406e03d54..d1c4254365 100644 --- a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectMessageTests.cs +++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectMessageTests.cs @@ -278,10 +278,7 @@ public void OidcCreateAuthenticationRequestUrl(string testId, OpenIdConnectMessa { TestUtilities.WriteHeader(testId, "OidcCreateAuthenticationRequestUrl", true); var context = new CompareContext(); -#if NET461 - if (!message.SkuTelemetryValue.Equals("ID_NET461")) - context.Diffs.Add($"{message.SkuTelemetryValue} != ID_NET461"); -#elif NET462 +#if NET462 if (!message.SkuTelemetryValue.Equals("ID_NET462")) context.Diffs.Add($"{message.SkuTelemetryValue} != ID_NET462"); #elif NET472 @@ -556,10 +553,7 @@ public void OidcCreateLogoutRequestUrl(string testId, OpenIdConnectMessage messa TestUtilities.WriteHeader("OidcCreateLogoutRequestUrl - " + testId, true); var context = new CompareContext(); -#if NET461 - if (!message.SkuTelemetryValue.Equals("ID_NET461")) - context.Diffs.Add($"{message.SkuTelemetryValue} != ID_NET461"); -#elif NET472 +#if NET472 if (!message.SkuTelemetryValue.Equals("ID_NET472")) context.Diffs.Add($"{message.SkuTelemetryValue} != ID_NET472"); #elif NET6_0 diff --git a/test/Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests/SignedHttpRequestTestUtils.cs b/test/Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests/SignedHttpRequestTestUtils.cs index 4bf1148703..f86bade886 100644 --- a/test/Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests/SignedHttpRequestTestUtils.cs +++ b/test/Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests/SignedHttpRequestTestUtils.cs @@ -103,7 +103,7 @@ public static class SignedHttpRequestTestUtils Kid = Base64UrlEncoder.Encode(new JsonWebKey(DefaultJwkEcdsa.ToString(Formatting.None)).ComputeJwkThumbprint()) }; -#if NET461 || NET462 +#if NET462 internal static JObject DefaultJwkEcdsa => new JObject { { "kty", "EC" }, diff --git a/test/Microsoft.IdentityModel.TestUtils/KeyingMaterial.cs b/test/Microsoft.IdentityModel.TestUtils/KeyingMaterial.cs index e867a8d028..9251fc487a 100644 --- a/test/Microsoft.IdentityModel.TestUtils/KeyingMaterial.cs +++ b/test/Microsoft.IdentityModel.TestUtils/KeyingMaterial.cs @@ -11,7 +11,7 @@ namespace Microsoft.IdentityModel.TestUtils { static public class KeyingMaterial { -#if NET461 || NET462 || NET472 +#if NET462 || NET472 static Type _rsaCngType = typeof(CngKey).Assembly.GetType("System.Security.Cryptography.RSACng", false); private static Lazy _rsaCng = new Lazy(CreateRSACng2048); #endif @@ -460,7 +460,7 @@ static KeyingMaterial() RsaSigningCreds_4096 = new SigningCredentials(RsaSecurityKey_2048, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256); RsaSigningCreds_4096_Public = new SigningCredentials(RsaSecurityKey_2048_Public, SecurityAlgorithms.RsaSha256Signature); -#if NET461 || NET462 +#if NET462 //ecdsa byte[] ecdsa256KeyBlob = TestUtilities.HexToByteArray("454353322000000096e476f7473cb17c5b38684daae437277ae1efadceb380fad3d7072be2ffe5f0b54a94c2d6951f073bfc25e7b81ac2a4c41317904929d167c3dfc99122175a9438e5fb3e7625493138d4149c9438f91a2fecc7f48f804a92b6363776892ee134"); byte[] ecdsa384KeyBlob = TestUtilities.HexToByteArray("45435334300000009dc6bb9cdc8dac31e3db6e6b5f58f8e3a304e5c08e632705ca9a236f1134646dca526b89f7ea98653962f4a781f2fc9bf479a2d627561b1269548050e6d2c388018b837f4ceba8ee7fe2eefea67c8418ad1e84f60c1309385e573ea5183e9ae8b6d5308a78da207c6e556af2053983321a5f8ac057b787089ee783c99093b9f2afb2f9a1e9a560ad3095b9667aa699fa"); @@ -506,7 +506,7 @@ static KeyingMaterial() } -#if NET461 || NET462 +#if NET462 public static RsaSecurityKey RsaSecurityKeyWithCspProvider_2048 { get @@ -548,7 +548,7 @@ public static RsaSecurityKey RsaSecurityKey_2048_FromRsa_Public } #endif -#if NET461 || NET462 +#if NET462 public static RsaSecurityKey RsaSecurityKeyWithCngProvider_2048 { get @@ -596,7 +596,7 @@ public static RsaSecurityKey RsaSecurityKey_2048_FromRsa_Public public static RsaSecurityKey RsaSecurityKey_2048 => new RsaSecurityKey(RsaParameters_2048) { KeyId = "RsaSecurityKey_2048" }; -#if NET461 || NET462 || NET472 +#if NET462 || NET472 public static RsaSecurityKey RsaSecurityKeyCng_2048 => new RsaSecurityKey(_rsaCng.Value as RSA) { KeyId = "RsaSecurityKeyRsaCng_2048" }; private static object CreateRSACng2048() diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/AsymmetricAdapterTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/AsymmetricAdapterTests.cs index a0d2570872..fd7c37f0e3 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/AsymmetricAdapterTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/AsymmetricAdapterTests.cs @@ -24,11 +24,8 @@ public void AsymmetricAdapterUsageTests(AsymmetricAdapterTheoryData theoryData) try { -#if NET461 || NET462 || NET472 || NETCOREAPP2_1 || NET6_0_OR_GREATER AsymmetricAdapter asymmetricdapter = new AsymmetricAdapter(theoryData.SecurityKey, theoryData.Algorithm, hashAlgorithm, SupportedAlgorithms.GetHashAlgorithmName(theoryData.Algorithm), true); -#else - AsymmetricAdapter asymmetricdapter = new AsymmetricAdapter(theoryData.SecurityKey, theoryData.Algorithm, hashAlgorithm, true); -#endif + byte[] signature = asymmetricdapter.Sign(bytes); if (!asymmetricdapter.Verify(bytes, signature)) context.AddDiff($"Verify failed for test: {theoryData.TestId}"); @@ -59,7 +56,6 @@ public static TheoryData AsymmetricAdapterUsageTest // RSA // RSACertificateExtensions.GetRSAPrivateKey - this results in - #if NET461 || NET462 || NET472 || NETCOREAPP2_1 || NET6_0_OR_GREATER new AsymmetricAdapterTheoryData { Algorithm = SecurityAlgorithms.RsaSha256, @@ -67,7 +63,6 @@ public static TheoryData AsymmetricAdapterUsageTest SecurityKey = new RsaSecurityKey(RSACertificateExtensions.GetRSAPrivateKey(KeyingMaterial.CertSelfSigned2048_SHA256) as RSA), TestId = "RSACertificateExtensions_GetRSAPrivateKey" }, - #endif // X509Certificte2.PrivateKey - this results in the RSA being of type RSACryptoServiceProviderProxy new AsymmetricAdapterTheoryData diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/AsymmetricSignatureTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/AsymmetricSignatureTests.cs index 768d3011d6..d7e5bb50f3 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/AsymmetricSignatureTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/AsymmetricSignatureTests.cs @@ -21,7 +21,7 @@ public void UnsupportedRSATypes() var context = new CompareContext("UnsupportedRSATypes"); TestUtilities.WriteHeader($"{this}.UnsupportedRSATypes"); -#if NET461 || NET462 || NET472 || NET_CORE +#if NET462 || NET472 || NET_CORE var expectedException = ExpectedException.NoExceptionExpected; #endif try @@ -34,7 +34,7 @@ public void UnsupportedRSATypes() expectedException.ProcessException(ex, context); } -#if NET461 || NET462 || NET472 || NET_CORE +#if NET462 || NET472 || NET_CORE expectedException = ExpectedException.NoExceptionExpected; #endif @@ -107,7 +107,7 @@ public static TheoryData SignVerifyTheoryData }, theoryData); -#if NET461 || NET462 || NET472 || NET_CORE +#if NET462 || NET472 || NET_CORE theoryData.Add(new SignatureProviderTheoryData() { SigningAlgorithm = SecurityAlgorithms.RsaSsaPssSha512, @@ -151,7 +151,7 @@ public static TheoryData SignVerifyTheoryData SigningKey = new RsaSecurityKey(certTuple.Item1.PrivateKey as RSA), TestId = "CapiCapi" + certTuple.Item3, VerifyKey = new RsaSecurityKey(certTuple.Item2.PublicKey.Key as RSA), -#if NET461 || NET462 || NET472 +#if NET462 || NET472 ExpectedException = ExpectedException.NotSupportedException("IDX10634:"), #elif NET_CORE ExpectedException = ExpectedException.NoExceptionExpected, @@ -165,7 +165,7 @@ public static TheoryData SignVerifyTheoryData SigningKey = new RsaSecurityKey(certTuple.Item1.PrivateKey as RSA), TestId = "CapiCng" + certTuple.Item3, VerifyKey = new RsaSecurityKey(certTuple.Item2.GetRSAPublicKey()), -#if NET461 || NET462 || NET472 +#if NET462 || NET472 ExpectedException = ExpectedException.NotSupportedException("IDX10634:"), #elif NET_CORE ExpectedException = ExpectedException.NoExceptionExpected, @@ -179,7 +179,7 @@ public static TheoryData SignVerifyTheoryData SigningKey = new RsaSecurityKey(certTuple.Item1.GetRSAPrivateKey()), TestId = "CngCapi" + certTuple.Item3, VerifyKey = new RsaSecurityKey(certTuple.Item2.PublicKey.Key as RSA), -#if NET461 || NET462 || NET472 +#if NET462 || NET472 ExpectedException = ExpectedException.NotSupportedException("IDX10634:"), #elif NET_CORE ExpectedException = ExpectedException.NoExceptionExpected, diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/CryptoProviderCacheTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/CryptoProviderCacheTests.cs index 43b99908b3..aa8a580535 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/CryptoProviderCacheTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/CryptoProviderCacheTests.cs @@ -602,7 +602,7 @@ public class CryptoProviderCacheTheoryData : TheoryDataBase, IDisposable #if NETCOREAPP public CryptoProviderCache CryptoProviderCache { get; set; } -#elif NET461 || NET462 || NET472 +#elif NET462 || NET472 public CryptoProviderCache CryptoProviderCache { get; set; } #endif diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/MultiThreadingTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/MultiThreadingTests.cs index c9e87b0abf..f1b5d636b6 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/MultiThreadingTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/MultiThreadingTests.cs @@ -152,7 +152,7 @@ public static TheoryData MultiThreadingCreateAndVerify var jwtEncryptedDir = jwtSecurityTokenHandler.CreateEncodedJwt(securityTokenDescriptorEncryptedDir); -#if NET461 || NET462 || NET472 +#if NET462 || NET472 // RSACng var securityTokenDescriptorRsaCng = new SecurityTokenDescriptor { @@ -235,7 +235,7 @@ public static TheoryData MultiThreadingCreateAndVerify TokenDescriptor = securityTokenDescriptorEncryptedDir, ValidationParameters = tokenValidationParametersEncryptedDir }, -#if NET461 || NET462 || NET472 +#if NET462 || NET472 new MultiThreadingTheoryData { JwtSecurityTokenHandler = jwtSecurityTokenHandler, diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/RsaCryptoServiceProviderProxyTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/RsaCryptoServiceProviderProxyTests.cs index 5c7b217422..222938fe89 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/RsaCryptoServiceProviderProxyTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/RsaCryptoServiceProviderProxyTests.cs @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -#if NET461 || NET462 || NET472 +#if NET462 || NET472 using System.Security.Cryptography.X509Certificates; #endif -#if NET461 || NET462 || NET472 +#if NET462 || NET472 using System; using System.Security.Cryptography; @@ -59,7 +59,7 @@ public static TheoryData RSADecryptTheo { get { -#if NET461 || NET462 || NET472 +#if NET462 || NET472 var rsaCsp = new RSACryptoServiceProvider(); rsaCsp.ImportParameters(KeyingMaterial.RsaParameters_2048); #else @@ -111,7 +111,7 @@ public static TheoryData RSAEncryptDecr { get { -#if NET461 || NET462 || NET472 +#if NET462 || NET472 var rsaFromX509Cert = new RSACryptoServiceProvider(); var rsaCng = KeyingMaterial.DefaultCert_2048.GetRSAPrivateKey() as RSACng; var parameters = rsaCng.ExportParameters(true); @@ -185,7 +185,7 @@ public static TheoryData RSAEncryptDecr { get { -#if NET461 || NET462 || NET472 +#if NET462 || NET472 var rsaCsp = new RSACryptoServiceProvider(); rsaCsp.ImportParameters(KeyingMaterial.RsaParameters_2048); #else @@ -254,7 +254,7 @@ public static TheoryData RSASignVerifyD { get { -#if NET461 || NET462 || NET472 +#if NET462 || NET472 var rsaCsp = new RSACryptoServiceProvider(); rsaCsp.ImportParameters(KeyingMaterial.RsaParameters_2048); #else @@ -328,7 +328,7 @@ public static TheoryData RSAVerifyDataT { get { -#if NET461 || NET462 || NET472 +#if NET462 || NET472 var rsaCsp = new RSACryptoServiceProvider(); rsaCsp.ImportParameters(KeyingMaterial.RsaParameters_2048); #else diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/RsaSecurityKeyTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/RsaSecurityKeyTests.cs index f39224deca..b41074c639 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/RsaSecurityKeyTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/RsaSecurityKeyTests.cs @@ -66,7 +66,7 @@ public void HasPrivateKey(string testId, AsymmetricSecurityKey key, bool expecte public static TheoryData HasPrivateKeyTheoryData() { var theoryData = new TheoryData(); -#if NET461 || NET462 +#if NET462 theoryData.Add( "KeyingMaterial.RsaSecurityKeyWithCspProvider_2048", KeyingMaterial.RsaSecurityKeyWithCspProvider_2048, @@ -80,7 +80,7 @@ public static TheoryData HasPrivateKeyTheoryData() ); #endif -#if NET461 || NET462 +#if NET462 theoryData.Add( "KeyingMaterial.RsaSecurityKeyWithCngProvider_2048", diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/SecurityKeyTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/SecurityKeyTests.cs index d11ad7c75b..2d4ef698b1 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/SecurityKeyTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/SecurityKeyTests.cs @@ -14,7 +14,7 @@ public class SecurityKeyTests [Fact] public void ComputeJwkThumbprint() { -#if NET461 || NET462 +#if NET462 var exception = Assert.Throws(() => new ECDsaSecurityKey(KeyingMaterial.JsonWebKeyP256, false).ComputeJwkThumbprint()); Assert.Contains("IDX10695", exception.Message); #else diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/SignatureProviderTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/SignatureProviderTests.cs index 1464e6d694..3668182bc0 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/SignatureProviderTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/SignatureProviderTests.cs @@ -1301,7 +1301,7 @@ internal static void AddSignUsingOffsets(byte[] bytes, SecurityKey securityKey, SignatureProvider = CreateProvider(securityKey, algorithm) }); -#if NET461 || NET462 +#if NET462 // RSA throws a different exception in the following three cases than HMAC or ECDSA 472+ theoryData.Add(new SignTheoryData($"{prefix}_CountNegative") { diff --git a/test/System.IdentityModel.Tokens.Jwt.Tests/CreateAndValidateTokens.cs b/test/System.IdentityModel.Tokens.Jwt.Tests/CreateAndValidateTokens.cs index 220ed19b8e..243bc2da24 100644 --- a/test/System.IdentityModel.Tokens.Jwt.Tests/CreateAndValidateTokens.cs +++ b/test/System.IdentityModel.Tokens.Jwt.Tests/CreateAndValidateTokens.cs @@ -611,7 +611,7 @@ public static TheoryData RoundTripTokensTheoryData() ValidationParameters = Default.SymmetricEncryptSignTokenValidationParameters }); -#if NET461 || NET462 || NET_CORE +#if NET462 || NET_CORE // RsaPss is not supported on .NET < 4.6 var rsaPssSigningCredentials = new SigningCredentials(Default.AsymmetricSigningKey, SecurityAlgorithms.RsaSsaPssSha256); theoryData.Add(new JwtTheoryData From 0c13bf06f01b3558b1876b51b52a0bd17bc64186 Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Tue, 18 Jun 2024 18:50:29 +0100 Subject: [PATCH 08/22] Remove exceptions thrown during issuer validation. Add validation source. (#2643) * Added error IDX10000 as a constant * Removed ArgumentNullExceptions within issuer validator * Added ValidationSource to IssuerValidationResult to identify the validation method * Removed TODO * Added default value for new parameter. Updated documentation --- .../LogMessages.cs | 2 +- .../Validation/IssuerValidationResult.cs | 18 +++- .../Validation/Validators.Issuer.cs | 32 +++++-- .../IdentityComparer.cs | 3 + .../Validation/IssuerValidationResultTests.cs | 89 ++++++++++++++++++- 5 files changed, 134 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs index 679fbf2b8f..d54c07479c 100644 --- a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs +++ b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs @@ -13,7 +13,7 @@ internal static class LogMessages { #pragma warning disable 1591 // general - // public const string IDX10000 = "IDX10000:"; + public const string IDX10000 = "IDX10000: The parameter '{0}' cannot be a 'null' or an empty object. "; // properties, configuration public const string IDX10101 = "IDX10101: MaximumTokenSizeInBytes must be greater than zero. value: '{0}'"; diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/IssuerValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/IssuerValidationResult.cs index 35c53dc561..292eacfab1 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/IssuerValidationResult.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/IssuerValidationResult.cs @@ -11,17 +11,27 @@ namespace Microsoft.IdentityModel.Tokens /// internal class IssuerValidationResult : ValidationResult { + internal enum ValidationSource + { + NotValidated = 0, + IssuerIsConfigurationIssuer, + IssuerIsValidIssuer, + IssuerIsAmongValidIssuers + } + private Exception _exception; /// /// Creates an instance of /// /// is the issuer that was validated successfully. - public IssuerValidationResult(string issuer) + /// is the indicating how this issuer was validated. + public IssuerValidationResult(string issuer, ValidationSource source = ValidationSource.NotValidated) : base(ValidationFailureType.ValidationSucceeded) { Issuer = issuer; IsValid = true; + Source = source; } /// @@ -30,11 +40,13 @@ public IssuerValidationResult(string issuer) /// is the issuer that was intended to be validated. /// is the that occurred during validation. /// is the that occurred during validation. - public IssuerValidationResult(string issuer, ValidationFailureType validationFailure, ExceptionDetail exceptionDetail) + /// is the indicating how this issuer was validated. + public IssuerValidationResult(string issuer, ValidationFailureType validationFailure, ExceptionDetail exceptionDetail, ValidationSource source = ValidationSource.NotValidated) : base(validationFailure, exceptionDetail) { Issuer = issuer; IsValid = false; + Source = source; } /// @@ -65,5 +77,7 @@ public override Exception Exception /// Gets the issuer that was validated or intended to be validated. /// public string Issuer { get; } + + public ValidationSource Source { get; } } } diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs index faca7d71e5..f4d580e1ef 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs @@ -209,10 +209,28 @@ internal static async Task ValidateIssuerAsync( } if (validationParameters == null) - throw LogHelper.LogArgumentNullException(nameof(validationParameters)); + return new IssuerValidationResult( + issuer, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10000, + LogHelper.MarkAsNonPII(nameof(validationParameters))), + typeof(ArgumentNullException), + new StackFrame(true), + null)); if (securityToken == null) - throw LogHelper.LogArgumentNullException(nameof(securityToken)); + return new IssuerValidationResult( + issuer, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10000, + LogHelper.MarkAsNonPII(nameof(securityToken))), + typeof(ArgumentNullException), + new StackFrame(true), + null)); BaseConfiguration configuration = null; if (validationParameters.ConfigurationManager != null) @@ -234,7 +252,6 @@ internal static async Task ValidateIssuerAsync( new StackFrame(true))); } - // TODO - we should distinguish if configuration, TVP.ValidIssuer or TVP.ValidIssuers was used to validate the issuer. if (configuration != null) { if (string.Equals(configuration.Issuer, issuer)) @@ -245,13 +262,15 @@ internal static async Task ValidateIssuerAsync( if (LogHelper.IsEnabled(EventLogLevel.Informational)) LogHelper.LogInformation(LogMessages.IDX10236, LogHelper.MarkAsNonPII(issuer), callContext); - return new IssuerValidationResult(issuer); + return new IssuerValidationResult(issuer, + IssuerValidationResult.ValidationSource.IssuerIsConfigurationIssuer); } } if (string.Equals(validationParameters.ValidIssuer, issuer)) { - return new IssuerValidationResult(issuer); + return new IssuerValidationResult(issuer, + IssuerValidationResult.ValidationSource.IssuerIsValidIssuer); } if (validationParameters.ValidIssuers != null) @@ -271,7 +290,8 @@ internal static async Task ValidateIssuerAsync( if (LogHelper.IsEnabled(EventLogLevel.Informational)) LogHelper.LogInformation(LogMessages.IDX10236, LogHelper.MarkAsNonPII(issuer)); - return new IssuerValidationResult(issuer); + return new IssuerValidationResult(issuer, + IssuerValidationResult.ValidationSource.IssuerIsAmongValidIssuers); } } } diff --git a/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs b/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs index 872a9e8cea..7ef5e4fa3e 100644 --- a/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs +++ b/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs @@ -574,6 +574,9 @@ internal static bool AreIssuerValidationResultsEqual( if (issuerValidationResult1.Issuer != issuerValidationResult2.Issuer) localContext.Diffs.Add($"IssuerValidationResult1.Issuer: {issuerValidationResult1.Issuer} != IssuerValidationResult2.Issuer: {issuerValidationResult2.Issuer}"); + if (issuerValidationResult1.Source != issuerValidationResult2.Source) + localContext.Diffs.Add($"IssuerValidationResult1.Source: {issuerValidationResult1.Source} != IssuerValidationResult2.Source: {issuerValidationResult2.Source}"); + // true => both are not null. if (ContinueCheckingEquality(issuerValidationResult1.Exception, issuerValidationResult2.Exception, localContext)) { diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs index 3c3f403fa2..c8b5694cc6 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs @@ -9,6 +9,7 @@ using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.TestUtils; using Microsoft.IdentityModel.Tokens.Json.Tests; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Xunit; namespace Microsoft.IdentityModel.Tokens.Validation.Tests @@ -29,7 +30,9 @@ public async Task IssuerValidatorAsyncTests(IssuerValidationResultsTheoryData th new CallContext(), CancellationToken.None).ConfigureAwait(false); - theoryData.ExpectedException.ProcessException(issuerValidationResult.Exception, context); + if (issuerValidationResult.Exception != null) + theoryData.ExpectedException.ProcessException(issuerValidationResult.Exception, context); + IdentityComparer.AreIssuerValidationResultsEqual( issuerValidationResult, theoryData.IssuerValidationResult, @@ -92,6 +95,90 @@ public static TheoryData IssuerValdationResul ValidationParameters = new TokenValidationParameters(), }); + theoryData.Add(new IssuerValidationResultsTheoryData("NULL_ValidationParameters") + { + ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), + Issuer = issClaim, + IssuerValidationResult = new IssuerValidationResult( + issClaim, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10000, + LogHelper.MarkAsNonPII("validationParameters")), + typeof(ArgumentNullException), + new StackFrame(true), + null)), + IsValid = false, + SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Iss, issClaim), + ValidationParameters = null + }); + + theoryData.Add(new IssuerValidationResultsTheoryData("NULL_SecurityToken") + { + ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), + Issuer = issClaim, + IssuerValidationResult = new IssuerValidationResult( + issClaim, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10000, + LogHelper.MarkAsNonPII("securityToken")), + typeof(ArgumentNullException), + new StackFrame(true), + null)), + IsValid = false, + SecurityToken = null, + ValidationParameters = new TokenValidationParameters() + }); + + var validConfig = new OpenIdConnectConfiguration() { Issuer = issClaim }; + theoryData.Add(new IssuerValidationResultsTheoryData("Valid_FromConfig") + { + ExpectedException = ExpectedException.NoExceptionExpected, + Issuer = issClaim, + IssuerValidationResult = new IssuerValidationResult( + issClaim, + IssuerValidationResult.ValidationSource.IssuerIsConfigurationIssuer), + IsValid = true, + SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Iss, issClaim), + ValidationParameters = new TokenValidationParameters() + { + ConfigurationManager = new MockConfigurationManager(validConfig) + } + }); + + theoryData.Add(new IssuerValidationResultsTheoryData("Valid_FromValidationParametersValidIssuer") + { + ExpectedException = ExpectedException.NoExceptionExpected, + Issuer = issClaim, + IssuerValidationResult = new IssuerValidationResult( + issClaim, + IssuerValidationResult.ValidationSource.IssuerIsValidIssuer), + IsValid = true, + SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Iss, issClaim), + ValidationParameters = new TokenValidationParameters() + { + ValidIssuer = issClaim + } + }); + + theoryData.Add(new IssuerValidationResultsTheoryData("Valid_FromValidationParametersValidIssuers") + { + ExpectedException = ExpectedException.NoExceptionExpected, + Issuer = issClaim, + IssuerValidationResult = new IssuerValidationResult( + issClaim, + IssuerValidationResult.ValidationSource.IssuerIsAmongValidIssuers), + IsValid = true, + SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Iss, issClaim), + ValidationParameters = new TokenValidationParameters() + { + ValidIssuers = [issClaim] + } + }); + return theoryData; } } From a2486af8f2eb5d6e470d0fb6fd1d3c1bf606e49a Mon Sep 17 00:00:00 2001 From: jennyf19 Date: Tue, 18 Jun 2024 11:23:41 -0700 Subject: [PATCH 09/22] Update CHANGELOG for 8x (#2645) --- CHANGELOG.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79b39aa066..f1991880f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,24 @@ See the [releases](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/releases) for details on bug fixes and added features. - -Pending Next Release +8.0.0-preview1 +==== +### Breaking changes: +- IdentityModel 8x no longer supports .net461, which has reached end of life and is no longer supported. See issue [#2544](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/2544) for details. +- Two IdentityModel extension dlls `Microsoft.IdentityModel.KeyVaultExtensions` and `Microsoft.IdentityModel.ManagedKeyVaultSecurityKey` were using ADAL, which is no longer supported . The affected packages have been removed, as the replacement is to use [Microsoft.Identity.Web](https://github.com/AzureAD/microsoft-identity-web/wiki/Certificates). See issue [#2454](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/2454) for details. +- `AppContext.SetSwitch` which were included in IdentityModel 7x, have been removed and are the default in IdentityModel 8x. The result is a more performant IdentityModel by default. See issue [#2629](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/2629) for details. + +7.6.1 ===== +### New Features: +- Add missing metadata parameters to OpenIdConnectConfiguration. See issue [#2498](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/2498) for details. + ### Bug Fixes: +- Fix over-reporting of `IDX14100`. See issue [#2058](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/2058) and PR [#2618](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/pull/2618) for details. +- `JwtRegisteredClaimNames` now contains previously missing Standard OpenIdConnect claims. See issue [#1598](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/1598) for details. + +### Performance Improvements: - Reduced allocations in `AadIssuerValidator` by not using `string.Replace` where appropriate. See issue [#2595](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/2595) and PR [#2597](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/pull/2597) for more details. +- No longer for every string claim, calling DateTime.TryParse on each value, whether it is expected to be a DateTime or not. See issue [#2615](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/2615) for details. 7.6.0 ===== From 1517cddc2231ab3a0981d72e5c4e802f68e98fe9 Mon Sep 17 00:00:00 2001 From: joegoldman2 <147369450+joegoldman2@users.noreply.github.com> Date: Tue, 18 Jun 2024 21:23:42 +0000 Subject: [PATCH 10/22] Add a constant for the profile scope in OpenIdConnectScope class (#2647) Co-authored-by: joegoldman2 <147369450+joegoldman@users.noreply.github.com> --- .../OpenIdConnectScope.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdConnectScope.cs b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdConnectScope.cs index 6a3c753e77..c32880eaaf 100644 --- a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdConnectScope.cs +++ b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdConnectScope.cs @@ -37,10 +37,15 @@ public static class OpenIdConnectScope public const string OpenIdProfile = "openid profile"; /// - /// Indicates phone profile scope see: https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims. + /// Indicates phone scope see: https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims. /// public const string Phone = "phone"; + /// + /// Indicates profile scope see: https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims. + /// + public const string Profile = "profile"; + /// /// Indicates user_impersonation scope for Azure Active Directory. /// From bd2bf8e7098e54b83729c716c30ca29aade7dc31 Mon Sep 17 00:00:00 2001 From: kellyyangsong <69649063+kellyyangsong@users.noreply.github.com> Date: Tue, 18 Jun 2024 14:35:19 -0700 Subject: [PATCH 11/22] Rm SkipAuthenticationTagLengthValidation and UseShortNameForRsaOaepKey App Context Switches (#2644) * rm app context switches * update tests * update changelog --- CHANGELOG.md | 2 +- .../AuthenticatedEncryptionProvider.cs | 9 +-- .../LogMessages.cs | 2 +- .../X509EncryptingCredentials.cs | 14 +--- .../JsonWebTokenHandlerTests.cs | 50 +------------- .../EncryptingCredentialsTests.cs | 8 +-- .../MultiThreadingTests.cs | 4 +- .../X509EncryptingCredentialsTests.cs | 8 +-- ...tyTokenHandlerTests.WithContextSwitches.cs | 66 ------------------- 9 files changed, 16 insertions(+), 147 deletions(-) delete mode 100644 test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTests.WithContextSwitches.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index f1991880f9..5a9f0c8101 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ See the [releases](https://github.com/AzureAD/azure-activedirectory-identitymode ### Breaking changes: - IdentityModel 8x no longer supports .net461, which has reached end of life and is no longer supported. See issue [#2544](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/2544) for details. - Two IdentityModel extension dlls `Microsoft.IdentityModel.KeyVaultExtensions` and `Microsoft.IdentityModel.ManagedKeyVaultSecurityKey` were using ADAL, which is no longer supported . The affected packages have been removed, as the replacement is to use [Microsoft.Identity.Web](https://github.com/AzureAD/microsoft-identity-web/wiki/Certificates). See issue [#2454](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/2454) for details. -- `AppContext.SetSwitch` which were included in IdentityModel 7x, have been removed and are the default in IdentityModel 8x. The result is a more performant IdentityModel by default. See issue [#2629](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/2629) for details. +- `AppContext.SetSwitch` which were included in IdentityModel 7x, have been removed and are the default in IdentityModel 8x. The result is a more performant IdentityModel by default. See issue [#2629](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/2629) and https://aka.ms/IdentityModel8x for details. 7.6.1 ===== diff --git a/src/Microsoft.IdentityModel.Tokens/Encryption/AuthenticatedEncryptionProvider.cs b/src/Microsoft.IdentityModel.Tokens/Encryption/AuthenticatedEncryptionProvider.cs index b6b063936d..b9459a1af8 100644 --- a/src/Microsoft.IdentityModel.Tokens/Encryption/AuthenticatedEncryptionProvider.cs +++ b/src/Microsoft.IdentityModel.Tokens/Encryption/AuthenticatedEncryptionProvider.cs @@ -33,7 +33,6 @@ private struct AuthenticatedKeys private DecryptionDelegate DecryptFunction; private EncryptionDelegate EncryptFunction; private const string _className = "Microsoft.IdentityModel.Tokens.AuthenticatedEncryptionProvider"; - internal const string _skipValidationOfAuthenticationTagLength = "Switch.Microsoft.IdentityModel.SkipAuthenticationTagLengthValidation"; /// /// Initializes a new instance of the class used for encryption and decryption. @@ -167,8 +166,7 @@ private AuthenticatedEncryptionResult EncryptWithAesCbc(byte[] plaintext, byte[] private byte[] DecryptWithAesCbc(byte[] ciphertext, byte[] authenticatedData, byte[] iv, byte[] authenticationTag) { // Verify authentication Tag - if (ShouldValidateAuthenticationTagLength() - && SymmetricSignatureProvider.ExpectedSignatureSizeInBytes.TryGetValue(Algorithm, out int expectedTagLength) + if (SymmetricSignatureProvider.ExpectedSignatureSizeInBytes.TryGetValue(Algorithm, out int expectedTagLength) && expectedTagLength != authenticationTag.Length) throw LogHelper.LogExceptionMessage(new SecurityTokenDecryptionFailedException( LogHelper.FormatInvariant(LogMessages.IDX10625, authenticationTag.Length, expectedTagLength, Base64UrlEncoder.Encode(authenticationTag), Algorithm))); @@ -197,11 +195,6 @@ private byte[] DecryptWithAesCbc(byte[] ciphertext, byte[] authenticatedData, by } } - private static bool ShouldValidateAuthenticationTagLength() - { - return !(AppContext.TryGetSwitch(_skipValidationOfAuthenticationTagLength, out bool skipValidation) && skipValidation); - } - private AuthenticatedKeys CreateAuthenticatedKeys() { ValidateKeySize(Key, Algorithm); diff --git a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs index d54c07479c..689b2ccb9d 100644 --- a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs +++ b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs @@ -136,7 +136,7 @@ internal static class LogMessages // public const string IDX10622 = "IDX10622:"; // public const string IDX10623 = "IDX10623:"; // public const string IDX10624 = "IDX10624:"; - public const string IDX10625 = "IDX10625: Failed to verify the authenticationTag length, the actual tag length '{0}' does not match the expected tag length '{1}'. authenticationTag: '{2}', algorithm: '{3}' See: https://aka.ms/IdentityModel/SkipAuthenticationTagLengthValidation"; + public const string IDX10625 = "IDX10625: Failed to verify the authenticationTag length, the actual tag length '{0}' does not match the expected tag length '{1}'. authenticationTag: '{2}', algorithm: '{3}'."; // public const string IDX10627 = "IDX10627:"; public const string IDX10628 = "IDX10628: Cannot set the MinimumSymmetricKeySizeInBits to less than '{0}'."; public const string IDX10630 = "IDX10630: The '{0}' for signing cannot be smaller than '{1}' bits. KeySize: '{2}'."; diff --git a/src/Microsoft.IdentityModel.Tokens/X509EncryptingCredentials.cs b/src/Microsoft.IdentityModel.Tokens/X509EncryptingCredentials.cs index ec0f483dd8..6a92eaedaa 100644 --- a/src/Microsoft.IdentityModel.Tokens/X509EncryptingCredentials.cs +++ b/src/Microsoft.IdentityModel.Tokens/X509EncryptingCredentials.cs @@ -11,8 +11,6 @@ namespace Microsoft.IdentityModel.Tokens /// public class X509EncryptingCredentials : EncryptingCredentials { - internal const string _useShortNameForRsaOaepKey = "Switch.Microsoft.IdentityModel.UseShortNameForRsaOaepKey"; - /// /// Designed to construct based on a x509 certificate. /// @@ -23,7 +21,7 @@ public class X509EncryptingCredentials : EncryptingCredentials /// /// if 'certificate' is null. public X509EncryptingCredentials(X509Certificate2 certificate) - : this(certificate, GetEncryptionAlgorithm(), SecurityAlgorithms.DefaultSymmetricEncryptionAlgorithm) + : this(certificate, SecurityAlgorithms.RsaOAEP, SecurityAlgorithms.DefaultSymmetricEncryptionAlgorithm) { } @@ -50,15 +48,5 @@ public X509Certificate2 Certificate get; private set; } - - private static string GetEncryptionAlgorithm() - { - return ShouldUseShortNameForRsaOaepKey() ? SecurityAlgorithms.RsaOAEP : SecurityAlgorithms.DefaultAsymmetricKeyWrapAlgorithm; - } - - private static bool ShouldUseShortNameForRsaOaepKey() - { - return AppContext.TryGetSwitch(_useShortNameForRsaOaepKey, out var useKeyWrap) && useKeyWrap; - } } } diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs index bd041cac2f..6b67a4f4e3 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs @@ -4190,10 +4190,9 @@ public static TheoryData IncludeSecurityTokenOnFailureTes } [Theory, MemberData(nameof(ValidateAuthenticationTagLengthTheoryData))] - public void ValidateTokenAsync_ModifiedAuthNTag(CreateTokenTheoryData theoryData) + public async Task ValidateTokenAsync_ModifiedAuthNTag(CreateTokenTheoryData theoryData) { // arrange - AppContext.SetSwitch(AuthenticatedEncryptionProvider._skipValidationOfAuthenticationTagLength, theoryData.EnableAppContextSwitch); var payload = new JObject() { { JwtRegisteredClaimNames.Email, "Bob@contoso.com" }, @@ -4217,9 +4216,7 @@ public void ValidateTokenAsync_ModifiedAuthNTag(CreateTokenTheoryData theoryData var jweWithExtraCharacters = jwe + "_cannoli_hunts_truffles_"; // act - // calling ValidateTokenAsync.Result to prevent tests from sharing app context switch property - // normally, we would want to await ValidateTokenAsync().ConfigureAwait(false) - var tokenValidationResult = jsonWebTokenHandler.ValidateTokenAsync(jweWithExtraCharacters, theoryData.ValidationParameters).Result; + var tokenValidationResult = await jsonWebTokenHandler.ValidateTokenAsync(jweWithExtraCharacters, theoryData.ValidationParameters).ConfigureAwait(false); // assert Assert.Equal(theoryData.IsValid, tokenValidationResult.IsValid); @@ -4281,47 +4278,6 @@ public static TheoryData ValidateAuthenticationTagLengthT ValidIssuer = "http://Default.Issuer.com", }, IsValid = false - }, - new("A128CBC-HS256_SkipTagLengthValidationAppContextSwitchOn_IsValid") - { - EnableAppContextSwitch = true, - Algorithm = SecurityAlgorithms.Aes128CbcHmacSha256, - EncryptingCredentials = new EncryptingCredentials(KeyingMaterial.RsaSecurityKey_2048, SecurityAlgorithms.RsaPKCS1, SecurityAlgorithms.Aes128CbcHmacSha256), - ValidationParameters = new TokenValidationParameters - { - TokenDecryptionKey = KeyingMaterial.JsonWebKeyRsa256SigningCredentials.Key, - IssuerSigningKey = Default.SymmetricSigningKey256, - ValidAudience = "http://Default.Audience.com", - ValidIssuer = "http://Default.Issuer.com", - }, - IsValid = true - }, - new("A192CBC-HS384_SkipTagLengthValidationAppContextSwitchOn_IsValid") - { - EnableAppContextSwitch = true, - Algorithm = SecurityAlgorithms.Aes192CbcHmacSha384, - EncryptingCredentials = new EncryptingCredentials(KeyingMaterial.RsaSecurityKey_2048, SecurityAlgorithms.RsaPKCS1, SecurityAlgorithms.Aes192CbcHmacSha384), - ValidationParameters = new TokenValidationParameters - { - TokenDecryptionKey = KeyingMaterial.JsonWebKeyRsa256SigningCredentials.Key, - IssuerSigningKey = Default.SymmetricSigningKey256, - ValidAudience = "http://Default.Audience.com", - ValidIssuer = "http://Default.Issuer.com", - }, - IsValid = true - }, - new("A256CBC-HS512_SkipTagLengthValidationAppContextSwitchOn_IsValid") - { - EnableAppContextSwitch = true, - EncryptingCredentials = new EncryptingCredentials(KeyingMaterial.RsaSecurityKey_2048, SecurityAlgorithms.RsaPKCS1, SecurityAlgorithms.Aes256CbcHmacSha512), - ValidationParameters = new TokenValidationParameters - { - TokenDecryptionKey = signingCredentials512.Key, - IssuerSigningKey = Default.SymmetricSigningKey256, - ValidAudience = "http://Default.Audience.com", - ValidIssuer = "http://Default.Issuer.com", - }, - IsValid = true } }; } @@ -4370,8 +4326,6 @@ public CreateTokenTheoryData(string testId) : base(testId) public IEnumerable ExpectedDecryptionKeys { get; set; } public Dictionary ExpectedClaims { get; set; } - - public bool EnableAppContextSwitch { get; set; } = false; } // Overrides CryptoProviderFactory.CreateAuthenticatedEncryptionProvider to create AuthenticatedEncryptionProviderMock that provides AesGcm encryption. diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/EncryptingCredentialsTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/EncryptingCredentialsTests.cs index 7b44f1b844..ad787f01d4 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/EncryptingCredentialsTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/EncryptingCredentialsTests.cs @@ -57,7 +57,7 @@ public static TheoryData ConstructorATheoryData new EncryptingCredentialsTheoryData { Key = null, - Alg = SecurityAlgorithms.RsaOaepKeyWrap, + Alg = SecurityAlgorithms.RsaOAEP, Enc = SecurityAlgorithms.Aes128CbcHmacSha256, ExpectedException = ExpectedException.ArgumentNullException("IDX10000: The parameter 'key'"), TestId = "NullKey" @@ -73,7 +73,7 @@ public static TheoryData ConstructorATheoryData new EncryptingCredentialsTheoryData { Key = Default.AsymmetricEncryptionKeyPublic, - Alg = SecurityAlgorithms.RsaOaepKeyWrap, + Alg = SecurityAlgorithms.RsaOAEP, Enc = String.Empty, ExpectedException = ExpectedException.ArgumentNullException("IDX10000: The parameter 'enc'"), TestId = "EmptyEncString" @@ -89,7 +89,7 @@ public static TheoryData ConstructorATheoryData new EncryptingCredentialsTheoryData { Key = Default.AsymmetricEncryptionKeyPublic, - Alg = SecurityAlgorithms.RsaOaepKeyWrap, + Alg = SecurityAlgorithms.RsaOAEP, Enc = null, ExpectedException = ExpectedException.ArgumentNullException("IDX10000: The parameter 'enc'"), TestId = "NullEncString" @@ -97,7 +97,7 @@ public static TheoryData ConstructorATheoryData new EncryptingCredentialsTheoryData { Key = Default.AsymmetricEncryptionKeyPublic, - Alg = SecurityAlgorithms.RsaOaepKeyWrap, + Alg = SecurityAlgorithms.RsaOAEP, Enc = SecurityAlgorithms.Aes128CbcHmacSha256, TestId = "ValidTest" } diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/MultiThreadingTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/MultiThreadingTests.cs index f1b5d636b6..6352fc6fe4 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/MultiThreadingTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/MultiThreadingTests.cs @@ -121,7 +121,7 @@ public static TheoryData MultiThreadingCreateAndVerify { Claims = Default.PayloadDictionary, SigningCredentials = new SigningCredentials(KeyingMaterial.RsaSecurityKey_2048, SecurityAlgorithms.RsaSha256, SecurityAlgorithms.Sha256), - EncryptingCredentials = new EncryptingCredentials(KeyingMaterial.RsaSecurityKey_2048, SecurityAlgorithms.RsaOaepKeyWrap, SecurityAlgorithms.Aes128CbcHmacSha256) + EncryptingCredentials = new EncryptingCredentials(KeyingMaterial.RsaSecurityKey_2048, SecurityAlgorithms.RsaOAEP, SecurityAlgorithms.Aes128CbcHmacSha256) }; var tokenValidationParametersEncryptedRsaKW = new TokenValidationParameters @@ -174,7 +174,7 @@ public static TheoryData MultiThreadingCreateAndVerify { Claims = Default.PayloadDictionary, SigningCredentials = new SigningCredentials(KeyingMaterial.RsaSecurityKeyCng_2048, SecurityAlgorithms.RsaSha256, SecurityAlgorithms.Sha256), - EncryptingCredentials = new EncryptingCredentials(KeyingMaterial.RsaSecurityKeyCng_2048, SecurityAlgorithms.RsaOaepKeyWrap, SecurityAlgorithms.Aes128CbcHmacSha256) + EncryptingCredentials = new EncryptingCredentials(KeyingMaterial.RsaSecurityKeyCng_2048, SecurityAlgorithms.RsaOAEP, SecurityAlgorithms.Aes128CbcHmacSha256) }; var tokenValidationParametersEncryptedRsaKWCng = new TokenValidationParameters diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/X509EncryptingCredentialsTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/X509EncryptingCredentialsTests.cs index 6590737fca..2dee4d2e62 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/X509EncryptingCredentialsTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/X509EncryptingCredentialsTests.cs @@ -42,7 +42,7 @@ public static TheoryData ConstructorsTheory new X509EncryptingCredentialsTheoryData { Certificate = null, - Alg = SecurityAlgorithms.RsaOaepKeyWrap, + Alg = SecurityAlgorithms.RsaOAEP, Enc = SecurityAlgorithms.Aes128CbcHmacSha256, ExpectedException = ExpectedException.ArgumentNullException("IDX10000: The parameter 'certificate'"), TestId = "NullCertificate" @@ -58,7 +58,7 @@ public static TheoryData ConstructorsTheory new X509EncryptingCredentialsTheoryData { Certificate = Default.Certificate, - Alg = SecurityAlgorithms.RsaOaepKeyWrap, + Alg = SecurityAlgorithms.RsaOAEP, Enc = String.Empty, ExpectedException = ExpectedException.ArgumentNullException("IDX10000: The parameter 'enc'"), TestId = "EmptyEncString" @@ -74,7 +74,7 @@ public static TheoryData ConstructorsTheory new X509EncryptingCredentialsTheoryData { Certificate = Default.Certificate, - Alg = SecurityAlgorithms.RsaOaepKeyWrap, + Alg = SecurityAlgorithms.RsaOAEP, Enc = null, ExpectedException = ExpectedException.ArgumentNullException("IDX10000: The parameter 'enc'"), TestId = "NullEncString" @@ -82,7 +82,7 @@ public static TheoryData ConstructorsTheory new X509EncryptingCredentialsTheoryData { Certificate = Default.Certificate, - Alg = SecurityAlgorithms.RsaOaepKeyWrap, + Alg = SecurityAlgorithms.RsaOAEP, Enc = SecurityAlgorithms.Aes128CbcHmacSha256, TestId = "ValidTest" } diff --git a/test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTests.WithContextSwitches.cs b/test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTests.WithContextSwitches.cs deleted file mode 100644 index 7e87575e11..0000000000 --- a/test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTests.WithContextSwitches.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.IdentityModel.JsonWebTokens; -using Microsoft.IdentityModel.TestUtils; -using Microsoft.IdentityModel.Tokens; -using Xunit; - -namespace System.IdentityModel.Tokens.Jwt.Tests -{ - [CollectionDefinition("JwtSecurityTokenHandlerTestsWithContextSwitches", DisableParallelization = true)] - public class JwtSecurityTokenHandlerTestsWithContextSwitches - { - [Theory] - [InlineData(SecurityAlgorithms.RsaOAEP, true)] - [InlineData(SecurityAlgorithms.RsaOaepKeyWrap, false)] - public void JwtSecurityTokenHandler_CreateToken_AddShortFormMappingForRsaOAEP(string algorithm, bool useShortNameForRsaOaepKey) - { - AppContext.SetSwitch(X509EncryptingCredentials._useShortNameForRsaOaepKey, useShortNameForRsaOaepKey); - var encryptingCredentials = new X509EncryptingCredentials(Default.Certificate); - JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler(); - - JwtSecurityToken token = CreateJwtSecurityToken(tokenHandler, encryptingCredentials); - - Assert.Equal(token.Header.Alg, algorithm); - - AppContext.SetSwitch(X509EncryptingCredentials._useShortNameForRsaOaepKey, false); - } - - [Theory] - [InlineData(SecurityAlgorithms.RsaOAEP, true)] - [InlineData(SecurityAlgorithms.RsaOaepKeyWrap, false)] - public void JsonWebTokenHandler_CreateToken_AddShortFormMappingForRsaOAEP(string algorithm, bool useShortNameForRsaOaepKey) - { - AppContext.SetSwitch(X509EncryptingCredentials._useShortNameForRsaOaepKey, useShortNameForRsaOaepKey); - var encryptingCredentials = new X509EncryptingCredentials(Default.Certificate); - JsonWebTokenHandler tokenHandler = new JsonWebTokenHandler(); - - JsonWebToken jsonToken = new JsonWebToken(CreateJwtSecurityTokenAsString(tokenHandler, encryptingCredentials)); - - Assert.Equal(jsonToken.Alg, algorithm); - - AppContext.SetSwitch(X509EncryptingCredentials._useShortNameForRsaOaepKey, false); - } - - private JwtSecurityToken CreateJwtSecurityToken(JwtSecurityTokenHandler tokenHandler, X509EncryptingCredentials encryptingCredentials) - { - return tokenHandler.CreateJwtSecurityToken(CreateTokenDescriptor(encryptingCredentials)); - } - - private string CreateJwtSecurityTokenAsString(JsonWebTokenHandler tokenHandler, X509EncryptingCredentials encryptingCredentials) - { - return tokenHandler.CreateToken(CreateTokenDescriptor(encryptingCredentials)); - } - - private SecurityTokenDescriptor CreateTokenDescriptor(X509EncryptingCredentials encryptingCredentials) - { - return new SecurityTokenDescriptor - { - Issuer = Default.Issuer, - SigningCredentials = Default.AsymmetricSigningCredentials, - EncryptingCredentials = encryptingCredentials, - }; - } - } -} From caf123fb476d29f61ef79ee58d6583e0a155bbd6 Mon Sep 17 00:00:00 2001 From: sruthikeerthi <73967733+sruke@users.noreply.github.com> Date: Tue, 18 Jun 2024 23:08:42 -0700 Subject: [PATCH 12/22] Revert PR2597 to reduce allocations (#2649) Co-authored-by: Sruthi Keerthi Rangavajhula (from Dev Box) --- .../AadIssuerValidator/AadIssuerValidator.cs | 24 +++++------------ .../AadTokenValidationParametersExtension.cs | 27 ++++++++----------- .../AadIssuerValidatorTests.cs | 24 ----------------- .../AadSigningKeyIssuerValidatorTests.cs | 15 ----------- 4 files changed, 17 insertions(+), 73 deletions(-) delete mode 100644 test/Microsoft.IdentityModel.Validators.Tests/AadIssuerValidatorTests.cs diff --git a/src/Microsoft.IdentityModel.Validators/AadIssuerValidator/AadIssuerValidator.cs b/src/Microsoft.IdentityModel.Validators/AadIssuerValidator/AadIssuerValidator.cs index 445671d134..323718cc84 100644 --- a/src/Microsoft.IdentityModel.Validators/AadIssuerValidator/AadIssuerValidator.cs +++ b/src/Microsoft.IdentityModel.Validators/AadIssuerValidator/AadIssuerValidator.cs @@ -13,6 +13,7 @@ using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; +using static Microsoft.IdentityModel.Validators.AadIssuerValidator; namespace Microsoft.IdentityModel.Validators { @@ -382,31 +383,18 @@ private ConfigurationManager CreateConfigManager( } } - internal static bool IsValidIssuer(string validIssuerTemplate, string tenantId, string actualIssuer) + private static bool IsValidIssuer(string validIssuerTemplate, string tenantId, string actualIssuer) { - if (string.IsNullOrEmpty(validIssuerTemplate) || string.IsNullOrEmpty(actualIssuer) || string.IsNullOrEmpty(tenantId)) + if (string.IsNullOrEmpty(validIssuerTemplate)) return false; - ReadOnlySpan validIssuerTemplateSpan = validIssuerTemplate.AsSpan(); - ReadOnlySpan actualIssuerSpan = actualIssuer.AsSpan(); - int indexOfTenantIdTemplate = validIssuerTemplate.IndexOf(TenantIdTemplate, StringComparison.Ordinal); - - if (indexOfTenantIdTemplate >= 0 && actualIssuer.Length > indexOfTenantIdTemplate) + if (validIssuerTemplate.Contains(TenantIdTemplate)) { - // ensure the first part of the validIssuerTemplate matches the first part of actualIssuer - if (!validIssuerTemplateSpan.Slice(0, indexOfTenantIdTemplate).SequenceEqual(actualIssuerSpan.Slice(0, indexOfTenantIdTemplate))) - return false; - - // ensure that actualIssuer contains the tenantId from indexOfTenantIdTemplate for the length of tenantId.Length - if (!actualIssuerSpan.Slice(indexOfTenantIdTemplate, tenantId.Length).SequenceEqual(tenantId.AsSpan())) - return false; - - // ensure the second halves are equal - return validIssuerTemplateSpan.Slice(indexOfTenantIdTemplate + TenantIdTemplate.Length).SequenceEqual(actualIssuerSpan.Slice(indexOfTenantIdTemplate + tenantId.Length)); + return validIssuerTemplate.Replace(TenantIdTemplate, tenantId) == actualIssuer; } else { - return validIssuerTemplateSpan.SequenceEqual(actualIssuerSpan); + return validIssuerTemplate == actualIssuer; } } diff --git a/src/Microsoft.IdentityModel.Validators/AadTokenValidationParametersExtension.cs b/src/Microsoft.IdentityModel.Validators/AadTokenValidationParametersExtension.cs index 5335284168..d745bf124c 100644 --- a/src/Microsoft.IdentityModel.Validators/AadTokenValidationParametersExtension.cs +++ b/src/Microsoft.IdentityModel.Validators/AadTokenValidationParametersExtension.cs @@ -76,36 +76,31 @@ internal static bool ValidateIssuerSigningKey(SecurityKey securityKey, SecurityT #if NET6_0_OR_GREATER if (!string.IsNullOrEmpty(tokenIssuer) && !tokenIssuer.Contains(tenantIdFromToken, StringComparison.Ordinal)) throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidIssuerException(LogHelper.FormatInvariant(LogMessages.IDX40004, LogHelper.MarkAsNonPII(tokenIssuer), LogHelper.MarkAsNonPII(tenantIdFromToken)))); + + // creating an effectiveSigningKeyIssuer is required as signingKeyIssuer might contain {tenantid} + var effectiveSigningKeyIssuer = signingKeyIssuer.Replace(AadIssuerValidator.TenantIdTemplate, tenantIdFromToken, StringComparison.Ordinal); + var v2TokenIssuer = openIdConnectConfiguration.Issuer?.Replace(AadIssuerValidator.TenantIdTemplate, tenantIdFromToken, StringComparison.Ordinal); #else if (!string.IsNullOrEmpty(tokenIssuer) && !tokenIssuer.Contains(tenantIdFromToken)) throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidIssuerException(LogHelper.FormatInvariant(LogMessages.IDX40004, LogHelper.MarkAsNonPII(tokenIssuer), LogHelper.MarkAsNonPII(tenantIdFromToken)))); + + // creating an effectiveSigningKeyIssuer is required as signingKeyIssuer might contain {tenantid} + var effectiveSigningKeyIssuer = signingKeyIssuer.Replace(AadIssuerValidator.TenantIdTemplate, tenantIdFromToken); + var v2TokenIssuer = openIdConnectConfiguration.Issuer?.Replace(AadIssuerValidator.TenantIdTemplate, tenantIdFromToken); #endif - // comparing effectiveSigningKeyIssuer with v2TokenIssuer is required because of the following scenario: + + // comparing effectiveSigningKeyIssuer with v2TokenIssuer is required as well because of the following scenario: // 1. service trusts /common/v2.0 endpoint // 2. service receieves a v1 token that has issuer like sts.windows.net // 3. signing key issuers will never match sts.windows.net as v1 endpoint doesn't have issuers attached to keys // v2TokenIssuer is the representation of Token.Issuer (if it was a v2 issuer) - if (!AadIssuerValidator.IsValidIssuer(signingKeyIssuer, tenantIdFromToken, tokenIssuer) - && !AadIssuerValidator.IsValidIssuer(signingKeyIssuer, tenantIdFromToken, openIdConnectConfiguration.Issuer)) - { - int templateStartIndex = signingKeyIssuer.IndexOf(AadIssuerValidator.TenantIdTemplate, StringComparison.Ordinal); - string effectiveSigningKeyIssuer = templateStartIndex > -1 ? CreateIssuer(signingKeyIssuer, AadIssuerValidator.TenantIdTemplate, tenantIdFromToken, templateStartIndex) : signingKeyIssuer; + if (effectiveSigningKeyIssuer != tokenIssuer && effectiveSigningKeyIssuer != v2TokenIssuer) throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidIssuerException(LogHelper.FormatInvariant(LogMessages.IDX40005, LogHelper.MarkAsNonPII(tokenIssuer), LogHelper.MarkAsNonPII(effectiveSigningKeyIssuer)))); - } } return true; } - internal static string CreateIssuer(string issuer, string tenantIdTemplate, string tenantId, int templateStartIndex) - { -#if NET6_0_OR_GREATER - return string.Concat(issuer.AsSpan(0, templateStartIndex), tenantId, issuer.AsSpan(templateStartIndex + tenantIdTemplate.Length, issuer.Length - tenantIdTemplate.Length - templateStartIndex)); -#else - return string.Concat(issuer.Substring(0, templateStartIndex), tenantId, issuer.Substring(templateStartIndex + tenantIdTemplate.Length)); -#endif - } - /// /// Validates the issuer signing key certificate. /// diff --git a/test/Microsoft.IdentityModel.Validators.Tests/AadIssuerValidatorTests.cs b/test/Microsoft.IdentityModel.Validators.Tests/AadIssuerValidatorTests.cs deleted file mode 100644 index 4e05268f1f..0000000000 --- a/test/Microsoft.IdentityModel.Validators.Tests/AadIssuerValidatorTests.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -using Xunit; - -namespace Microsoft.IdentityModel.Validators.Tests -{ - public class AadIssuerValidatorTests - { - [Theory] - [InlineData(ValidatorConstants.AadInstance + AadIssuerValidator.TenantIdTemplate, ValidatorConstants.AadInstance + ValidatorConstants.TenantIdAsGuid, true)] - [InlineData(ValidatorConstants.AadInstancePPE + AadIssuerValidator.TenantIdTemplate, ValidatorConstants.AadInstance + ValidatorConstants.TenantIdAsGuid, false)] - [InlineData(ValidatorConstants.AadInstance + AadIssuerValidator.TenantIdTemplate, ValidatorConstants.AadInstance + ValidatorConstants.B2CTenantAsGuid, false)] - [InlineData("", ValidatorConstants.AadInstance + ValidatorConstants.TenantIdAsGuid, false)] - [InlineData(ValidatorConstants.AadInstance + AadIssuerValidator.TenantIdTemplate, "", false)] - public static void IsValidIssuer_CanValidateTemplatedIssuers(string templatedIssuer, string issuer, bool expectedResult) - { - // act - var result = AadIssuerValidator.IsValidIssuer(templatedIssuer, ValidatorConstants.TenantIdAsGuid, issuer); - - // assert - Assert.Equal(expectedResult, result); - } - } -} diff --git a/test/Microsoft.IdentityModel.Validators.Tests/AadSigningKeyIssuerValidatorTests.cs b/test/Microsoft.IdentityModel.Validators.Tests/AadSigningKeyIssuerValidatorTests.cs index b979f7c49e..e768a2372f 100644 --- a/test/Microsoft.IdentityModel.Validators.Tests/AadSigningKeyIssuerValidatorTests.cs +++ b/test/Microsoft.IdentityModel.Validators.Tests/AadSigningKeyIssuerValidatorTests.cs @@ -325,21 +325,6 @@ public static TheoryData ValidateIssuerSigningKey } } - [Fact] - public static void CreateIssuer_ReturnsExpectedIssuer() - { - // arrange - var issuerTemplate = "{tenantId}"; - var issuer = ValidatorConstants.AadInstance + issuerTemplate; - int templateStartIndex = issuer.IndexOf(issuerTemplate); - - // act - var result = AadTokenValidationParametersExtension.CreateIssuer(issuer, issuerTemplate, ValidatorConstants.TenantIdAsGuid, templateStartIndex); - - // assert - Assert.Equal(ValidatorConstants.AadInstance + ValidatorConstants.TenantIdAsGuid, result); - } - private static OpenIdConnectConfiguration GetConfigurationMock() { var config = new OpenIdConnectConfiguration(); From 0183521b0f127a214aa28cfb8385acfef8c4aa22 Mon Sep 17 00:00:00 2001 From: joegoldman2 <147369450+joegoldman2@users.noreply.github.com> Date: Wed, 19 Jun 2024 15:02:46 +0000 Subject: [PATCH 13/22] Add additional metadata parameters to OpenIdConnectConfiguration (#2646) Co-authored-by: joegoldman2 <147369450+joegoldman@users.noreply.github.com> --- .../OpenIdConnectConfiguration.cs | 72 +++++++++++++++++++ .../OpenIdConnectConfigurationSerializer.cs | 40 +++++++++++ .../OpenIdProviderMetadataNames.cs | 8 +++ .../OpenIdConfigData.cs | 8 +++ .../OpenIdConnectConfigurationTests.cs | 15 +++- .../OpenIdConnectMetadata.json | 4 ++ 6 files changed, 145 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Configuration/OpenIdConnectConfiguration.cs b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Configuration/OpenIdConnectConfiguration.cs index 36fb436a61..c513056b7d 100644 --- a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Configuration/OpenIdConnectConfiguration.cs +++ b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Configuration/OpenIdConnectConfiguration.cs @@ -23,6 +23,9 @@ public class OpenIdConnectConfiguration : BaseConfiguration // these are used to lazy create private Dictionary _additionalData; private ICollection _acrValuesSupported; + private ICollection _authorizationEncryptionAlgValuesSupported; + private ICollection _authorizationEncryptionEncValuesSupported; + private ICollection _authorizationSigningAlgValuesSupported; private ICollection _backchannelAuthenticationRequestSigningAlgValuesSupported; private ICollection _backchannelTokenDeliveryModesSupported; private ICollection _claimsSupported; @@ -146,6 +149,24 @@ public OpenIdConnectConfiguration(string json) #endif public string AuthorizationEndpoint { get; set; } + /// + /// Gets the collection of 'authorization_encryption_alg_values_supported' + /// + [JsonPropertyName(OpenIdProviderMetadataNames.AuthorizationEncryptionAlgValuesSupported)] + public ICollection AuthorizationEncryptionAlgValuesSupported => + _authorizationEncryptionAlgValuesSupported ?? + Interlocked.CompareExchange(ref _authorizationEncryptionAlgValuesSupported, new Collection(), null) ?? + _authorizationEncryptionAlgValuesSupported; + + /// + /// Gets the collection of 'authorization_encryption_enc_values_supported' + /// + [JsonPropertyName(OpenIdProviderMetadataNames.AuthorizationEncryptionEncValuesSupported)] + public ICollection AuthorizationEncryptionEncValuesSupported => + _authorizationEncryptionEncValuesSupported ?? + Interlocked.CompareExchange(ref _authorizationEncryptionEncValuesSupported, new Collection(), null) ?? + _authorizationEncryptionEncValuesSupported; + /// /// Gets or sets the 'authorization_response_iss_parameter_supported' /// @@ -155,6 +176,15 @@ public OpenIdConnectConfiguration(string json) #endif public bool AuthorizationResponseIssParameterSupported { get; set; } + /// + /// Gets the collection of 'authorization_signing_alg_values_supported' + /// + [JsonPropertyName(OpenIdProviderMetadataNames.AuthorizationSigningAlgValuesSupported)] + public ICollection AuthorizationSigningAlgValuesSupported => + _authorizationSigningAlgValuesSupported ?? + Interlocked.CompareExchange(ref _authorizationSigningAlgValuesSupported, new Collection(), null) ?? + _authorizationSigningAlgValuesSupported; + /// /// Gets or sets the 'backchannel_authentication_endpoint'. /// @@ -622,6 +652,15 @@ public OpenIdConnectConfiguration(string json) Interlocked.CompareExchange(ref _tokenEndpointAuthSigningAlgValuesSupported, new Collection(), null) ?? _tokenEndpointAuthSigningAlgValuesSupported; + /// + /// Gets or sets the 'tls_client_certificate_bound_access_tokens' + /// + [JsonPropertyName(OpenIdProviderMetadataNames.TlsClientCertificateBoundAccessTokens)] +#if NET6_0_OR_GREATER + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] +#endif + public bool TlsClientCertificateBoundAccessTokens { get; set; } + /// /// Gets the collection of 'ui_locales_supported' /// @@ -681,6 +720,39 @@ public bool ShouldSerializeAcrValuesSupported() return AcrValuesSupported.Count > 0; } + /// + /// Gets a bool that determines if the 'authorization_encryption_alg_values_supported' (AuthorizationEncryptionAlgValuesSupported) property should be serialized. + /// This is used by Json.NET in order to conditionally serialize properties. + /// + /// true if 'authorization_encryption_alg_values_supported' (AuthorizationEncryptionAlgValuesSupported) is not empty; otherwise, false. + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeAuthorizationEncryptionAlgValuesSupported() + { + return AuthorizationEncryptionAlgValuesSupported.Count > 0; + } + + /// + /// Gets a bool that determines if the 'authorization_encryption_enc_values_supported' (AuthorizationEncryptionEncValuesSupported) property should be serialized. + /// This is used by Json.NET in order to conditionally serialize properties. + /// + /// true if 'authorization_encryption_enc_values_supported' (AuthorizationEncryptionEncValuesSupported) is not empty; otherwise, false. + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeAuthorizationEncryptionEncValuesSupported() + { + return AuthorizationEncryptionEncValuesSupported.Count > 0; + } + + /// + /// Gets a bool that determines if the 'authorization_signing_alg_values_supported' (AuthorizationSigningAlgValuesSupported) property should be serialized. + /// This is used by Json.NET in order to conditionally serialize properties. + /// + /// true if 'authorization_signing_alg_values_supported' (AuthorizationSigningAlgValuesSupported) is not empty; otherwise, false. + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeAuthorizationSigningAlgValuesSupported() + { + return AuthorizationSigningAlgValuesSupported.Count > 0; + } + /// /// Gets a bool that determines if the 'backchannel_token_delivery_modes_supported' (BackchannelTokenDeliveryModesSupported) property should be serialized. /// This is used by Json.NET in order to conditionally serialize properties. diff --git a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Json/OpenIdConnectConfigurationSerializer.cs b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Json/OpenIdConnectConfigurationSerializer.cs index 9e99a0899d..cffe63744f 100644 --- a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Json/OpenIdConnectConfigurationSerializer.cs +++ b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Json/OpenIdConnectConfigurationSerializer.cs @@ -39,7 +39,10 @@ public static readonly { "ACR_VALUES_SUPPORTED", "AUTHORIZATION_ENDPOINT", + "AUTHORIZATION_ENCRYPTION_ALG_VALUES_SUPPORTED", + "AUTHORIZATION_ENCRYPTION_ENC_VALUES_SUPPORTED", "AUTHORIZATION_RESPONSE_ISS_PARAMETER_SUPPORTED", + "AUTHORIZATION_SIGNING_ALG_VALUES_SUPPORTED", "BACKCHANNEL_AUTHENTICATION_ENDPOINT", "BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG_VALUES_SUPPORTED", "BACKCHANNEL_TOKEN_DELIVERY_MODES_SUPPORTED", @@ -91,6 +94,7 @@ public static readonly "TOKEN_ENDPOINT", "TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED", "TOKEN_ENDPOINT_AUTH_SIGNING_ALG_VALUES_SUPPORTED", + "TLS_CLIENT_CERTIFICATE_BOUND_ACCESS_TOKENS", "UI_LOCALES_SUPPORTED", "USERINFO_ENDPOINT", "USERINFO_ENCRYPTION_ALG_VALUES_SUPPORTED", @@ -162,9 +166,18 @@ public static OpenIdConnectConfiguration Read(ref Utf8JsonReader reader, OpenIdC else if (reader.ValueTextEquals(Utf8Bytes.AuthorizationEndpoint)) config.AuthorizationEndpoint = JsonPrimitives.ReadString(ref reader, MetadataName.AuthorizationEndpoint, ClassName, true); + else if (reader.ValueTextEquals(Utf8Bytes.AuthorizationEncryptionAlgValuesSupported)) + JsonPrimitives.ReadStrings(ref reader, config.AuthorizationEncryptionAlgValuesSupported, MetadataName.AuthorizationEncryptionAlgValuesSupported, ClassName, true); + + else if (reader.ValueTextEquals(Utf8Bytes.AuthorizationEncryptionEncValuesSupported)) + JsonPrimitives.ReadStrings(ref reader, config.AuthorizationEncryptionEncValuesSupported, MetadataName.AuthorizationEncryptionEncValuesSupported, ClassName, true); + else if (reader.ValueTextEquals(Utf8Bytes.AuthorizationResponseIssParameterSupported)) config.AuthorizationResponseIssParameterSupported = JsonPrimitives.ReadBoolean(ref reader, MetadataName.AuthorizationResponseIssParameterSupported, ClassName, true); + else if (reader.ValueTextEquals(Utf8Bytes.AuthorizationSigningAlgValuesSupported)) + JsonPrimitives.ReadStrings(ref reader, config.AuthorizationSigningAlgValuesSupported, MetadataName.AuthorizationSigningAlgValuesSupported, ClassName, true); + else if (reader.ValueTextEquals(Utf8Bytes.BackchannelAuthenticationEndpoint)) config.BackchannelAuthenticationEndpoint = JsonPrimitives.ReadString(ref reader, MetadataName.BackchannelAuthenticationEndpoint, ClassName, true); @@ -328,6 +341,9 @@ public static OpenIdConnectConfiguration Read(ref Utf8JsonReader reader, OpenIdC else if (reader.ValueTextEquals(Utf8Bytes.TokenEndpointAuthSigningAlgValuesSupported)) JsonPrimitives.ReadStrings(ref reader, config.TokenEndpointAuthSigningAlgValuesSupported, MetadataName.TokenEndpointAuthSigningAlgValuesSupported, ClassName, true); + else if (reader.ValueTextEquals(Utf8Bytes.TlsClientCertificateBoundAccessTokens)) + config.TlsClientCertificateBoundAccessTokens = JsonPrimitives.ReadBoolean(ref reader, MetadataName.TlsClientCertificateBoundAccessTokens, ClassName, true); + else if (reader.ValueTextEquals(Utf8Bytes.UILocalesSupported)) JsonPrimitives.ReadStrings(ref reader, config.UILocalesSupported, MetadataName.UILocalesSupported, ClassName, true); @@ -366,9 +382,18 @@ public static OpenIdConnectConfiguration Read(ref Utf8JsonReader reader, OpenIdC else if (propertyName.Equals(MetadataName.AuthorizationEndpoint, StringComparison.OrdinalIgnoreCase)) config.AuthorizationEndpoint = JsonPrimitives.ReadString(ref reader, propertyName, ClassName); + else if (propertyName.Equals(MetadataName.AuthorizationEncryptionAlgValuesSupported, StringComparison.OrdinalIgnoreCase)) + JsonPrimitives.ReadStrings(ref reader, config.AuthorizationEncryptionAlgValuesSupported, propertyName, ClassName); + + else if (propertyName.Equals(MetadataName.AuthorizationEncryptionEncValuesSupported, StringComparison.OrdinalIgnoreCase)) + JsonPrimitives.ReadStrings(ref reader, config.AuthorizationEncryptionEncValuesSupported, propertyName, ClassName); + else if (propertyName.Equals(MetadataName.AuthorizationResponseIssParameterSupported, StringComparison.OrdinalIgnoreCase)) config.AuthorizationResponseIssParameterSupported = JsonPrimitives.ReadBoolean(ref reader, propertyName, ClassName); + else if (propertyName.Equals(MetadataName.AuthorizationSigningAlgValuesSupported, StringComparison.OrdinalIgnoreCase)) + JsonPrimitives.ReadStrings(ref reader, config.AuthorizationSigningAlgValuesSupported, propertyName, ClassName); + else if (propertyName.Equals(MetadataName.BackchannelAuthenticationEndpoint, StringComparison.OrdinalIgnoreCase)) config.BackchannelAuthenticationEndpoint = JsonPrimitives.ReadString(ref reader, propertyName, ClassName); @@ -533,6 +558,9 @@ public static OpenIdConnectConfiguration Read(ref Utf8JsonReader reader, OpenIdC else if (propertyName.Equals(MetadataName.TokenEndpointAuthSigningAlgValuesSupported, StringComparison.OrdinalIgnoreCase)) JsonPrimitives.ReadStrings(ref reader, config.TokenEndpointAuthSigningAlgValuesSupported, propertyName, ClassName); + else if (propertyName.Equals(MetadataName.TlsClientCertificateBoundAccessTokens, StringComparison.OrdinalIgnoreCase)) + config.TlsClientCertificateBoundAccessTokens = JsonPrimitives.ReadBoolean(ref reader, propertyName, ClassName); + else if (propertyName.Equals(MetadataName.UILocalesSupported, StringComparison.OrdinalIgnoreCase)) JsonPrimitives.ReadStrings(ref reader, config.UILocalesSupported, propertyName, ClassName); @@ -592,9 +620,18 @@ public static void Write(ref Utf8JsonWriter writer, OpenIdConnectConfiguration c if (!string.IsNullOrEmpty(config.AuthorizationEndpoint)) writer.WriteString(Utf8Bytes.AuthorizationEndpoint, config.AuthorizationEndpoint); + if (config.AuthorizationEncryptionAlgValuesSupported.Count > 0) + JsonPrimitives.WriteStrings(ref writer, Utf8Bytes.AuthorizationEncryptionAlgValuesSupported, config.AuthorizationEncryptionAlgValuesSupported); + + if (config.AuthorizationEncryptionEncValuesSupported.Count > 0) + JsonPrimitives.WriteStrings(ref writer, Utf8Bytes.AuthorizationEncryptionEncValuesSupported, config.AuthorizationEncryptionEncValuesSupported); + if (config.AuthorizationResponseIssParameterSupported) writer.WriteBoolean(Utf8Bytes.AuthorizationResponseIssParameterSupported, config.AuthorizationResponseIssParameterSupported); + if (config.AuthorizationSigningAlgValuesSupported.Count > 0) + JsonPrimitives.WriteStrings(ref writer, Utf8Bytes.AuthorizationSigningAlgValuesSupported, config.AuthorizationSigningAlgValuesSupported); + if (!string.IsNullOrEmpty(config.BackchannelAuthenticationEndpoint)) writer.WriteString(Utf8Bytes.BackchannelAuthenticationEndpoint, config.BackchannelAuthenticationEndpoint); @@ -745,6 +782,9 @@ public static void Write(ref Utf8JsonWriter writer, OpenIdConnectConfiguration c if (config.TokenEndpointAuthSigningAlgValuesSupported.Count > 0) JsonPrimitives.WriteStrings(ref writer, Utf8Bytes.TokenEndpointAuthSigningAlgValuesSupported, config.TokenEndpointAuthSigningAlgValuesSupported); + if (config.TlsClientCertificateBoundAccessTokens) + writer.WriteBoolean(Utf8Bytes.TlsClientCertificateBoundAccessTokens, config.TlsClientCertificateBoundAccessTokens); + if (config.UILocalesSupported.Count > 0) JsonPrimitives.WriteStrings(ref writer, Utf8Bytes.UILocalesSupported, config.UILocalesSupported); diff --git a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdProviderMetadataNames.cs b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdProviderMetadataNames.cs index 62f15637c6..b5c802d3dd 100644 --- a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdProviderMetadataNames.cs +++ b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdProviderMetadataNames.cs @@ -14,7 +14,10 @@ public static class OpenIdProviderMetadataNames #pragma warning disable 1591 public const string AcrValuesSupported = "acr_values_supported"; public const string AuthorizationEndpoint = "authorization_endpoint"; + public const string AuthorizationEncryptionAlgValuesSupported = "authorization_encryption_alg_values_supported"; + public const string AuthorizationEncryptionEncValuesSupported = "authorization_encryption_enc_values_supported"; public const string AuthorizationResponseIssParameterSupported = "authorization_response_iss_parameter_supported"; + public const string AuthorizationSigningAlgValuesSupported = "authorization_signing_alg_values_supported"; public const string BackchannelAuthenticationEndpoint = "backchannel_authentication_endpoint"; public const string BackchannelAuthenticationRequestSigningAlgValuesSupported = "backchannel_authentication_request_signing_alg_values_supported"; public const string BackchannelTokenDeliveryModesSupported = "backchannel_token_delivery_modes_supported"; @@ -68,6 +71,7 @@ public static class OpenIdProviderMetadataNames public const string TokenEndpointAuthMethodsSupported = "token_endpoint_auth_methods_supported"; public const string TokenEndpointAuthSigningAlgValuesSupported = "token_endpoint_auth_signing_alg_values_supported"; public const string UILocalesSupported = "ui_locales_supported"; + public const string TlsClientCertificateBoundAccessTokens = "tls_client_certificate_bound_access_tokens"; public const string UserInfoEndpoint = "userinfo_endpoint"; public const string UserInfoEncryptionAlgValuesSupported = "userinfo_encryption_alg_values_supported"; public const string UserInfoEncryptionEncValuesSupported = "userinfo_encryption_enc_values_supported"; @@ -84,7 +88,10 @@ internal static class OpenIdProviderMetadataUtf8Bytes { public static ReadOnlySpan AcrValuesSupported => "acr_values_supported"u8; public static ReadOnlySpan AuthorizationEndpoint => "authorization_endpoint"u8; + public static ReadOnlySpan AuthorizationEncryptionAlgValuesSupported => "authorization_encryption_alg_values_supported"u8; + public static ReadOnlySpan AuthorizationEncryptionEncValuesSupported => "authorization_encryption_enc_values_supported"u8; public static ReadOnlySpan AuthorizationResponseIssParameterSupported => "authorization_response_iss_parameter_supported"u8; + public static ReadOnlySpan AuthorizationSigningAlgValuesSupported => "authorization_signing_alg_values_supported"u8; public static ReadOnlySpan BackchannelAuthenticationEndpoint => "backchannel_authentication_endpoint"u8; public static ReadOnlySpan BackchannelAuthenticationRequestSigningAlgValuesSupported => "backchannel_authentication_request_signing_alg_values_supported"u8; public static ReadOnlySpan BackchannelTokenDeliveryModesSupported => "backchannel_token_delivery_modes_supported"u8; @@ -137,6 +144,7 @@ internal static class OpenIdProviderMetadataUtf8Bytes public static ReadOnlySpan TokenEndpoint => "token_endpoint"u8; public static ReadOnlySpan TokenEndpointAuthMethodsSupported => "token_endpoint_auth_methods_supported"u8; public static ReadOnlySpan TokenEndpointAuthSigningAlgValuesSupported => "token_endpoint_auth_signing_alg_values_supported"u8; + public static ReadOnlySpan TlsClientCertificateBoundAccessTokens => "tls_client_certificate_bound_access_tokens"u8; public static ReadOnlySpan UILocalesSupported => "ui_locales_supported"u8; public static ReadOnlySpan UserInfoEndpoint => "userinfo_endpoint"u8; public static ReadOnlySpan UserInfoEncryptionAlgValuesSupported => "userinfo_encryption_alg_values_supported"u8; diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConfigData.cs b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConfigData.cs index 1f60d43365..75a62b1539 100644 --- a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConfigData.cs +++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConfigData.cs @@ -68,7 +68,10 @@ public static OpenIdConnectConfiguration FullyPopulatedWithKeys public static string JsonAllValues = @"{ ""acr_values_supported"": [""acr_value1"", ""acr_value2"", ""acr_value3""], ""authorization_endpoint"": ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/authorize"", + ""authorization_encryption_alg_values_supported"": [""A192KW"", ""A256KW""], + ""authorization_encryption_enc_values_supported"": [""A128CBC-HS256"", ""A256CBC-HS512""], ""authorization_response_iss_parameter_supported"": false, + ""authorization_signing_alg_values_supported"": [""ES384"", ""ES512""], ""backchannel_authentication_endpoint"": ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/bc-authorize"", ""backchannel_authentication_request_signing_alg_values_supported"": [""ES384"", ""ES512""], ""backchannel_token_delivery_modes_supported"": [""poll"", ""ping""], @@ -119,6 +122,7 @@ public static OpenIdConnectConfiguration FullyPopulatedWithKeys ""token_endpoint"": ""https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/token"", ""token_endpoint_auth_methods_supported"": [""client_secret_post"", ""private_key_jwt""], ""token_endpoint_auth_signing_alg_values_supported"": [""ES192"", ""ES256""], + ""tls_client_certificate_bound_access_tokens"": true, ""ui_locales_supported"": [""hak-CN"", ""en-us""], ""userinfo_endpoint"": ""https://login.microsoftonline.com/add29489-7269-41f4-8841-b63c95564420/openid/userinfo"", ""userinfo_encryption_alg_values_supported"": [""ECDH-ES+A128KW"", ""ECDH-ES+A192KW""], @@ -610,7 +614,10 @@ private static OpenIdConnectConfiguration SetDefaultConfiguration(OpenIdConnectC { AddToCollection(config.AcrValuesSupported, "acr_value1", "acr_value2", "acr_value3"); config.AuthorizationEndpoint = "https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/authorize"; + AddToCollection(config.AuthorizationEncryptionAlgValuesSupported, "A192KW", "A256KW"); + AddToCollection(config.AuthorizationEncryptionEncValuesSupported, "A128CBC-HS256", "A256CBC-HS512"); config.AuthorizationResponseIssParameterSupported = false; + AddToCollection(config.AuthorizationSigningAlgValuesSupported, "ES384", "ES512"); config.BackchannelAuthenticationEndpoint = "https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/bc-authorize"; AddToCollection(config.BackchannelAuthenticationRequestSigningAlgValuesSupported, "ES384", "ES512"); AddToCollection(config.BackchannelTokenDeliveryModesSupported, "poll", "ping"); @@ -660,6 +667,7 @@ private static OpenIdConnectConfiguration SetDefaultConfiguration(OpenIdConnectC config.TokenEndpoint = "https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/token"; AddToCollection(config.TokenEndpointAuthMethodsSupported, "client_secret_post", "private_key_jwt"); AddToCollection(config.TokenEndpointAuthSigningAlgValuesSupported, "ES192", "ES256"); + config.TlsClientCertificateBoundAccessTokens = true; AddToCollection(config.UILocalesSupported, "hak-CN", "en-us"); config.UserInfoEndpoint = "https://login.microsoftonline.com/add29489-7269-41f4-8841-b63c95564420/openid/userinfo"; AddToCollection(config.UserInfoEndpointEncryptionAlgValuesSupported, "ECDH-ES+A128KW", "ECDH-ES+A192KW"); diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectConfigurationTests.cs b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectConfigurationTests.cs index 9ee0bb7cdb..c31a60b085 100644 --- a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectConfigurationTests.cs +++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectConfigurationTests.cs @@ -75,6 +75,9 @@ public void Defaults() { OpenIdConnectConfiguration configuration = new OpenIdConnectConfiguration(); Assert.NotNull(configuration.AcrValuesSupported); + Assert.NotNull(configuration.AuthorizationEncryptionAlgValuesSupported); + Assert.NotNull(configuration.AuthorizationEncryptionEncValuesSupported); + Assert.NotNull(configuration.AuthorizationSigningAlgValuesSupported); Assert.False(configuration.AuthorizationResponseIssParameterSupported); Assert.NotNull(configuration.BackchannelAuthenticationRequestSigningAlgValuesSupported); Assert.NotNull(configuration.BackchannelTokenDeliveryModesSupported); @@ -110,6 +113,7 @@ public void Defaults() Assert.NotNull(configuration.SubjectTypesSupported); Assert.NotNull(configuration.TokenEndpointAuthMethodsSupported); Assert.NotNull(configuration.TokenEndpointAuthSigningAlgValuesSupported); + Assert.False(configuration.TlsClientCertificateBoundAccessTokens); Assert.NotNull(configuration.UILocalesSupported); Assert.NotNull(configuration.UserInfoEndpointEncryptionAlgValuesSupported); Assert.NotNull(configuration.UserInfoEndpointEncryptionEncValuesSupported); @@ -141,8 +145,8 @@ public void GetSets() OpenIdConnectConfiguration configuration = new OpenIdConnectConfiguration(); Type type = typeof(OpenIdConnectConfiguration); PropertyInfo[] properties = type.GetProperties(); - if (properties.Length != 63) - Assert.True(false, "Number of properties has changed from 63 to: " + properties.Length + ", adjust tests"); + if (properties.Length != 67) + Assert.True(false, "Number of properties has changed from 67 to: " + properties.Length + ", adjust tests"); TestUtilities.CallAllPublicInstanceAndStaticPropertyGets(configuration, "OpenIdConnectConfiguration_GetSets"); @@ -152,7 +156,10 @@ public void GetSets() PropertyNamesAndSetGetValue = new List>> { new KeyValuePair>("AuthorizationEndpoint", new List{ (string)null, Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }), + new KeyValuePair>("AuthorizationEncryptionAlgValuesSupported", new List{ false, true, true }), + new KeyValuePair>("AuthorizationEncryptionEncValuesSupported", new List{ false, true, true }), new KeyValuePair>("AuthorizationResponseIssParameterSupported", new List{ false, true, true }), + new KeyValuePair>("AuthorizationSigningAlgValuesSupported", new List{ false, true, true }), new KeyValuePair>("BackchannelAuthenticationEndpoint", new List{ (string)null, Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }), new KeyValuePair>("BackchannelUserCodeParameterSupported", new List{ false, true, true }), new KeyValuePair>("CheckSessionIframe", new List{ (string)null, Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }), @@ -178,6 +185,7 @@ public void GetSets() new KeyValuePair>("RevocationEndpointAuthMethodsSupported", new List{ false, true, true }), new KeyValuePair>("RevocationEndpointAuthSigningAlgValuesSupported", new List{ false, true, true }), new KeyValuePair>("ServiceDocumentation", new List{ (string)null, Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }), + new KeyValuePair>("TlsClientCertificateBoundAccessTokens", new List{ false, true, false }), new KeyValuePair>("TokenEndpoint", new List{ (string)null, Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }), new KeyValuePair>("UserInfoEndpoint", new List{ (string)null, Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }), }, @@ -289,6 +297,9 @@ public void NonemptyCollectionSerialization() var collectionNames = new List { "acr_values_supported", + "authorization_encryption_alg_values_supported", + "authorization_encryption_enc_values_supported", + "authorization_signing_alg_values_supported", "backchannel_authentication_request_signing_alg_values_supported", "backchannel_token_delivery_modes_supported", "claims_supported", diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectMetadata.json b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectMetadata.json index bf10bb1f2c..90af72ef9f 100644 --- a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectMetadata.json +++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectMetadata.json @@ -1,7 +1,10 @@ { "acr_values_supported": ["acr_value1", "acr_value2", "acr_value3"], "authorization_endpoint": "https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/authorize", + "authorization_encryption_alg_values_supported": ["A192KW", "A256KW"], + "authorization_encryption_enc_values_supported": ["A128CBC-HS256", "A256CBC-HS512"], "authorization_response_iss_parameter_supported": false, + "authorization_signing_alg_values_supported": ["ES384", "ES512"], "backchannel_authentication_endpoint": "https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/bc-authorize", "backchannel_authentication_request_signing_alg_values_supported": ["ES384", "ES512"], "backchannel_token_delivery_modes_supported": ["poll", "ping"], @@ -52,6 +55,7 @@ "token_endpoint": "https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/token", "token_endpoint_auth_methods_supported": ["client_secret_post", "private_key_jwt"], "token_endpoint_auth_signing_alg_values_supported": ["ES192", "ES256"], + "tls_client_certificate_bound_access_tokens": true, "ui_locales_supported": ["hak-CN", "en-us"], "userinfo_endpoint": "https://login.microsoftonline.com/add29489-7269-41f4-8841-b63c95564420/openid/userinfo", "userinfo_encryption_alg_values_supported": ["ECDH-ES+A128KW", "ECDH-ES+A192KW"], From c24bfe683427dbad566fe617d0d590ec3a61d8aa Mon Sep 17 00:00:00 2001 From: jennyf19 Date: Wed, 19 Jun 2024 09:08:34 -0700 Subject: [PATCH 14/22] Update CHANGELOG.md (#2653) --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a9f0c8101..fa81e50d90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,6 @@ See the [releases](https://github.com/AzureAD/azure-activedirectory-identitymode - `JwtRegisteredClaimNames` now contains previously missing Standard OpenIdConnect claims. See issue [#1598](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/1598) for details. ### Performance Improvements: -- Reduced allocations in `AadIssuerValidator` by not using `string.Replace` where appropriate. See issue [#2595](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/2595) and PR [#2597](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/pull/2597) for more details. - No longer for every string claim, calling DateTime.TryParse on each value, whether it is expected to be a DateTime or not. See issue [#2615](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/2615) for details. 7.6.0 From 55cc10ea53503b129afd55734ad9e9dd8203b339 Mon Sep 17 00:00:00 2001 From: JoshLozensky <103777376+JoshLozensky@users.noreply.github.com> Date: Fri, 21 Jun 2024 17:26:55 -0700 Subject: [PATCH 15/22] Add audiences to security token descriptor (#2575) * added SecurityTokenDescriptor.Audiences and refactored code to support * refactored Audience vs Audiences logic * adjusted Audiences validation logic to more accurately evaluate for null or empty strings * Redesigned to avoid adding internal members and to support using both Audience and Audiences simultaneously * Fixed json formatting of audiences * Wrote unit tests for JsonWebTokenHandler jws and jwe * fixed bug in JwtSecurityTokenHandler * added test for audiences validation * added missing brackets * refactor WriteObject for readability and change IList case to IEnumerable * Add public method AddAudience for inexpensive deduplication when adding values * moved Audiences injection for JwtSecurityTokenHandler to the claims dictionary * added error msg * added unit tests ensuring same correct behavior for JwtSecurityTokenHandler and JsonWebTokenHandler * restoring existing note * samlv1 unit tests * saml2 unit tests * changed logic to avoid altering the claims object in the securityTokenDescriptor * added a couple more unit tests * private method renamed for accuracy * Added benchmarks to look at the performance of Audiences Vs Audience Members * removing use of 'collection expressions' as they don't work in ADO build * Redesigned Audiences to use IList * removing unneeded string * Add constructor overload to maintain public api * changed serializer back to using IList * removed unneeded using * re-adding enumerable to switch * reverting change to IList in jsonserializerprimitives * added a method to concat Audience and Audiences when writing to json * added duplicate check * changed _audiences private member to List * Changed IEnumerable back to IList * altered logging logic to avoid unneeded alloc * removed Linq where from hotpath * formatting fixes/changes * Added UriKind.Absolute * removed extra space * Removed AddAudiences method * Added API taking multiple Audiences but not single one for completeness * added details on Aud claim priority to method summary * fixed bug and made variable names more clear * set up tests to track expected behavior * changed variables to original names * reverted changes to WriteObject * formatting changes * small changes from PR feedback * syntax fix * changed IsNullOrWhitespace to IsNullOrEmpty * reverted change to test since incepting code change was reverted * removed unnecessary code leftover from old solutions * replaced linq with foreach * added null check as a result of dropping Linq usage * fixing accidental comment edit * removed unnecessary local string * refactored to only create one list when making SamlAudienceRestrictionCondition * reduced list allocation from one to zero or one * removing duplicate methods * removed unneeded using * add note to features section in changelog * Changes per latest PR comments * adjusted logic to use local var since ICollection can't return last item without iterating through the entire collection * fixed which test was first in theory data --- CHANGELOG.md | 2 + .../BenchmarkUtils.cs | 45 ++++ .../CreateTokenTests.cs | 44 +++- .../JsonWebTokenHandler.CreateToken.cs | 27 +- .../Saml/SamlSecurityTokenHandler.cs | 34 ++- .../Saml2/Saml2SecurityTokenHandler.cs | 9 +- .../Json/JsonSerializerPrimitives.cs | 22 +- .../LogMessages.cs | 1 - .../SecurityTokenDescriptor.cs | 12 +- .../JwtPayload.cs | 75 +++++- .../JwtSecurityTokenHandler.cs | 30 ++- .../JsonWebTokenHandlerTests.cs | 59 +++++ .../json/JsonWebTokenHandler.cs | 36 ++- .../Default.cs | 33 +++ .../CreateTokenTheoryData.cs | 2 + .../Saml2SecurityTokenHandlerTests.cs | 86 ++++++- .../SamlSecurityTokenHandlerTests.cs | 86 ++++++- .../SamlTestData.cs | 4 + .../JwtSecurityTokenHandlerTests.cs | 237 +++++++++++++++++- 19 files changed, 792 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa81e50d90..fe411465ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,10 @@ See the [releases](https://github.com/AzureAD/azure-activedirectory-identitymode 7.6.1 ===== ### New Features: +- Added an Audiences member to the SecurityTokenDescriptor to make it easier to define multiple audiences in JWT and SAML tokens. Addresses issue [#1479](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/1479) with PR [#2575](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/pull/2575) - Add missing metadata parameters to OpenIdConnectConfiguration. See issue [#2498](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/2498) for details. + ### Bug Fixes: - Fix over-reporting of `IDX14100`. See issue [#2058](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/2058) and PR [#2618](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/pull/2618) for details. - `JwtRegisteredClaimNames` now contains previously missing Standard OpenIdConnect claims. See issue [#1598](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/1598) for details. diff --git a/benchmark/Microsoft.IdentityModel.Benchmarks/BenchmarkUtils.cs b/benchmark/Microsoft.IdentityModel.Benchmarks/BenchmarkUtils.cs index ecce92c3e4..f94bfb0966 100644 --- a/benchmark/Microsoft.IdentityModel.Benchmarks/BenchmarkUtils.cs +++ b/benchmark/Microsoft.IdentityModel.Benchmarks/BenchmarkUtils.cs @@ -16,6 +16,14 @@ public class BenchmarkUtils public const string Audience = "http://www.contoso.com/protected"; + public readonly static IList Audiences = new string[] { + "http://www.contoso.com/protected", + "http://www.contoso.com/protected1", + "http://www.contoso.com/protected2", + "http://www.contoso.com/protected3", + "http://www.contoso.com/protected4" + }; + private static RSA _rsa; private static SymmetricSecurityKey _symmetricKey; @@ -60,6 +68,43 @@ public static Dictionary Claims } } + public static Dictionary ClaimsNoAudience + { + get + { + DateTime now = DateTime.UtcNow; + return new Dictionary() + { + { "role", new List() { "role1", "Developer", "Sales"} }, + { JwtRegisteredClaimNames.Email, "Bob@contoso.com" }, + { JwtRegisteredClaimNames.Exp, EpochTime.GetIntDate(now + TimeSpan.FromDays(1)) }, + { JwtRegisteredClaimNames.Nbf, EpochTime.GetIntDate(now) }, + { JwtRegisteredClaimNames.Iat, EpochTime.GetIntDate(now) }, + { JwtRegisteredClaimNames.GivenName, "Bob" }, + { JwtRegisteredClaimNames.Iss, Issuer }, + }; + } + } + + public static Dictionary ClaimsMultipleAudiences + { + get + { + DateTime now = DateTime.UtcNow; + return new Dictionary() + { + { "role", new List() { "role1", "Developer", "Sales"} }, + { JwtRegisteredClaimNames.Email, "Bob@contoso.com" }, + { JwtRegisteredClaimNames.Exp, EpochTime.GetIntDate(now + TimeSpan.FromDays(1)) }, + { JwtRegisteredClaimNames.Nbf, EpochTime.GetIntDate(now) }, + { JwtRegisteredClaimNames.Iat, EpochTime.GetIntDate(now) }, + { JwtRegisteredClaimNames.GivenName, "Bob" }, + { JwtRegisteredClaimNames.Iss, Issuer }, + { JwtRegisteredClaimNames.Aud, Audiences } + }; + } + } + public static Dictionary ClaimsExtendedExample { get diff --git a/benchmark/Microsoft.IdentityModel.Benchmarks/CreateTokenTests.cs b/benchmark/Microsoft.IdentityModel.Benchmarks/CreateTokenTests.cs index 74c36f5428..6f83f1115d 100644 --- a/benchmark/Microsoft.IdentityModel.Benchmarks/CreateTokenTests.cs +++ b/benchmark/Microsoft.IdentityModel.Benchmarks/CreateTokenTests.cs @@ -14,6 +14,9 @@ public class CreateTokenTests { private JsonWebTokenHandler _jsonWebTokenHandler; private SecurityTokenDescriptor _tokenDescriptor; + private SecurityTokenDescriptor _tokenDescriptorMultipleAudiencesMemberAndClaims; + private SecurityTokenDescriptor _tokenDescriptorMultipleAudiencesMemberOnly; + private SecurityTokenDescriptor _tokenDescriptorSingleAudienceUsingAudiencesMember; [GlobalSetup] public void Setup() @@ -23,11 +26,50 @@ public void Setup() _tokenDescriptor = new SecurityTokenDescriptor { Claims = BenchmarkUtils.Claims, - SigningCredentials = BenchmarkUtils.SigningCredentialsRsaSha256, + SigningCredentials = BenchmarkUtils.SigningCredentialsRsaSha256 }; + + _tokenDescriptorSingleAudienceUsingAudiencesMember = new SecurityTokenDescriptor + { + Claims = BenchmarkUtils.ClaimsNoAudience, + SigningCredentials = BenchmarkUtils.SigningCredentialsRsaSha256 + }; + + _tokenDescriptorMultipleAudiencesMemberOnly = new SecurityTokenDescriptor + { + Claims = BenchmarkUtils.ClaimsNoAudience, + SigningCredentials = BenchmarkUtils.SigningCredentialsRsaSha256 + }; + + _tokenDescriptorMultipleAudiencesMemberAndClaims = new SecurityTokenDescriptor + { + Claims = BenchmarkUtils.ClaimsMultipleAudiences, + SigningCredentials = BenchmarkUtils.SigningCredentialsRsaSha256 + }; + + _tokenDescriptorSingleAudienceUsingAudiencesMember.Audiences.Add(BenchmarkUtils.Audience); + foreach (var audience in BenchmarkUtils.Audiences) + { + _tokenDescriptorMultipleAudiencesMemberOnly.Audiences.Add(audience); + _tokenDescriptorMultipleAudiencesMemberAndClaims.Audiences.Add(audience); + } } [Benchmark] public string JsonWebTokenHandler_CreateToken() => _jsonWebTokenHandler.CreateToken(_tokenDescriptor); + + [Benchmark] + public string JsonWebTokenHandler_CreateToken_SingleAudienceUsingAudiencesMemberOnly() => + _jsonWebTokenHandler.CreateToken(_tokenDescriptorSingleAudienceUsingAudiencesMember); + + [Benchmark] + public string JsonWebTokenHandler_CreateToken_MultipleAudiencesMemberOnly() => + _jsonWebTokenHandler.CreateToken(_tokenDescriptorMultipleAudiencesMemberOnly); + + [Benchmark] + public string JsonWebTokenHandler_CreateToken_MultipleAudiencesMemberAndClaims() => + _jsonWebTokenHandler.CreateToken(_tokenDescriptorMultipleAudiencesMemberAndClaims); + + } } diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index 468a1de4a8..0ceee47460 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -681,6 +681,10 @@ int sizeOfEncodedHeaderAndPayloadAsciiBytes /// /// A can contain claims from multiple locations. /// This method consolidates the claims and adds default times {exp, iat, nbf} if needed. + /// In the case of a claim from this set: {Audience, Issuer, Expires, IssuedAt, NotBefore} being defined in multiple + /// locations in the SecurityTokenDescriptor, the following priority is used: + /// SecurityTokenDescriptor.{Audience/Audiences, Issuer, Expires, IssuedAt, NotBefore} > SecurityTokenDescriptor.Claims > + /// SecurityTokenDescriptor.Subject.Claims /// /// The to use. /// The used to create the token. @@ -706,11 +710,20 @@ internal static void WriteJwsPayload( writer.WriteStartObject(); - if (!string.IsNullOrEmpty(tokenDescriptor.Audience)) + if (tokenDescriptor.Audiences.Count > 0) { + if (!tokenDescriptor.Audience.IsNullOrEmpty()) + JsonPrimitives.WriteStrings(ref writer, JwtPayloadUtf8Bytes.Aud, tokenDescriptor.Audiences, tokenDescriptor.Audience); + else + JsonPrimitives.WriteStrings(ref writer, JwtPayloadUtf8Bytes.Aud, tokenDescriptor.Audiences); + audienceSet = true; + } + else if (!tokenDescriptor.Audience.IsNullOrEmpty()) + { writer.WritePropertyName(JwtPayloadUtf8Bytes.Aud); writer.WriteStringValue(tokenDescriptor.Audience); + audienceSet = true; } if (!string.IsNullOrEmpty(tokenDescriptor.Issuer)) @@ -742,7 +755,7 @@ internal static void WriteJwsPayload( } // Duplicates are resolved according to the following priority: - // SecurityTokenDescriptor.{Audience, Issuer, Expires, IssuedAt, NotBefore}, SecurityTokenDescriptor.Claims, SecurityTokenDescriptor.Subject.Claims + // SecurityTokenDescriptor.{Audience/Audiences, Issuer, Expires, IssuedAt, NotBefore}, SecurityTokenDescriptor.Claims, SecurityTokenDescriptor.Subject.Claims // SecurityTokenDescriptor.Claims are KeyValuePairs, whereas SecurityTokenDescriptor.Subject.Claims are System.Security.Claims.Claim and are processed differently. if (tokenDescriptor.Claims != null && tokenDescriptor.Claims.Count > 0) @@ -755,7 +768,15 @@ internal static void WriteJwsPayload( if (audienceSet) { if (LogHelper.IsEnabled(EventLogLevel.Informational)) - LogHelper.LogInformation(LogHelper.FormatInvariant(LogMessages.IDX14113, LogHelper.MarkAsNonPII(nameof(tokenDescriptor.Audience)))); + { + string descriptorMemberName = null; + if (tokenDescriptor.Audiences.Count > 0) + descriptorMemberName = nameof(tokenDescriptor.Audiences); + else if (!string.IsNullOrEmpty(tokenDescriptor.Audience)) + descriptorMemberName = nameof(tokenDescriptor.Audience); + + LogHelper.LogInformation(LogHelper.FormatInvariant(LogMessages.IDX14113, LogHelper.MarkAsNonPII(descriptorMemberName))); + } continue; } diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs index 9b70430151..32573a0bcf 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Security.Claims; using System.Text; -using System.Threading; using System.Threading.Tasks; using System.Xml; using Microsoft.IdentityModel.Abstractions; @@ -354,7 +353,7 @@ protected virtual IEnumerable CreateClaimsIdentities(SamlSecurit /// if is null. protected virtual SamlConditions CreateConditions(SecurityTokenDescriptor tokenDescriptor) { - if (null == tokenDescriptor) + if (tokenDescriptor == null) throw LogArgumentNullException(nameof(tokenDescriptor)); var conditions = new SamlConditions(); @@ -368,12 +367,41 @@ protected virtual SamlConditions CreateConditions(SecurityTokenDescriptor tokenD else if (SetDefaultTimesOnTokenCreation) conditions.NotOnOrAfter = DateTime.UtcNow + TimeSpan.FromMinutes(TokenLifetimeInMinutes); - if (!string.IsNullOrEmpty(tokenDescriptor.Audience)) + if (tokenDescriptor.Audiences.Count > 0) + { + if (!tokenDescriptor.Audience.IsNullOrEmpty()) + conditions.Conditions.Add(CreateAudienceRestrictionCondition(tokenDescriptor.Audience, tokenDescriptor.Audiences)); + else + conditions.Conditions.Add(CreateAudienceRestrictionCondition(tokenDescriptor.Audiences)); + } + else if (!tokenDescriptor.Audience.IsNullOrEmpty()) + { conditions.Conditions.Add(new SamlAudienceRestrictionCondition(new Uri(tokenDescriptor.Audience))); + } return conditions; } + + private static SamlAudienceRestrictionCondition CreateAudienceRestrictionCondition(IList audiences) + { + SamlAudienceRestrictionCondition audRestrictionCondition = new(); + for (int i = 0; i < audiences.Count; i++) + audRestrictionCondition.Audiences.Add(new Uri(audiences[i])); + + return audRestrictionCondition; + } + + private static SamlCondition CreateAudienceRestrictionCondition(string audience, IList audiences) + { + SamlAudienceRestrictionCondition audRestrictionCondition = new(new Uri(audience)); + for (int i = 0; i < audiences.Count; i++) + audRestrictionCondition.Audiences.Add(new Uri(audiences[i])); + + return audRestrictionCondition; + } + + /// /// Generates an enumeration of SamlStatements from a SecurityTokenDescriptor. /// Only SamlAttributeStatements and SamlAuthenticationStatements are generated. diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs index 4100330a51..171b0a7a5f 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs @@ -644,7 +644,14 @@ protected virtual Saml2Conditions CreateConditions(SecurityTokenDescriptor token else if (SetDefaultTimesOnTokenCreation) conditions.NotOnOrAfter = DateTime.UtcNow + TimeSpan.FromMinutes(TokenLifetimeInMinutes); - if (!string.IsNullOrEmpty(tokenDescriptor.Audience)) + if (tokenDescriptor.Audiences.Count > 0) + { + var audienceRestriction = new Saml2AudienceRestriction(tokenDescriptor.Audiences); + if (!string.IsNullOrEmpty(tokenDescriptor.Audience)) + audienceRestriction.Audiences.Add(tokenDescriptor.Audience); + conditions.AudienceRestrictions.Add(audienceRestriction); + } + else if (!string.IsNullOrEmpty(tokenDescriptor.Audience)) conditions.AudienceRestrictions.Add(new Saml2AudienceRestriction(tokenDescriptor.Audience)); return conditions; diff --git a/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs b/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs index f539555a8b..e8e9590eb4 100644 --- a/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs +++ b/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs @@ -1152,7 +1152,7 @@ public static void WriteObject(ref Utf8JsonWriter writer, string key, object obj #if NET6_0_OR_GREATER writer.WriteNumber(key, dub); #else - #pragma warning disable CA1031 // Do not catch general exception types, we have seen TryParse fault. +#pragma warning disable CA1031 // Do not catch general exception types, we have seen TryParse fault. try { if (decimal.TryParse(dub.ToString(CultureInfo.InvariantCulture), out decimal dec)) @@ -1164,7 +1164,7 @@ public static void WriteObject(ref Utf8JsonWriter writer, string key, object obj { writer.WriteNumber(key, dub); } - #pragma warning restore CA1031 +#pragma warning restore CA1031 #endif else if (obj is decimal d) writer.WriteNumber(key, d); @@ -1174,7 +1174,7 @@ public static void WriteObject(ref Utf8JsonWriter writer, string key, object obj #if NET6_0_OR_GREATER writer.WriteNumber(key, f); #else - #pragma warning disable CA1031 // Do not catch general exception types, we have seen TryParse fault. +#pragma warning disable CA1031 // Do not catch general exception types, we have seen TryParse fault. try { if (decimal.TryParse(f.ToString(CultureInfo.InvariantCulture), out decimal dec)) @@ -1186,7 +1186,7 @@ public static void WriteObject(ref Utf8JsonWriter writer, string key, object obj { writer.WriteNumber(key, f); } - #pragma warning restore CA1031 +#pragma warning restore CA1031 #endif else if (obj is Guid g) writer.WriteString(key, g); @@ -1197,7 +1197,7 @@ public static void WriteObject(ref Utf8JsonWriter writer, string key, object obj LogMessages.IDX11025, LogHelper.MarkAsNonPII(objType.ToString()), LogHelper.MarkAsNonPII(key)))); - } + } /// /// Writes values into an array. @@ -1318,6 +1318,16 @@ public static void WriteStrings(ref Utf8JsonWriter writer, ReadOnlySpan pr writer.WriteEndArray(); } -#endregion + + public static void WriteStrings(ref Utf8JsonWriter writer, ReadOnlySpan propertyName, IList strings, string extraString) + { + writer.WriteStartArray(propertyName); + foreach (string str in strings) + writer.WriteStringValue(str); + + writer.WriteStringValue(extraString); + writer.WriteEndArray(); + } + #endregion } } diff --git a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs index 689b2ccb9d..c644c0936f 100644 --- a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs +++ b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs @@ -267,7 +267,6 @@ internal static class LogMessages public const string IDX11025 = "IDX11025: Cannot serialize object of type: '{0}' into property: '{1}'."; public const string IDX11026 = "IDX11026: Unable to get claim value as a string from claim type:'{0}', value type was:'{1}'. Acceptable types are String, IList, and System.Text.Json.JsonElement."; - #pragma warning restore 1591 } } diff --git a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs index 8e3f07571f..b9b5843143 100644 --- a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs +++ b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Security.Claims; +using System.Threading; namespace Microsoft.IdentityModel.Tokens { @@ -12,11 +13,20 @@ namespace Microsoft.IdentityModel.Tokens /// public class SecurityTokenDescriptor { + private List _audiences; + /// - /// Gets or sets the value of the 'audience' claim. + /// Gets or sets the value of the {"": audience} claim. Will be combined with and any "Aud" claims in + /// or when creating a token. /// public string Audience { get; set; } + /// + /// Gets the list audiences to include in the token's 'Aud' claim. Will be combined with and any + /// "Aud" claims in or when creating a token. + /// + public IList Audiences => _audiences ?? Interlocked.CompareExchange(ref _audiences, [], null) ?? _audiences; + /// /// Defines the compression algorithm that will be used to compress the JWT token payload. /// diff --git a/src/System.IdentityModel.Tokens.Jwt/JwtPayload.cs b/src/System.IdentityModel.Tokens.Jwt/JwtPayload.cs index 506bd31983..af38253fc8 100644 --- a/src/System.IdentityModel.Tokens.Jwt/JwtPayload.cs +++ b/src/System.IdentityModel.Tokens.Jwt/JwtPayload.cs @@ -72,7 +72,7 @@ internal static JwtPayload CreatePayload(byte[] bytes, int length) if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Aud)) { reader.Read(); - payload._audiences = new List(); + payload._audiences = new(); if (reader.TokenType == JsonTokenType.StartArray) { JsonSerializerPrimitives.ReadStrings(ref reader, payload._audiences, JwtRegisteredClaimNames.Aud, ClassName, false); @@ -179,18 +179,37 @@ public JwtPayload(string issuer, string audience, IEnumerable claims, Dat /// /// Initializes a new instance of the class with claims added for each parameter specified. Default string comparer . /// - /// If this value is not null, a { iss, 'issuer' } claim will be added, overwriting any 'iss' claim in 'claims' and 'claimCollection' if present. - /// If this value is not null, a { aud, 'audience' } claim will be added, appending to any 'aud' claims in 'claims' or 'claimCollection' if present. + /// If this value is not null, an { iss, 'issuer' } claim will be added, overwriting any 'iss' claim in 'claims' and 'claimCollection' if present. + /// If this value is not null, all non-null non-whitespace strings in it will be added to an { aud, 'audience' }, appending to any 'aud' claims in 'claims' or 'claimCollection' if present. /// If this value is not null then for each a { 'Claim.Type', 'Claim.Value' } is added. If duplicate claims are found then a { 'Claim.Type', List<object> } will be created to contain the duplicate values. /// If both and are not null then the values in claims will be combined with the values in claimsCollection. The values found in claimCollection take precedence over those found in claims, so any duplicate /// values will be overridden. - /// If notbefore.HasValue a { nbf, 'value' } claim is added, overwriting any 'nbf' claim in 'claims' and 'claimcollection' if present. - /// If expires.HasValue a { exp, 'value' } claim is added, overwriting any 'exp' claim in 'claims' and 'claimcollection' if present. - /// If issuedAt.HasValue is 'true' a { iat, 'value' } claim is added, overwriting any 'iat' claim in 'claims' and 'claimcollection' if present. + /// If notbefore.HasValue an { nbf, 'value' } claim is added, overwriting any 'nbf' claim in 'claims' and 'claimcollection' if present. + /// If expires.HasValue an { exp, 'value' } claim is added, overwriting any 'exp' claim in 'claims' and 'claimcollection' if present. + /// If issuedAt.HasValue is 'true' an { iat, 'value' } claim is added, overwriting any 'iat' claim in 'claims' and 'claimcollection' if present. /// Comparison is set to /// The 4 parameters: 'issuer', 'audience', 'notBefore', 'expires' take precedence over (s) in 'claims' and 'claimcollection'. The values will be overridden. /// If 'expires' <= 'notbefore'. - public JwtPayload(string issuer, string audience, IEnumerable claims, IDictionary claimsCollection, DateTime? notBefore, DateTime? expires, DateTime? issuedAt) + public JwtPayload(string issuer, IList audiences, IEnumerable claims, IDictionary claimsCollection, DateTime? notBefore, DateTime? expires, DateTime? issuedAt) + : this(issuer, "", audiences, claims, claimsCollection, notBefore, expires, issuedAt) + { } + + /// + /// Initializes a new instance of the class with claims added for each parameter specified. Default string comparer . + /// + /// If this value is not null, an { iss, 'issuer' } claim will be added, overwriting any 'iss' claim in 'claims' and 'claimCollection' if present. + /// If this value is not null, an { aud, 'audience' } claim will be added, appending to any 'aud' claims in 'claims' or 'claimCollection' if present. + /// If this value is not null, all non-null non-whitespace strings in it will be added to an { aud, 'audience' }, appending to any 'aud' claims in 'claims' or 'claimCollection' if present. + /// If this value is not null then for each a { 'Claim.Type', 'Claim.Value' } is added. If duplicate claims are found then a { 'Claim.Type', List<object> } will be created to contain the duplicate values. + /// If both and are not null then the values in claims will be combined with the values in claimsCollection. The values found in claimCollection take precedence over those found in claims, so any duplicate + /// values will be overridden. + /// If notbefore.HasValue an { nbf, 'value' } claim is added, overwriting any 'nbf' claim in 'claims' and 'claimcollection' if present. + /// If expires.HasValue an { exp, 'value' } claim is added, overwriting any 'exp' claim in 'claims' and 'claimcollection' if present. + /// If issuedAt.HasValue is 'true' an { iat, 'value' } claim is added, overwriting any 'iat' claim in 'claims' and 'claimcollection' if present. + /// Comparison is set to + /// The 4 parameters: 'issuer', 'audience', 'notBefore', 'expires' take precedence over (s) in 'claims' and 'claimcollection'. The values will be overridden. + /// If 'expires' <= 'notbefore'. + public JwtPayload(string issuer, string audience, IList audiences, IEnumerable claims, IDictionary claimsCollection, DateTime? notBefore, DateTime? expires, DateTime? issuedAt) : base(StringComparer.Ordinal) { if (claims != null) @@ -199,7 +218,34 @@ public JwtPayload(string issuer, string audience, IEnumerable claims, IDi if (claimsCollection != null && claimsCollection.Any()) AddDictionaryClaims(claimsCollection); - AddFirstPriorityClaims(issuer, audience, notBefore, expires, issuedAt); + if (audiences != null) + AddFirstPriorityClaims(issuer, audience, audiences, notBefore, expires, issuedAt); + else + AddFirstPriorityClaims(issuer, audience, [], notBefore, expires, issuedAt); + } + + internal void AddFirstPriorityClaims(string issuer, string audience, DateTime? notBefore, DateTime? expires, DateTime? issuedAt) + { + AddFirstPriorityClaims(issuer, audience, [], notBefore, expires, issuedAt); + } + + /// + /// Initializes a new instance of the class with claims added for each parameter specified. Default string comparer . + /// + /// If this value is not null, an { iss, 'issuer' } claim will be added, overwriting any 'iss' claim in 'claims' and 'claimCollection' if present. + /// If this value is not null, an { aud, 'audience' } claim will be added, appending to any 'aud' claims in 'claims' or 'claimCollection' if present. + /// If this value is not null then for each a { 'Claim.Type', 'Claim.Value' } is added. If duplicate claims are found then a { 'Claim.Type', List<object> } will be created to contain the duplicate values. + /// If both and are not null then the values in claims will be combined with the values in claimsCollection. The values found in claimCollection take precedence over those found in claims, so any duplicate + /// values will be overridden. + /// If notbefore.HasValue an { nbf, 'value' } claim is added, overwriting any 'nbf' claim in 'claims' and 'claimcollection' if present. + /// If expires.HasValue an { exp, 'value' } claim is added, overwriting any 'exp' claim in 'claims' and 'claimcollection' if present. + /// If issuedAt.HasValue is 'true' an { iat, 'value' } claim is added, overwriting any 'iat' claim in 'claims' and 'claimcollection' if present. + /// Comparison is set to + /// The 4 parameters: 'issuer', 'audience', 'notBefore', 'expires' take precedence over (s) in 'claims' and 'claimcollection'. The values will be overridden. + /// If 'expires' <= 'notbefore'. + public JwtPayload(string issuer, string audience, IEnumerable claims, IDictionary claimsCollection, DateTime? notBefore, DateTime? expires, DateTime? issuedAt) + : this(issuer, audience, [], claims, claimsCollection, notBefore, expires, issuedAt) + { } /// @@ -207,10 +253,11 @@ public JwtPayload(string issuer, string audience, IEnumerable claims, IDi /// /// If this value is not null, a { iss, 'issuer' } claim will be added, overwriting any 'iss' claim in instance. /// If this value is not null, a { aud, 'audience' } claim will be added, appending to any 'aud' claims in instance. + /// If there are strings that are not null or empty append them to any other Aud claims /// If notbefore.HasValue a { nbf, 'value' } claim is added, overwriting any 'nbf' claim in instance. /// If expires.HasValue a { exp, 'value' } claim is added, overwriting any 'exp' claim in instance. /// If issuedAt.HasValue is 'true' a { iat, 'value' } claim is added, overwriting any 'iat' claim in instance. - internal void AddFirstPriorityClaims(string issuer, string audience, DateTime? notBefore, DateTime? expires, DateTime? issuedAt) + internal void AddFirstPriorityClaims(string issuer, string audience, IList audiences, DateTime? notBefore, DateTime? expires, DateTime? issuedAt) { if (expires.HasValue) { @@ -233,9 +280,16 @@ internal void AddFirstPriorityClaims(string issuer, string audience, DateTime? n if (!string.IsNullOrEmpty(issuer)) this[JwtRegisteredClaimNames.Iss] = issuer; - // if could be the case that some of the claims above had an 'aud' claim; + // Using AddClaim() since it could be the case that some of the claims processed earlier in the flow had 'aud' claims + // we don't want to overwrite; if (!string.IsNullOrEmpty(audience)) AddClaim(new Claim(JwtRegisteredClaimNames.Aud, audience, ClaimValueTypes.String)); + + foreach (string aud in audiences) + { + if (!string.IsNullOrEmpty(aud)) + AddClaim(new Claim(JwtRegisteredClaimNames.Aud, aud, ClaimValueTypes.String)); + } } /// @@ -299,7 +353,6 @@ public IList Aud List tmp = GetListOfClaims(JwtRegisteredClaimNames.Aud); Interlocked.CompareExchange(ref _audiences, tmp, null); } - return _audiences; } } diff --git a/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs b/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs index 4308d48d77..9c315c7da6 100644 --- a/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs +++ b/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs @@ -453,6 +453,7 @@ public virtual JwtSecurityToken CreateJwtSecurityToken(SecurityTokenDescriptor t return CreateJwtSecurityTokenPrivate( tokenDescriptor.Issuer, tokenDescriptor.Audience, + tokenDescriptor.Audiences, tokenDescriptor.Subject, tokenDescriptor.NotBefore, tokenDescriptor.Expires, @@ -603,6 +604,7 @@ public override SecurityToken CreateToken(SecurityTokenDescriptor tokenDescripto return CreateJwtSecurityTokenPrivate( tokenDescriptor.Issuer, tokenDescriptor.Audience, + tokenDescriptor.Audiences, tokenDescriptor.Subject, tokenDescriptor.NotBefore, tokenDescriptor.Expires, @@ -628,6 +630,26 @@ private JwtSecurityToken CreateJwtSecurityTokenPrivate( string tokenType, IDictionary additionalHeaderClaims, IDictionary additionalInnerHeaderClaims) + { + return CreateJwtSecurityTokenPrivate( + issuer, audience, [], subject, notBefore, expires, issuedAt, signingCredentials, encryptingCredentials, + claimCollection, tokenType, additionalHeaderClaims, additionalInnerHeaderClaims); + } + + private JwtSecurityToken CreateJwtSecurityTokenPrivate( + string issuer, + string audience, + IList audiences, + ClaimsIdentity subject, + DateTime? notBefore, + DateTime? expires, + DateTime? issuedAt, + SigningCredentials signingCredentials, + EncryptingCredentials encryptingCredentials, + IDictionary claimCollection, + string tokenType, + IDictionary additionalHeaderClaims, + IDictionary additionalInnerHeaderClaims) { if (SetDefaultTimesOnTokenCreation && (!expires.HasValue || !issuedAt.HasValue || !notBefore.HasValue)) { @@ -642,12 +664,12 @@ private JwtSecurityToken CreateJwtSecurityTokenPrivate( notBefore = now; } - if (LogHelper.IsEnabled(EventLogLevel.Verbose)) - LogHelper.LogVerbose(LogMessages.IDX12721, LogHelper.MarkAsNonPII(issuer ?? "null"), LogHelper.MarkAsNonPII(audience ?? "null")); - - JwtPayload payload = new JwtPayload(issuer, audience, (subject == null ? null : OutboundClaimTypeTransform(subject.Claims)), (claimCollection == null ? null : OutboundClaimTypeTransform(claimCollection)), notBefore, expires, issuedAt); + JwtPayload payload = new JwtPayload(issuer, audience, audiences, (subject == null ? null : OutboundClaimTypeTransform(subject.Claims)), (claimCollection == null ? null : OutboundClaimTypeTransform(claimCollection)), notBefore, expires, issuedAt); JwtHeader header = new JwtHeader(signingCredentials, OutboundAlgorithmMap, tokenType, additionalInnerHeaderClaims); + if (LogHelper.IsEnabled(EventLogLevel.Verbose)) + LogHelper.LogVerbose(LogMessages.IDX12721, LogHelper.MarkAsNonPII(issuer ?? "null"), LogHelper.MarkAsNonPII(payload.Aud.ToString() ?? "null")); + if (subject?.Actor != null) payload.AddClaim(new Claim(JwtRegisteredClaimNames.Actort, CreateActorValue(subject.Actor))); diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs index 6b67a4f4e3..7e88894509 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs @@ -801,6 +801,12 @@ public void CreateJWEUsingSecurityTokenDescriptor(CreateTokenTheoryData theoryDa { var context = TestUtilities.WriteHeader($"{this}.CreateJWEUsingSecurityTokenDescriptor", theoryData); theoryData.ValidationParameters.ValidateLifetime = false; + if (theoryData.TokenDescriptor != null && !theoryData.AudiencesForSecurityTokenDescriptor.IsNullOrEmpty()) + { + foreach (var audience in theoryData.AudiencesForSecurityTokenDescriptor) + theoryData.TokenDescriptor.Audiences.Add(audience); + } + try { string jweFromSecurityTokenDescriptor = theoryData.JsonWebTokenHandler.CreateToken(theoryData.TokenDescriptor); @@ -892,6 +898,35 @@ public static TheoryData CreateJWEUsingSecurityTokenDescr } }, new CreateTokenTheoryData + { + TestId = "SecurityTokenDescriptorMultipleAudiences", + Payload = Default.PayloadStringMultipleAudiences, + TokenDescriptor = new SecurityTokenDescriptor + { + SigningCredentials = KeyingMaterial.JsonWebKeyRsa256SigningCredentials, + EncryptingCredentials = KeyingMaterial.DefaultSymmetricEncryptingCreds_Aes256_Sha512_512, + Claims = Default.PayloadDictionaryMultipleAudiences + }, + AudiencesForSecurityTokenDescriptor = Default.Audiences, + JsonWebTokenHandler = new JsonWebTokenHandler(), + ValidationParameters = new TokenValidationParameters + { + IssuerSigningKey = KeyingMaterial.JsonWebKeyRsa256SigningCredentials.Key, + TokenDecryptionKey = KeyingMaterial.DefaultSymmetricSecurityKey_512, + ValidAudiences = Default.Audiences, + ValidIssuer = Default.Issuer + }, + + // There is a known difference in the 'Aud' claim between the two tokens. Since the JsonWebTokenHandler + // will only include the SecurityTokenDescriptor Audience and Audiences members if one or both are set in + // the 'Aud' claim, while the JwtSecurityTokenHandler will also include any aud claims set in either the + // Claims or Subject members even if Audience/Audiences is defined (not both, Claims takes precedence). + PropertiesToIgnoreWhenComparing = new Dictionary> + { + { typeof(JsonWebToken), new List {"EncodedPayload", "EncodedSignature"} }, + } + }, + new CreateTokenTheoryData { TestId = "ValidUsingX509SecurityKey", Payload = Default.PayloadString, @@ -1890,6 +1925,11 @@ public void CreateJWSUsingSecurityTokenDescriptor(CreateTokenTheoryData theoryDa try { JsonWebTokenHandler6x jsonWebTokenHandler6x = new JsonWebTokenHandler6x(); + if (theoryData.TokenDescriptor != null && !theoryData.AudiencesForSecurityTokenDescriptor.IsNullOrEmpty()) + { + foreach (var audience in theoryData.AudiencesForSecurityTokenDescriptor) + theoryData.TokenDescriptor.Audiences.Add(audience); + } string jwtFromSecurityTokenDescriptor6x = jwtFromSecurityTokenDescriptor6x = jsonWebTokenHandler6x.CreateToken(theoryData.TokenDescriptor6x ?? theoryData.TokenDescriptor); string jwtFromSecurityTokenDescriptor = theoryData.JsonWebTokenHandler.CreateToken(theoryData.TokenDescriptor); @@ -2098,6 +2138,23 @@ public static TheoryData CreateJWSUsingSecurityTokenDescr ValidIssuer = Default.Issuer } }, + new CreateTokenTheoryData("ValidUsingMultipleAudiences") + { + Payload = Default.PayloadStringMultipleAudiences, + TokenDescriptor = new SecurityTokenDescriptor + { + SigningCredentials = KeyingMaterial.JsonWebKeyRsa256SigningCredentials, + Claims = Default.PayloadDictionaryMultipleAudiences + }, + AudiencesForSecurityTokenDescriptor = Default.Audiences, + JsonWebTokenHandler = new JsonWebTokenHandler(), + ValidationParameters = new TokenValidationParameters + { + IssuerSigningKey = KeyingMaterial.JsonWebKeyRsa256SigningCredentials.Key, + ValidAudiences = Default.Audiences, + ValidIssuer = Default.Issuer + } + }, new CreateTokenTheoryData("ValidUsingSubject") { Payload = Default.PayloadString, @@ -4326,6 +4383,8 @@ public CreateTokenTheoryData(string testId) : base(testId) public IEnumerable ExpectedDecryptionKeys { get; set; } public Dictionary ExpectedClaims { get; set; } + + public List AudiencesForSecurityTokenDescriptor { get; set; } } // Overrides CryptoProviderFactory.CreateAuthenticatedEncryptionProvider to create AuthenticatedEncryptionProviderMock that provides AesGcm encryption. diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/json/JsonWebTokenHandler.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/json/JsonWebTokenHandler.cs index 9f0a37702a..22b37229a6 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/json/JsonWebTokenHandler.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/json/JsonWebTokenHandler.cs @@ -12,6 +12,7 @@ using Microsoft.IdentityModel.Abstractions; using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Tokens.Json; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; @@ -358,42 +359,49 @@ public virtual string CreateToken(SecurityTokenDescriptor tokenDescriptor) if (tokenDescriptor.Claims != null && tokenDescriptor.Claims.Count > 0) payload.Merge(JObject.FromObject(tokenDescriptor.Claims), new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Replace }); - if (tokenDescriptor.Audience != null) + if (tokenDescriptor.Audiences.Count > 0) { - if (LogHelper.IsEnabled(EventLogLevel.Informational) && payload.ContainsKey(JwtRegisteredClaimNames.Aud)) - LogHelper.LogInformation(LogHelper.FormatInvariant(LogMessages.IDX14113, LogHelper.MarkAsNonPII(nameof(tokenDescriptor.Audience)))); + if (payload.ContainsKey(JwtRegisteredClaimNames.Aud)) + LogDuplicatedClaim(nameof(tokenDescriptor.Audiences)); + + payload[JwtRegisteredClaimNames.Aud] = JArray.FromObject(tokenDescriptor.Audiences); + } + else if (!string.IsNullOrEmpty(tokenDescriptor.Audience)) + { + if (payload.ContainsKey(JwtRegisteredClaimNames.Aud)) + LogDuplicatedClaim(nameof(tokenDescriptor.Audience)); payload[JwtRegisteredClaimNames.Aud] = tokenDescriptor.Audience; } if (tokenDescriptor.Expires.HasValue) { - if (LogHelper.IsEnabled(EventLogLevel.Informational) && payload.ContainsKey(JwtRegisteredClaimNames.Exp)) - LogHelper.LogInformation(LogHelper.FormatInvariant(LogMessages.IDX14113, LogHelper.MarkAsNonPII(nameof(tokenDescriptor.Expires)))); + if (payload.ContainsKey(JwtRegisteredClaimNames.Exp)) + LogDuplicatedClaim(nameof(tokenDescriptor.Expires)); payload[JwtRegisteredClaimNames.Exp] = EpochTime.GetIntDate(tokenDescriptor.Expires.Value); } if (tokenDescriptor.Issuer != null) { - if (LogHelper.IsEnabled(EventLogLevel.Informational) && payload.ContainsKey(JwtRegisteredClaimNames.Iss)) - LogHelper.LogInformation(LogHelper.FormatInvariant(LogMessages.IDX14113, LogHelper.MarkAsNonPII(nameof(tokenDescriptor.Issuer)))); + if (payload.ContainsKey(JwtRegisteredClaimNames.Iss)) + LogDuplicatedClaim(nameof(tokenDescriptor.Issuer)); payload[JwtRegisteredClaimNames.Iss] = tokenDescriptor.Issuer; } if (tokenDescriptor.IssuedAt.HasValue) { - if (LogHelper.IsEnabled(EventLogLevel.Informational) && payload.ContainsKey(JwtRegisteredClaimNames.Iat)) - LogHelper.LogInformation(LogHelper.FormatInvariant(LogMessages.IDX14113, LogHelper.MarkAsNonPII(nameof(tokenDescriptor.IssuedAt)))); + if (payload.ContainsKey(JwtRegisteredClaimNames.Iat)) + LogDuplicatedClaim(nameof(tokenDescriptor.IssuedAt)); payload[JwtRegisteredClaimNames.Iat] = EpochTime.GetIntDate(tokenDescriptor.IssuedAt.Value); } if (tokenDescriptor.NotBefore.HasValue) { - if (LogHelper.IsEnabled(EventLogLevel.Informational) && payload.ContainsKey(JwtRegisteredClaimNames.Nbf)) - LogHelper.LogInformation(LogHelper.FormatInvariant(LogMessages.IDX14113, LogHelper.MarkAsNonPII(nameof(tokenDescriptor.NotBefore)))); + if (payload.ContainsKey(JwtRegisteredClaimNames.Nbf)) + LogDuplicatedClaim(nameof(tokenDescriptor.NotBefore)); payload[JwtRegisteredClaimNames.Nbf] = EpochTime.GetIntDate(tokenDescriptor.NotBefore.Value); } @@ -408,6 +416,12 @@ public virtual string CreateToken(SecurityTokenDescriptor tokenDescriptor) tokenDescriptor.TokenType); } + private void LogDuplicatedClaim(string claimType) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogHelper.FormatInvariant(LogMessages.IDX14113, LogHelper.MarkAsNonPII(claimType))); + } + /// /// Creates a JWE (Json Web Encryption). /// diff --git a/test/Microsoft.IdentityModel.TestUtils/Default.cs b/test/Microsoft.IdentityModel.TestUtils/Default.cs index c1bd9353cc..79c14f3c44 100644 --- a/test/Microsoft.IdentityModel.TestUtils/Default.cs +++ b/test/Microsoft.IdentityModel.TestUtils/Default.cs @@ -9,6 +9,7 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Tokens.Json; using Microsoft.IdentityModel.Tokens.Saml; using Microsoft.IdentityModel.Tokens.Saml2; using Microsoft.IdentityModel.Xml; @@ -404,6 +405,22 @@ public static string PayloadString }.ToString(Formatting.None); } + public static string PayloadStringMultipleAudiences + { + get => new JObject() + { + { JwtRegisteredClaimNames.Aud, JArray.FromObject(Audiences) }, + { JwtRegisteredClaimNames.Azp, Azp }, + { JwtRegisteredClaimNames.Email, "Bob@contoso.com" }, + { JwtRegisteredClaimNames.Exp, EpochTime.GetIntDate(Expires).ToString() }, + { JwtRegisteredClaimNames.GivenName, "Bob" }, + { JwtRegisteredClaimNames.Iss, Issuer }, + { JwtRegisteredClaimNames.Iat, EpochTime.GetIntDate(IssueInstant).ToString() }, + { JwtRegisteredClaimNames.Jti, Jti }, + { JwtRegisteredClaimNames.Nbf, EpochTime.GetIntDate(NotBefore).ToString()}, + }.ToString(Formatting.None); + } + public static List PayloadClaims { get => new List() @@ -499,6 +516,22 @@ public static Dictionary PayloadDictionary }; } + public static Dictionary PayloadDictionaryMultipleAudiences + { + get => new Dictionary() + { + { JwtRegisteredClaimNames.Aud, JsonSerializerPrimitives.CreateJsonElement(Default.Audiences) }, + { JwtRegisteredClaimNames.Azp, Azp }, + { JwtRegisteredClaimNames.Email, "Bob@contoso.com" }, + { JwtRegisteredClaimNames.Exp, EpochTime.GetIntDate(Expires).ToString() }, + { JwtRegisteredClaimNames.GivenName, "Bob" }, + { JwtRegisteredClaimNames.Iss, Issuer }, + { JwtRegisteredClaimNames.Iat, EpochTime.GetIntDate(IssueInstant).ToString() }, + { JwtRegisteredClaimNames.Jti, Jti }, + { JwtRegisteredClaimNames.Nbf, EpochTime.GetIntDate(NotBefore).ToString()}, + }; + } + public static Dictionary RemoveClaim(this Dictionary payloadClaims, string claimName) { payloadClaims.Remove(claimName); diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/CreateTokenTheoryData.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/CreateTokenTheoryData.cs index 6c1ecd9b51..813a5a0a69 100644 --- a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/CreateTokenTheoryData.cs +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/CreateTokenTheoryData.cs @@ -34,6 +34,8 @@ public class CreateTokenTheoryData : TheoryDataBase public string SamlToken { get; set; } public TokenValidationParameters ValidationParameters { get; set; } + + public List AudiencesForSecurityTokenDescriptor { get; set; } } } diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.cs index bf4c87f786..d9bb11a79b 100644 --- a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.cs @@ -1093,6 +1093,12 @@ public void CreateSaml2TokenUsingTokenDescriptor(CreateTokenTheoryData theoryDat { typeof(Saml2SecurityToken), new List { "SigningKey" } }, }; + if (!theoryData.AudiencesForSecurityTokenDescriptor.IsNullOrEmpty()) + { + foreach (var audience in theoryData.AudiencesForSecurityTokenDescriptor) + theoryData.TokenDescriptor.Audiences.Add(audience); + } + try { SecurityToken samlTokenFromSecurityTokenDescriptor = theoryData.Saml2SecurityTokenHandler.CreateToken(theoryData.TokenDescriptor) as Saml2SecurityToken; @@ -1123,8 +1129,87 @@ public static TheoryData CreateSaml2TokenUsingTokenDescri ValidateLifetime = false, IssuerSigningKey = KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256 }; + var validationParametersWithAudiences = new TokenValidationParameters + { + AuthenticationType = "Federation", + ValidateAudience = true, + ValidateIssuer = false, + ValidateLifetime = false, + IssuerSigningKey = KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256, + ValidAudiences = Default.Audiences + }; + var invalidAudience = "http://NotValid.Audience.com"; + var invalidAudiences = new List { invalidAudience, "http://NotValid.Audience2.com" }; return new TheoryData { + new CreateTokenTheoryData + { + First = true, + TestId = "ValidAudiences", + TokenDescriptor = new SecurityTokenDescriptor + { + NotBefore = Default.NotBefore, + Expires = Default.Expires, + Issuer = Default.Issuer, + SigningCredentials = new SigningCredentials(KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest), + EncryptingCredentials = null, + Subject = new ClaimsIdentity(Default.SamlClaims) + }, + Saml2SecurityTokenHandler = new Saml2SecurityTokenHandler(), + ValidationParameters = validationParametersWithAudiences, + AudiencesForSecurityTokenDescriptor = Default.Audiences + }, + new CreateTokenTheoryData + { + TestId = "InvalidAudiences", + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TokenDescriptor = new SecurityTokenDescriptor + { + NotBefore = Default.NotBefore, + Expires = Default.Expires, + Issuer = Default.Issuer, + SigningCredentials = new SigningCredentials(KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest), + EncryptingCredentials = null, + Subject = new ClaimsIdentity(Default.SamlClaims) + }, + Saml2SecurityTokenHandler = new Saml2SecurityTokenHandler(), + ValidationParameters = validationParametersWithAudiences, + AudiencesForSecurityTokenDescriptor = invalidAudiences + }, + new CreateTokenTheoryData + { + TestId = "UsingAudienceAndAudiences_OnlyAudienceValid", + TokenDescriptor = new SecurityTokenDescriptor + { + Audience = Default.Audience, + NotBefore = Default.NotBefore, + Expires = Default.Expires, + Issuer = Default.Issuer, + SigningCredentials = new SigningCredentials(KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest), + EncryptingCredentials = null, + Subject = new ClaimsIdentity(Default.SamlClaims) + }, + Saml2SecurityTokenHandler = new Saml2SecurityTokenHandler(), + ValidationParameters = validationParametersWithAudiences, + AudiencesForSecurityTokenDescriptor = invalidAudiences + }, + new CreateTokenTheoryData + { + TestId = "UsingAudienceAndAudiences_OnlyAudiencesValid", + TokenDescriptor = new SecurityTokenDescriptor + { + Audience = invalidAudience, + NotBefore = Default.NotBefore, + Expires = Default.Expires, + Issuer = Default.Issuer, + SigningCredentials = new SigningCredentials(KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest), + EncryptingCredentials = null, + Subject = new ClaimsIdentity(Default.SamlClaims) + }, + Saml2SecurityTokenHandler = new Saml2SecurityTokenHandler(), + ValidationParameters = validationParametersWithAudiences, + AudiencesForSecurityTokenDescriptor = Default.Audiences + }, new CreateTokenTheoryData { TestId = "NotSupportedClaimValue", @@ -1147,7 +1232,6 @@ public static TheoryData CreateSaml2TokenUsingTokenDescri }, new CreateTokenTheoryData { - First = true, TestId = "OnlySubjectClaims", TokenDescriptor = new SecurityTokenDescriptor { diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.cs index c22802d852..4ec30af29c 100644 --- a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.cs @@ -1076,6 +1076,11 @@ public void CreateSamlTokenUsingTokenDescriptor(CreateTokenTheoryData theoryData { typeof(SamlAssertion), new List { "IssueInstant", "InclusiveNamespacesPrefixList", "Signature", "SigningCredentials", "CanonicalString" } }, { typeof(SamlSecurityToken), new List { "SigningKey" } }, }; + if (!theoryData.AudiencesForSecurityTokenDescriptor.IsNullOrEmpty()) + { + foreach (var audience in theoryData.AudiencesForSecurityTokenDescriptor) + theoryData.TokenDescriptor.Audiences.Add(audience); + } try { @@ -1099,6 +1104,7 @@ public static TheoryData CreateSamlTokenUsingTokenDescrip { get { + var validationParameters = new TokenValidationParameters { AuthenticationType = "Federation", @@ -1107,8 +1113,87 @@ public static TheoryData CreateSamlTokenUsingTokenDescrip ValidateLifetime = false, IssuerSigningKey = KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256 }; + var validationParametersWithAudiences = new TokenValidationParameters + { + AuthenticationType = "Federation", + ValidateAudience = true, + ValidateIssuer = false, + ValidateLifetime = false, + IssuerSigningKey = KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256, + ValidAudiences = Default.Audiences + }; + var invalidAudience = "http://NotValid.Audience.com"; + var invalidAudiences = new List { invalidAudience, "http://NotValid.Audience2.com" }; return new TheoryData { + new CreateTokenTheoryData + { + First =true, + TestId = "ValidAudiences", + TokenDescriptor = new SecurityTokenDescriptor + { + NotBefore = Default.NotBefore, + Expires = Default.Expires, + Issuer = Default.Issuer, + SigningCredentials = new SigningCredentials(KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest), + EncryptingCredentials = null, + Claims = Default.SamlClaimsDictionary, + }, + SamlSecurityTokenHandler = new SamlSecurityTokenHandler(), + ValidationParameters = validationParametersWithAudiences, + AudiencesForSecurityTokenDescriptor = Default.Audiences + }, + new CreateTokenTheoryData + { + TestId = "InvalidAudiences", + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TokenDescriptor = new SecurityTokenDescriptor + { + NotBefore = Default.NotBefore, + Expires = Default.Expires, + Issuer = Default.Issuer, + SigningCredentials = new SigningCredentials(KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest), + EncryptingCredentials = null, + Claims = Default.SamlClaimsDictionary, + }, + SamlSecurityTokenHandler = new SamlSecurityTokenHandler(), + ValidationParameters = validationParametersWithAudiences, + AudiencesForSecurityTokenDescriptor = invalidAudiences + }, + new CreateTokenTheoryData + { + TestId = "UsingAudienceAndAudiences_OnlyAudienceValid", + TokenDescriptor = new SecurityTokenDescriptor + { + Audience = Default.Audience, + NotBefore = Default.NotBefore, + Expires = Default.Expires, + Issuer = Default.Issuer, + SigningCredentials = new SigningCredentials(KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest), + EncryptingCredentials = null, + Claims = Default.SamlClaimsDictionary, + }, + SamlSecurityTokenHandler = new SamlSecurityTokenHandler(), + ValidationParameters = validationParametersWithAudiences, + AudiencesForSecurityTokenDescriptor = [invalidAudience] + }, + new CreateTokenTheoryData + { + TestId = "UsingAudienceAndAudiences_OnlyAudiencesValid", + TokenDescriptor = new SecurityTokenDescriptor + { + Audience = invalidAudience, + NotBefore = Default.NotBefore, + Expires = Default.Expires, + Issuer = Default.Issuer, + SigningCredentials = new SigningCredentials(KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest), + EncryptingCredentials = null, + Claims = Default.SamlClaimsDictionary, + }, + SamlSecurityTokenHandler = new SamlSecurityTokenHandler(), + ValidationParameters = validationParametersWithAudiences, + AudiencesForSecurityTokenDescriptor = Default.Audiences + }, new CreateTokenTheoryData { TestId = "NotSupportedClaimValue", @@ -1131,7 +1216,6 @@ public static TheoryData CreateSamlTokenUsingTokenDescrip }, new CreateTokenTheoryData { - First =true, TestId = "NoSubjectClaims", TokenDescriptor = new SecurityTokenDescriptor { diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlTestData.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlTestData.cs index 0244bbd194..889b954610 100644 --- a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlTestData.cs +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlTestData.cs @@ -20,6 +20,7 @@ public static TheoryData SecurityKeyNotFoundExceptionTest TestId = "TokenExpired", TokenDescriptor = new SecurityTokenDescriptor { + Audience = Default.Audience, Subject = new ClaimsIdentity(Default.SamlClaims), Expires = DateTime.UtcNow.Subtract(new TimeSpan(0, 10, 0)), IssuedAt = DateTime.UtcNow.Subtract(new TimeSpan(1, 0, 0)), @@ -39,6 +40,7 @@ public static TheoryData SecurityKeyNotFoundExceptionTest TestId = "InvalidIssuer", TokenDescriptor = new SecurityTokenDescriptor { + Audience = Default.Audience, Subject = new ClaimsIdentity(Default.SamlClaims), SigningCredentials = Default.AsymmetricSigningCredentials, Issuer = Default.Issuer, @@ -54,6 +56,7 @@ public static TheoryData SecurityKeyNotFoundExceptionTest TestId = "ExpiredAndInvalidIssuer", TokenDescriptor = new SecurityTokenDescriptor { + Audience = Default.Audience, Subject = new ClaimsIdentity(Default.SamlClaims), Expires = DateTime.UtcNow.Subtract(new TimeSpan(0, 10, 0)), IssuedAt = DateTime.UtcNow.Subtract(new TimeSpan(1, 0, 0)), @@ -74,6 +77,7 @@ public static TheoryData SecurityKeyNotFoundExceptionTest TestId = "KeysDontMatchValidLifetimeAndIssuer", TokenDescriptor = new SecurityTokenDescriptor { + Audience = Default.Audience, Subject = new ClaimsIdentity(Default.SamlClaims), SigningCredentials = Default.AsymmetricSigningCredentials, Issuer = Default.Issuer, diff --git a/test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTests.cs b/test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTests.cs index 9400e6770a..c1ccc8e641 100644 --- a/test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTests.cs +++ b/test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTests.cs @@ -93,7 +93,6 @@ public void CreateJWEWithAdditionalHeaderClaims(CreateTokenTheoryData theoryData } } - if (theoryData.TokenDescriptor.AdditionalInnerHeaderClaims != null) { theoryData.ValidationParameters.ValidateLifetime = false; @@ -258,6 +257,130 @@ public static TheoryData CreateJWEWithPayloadStringTheory } } + // Tests for expected differences between the JwtSecurityTokenHandler and the JsonWebTokenHandler. + [Theory, MemberData(nameof(CreateJWEUsingSecurityTokenDescriptorTheoryData))] + public void CheckExpectedDifferenceInAudClaimUsingSecurityTokenDescriptor(CreateTokenTheoryData theoryData) + { + if (theoryData.TokenDescriptor == null) + return; + + CompareContext context = TestUtilities.WriteHeader($"{this}.CheckExpectedDifferencesUsingSecurityTokenDescriptor", theoryData); + theoryData.ValidationParameters.ValidateLifetime = false; + var audMemberSet = false; + var audSetInClaims = false; + var audSetInSubject = false; + + if (!theoryData.AudiencesForSecurityTokenDescriptor.IsNullOrEmpty()) + { + audMemberSet = true; + foreach (var audience in theoryData.AudiencesForSecurityTokenDescriptor) + theoryData.TokenDescriptor.Audiences.Add(audience); + } + else if (!theoryData.TokenDescriptor.Audience.IsNullOrEmpty()) + { + audMemberSet = true; + } + + if (!theoryData.TokenDescriptor.Claims.IsNullOrEmpty() && theoryData.TokenDescriptor.Claims.ContainsKey(JwtRegisteredClaimNames.Aud)) + audSetInClaims = true; + + if (theoryData.TokenDescriptor.Subject != null && theoryData.TokenDescriptor.Subject.Claims.Any(c => c.Type == JwtRegisteredClaimNames.Aud)) + audSetInSubject = true; + + try + { + SecurityToken jweFromJwtHandler = theoryData.JwtSecurityTokenHandler.CreateToken(theoryData.TokenDescriptor); + string tokenFromJwtHandler = theoryData.JwtSecurityTokenHandler.WriteToken(jweFromJwtHandler); + + string jweFromJsonHandler = theoryData.JsonWebTokenHandler.CreateToken(theoryData.TokenDescriptor); + + ClaimsPrincipal claimsPrincipalJwtHandler = theoryData.JwtSecurityTokenHandler.ValidateToken(tokenFromJwtHandler, theoryData.ValidationParameters, out SecurityToken validatedTokenFromJwtHandler); + TokenValidationResult validationResultJsonHandler = theoryData.JsonWebTokenHandler.ValidateTokenAsync(jweFromJsonHandler, theoryData.ValidationParameters).Result; + + int expectedAudClaimCount = 0; + int additionalAudClaimsForJwtHandler = 0; + + + if (audMemberSet) + { + if (theoryData.TokenDescriptor.Audiences.Count > 0) + expectedAudClaimCount += theoryData.TokenDescriptor.Audiences.Count; + + if (!theoryData.TokenDescriptor.Audience.IsNullOrEmpty()) + expectedAudClaimCount++; + + if (audSetInClaims) + additionalAudClaimsForJwtHandler += GetClaimCountFromTokenDescriptorClaims(theoryData.TokenDescriptor, JwtRegisteredClaimNames.Aud); + + else if (audSetInSubject) + additionalAudClaimsForJwtHandler += GetClaimCountFromTokenDescriptorSubject(theoryData.TokenDescriptor, JwtRegisteredClaimNames.Aud); + } + else if (audSetInClaims) + { + expectedAudClaimCount += GetClaimCountFromTokenDescriptorClaims(theoryData.TokenDescriptor, JwtRegisteredClaimNames.Aud); + } + else if (audSetInSubject) + { + expectedAudClaimCount += GetClaimCountFromTokenDescriptorSubject(theoryData.TokenDescriptor, JwtRegisteredClaimNames.Aud); + } + + if (validationResultJsonHandler != null && validationResultJsonHandler.Claims != null) + { + if (validationResultJsonHandler.Claims.TryGetValue(JwtRegisteredClaimNames.Aud, out var audClaims)) + { + switch (audClaims) + { + case string _: + Assert.True(expectedAudClaimCount == 1); + break; + case IEnumerable enumerable: + Assert.True(expectedAudClaimCount == enumerable.Count()); + break; + case IEnumerable enumerable: + Assert.True(expectedAudClaimCount == enumerable.Count()); + break; + default: + Assert.True(false, "Unexpected type for 'aud' claim."); + break; + } + } + } + if (claimsPrincipalJwtHandler != null) + { + IEnumerable jwtHandlerAudClaims = claimsPrincipalJwtHandler.FindAll(JwtRegisteredClaimNames.Aud); + Assert.True(expectedAudClaimCount + additionalAudClaimsForJwtHandler == jwtHandlerAudClaims.Count()); + } + } + catch (Exception ex) + { + theoryData.ExpectedException.ProcessException(ex, context); + } + + TestUtilities.AssertFailIfErrors(context); + } + + private static int GetClaimCountFromTokenDescriptorClaims(SecurityTokenDescriptor tokenDescriptor, string claimName) + { + if (tokenDescriptor.Claims != null && tokenDescriptor.Claims.ContainsKey(claimName)) + { + switch (tokenDescriptor.Claims[claimName]) + { + case string _: + return 1; + case IEnumerable enumerable: + return enumerable.Count(); + } + } + return 0; + } + private static int GetClaimCountFromTokenDescriptorSubject(SecurityTokenDescriptor tokenDescriptor, string claimName) + { + if (tokenDescriptor.Subject != null && tokenDescriptor.Subject.Claims != null) + return tokenDescriptor.Subject.Claims.Where(c => c.Type.Equals(claimName, StringComparison.Ordinal)).Count(); + + return 0; + } + // Tests checks to make sure that the token string created by the JwtSecurityTokenHandler is consistent with the // token string created by the JsonWebTokenHandler. [Theory, MemberData(nameof(CreateJWEUsingSecurityTokenDescriptorTheoryData))] @@ -265,6 +388,13 @@ public void CreateJWEUsingSecurityTokenDescriptor(CreateTokenTheoryData theoryDa { CompareContext context = TestUtilities.WriteHeader($"{this}.CreateJWEUsingSecurityTokenDescriptor", theoryData); theoryData.ValidationParameters.ValidateLifetime = false; + + if (!theoryData.AudiencesForSecurityTokenDescriptor.IsNullOrEmpty()) + { + foreach (var audience in theoryData.AudiencesForSecurityTokenDescriptor) + theoryData.TokenDescriptor.Audiences.Add(audience); + } + try { SecurityToken jweFromJwtHandler = theoryData.JwtSecurityTokenHandler.CreateToken(theoryData.TokenDescriptor); @@ -276,7 +406,7 @@ public void CreateJWEUsingSecurityTokenDescriptor(CreateTokenTheoryData theoryDa var validationResultJson = theoryData.JsonWebTokenHandler.ValidateTokenAsync(jweFromJsonHandler, theoryData.ValidationParameters).Result; if (validationResultJson.Exception != null && validationResultJson.IsValid) - context.Diffs.Add("validationResultJson.IsValid, validationResultJson.Exception != null"); + context.Diffs.Add("validationResultJsonHandler.IsValid, validationResultJsonHandler.Exception != null"); IdentityComparer.AreEqual(validationResultJson.IsValid, theoryData.IsValid, context); @@ -293,7 +423,7 @@ public void CreateJWEUsingSecurityTokenDescriptor(CreateTokenTheoryData theoryDa if (!IdentityComparer.AreEqual(claimsPrincipalJwt.Identity, validationResultJson.ClaimsIdentity, context)) { - context.Diffs.Add("claimsPrincipalJwt.Identity != validationResultJson.ClaimsIdentity"); + context.Diffs.Add("claimsPrincipalJwtHandler.Identity != validationResultJsonHandler.ClaimsIdentity"); context.Diffs.Add("*******************************************************************"); } @@ -336,11 +466,90 @@ public static TheoryData CreateJWEUsingSecurityTokenDescr SetDefaultTimesOnTokenCreation = false }; + var invalidAudience = "http://NotValid.Audience.com"; + var invalidAudiences = new List { invalidAudience, "http://NotValid.Audience2.com" }; + + var claimsDictionaryNoAudience = Default.PayloadJsonDictionary; + claimsDictionaryNoAudience.Remove(JwtRegisteredClaimNames.Aud); + return new TheoryData { new CreateTokenTheoryData { First = true, + TestId = "ValidAudiences", + TokenDescriptor = new SecurityTokenDescriptor + { + SigningCredentials = Default.X509AsymmetricSigningCredentials, + EncryptingCredentials = null, + Claims = claimsDictionaryNoAudience + }, + AudiencesForSecurityTokenDescriptor = Default.Audiences, + JwtSecurityTokenHandler = tokenHandler, + JsonWebTokenHandler = jsonTokenHandler, + ValidationParameters = Default.AsymmetricSignTokenValidationParameters + }, + new CreateTokenTheoryData + { + TestId = "InvalidAudiences", + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214"), + TokenDescriptor = new SecurityTokenDescriptor + { + SigningCredentials = Default.X509AsymmetricSigningCredentials, + EncryptingCredentials = null, + Claims = claimsDictionaryNoAudience + }, + AudiencesForSecurityTokenDescriptor = invalidAudiences, + JwtSecurityTokenHandler = tokenHandler, + JsonWebTokenHandler = jsonTokenHandler, + ValidationParameters = Default.AsymmetricSignTokenValidationParameters + }, + new CreateTokenTheoryData + { + TestId = "ValidAudienceInvalidAudiences", + TokenDescriptor = new SecurityTokenDescriptor + { + SigningCredentials = Default.X509AsymmetricSigningCredentials, + EncryptingCredentials = null, + Claims = claimsDictionaryNoAudience, + Audience = Default.Audience + }, + AudiencesForSecurityTokenDescriptor = invalidAudiences, + JwtSecurityTokenHandler = tokenHandler, + JsonWebTokenHandler = jsonTokenHandler, + ValidationParameters = Default.AsymmetricSignTokenValidationParameters + }, + new CreateTokenTheoryData + { + TestId = "InvalidAudienceValidAudiences", + TokenDescriptor = new SecurityTokenDescriptor + { + SigningCredentials = Default.X509AsymmetricSigningCredentials, + EncryptingCredentials = null, + Claims = claimsDictionaryNoAudience, + Audience = invalidAudience + }, + AudiencesForSecurityTokenDescriptor = Default.Audiences, + JwtSecurityTokenHandler = tokenHandler, + JsonWebTokenHandler = jsonTokenHandler, + ValidationParameters = Default.AsymmetricSignTokenValidationParameters + }, + new CreateTokenTheoryData + { + TestId = "AudClaimNotPresentInClaimsDictionary_MultipleAudiences", + TokenDescriptor = new SecurityTokenDescriptor + { + SigningCredentials = Default.AsymmetricSigningCredentials, + EncryptingCredentials = null, + Claims = claimsDictionaryNoAudience, + }, + AudiencesForSecurityTokenDescriptor = Default.Audiences, + JwtSecurityTokenHandler = tokenHandler, + JsonWebTokenHandler = jsonTokenHandler, + ValidationParameters = Default.AsymmetricSignTokenValidationParameters + }, + new CreateTokenTheoryData + { TestId = "IdenticalClaims", TokenDescriptor = new SecurityTokenDescriptor { @@ -425,7 +634,7 @@ public static TheoryData CreateJWEUsingSecurityTokenDescr JwtSecurityTokenHandler = tokenHandler, JsonWebTokenHandler = jsonTokenHandler, ValidationParameters = Default.AsymmetricSignTokenValidationParameters - }, + } }; } } @@ -1192,7 +1401,7 @@ public void JWEDecompressionTest(JWEDecompressionTheoryData theoryData) var claimsPrincipal = handler.ValidateToken(theoryData.JWECompressionString, theoryData.ValidationParameters, out var validatedToken); if (!claimsPrincipal.Claims.Any()) - context.Diffs.Add("claimsPrincipalJwt.Claims is empty."); + context.Diffs.Add("claimsPrincipalJwtHandler.Claims is empty."); theoryData.ExpectedException.ProcessNoException(context); } @@ -1602,6 +1811,10 @@ public static TheoryData ValidateAudienceTheoryData get { var tokenHandler = new JwtSecurityTokenHandler(); + var securityTokenDescriptorWithAudiences = new SecurityTokenDescriptor{ Issuer = Default.Issuer }; + foreach (var audience in Default.Audiences) + securityTokenDescriptorWithAudiences.Audiences.Add(audience); + return new TheoryData { new JwtTheoryData @@ -1710,12 +1923,18 @@ public static TheoryData ValidateAudienceTheoryData TestId = "'validateAudience == false, AudienceValidator return false'", SecurityToken = tokenHandler.CreateJwtSecurityToken(issuer: Default.Issuer, audience: Default.Audience), ValidationParameters = ValidateAudienceValidationParameters(null, null, ValidationDelegates.AudienceValidatorReturnsFalse, false), + }, + new JwtTheoryData + { + TestId = "'validAudiences == audiences, validates successfully'", + SecurityToken = tokenHandler.CreateJwtSecurityToken(issuer: Default.Issuer, audience: Default.Audience), + ValidationParameters = ValidateAudienceValidationParameters(null, null, null, true, Default.Audiences), } }; } } - private static TokenValidationParameters ValidateAudienceValidationParameters(string validAudience, IEnumerable validAudiences, AudienceValidator audienceValidator, bool validateAudience) + private static TokenValidationParameters ValidateAudienceValidationParameters(string validAudience, IEnumerable validIssuers, AudienceValidator audienceValidator, bool validateAudience, IEnumerable validAudiences = null) { return new TokenValidationParameters { @@ -1725,7 +1944,8 @@ private static TokenValidationParameters ValidateAudienceValidationParameters(st ValidateIssuer = false, ValidateLifetime = false, ValidAudience = validAudience, - ValidIssuers = validAudiences + ValidAudiences = validAudiences, + ValidIssuers = validIssuers }; } @@ -2171,7 +2391,7 @@ public void ValidateJWSWithConfig(JwtTheoryData theoryData) AadIssuerValidator.GetAadIssuerValidator(Default.AadV1Authority).ConfigurationManagerV1 = theoryData.ValidationParameters.ConfigurationManager; new JwtSecurityTokenHandler().ValidateToken(theoryData.Token, theoryData.ValidationParameters, out _); if (theoryData.ShouldSetLastKnownConfiguration && theoryData.ValidationParameters.ConfigurationManager.LastKnownGoodConfiguration == null) - context.AddDiff("validationResultJson.IsValid, but the configuration was not set as the LastKnownGoodConfiguration"); + context.AddDiff("validationResultJsonHandler.IsValid, but the configuration was not set as the LastKnownGoodConfiguration"); theoryData.ExpectedException.ProcessNoException(context); } catch (Exception ex) @@ -3080,5 +3300,6 @@ public class CreateTokenTheoryData : TheoryDataBase public string Algorithm { get; set; } public IEnumerable ExpectedDecryptionKeys { get; set; } + public List AudiencesForSecurityTokenDescriptor { get; set; } } } From 3187d3448046520c70e6520f0ae4318d666f34c7 Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Tue, 25 Jun 2024 06:41:09 +0100 Subject: [PATCH 16/22] Audience validation: remove exceptions (#2655) * Added AudienceValidationResult type * Added new Validators.ValidateAudience method with CallContext and Result as return value * Disambiguate reference in documentation * Fixed issuer validation tests not ensuring no exception was thrown * Removed initial value from Audience in AudienceValidationResult. * Fix typo * Made securityToken optional, removed null check and updated tests * Updated audience matching method to remove allocations, return the actual audience that passed validation. Updated tests * Use pattern matching to cast exception * Use List over IEnumerable to avoid allocations * Refactored to always iterate over List and avoid allocations. Original method remains IEnumerable. * Keep public interface receiving IEnumerable and internally use List --- .../Saml/SamlSecurityTokenHandler.cs | 2 +- .../Validation/AudienceValidationResult.cs | 67 ++ .../Validation/ValidationFailureType.cs | 6 + .../Validation/Validators.Audience.cs | 186 +++++- .../JwtSecurityTokenHandler.cs | 2 +- .../IdentityComparer.cs | 66 ++ .../AudienceValidationResultTests.cs | 597 ++++++++++++++++++ .../Validation/IssuerValidationResultTests.cs | 2 + 8 files changed, 903 insertions(+), 25 deletions(-) create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/AudienceValidationResult.cs create mode 100644 test/Microsoft.IdentityModel.Tokens.Tests/Validation/AudienceValidationResultTests.cs diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs index 32573a0bcf..5ec1f7b90c 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs @@ -908,7 +908,7 @@ protected virtual void SetDelegateFromAttribute(SamlAttribute attribute, ClaimsI /// . /// The being validated. /// required for validation. - /// see for additional details. + /// see for additional details. protected virtual void ValidateAudience(IEnumerable audiences, SecurityToken securityToken, TokenValidationParameters validationParameters) { Validators.ValidateAudience(audiences, securityToken, validationParameters); diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/AudienceValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/AudienceValidationResult.cs new file mode 100644 index 0000000000..024cca68ba --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/AudienceValidationResult.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// Contains the result of validating the audiences from a . + /// The contains a collection of for each step in the token validation. + /// + internal class AudienceValidationResult : ValidationResult + { + private Exception _exception; + + /// + /// Creates an instance of . + /// + /// is the audience that was validated successfully. + public AudienceValidationResult(string audience) : base(ValidationFailureType.ValidationSucceeded) + { + IsValid = true; + Audience = audience; + } + + /// + /// Creates an instance of + /// + /// is the audience that was intended to be validated. + /// is the that occurred during validation. + /// is the that occurred during validation. + public AudienceValidationResult(string audience, ValidationFailureType validationFailure, ExceptionDetail exceptionDetail) + : base(validationFailure, exceptionDetail) + { + IsValid = false; + Audience = audience; + } + + /// + /// Gets the that occurred during validation. + /// + public override Exception Exception + { + get + { + if (_exception != null || ExceptionDetail == null) + return _exception; + + HasValidOrExceptionWasRead = true; + _exception = ExceptionDetail.GetException(); + if (_exception is SecurityTokenInvalidAudienceException securityTokenInvalidAudienceException) + { + securityTokenInvalidAudienceException.InvalidAudience = Audience; + securityTokenInvalidAudienceException.ExceptionDetail = ExceptionDetail; + securityTokenInvalidAudienceException.Source = "Microsoft.IdentityModel.Tokens"; + } + + return _exception; + } + } + + /// + /// Gets the audience that was validated or intended to be validated. + /// + public string Audience { get; } + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs index 3d872bb63a..4c561994fb 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs @@ -33,6 +33,12 @@ private class NullArgumentFailure : ValidationFailureType { internal NullArgumen public static readonly ValidationFailureType IssuerValidationFailed = new IssuerValidationFailure("IssuerValidationFailed"); private class IssuerValidationFailure : ValidationFailureType { internal IssuerValidationFailure(string name) : base(name) { } } + /// + /// Defines a type that represents that audience validation failed. + /// + public static readonly ValidationFailureType AudienceValidationFailed = new AudienceValidationFailure("AudienceValidationFailed"); + private class AudienceValidationFailure : ValidationFailureType { internal AudienceValidationFailure(string name) : base(name) { } } + /// /// Defines a type that represents that no evaluation has taken place. /// diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Audience.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Audience.cs index 9c41082c53..e56c166e70 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Audience.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Audience.cs @@ -3,12 +3,29 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using Microsoft.IdentityModel.Abstractions; using Microsoft.IdentityModel.Logging; +#nullable enable namespace Microsoft.IdentityModel.Tokens { + /// + /// Definition for delegate that will validate the audiences value in a token. + /// + /// The audiences to validate. + /// The that is being validated. + /// required for validation. + /// + /// A that contains the results of validating the issuer. + /// This delegate is not expected to throw. + internal delegate AudienceValidationResult ValidateAudience( + IEnumerable audiences, + SecurityToken? securityToken, + TokenValidationParameters validationParameters, + CallContext callContext); + /// /// Partial class for Audience Validation. /// @@ -62,17 +79,10 @@ public static void ValidateAudience(IEnumerable audiences, SecurityToken new SecurityTokenInvalidAudienceException(LogHelper.FormatInvariant(LogMessages.IDX10206)) { InvalidAudience = Utility.SerializeAsSingleCommaDelimitedString(audiences) }); - // create enumeration of all valid audiences from validationParameters - IEnumerable validationParametersAudiences; - - if (validationParameters.ValidAudiences == null) - validationParametersAudiences = new[] { validationParameters.ValidAudience }; - else if (string.IsNullOrWhiteSpace(validationParameters.ValidAudience)) - validationParametersAudiences = validationParameters.ValidAudiences; - else - validationParametersAudiences = validationParameters.ValidAudiences.Concat(new[] { validationParameters.ValidAudience }); + if (audiences is not List audiencesAsList) + audiencesAsList = audiences.ToList(); - if (AudienceIsValid(audiences, validationParameters, validationParametersAudiences)) + if (AudienceIsValid(audiencesAsList, validationParameters)) return; SecurityTokenInvalidAudienceException ex = new SecurityTokenInvalidAudienceException( @@ -88,39 +98,168 @@ public static void ValidateAudience(IEnumerable audiences, SecurityToken throw LogHelper.LogExceptionMessage(ex); } - private static bool AudienceIsValid(IEnumerable audiences, TokenValidationParameters validationParameters, IEnumerable validationParametersAudiences) + + /// + /// Determines if the audiences found in a are valid. + /// + /// The audiences found in the . + /// The being validated. + /// required for validation. + /// + /// If 'validationParameters' is null. + /// If 'audiences' is null and is true. + /// If is null or whitespace and is null. + /// If none of the 'audiences' matched either or one of . + /// An EXACT match is required. +#pragma warning disable CA1801 // TODO: remove pragma disable once callContext is used for logging + internal static AudienceValidationResult ValidateAudience(IEnumerable audiences, SecurityToken? securityToken, TokenValidationParameters validationParameters, CallContext callContext) +#pragma warning restore CA1801 { - foreach (string tokenAudience in audiences) + if (validationParameters == null) + return new AudienceValidationResult( + Utility.SerializeAsSingleCommaDelimitedString(audiences), + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10000, + LogHelper.MarkAsNonPII(nameof(validationParameters))), + typeof(ArgumentNullException), + new StackFrame(true))); + + if (!validationParameters.ValidateAudience) { + LogHelper.LogWarning(LogMessages.IDX10233); + return new AudienceValidationResult(Utility.SerializeAsSingleCommaDelimitedString(audiences)); + } + + if (audiences == null) + return new AudienceValidationResult( + Utility.SerializeAsSingleCommaDelimitedString(audiences), + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10207, + null), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true))); + + if (string.IsNullOrWhiteSpace(validationParameters.ValidAudience) && (validationParameters.ValidAudiences == null)) + return new AudienceValidationResult( + Utility.SerializeAsSingleCommaDelimitedString(audiences), + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10208, + null), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true))); + + if (audiences is not List audiencesAsList) + audiencesAsList = audiences.ToList(); + + if (audiencesAsList.Count == 0) + return new AudienceValidationResult( + Utility.SerializeAsSingleCommaDelimitedString(audiencesAsList), + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10206, + null), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true))); + + string? validAudience = AudienceIsValidReturning(audiencesAsList, validationParameters); + if (validAudience != null) + { + return new AudienceValidationResult(validAudience); + } + + return new AudienceValidationResult( + Utility.SerializeAsSingleCommaDelimitedString(audiencesAsList), + ValidationFailureType.AudienceValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(audiencesAsList)), + LogHelper.MarkAsNonPII(validationParameters.ValidAudience ?? "null"), + LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(validationParameters.ValidAudiences))), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true))); + } + + private static bool AudienceIsValid(List audiences, TokenValidationParameters validationParameters) + { + return AudienceIsValidReturning(audiences, validationParameters) != null; + } + + private static string? AudienceIsValidReturning(List audiences, TokenValidationParameters validationParameters) + { + string? validAudience = null; + if (!string.IsNullOrWhiteSpace(validationParameters.ValidAudience)) + validAudience = AudiencesMatchSingle(audiences, validationParameters.ValidAudience, validationParameters.IgnoreTrailingSlashWhenValidatingAudience); + + if (validAudience == null && validationParameters.ValidAudiences != null) + { + if (validationParameters.ValidAudiences is not List validAudiences) + validAudiences = validationParameters.ValidAudiences.ToList(); + + validAudience = AudiencesMatchList(audiences, validAudiences, validationParameters.IgnoreTrailingSlashWhenValidatingAudience); + } + + return validAudience; + } + + private static string? AudiencesMatchSingle(List audiences, string validAudience, bool ignoreTrailingSlashWhenValidatingAudience) + { + for (int i = 0; i < audiences.Count; i++) + { + string tokenAudience = audiences[i]; if (string.IsNullOrWhiteSpace(tokenAudience)) continue; - foreach (string validAudience in validationParametersAudiences) + if (AudiencesMatch(ignoreTrailingSlashWhenValidatingAudience, tokenAudience, validAudience)) { - if (string.IsNullOrWhiteSpace(validAudience)) + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX10234, LogHelper.MarkAsNonPII(tokenAudience)); + + return tokenAudience; + } + } + + return null; + } + + private static string? AudiencesMatchList(IList audiences, List validAudiences, bool ignoreTrailingSlashWhenValidatingAudience) + { + for (int i = 0; i < audiences.Count; i++) + { + string tokenAudience = audiences[i]; + if (string.IsNullOrWhiteSpace(tokenAudience)) + continue; + + foreach (string validAudience in validAudiences) + { + if (string.IsNullOrEmpty(validAudience)) continue; - if (AudiencesMatch(validationParameters, tokenAudience, validAudience)) + if (AudiencesMatch(ignoreTrailingSlashWhenValidatingAudience, tokenAudience, validAudience)) { if (LogHelper.IsEnabled(EventLogLevel.Informational)) LogHelper.LogInformation(LogMessages.IDX10234, LogHelper.MarkAsNonPII(tokenAudience)); - return true; + return tokenAudience; } } } - return false; + return null; } - private static bool AudiencesMatch(TokenValidationParameters validationParameters, string tokenAudience, string validAudience) + private static bool AudiencesMatch(bool ignoreTrailingSlashWhenValidatingAudience, string tokenAudience, string validAudience) { if (validAudience.Length == tokenAudience.Length) - { - if (string.Equals(validAudience, tokenAudience)) - return true; - } - else if (validationParameters.IgnoreTrailingSlashWhenValidatingAudience && AudiencesMatchIgnoringTrailingSlash(tokenAudience, validAudience)) + return string.Equals(validAudience, tokenAudience); + else if (ignoreTrailingSlashWhenValidatingAudience && AudiencesMatchIgnoringTrailingSlash(tokenAudience, validAudience)) return true; return false; @@ -152,3 +291,4 @@ private static bool AudiencesMatchIgnoringTrailingSlash(string tokenAudience, st } } +#nullable disable diff --git a/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs b/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs index 9c315c7da6..0d3d48d65a 100644 --- a/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs +++ b/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs @@ -1661,7 +1661,7 @@ protected virtual string CreateActorValue(ClaimsIdentity actor) /// The audiences found in the . /// The being validated. /// required for validation. - /// See for additional details. + /// See for additional details. protected virtual void ValidateAudience(IEnumerable audiences, JwtSecurityToken jwtToken, TokenValidationParameters validationParameters) { Validators.ValidateAudience(audiences, jwtToken, validationParameters); diff --git a/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs b/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs index 7ef5e4fa3e..c8caa5156d 100644 --- a/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs +++ b/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs @@ -574,6 +574,9 @@ internal static bool AreIssuerValidationResultsEqual( if (issuerValidationResult1.Issuer != issuerValidationResult2.Issuer) localContext.Diffs.Add($"IssuerValidationResult1.Issuer: {issuerValidationResult1.Issuer} != IssuerValidationResult2.Issuer: {issuerValidationResult2.Issuer}"); + if (issuerValidationResult1.IsValid != issuerValidationResult2.IsValid) + localContext.Diffs.Add($"IssuerValidationResult1.IsValid: {issuerValidationResult1.IsValid} != IssuerValidationResult2.IsValid: {issuerValidationResult2.IsValid}"); + if (issuerValidationResult1.Source != issuerValidationResult2.Source) localContext.Diffs.Add($"IssuerValidationResult1.Source: {issuerValidationResult1.Source} != IssuerValidationResult2.Source: {issuerValidationResult2.Source}"); @@ -607,6 +610,69 @@ internal static bool AreIssuerValidationResultsEqual( return context.Merge(localContext); } + public static bool AreAudienceValidationResultsEqual(object object1, object object2, CompareContext context) + { + var localContext = new CompareContext(context); + if (!ContinueCheckingEquality(object1, object2, context)) + return context.Merge(localContext); + + return AreAudienceValidationResultsEqual( + object1 as AudienceValidationResult, + object2 as AudienceValidationResult, + "AudienceValidationResult1", + "AudienceValidationResult2", + null, + context); + } + + internal static bool AreAudienceValidationResultsEqual( + AudienceValidationResult audienceValidationResult1, + AudienceValidationResult audienceValidationResult2, + string name1, + string name2, + string stackPrefix, + CompareContext context) + { + var localContext = new CompareContext(context); + if (!ContinueCheckingEquality(audienceValidationResult1, audienceValidationResult2, localContext)) + return context.Merge(localContext); + + if (audienceValidationResult1.Audience != audienceValidationResult2.Audience) + localContext.Diffs.Add($"AudienceValidationResult1.Audience: '{audienceValidationResult1.Audience}' != AudienceValidationResult2.Audience: '{audienceValidationResult2.Audience}'"); + + if (audienceValidationResult1.IsValid != audienceValidationResult2.IsValid) + localContext.Diffs.Add($"AudienceValidationResult1.IsValid: {audienceValidationResult1.IsValid} != AudienceValidationResult2.IsValid: {audienceValidationResult2.IsValid}"); + + // true => both are not null. + if (ContinueCheckingEquality(audienceValidationResult1.Exception, audienceValidationResult2.Exception, localContext)) + { + AreStringsEqual( + audienceValidationResult1.Exception.Message, + audienceValidationResult2.Exception.Message, + $"({name1})audienceValidationResult1.Exception.Message", + $"({name2})audienceValidationResult2.Exception.Message", + localContext); + + AreStringsEqual( + audienceValidationResult1.Exception.Source, + audienceValidationResult2.Exception.Source, + $"({name1})audienceValidationResult1.Exception.Source", + $"({name2})audienceValidationResult2.Exception.Source", + localContext); + + if (!string.IsNullOrEmpty(stackPrefix)) + AreStringPrefixesEqual( + audienceValidationResult1.Exception.StackTrace.Trim(), + audienceValidationResult2.Exception.StackTrace.Trim(), + $"({name1})audienceValidationResult1.Exception.StackTrace", + $"({name2})audienceValidationResult2.Exception.StackTrace", + stackPrefix.Trim(), + localContext); + } + + return context.Merge(localContext); + } + public static bool AreJArraysEqual(object object1, object object2, CompareContext context) { var localContext = new CompareContext(context); diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/AudienceValidationResultTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/AudienceValidationResultTests.cs new file mode 100644 index 0000000000..aa6a45b784 --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/AudienceValidationResultTests.cs @@ -0,0 +1,597 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.TestUtils; +using Microsoft.IdentityModel.Tokens.Json.Tests; +using Xunit; +using System.Collections.Generic; + +namespace Microsoft.IdentityModel.Tokens.Validation.Tests +{ + public class AudienceValidationResultTests + { + [Theory, MemberData(nameof(ValidateAudienceParametersTestCases), DisableDiscoveryEnumeration = true)] + public void ValidateAudienceParameters(AudienceValidationTheoryData theoryData) + { + CompareContext context = TestUtilities.WriteHeader($"{this}.AudienceValidatorResultTests", theoryData); + + AudienceValidationResult audienceValidationResult = Validators.ValidateAudience( + theoryData.Audiences, + theoryData.SecurityToken, + theoryData.ValidationParameters, + new CallContext()); + + if (audienceValidationResult.Exception == null) + theoryData.ExpectedException.ProcessNoException(); + else + theoryData.ExpectedException.ProcessException(audienceValidationResult.Exception, context); + + IdentityComparer.AreAudienceValidationResultsEqual( + audienceValidationResult, + theoryData.AudienceValidationResult, + context); + + TestUtilities.AssertFailIfErrors(context); + } + + public static TheoryData ValidateAudienceParametersTestCases + { + get + { + return new TheoryData + { + new AudienceValidationTheoryData + { + Audiences = new List { "audience1" }, + ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), + TestId = "ValidationParametersNull", + ValidationParameters = null, + AudienceValidationResult = new AudienceValidationResult( + "audience1", + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10000, + LogHelper.MarkAsNonPII("validationParameters")), + typeof(ArgumentNullException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = new List { "" }, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "AudiencesEmptyString", + ValidationParameters = new TokenValidationParameters{ ValidAudience = "audience"}, + AudienceValidationResult = new AudienceValidationResult( + "", + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(""), + LogHelper.MarkAsNonPII("audience"), + LogHelper.MarkAsNonPII("null")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = new List { " " }, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "AudiencesWhiteSpace", + ValidationParameters = new TokenValidationParameters{ ValidAudience = "audience"}, + AudienceValidationResult = new AudienceValidationResult( + " ", + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(" "), + LogHelper.MarkAsNonPII("audience"), + LogHelper.MarkAsNonPII("null")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = null, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10207:"), + TestId = "AudiencesNull", + AudienceValidationResult = new AudienceValidationResult( + "null", + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10207, + null), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = new List{ }, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10206:"), + TestId = "AudiencesEmptyList", + ValidationParameters = new TokenValidationParameters{ ValidAudience = "audience"}, + AudienceValidationResult = new AudienceValidationResult( + "empty", + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10206, + null), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = new List{ }, + TestId = "ValidateAudienceFalseAudiencesEmptyList", + ValidationParameters = new TokenValidationParameters{ ValidateAudience = false }, + AudienceValidationResult = new AudienceValidationResult("empty") + }, + new AudienceValidationTheoryData + { + Audiences = null, + TestId = "ValidateAudienceFalseAudiencesNull", + ValidationParameters = new TokenValidationParameters{ ValidateAudience = false }, + AudienceValidationResult = new AudienceValidationResult("null") + }, + new AudienceValidationTheoryData + { + Audiences = new List { "audience1" }, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10208:"), + TestId = "ValidAudienceEmptyString", + ValidationParameters = new TokenValidationParameters{ ValidAudience = "" }, + AudienceValidationResult = new AudienceValidationResult( + "audience1", + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10208, + null), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = new List { "audience1" }, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10208:"), + TestId = "ValidAudienceWhiteSpace", + ValidationParameters = new TokenValidationParameters{ ValidAudience = " " }, + AudienceValidationResult = new AudienceValidationResult( + "audience1", + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10208, + null), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = new List { "audience1" }, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "ValidAudiencesEmptyString", + ValidationParameters = new TokenValidationParameters{ ValidAudiences = new List{ "" } }, + AudienceValidationResult = new AudienceValidationResult( + "audience1", + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII("audience1"), + LogHelper.MarkAsNonPII("null"), + LogHelper.MarkAsNonPII("")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = new List { "audience1" }, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "ValidAudiencesWhiteSpace", + ValidationParameters = new TokenValidationParameters{ ValidAudiences = new List{ " " } }, + AudienceValidationResult = new AudienceValidationResult( + "audience1", + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII("audience1"), + LogHelper.MarkAsNonPII("null"), + LogHelper.MarkAsNonPII(" ")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = new List { "audience1" }, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10208:"), + TestId = "ValidateAudienceTrueValidAudienceAndValidAudiencesNull", + AudienceValidationResult = new AudienceValidationResult( + "audience1", + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10208, + null), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + } + }; + } + } + + [Theory, MemberData(nameof(ValidateAudienceTheoryData))] + public void ValidateAudience(AudienceValidationTheoryData theoryData) + { + var context = TestUtilities.WriteHeader($"{this}.ValidateAudience", theoryData); + AudienceValidationResult audienceValidationResult = Validators.ValidateAudience( + theoryData.Audiences, + theoryData.SecurityToken, + theoryData.ValidationParameters, + new CallContext()); + + if (audienceValidationResult.Exception != null) + theoryData.ExpectedException.ProcessException(audienceValidationResult.Exception); + else + theoryData.ExpectedException.ProcessNoException(context); + + IdentityComparer.AreAudienceValidationResultsEqual( + audienceValidationResult, + theoryData.AudienceValidationResult, + context); + + TestUtilities.AssertFailIfErrors(context); + } + + public static TheoryData ValidateAudienceTheoryData + { + get + { + var audience1 = "http://audience1.com"; + var audience2 = "http://audience2.com"; + List audiences1 = new List { "", audience1 }; + List audiences1WithSlash = new List { "", audience1 + "/" }; + List audiences1WithTwoSlashes = new List { "", audience1 + "//" }; + List audiences2 = new List { "", audience2 }; + List audiences2WithSlash = new List { "", audience2 + "/" }; + + var commaAudience1 = ", " + audience1; + var commaAudience2 = ", " + audience2; + var audience1Slash = audience1 + "/"; + var audience2Slash = audience2 + "/"; + var commaAudience1Slash = commaAudience1 + "/"; + var commaAudience2Slash = commaAudience2 + "/"; + + return new TheoryData + { + new AudienceValidationTheoryData + { + Audiences = audiences1, + TestId = "SameLengthMatched", + ValidationParameters = new TokenValidationParameters{ ValidAudience = audience1 }, + SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Iss, "Issuer"), + AudienceValidationResult = new AudienceValidationResult(audience1) + }, + new AudienceValidationTheoryData + { + Audiences = audiences1, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "SameLengthNotMatched", + ValidationParameters = new TokenValidationParameters{ ValidAudience = audience2 }, + SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Iss, "Issuer"), + AudienceValidationResult = new AudienceValidationResult( + commaAudience1, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(commaAudience1), + LogHelper.MarkAsNonPII(audience2), + LogHelper.MarkAsNonPII("null")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = audiences1, + TestId = "NoMatchTVPValidateFalse", + ValidationParameters = new TokenValidationParameters{ ValidAudience = audience2, ValidateAudience = false }, + SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Iss, "Issuer"), + AudienceValidationResult = new AudienceValidationResult(commaAudience1) + }, + new AudienceValidationTheoryData + { + Audiences = audiences1, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "AudiencesValidAudienceWithSlashNotMatched", + ValidationParameters = new TokenValidationParameters{ ValidAudience = audience2 + "/" }, + SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Iss, "Issuer"), + AudienceValidationResult = new AudienceValidationResult( + commaAudience1, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(commaAudience1), + LogHelper.MarkAsNonPII(audience2Slash), + LogHelper.MarkAsNonPII("null")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = audiences2WithSlash, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "AudiencesWithSlashValidAudienceSameLengthNotMatched", + ValidationParameters = new TokenValidationParameters{ ValidAudience = audience1 }, + AudienceValidationResult = new AudienceValidationResult( + commaAudience2Slash, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(commaAudience2Slash), + LogHelper.MarkAsNonPII(audience1), + LogHelper.MarkAsNonPII("null")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = audiences1, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "ValidAudienceWithSlashTVPFalse", + ValidationParameters = new TokenValidationParameters{ IgnoreTrailingSlashWhenValidatingAudience = false, ValidAudience = audience1 + "/" }, + AudienceValidationResult = new AudienceValidationResult( + commaAudience1, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(commaAudience1), + LogHelper.MarkAsNonPII(audience1Slash), + LogHelper.MarkAsNonPII("null")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = audiences1, + TestId = "ValidAudienceWithSlashTVPTrue", + ValidationParameters = new TokenValidationParameters{ ValidAudience = audience1 + "/" }, + AudienceValidationResult = new AudienceValidationResult(audience1) + }, + new AudienceValidationTheoryData + { + Audiences = audiences1, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "ValidAudiencesWithSlashTVPFalse", + ValidationParameters = new TokenValidationParameters{ IgnoreTrailingSlashWhenValidatingAudience = false, ValidAudiences = audiences1WithSlash }, + AudienceValidationResult = new AudienceValidationResult( + commaAudience1, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(commaAudience1), + LogHelper.MarkAsNonPII("null"), + LogHelper.MarkAsNonPII(commaAudience1Slash)), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = audiences1, + TestId = "ValidAudiencesWithSlashTVPTrue", + ValidationParameters = new TokenValidationParameters{ ValidAudiences = audiences1WithSlash }, + AudienceValidationResult = new AudienceValidationResult(audience1) + }, + new AudienceValidationTheoryData + { + Audiences = audiences1, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "ValidAudienceWithExtraChar", + ValidationParameters = new TokenValidationParameters{ ValidAudience = audience1 + "A" }, + AudienceValidationResult = new AudienceValidationResult( + commaAudience1, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(commaAudience1), + LogHelper.MarkAsNonPII(audience1 + "A"), + LogHelper.MarkAsNonPII("null")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = audiences1, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "ValidAudienceWithDoubleSlashTVPTrue", + ValidationParameters = new TokenValidationParameters{ ValidAudience = audience1 + "//" }, + AudienceValidationResult = new AudienceValidationResult( + commaAudience1, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(commaAudience1), + LogHelper.MarkAsNonPII(audience1 + "//"), + LogHelper.MarkAsNonPII("null")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = audiences1, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "ValidAudiencesWithDoubleSlashTVPTrue", + ValidationParameters = new TokenValidationParameters{ ValidAudiences = audiences1WithTwoSlashes }, + AudienceValidationResult = new AudienceValidationResult( + commaAudience1, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(commaAudience1), + LogHelper.MarkAsNonPII("null"), + LogHelper.MarkAsNonPII(commaAudience1 + "//")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = audiences1WithSlash, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "TokenAudienceWithSlashTVPFalse", + ValidationParameters = new TokenValidationParameters{ IgnoreTrailingSlashWhenValidatingAudience = false, ValidAudience = audience1 }, + AudienceValidationResult = new AudienceValidationResult( + commaAudience1Slash, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(commaAudience1Slash), + LogHelper.MarkAsNonPII(audience1), + LogHelper.MarkAsNonPII("null")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = audiences1WithSlash, + TestId = "TokenAudienceWithSlashTVPTrue", + ValidationParameters = new TokenValidationParameters{ ValidAudience = audience1 }, + AudienceValidationResult = new AudienceValidationResult(audience1Slash) + }, + new AudienceValidationTheoryData + { + Audiences = audiences2WithSlash, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "TokenAudienceWithSlashNotEqual", + ValidationParameters = new TokenValidationParameters{ ValidAudience = audience1 }, + AudienceValidationResult = new AudienceValidationResult( + commaAudience2Slash, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(commaAudience2Slash), + LogHelper.MarkAsNonPII(audience1), + LogHelper.MarkAsNonPII("null")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = audiences1WithSlash, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "TokenAudiencesWithSlashTVPFalse", + ValidationParameters = new TokenValidationParameters{ IgnoreTrailingSlashWhenValidatingAudience = false, ValidAudience = audience1 }, + AudienceValidationResult = new AudienceValidationResult( + commaAudience1Slash, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(commaAudience1Slash), + LogHelper.MarkAsNonPII(audience1), + LogHelper.MarkAsNonPII("null")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = audiences1WithSlash, + TestId = "TokenAudiencesWithSlashTVPTrue", + ValidationParameters = new TokenValidationParameters{ ValidAudience = audience1 }, + AudienceValidationResult = new AudienceValidationResult(audience1Slash) + }, + new AudienceValidationTheoryData + { + Audiences = audiences1WithSlash, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "TokenAudiencesWithSlashValidAudiencesNotMatchedTVPTrue", + ValidationParameters = new TokenValidationParameters{ ValidAudiences = audiences2 }, + AudienceValidationResult = new AudienceValidationResult( + commaAudience1Slash, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(commaAudience1Slash), + LogHelper.MarkAsNonPII("null"), + LogHelper.MarkAsNonPII(commaAudience2)), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = audiences1WithTwoSlashes, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "TokenAudienceWithTwoSlashesTVPTrue", + ValidationParameters = new TokenValidationParameters{ ValidAudience = audience1 }, + AudienceValidationResult = new AudienceValidationResult( + commaAudience1 + "//", + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(commaAudience1 + "//"), + LogHelper.MarkAsNonPII(audience1), + LogHelper.MarkAsNonPII("null")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + } + }; + } + } + + public class AudienceValidationTheoryData : TheoryDataBase + { + public List Audiences { get; set; } + + internal AudienceValidationResult AudienceValidationResult { get; set; } + + public SecurityToken SecurityToken { get; set; } + + public TokenValidationParameters ValidationParameters { get; set; } = new TokenValidationParameters(); + + internal ValidationFailureType ValidationFailureType { get; set; } + } + + + } +} diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs index c8b5694cc6..51ad101026 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs @@ -32,6 +32,8 @@ public async Task IssuerValidatorAsyncTests(IssuerValidationResultsTheoryData th if (issuerValidationResult.Exception != null) theoryData.ExpectedException.ProcessException(issuerValidationResult.Exception, context); + else + theoryData.ExpectedException.ProcessNoException(); IdentityComparer.AreIssuerValidationResultsEqual( issuerValidationResult, From e79c59e0df4fc347079658fe8c825be729afdfcb Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Wed, 26 Jun 2024 17:28:57 +0100 Subject: [PATCH 17/22] Move validators to partial classes (#2671) * Split remaining validators into partial classes --- .../Validation/Validators.Algorithm.cs | 46 +++ .../Validators.IssuerSecurityKey.cs | 108 ++++++++ .../Validation/Validators.TokenReplay.cs | 82 ++++++ .../Validation/Validators.TokenType.cs | 61 ++++ .../Validators.cs | 262 ------------------ 5 files changed, 297 insertions(+), 262 deletions(-) create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/Validators.Algorithm.cs create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/Validators.IssuerSecurityKey.cs create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/Validators.TokenReplay.cs create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/Validators.TokenType.cs delete mode 100644 src/Microsoft.IdentityModel.Tokens/Validators.cs diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Algorithm.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Algorithm.cs new file mode 100644 index 0000000000..7b03368139 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Algorithm.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Linq; +using Microsoft.IdentityModel.Logging; + +namespace Microsoft.IdentityModel.Tokens +{ + public static partial class Validators + { + /// + /// Validates if a given algorithm for a is valid. + /// + /// The algorithm to be validated. + /// The that signed the . + /// The being validated. + /// required for validation. + public static void ValidateAlgorithm(string algorithm, SecurityKey securityKey, SecurityToken securityToken, TokenValidationParameters validationParameters) + { + if (validationParameters == null) + throw LogHelper.LogArgumentNullException(nameof(validationParameters)); + + if (validationParameters.AlgorithmValidator != null) + { + if (!validationParameters.AlgorithmValidator(algorithm, securityKey, securityToken, validationParameters)) + { + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidAlgorithmException(LogHelper.FormatInvariant(LogMessages.IDX10697, LogHelper.MarkAsNonPII(algorithm), securityKey)) + { + InvalidAlgorithm = algorithm, + }); + } + + return; + } + + if (validationParameters.ValidAlgorithms != null && validationParameters.ValidAlgorithms.Any() && !validationParameters.ValidAlgorithms.Contains(algorithm, StringComparer.Ordinal)) + { + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidAlgorithmException(LogHelper.FormatInvariant(LogMessages.IDX10696, LogHelper.MarkAsNonPII(algorithm))) + { + InvalidAlgorithm = algorithm, + }); + } + } + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.IssuerSecurityKey.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.IssuerSecurityKey.cs new file mode 100644 index 0000000000..1a50cb9d45 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.IssuerSecurityKey.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Security.Cryptography.X509Certificates; +using Microsoft.IdentityModel.Abstractions; +using Microsoft.IdentityModel.Logging; + +namespace Microsoft.IdentityModel.Tokens +{ + public static partial class Validators + { + /// + /// Validates the that signed a . + /// + /// The that signed the . + /// The being validated. + /// required for validation. + /// if 'securityKey' is null and ValidateIssuerSigningKey is true. + /// if 'securityToken' is null and ValidateIssuerSigningKey is true. + /// if 'validationParameters' is null. + public static void ValidateIssuerSecurityKey(SecurityKey securityKey, SecurityToken securityToken, TokenValidationParameters validationParameters) + { + ValidateIssuerSecurityKey(securityKey, securityToken, validationParameters, null); + } + + /// + /// Validates the that signed a . + /// + /// The that signed the . + /// The being validated. + /// required for validation. + /// The required for issuer and signing key validation. + /// if 'securityKey' is null and ValidateIssuerSigningKey is true. + /// if 'securityToken' is null and ValidateIssuerSigningKey is true. + /// if 'validationParameters' is null. + internal static void ValidateIssuerSecurityKey(SecurityKey securityKey, SecurityToken securityToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) + { + if (validationParameters == null) + throw LogHelper.LogArgumentNullException(nameof(validationParameters)); + + if (validationParameters.IssuerSigningKeyValidatorUsingConfiguration != null) + { + if (!validationParameters.IssuerSigningKeyValidatorUsingConfiguration(securityKey, securityToken, validationParameters, configuration)) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSigningKeyException(LogHelper.FormatInvariant(LogMessages.IDX10232, securityKey)) { SigningKey = securityKey }); + + return; + } + + if (validationParameters.IssuerSigningKeyValidator != null) + { + if (!validationParameters.IssuerSigningKeyValidator(securityKey, securityToken, validationParameters)) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSigningKeyException(LogHelper.FormatInvariant(LogMessages.IDX10232, securityKey)) { SigningKey = securityKey }); + + return; + } + + if (!validationParameters.ValidateIssuerSigningKey) + { + LogHelper.LogVerbose(LogMessages.IDX10237); + return; + } + + if (!validationParameters.RequireSignedTokens && securityKey == null) + { + LogHelper.LogInformation(LogMessages.IDX10252); + return; + } + else if (securityKey == null) + { + throw LogHelper.LogExceptionMessage(new ArgumentNullException(nameof(securityKey), LogMessages.IDX10253)); + } + + if (securityToken == null) + throw LogHelper.LogArgumentNullException(nameof(securityToken)); + + ValidateIssuerSigningKeyLifeTime(securityKey, validationParameters); + } + + /// + /// Given a signing key, when it's derived from a certificate, validates that the certificate is already active and non-expired + /// + /// The that signed the . + /// The that are used to validate the token. + internal static void ValidateIssuerSigningKeyLifeTime(SecurityKey securityKey, TokenValidationParameters validationParameters) + { + X509SecurityKey x509SecurityKey = securityKey as X509SecurityKey; + if (x509SecurityKey?.Certificate is X509Certificate2 cert) + { + DateTime utcNow = DateTime.UtcNow; + var notBeforeUtc = cert.NotBefore.ToUniversalTime(); + var notAfterUtc = cert.NotAfter.ToUniversalTime(); + + if (notBeforeUtc > DateTimeUtil.Add(utcNow, validationParameters.ClockSkew)) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSigningKeyException(LogHelper.FormatInvariant(LogMessages.IDX10248, LogHelper.MarkAsNonPII(notBeforeUtc), LogHelper.MarkAsNonPII(utcNow)))); + + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX10250, LogHelper.MarkAsNonPII(notBeforeUtc), LogHelper.MarkAsNonPII(utcNow)); + + if (notAfterUtc < DateTimeUtil.Add(utcNow, validationParameters.ClockSkew.Negate())) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSigningKeyException(LogHelper.FormatInvariant(LogMessages.IDX10249, LogHelper.MarkAsNonPII(notAfterUtc), LogHelper.MarkAsNonPII(utcNow)))); + + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX10251, LogHelper.MarkAsNonPII(notAfterUtc), LogHelper.MarkAsNonPII(utcNow)); + } + } + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.TokenReplay.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.TokenReplay.cs new file mode 100644 index 0000000000..82d761c314 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.TokenReplay.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using Microsoft.IdentityModel.Abstractions; +using Microsoft.IdentityModel.Logging; + +namespace Microsoft.IdentityModel.Tokens +{ + public static partial class Validators + { + /// + /// Validates if a token has been replayed. + /// + /// When does the security token expire. + /// The being validated. + /// required for validation. + /// If 'securityToken' is null or whitespace. + /// If 'validationParameters' is null or whitespace. + /// If is not null and expirationTime.HasValue is false. When a TokenReplayCache is set, tokens require an expiration time. + /// If the 'securityToken' is found in the cache. + /// If the 'securityToken' could not be added to the . + public static void ValidateTokenReplay(DateTime? expirationTime, string securityToken, TokenValidationParameters validationParameters) + { + if (string.IsNullOrWhiteSpace(securityToken)) + throw LogHelper.LogArgumentNullException(nameof(securityToken)); + + if (validationParameters == null) + throw LogHelper.LogArgumentNullException(nameof(validationParameters)); + + if (validationParameters.TokenReplayValidator != null) + { + if (!validationParameters.TokenReplayValidator(expirationTime, securityToken, validationParameters)) + throw LogHelper.LogExceptionMessage(new SecurityTokenReplayDetectedException( + LogHelper.FormatInvariant( + LogMessages.IDX10228, + LogHelper.MarkAsUnsafeSecurityArtifact(securityToken, t => t.ToString())))); + return; + } + + if (!validationParameters.ValidateTokenReplay) + { + LogHelper.LogVerbose(LogMessages.IDX10246); + return; + } + + // check if token if replay cache is set, then there must be an expiration time. + if (validationParameters.TokenReplayCache != null) + { + if (!expirationTime.HasValue) + throw LogHelper.LogExceptionMessage(new SecurityTokenNoExpirationException(LogHelper.FormatInvariant(LogMessages.IDX10227, securityToken))); + + if (validationParameters.TokenReplayCache.TryFind(securityToken)) + throw LogHelper.LogExceptionMessage(new SecurityTokenReplayDetectedException(LogHelper.FormatInvariant(LogMessages.IDX10228, securityToken))); + + if (!validationParameters.TokenReplayCache.TryAdd(securityToken, expirationTime.Value)) + throw LogHelper.LogExceptionMessage(new SecurityTokenReplayAddFailedException(LogHelper.FormatInvariant(LogMessages.IDX10229, securityToken))); + } + + // if it reaches here, that means no token replay is detected. + LogHelper.LogInformation(LogMessages.IDX10240); + } + + /// + /// Validates if a token has been replayed. + /// + /// The being validated. + /// When does the security token expire. + /// required for validation. + /// If 'securityToken' is null or whitespace. + /// If 'validationParameters' is null or whitespace. + /// If is not null and expirationTime.HasValue is false. When a TokenReplayCache is set, tokens require an expiration time. + /// If the 'securityToken' is found in the cache. + /// If the 'securityToken' could not be added to the . + public static void ValidateTokenReplay(string securityToken, DateTime? expirationTime, TokenValidationParameters validationParameters) + { + ValidateTokenReplay(expirationTime, securityToken, validationParameters); + } + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.TokenType.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.TokenType.cs new file mode 100644 index 0000000000..6fd744fd9d --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.TokenType.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Linq; +using Microsoft.IdentityModel.Abstractions; +using Microsoft.IdentityModel.Logging; + +namespace Microsoft.IdentityModel.Tokens +{ + public static partial class Validators + { + /// + /// Validates the type of the token. + /// + /// The token type or null if it couldn't be resolved (e.g from the 'typ' header for a JWT). + /// The that is being validated. + /// required for validation. + /// If is null. + /// If is null. + /// If is null or whitespace and is not null. + /// If failed to match . + /// An EXACT match is required. (case sensitive) is used for comparing against . + /// The actual token type, that may be the same as or a different value if the token type was resolved from a different location. + public static string ValidateTokenType(string type, SecurityToken securityToken, TokenValidationParameters validationParameters) + { + if (securityToken == null) + throw new ArgumentNullException(nameof(securityToken)); + + if (validationParameters == null) + throw LogHelper.LogArgumentNullException(nameof(validationParameters)); + + if (validationParameters.TypeValidator == null && (validationParameters.ValidTypes == null || !validationParameters.ValidTypes.Any())) + { + LogHelper.LogVerbose(LogMessages.IDX10255); + return type; + } + + if (validationParameters.TypeValidator != null) + return validationParameters.TypeValidator(type, securityToken, validationParameters); + + // Note: don't throw an exception for a null or empty token type when a user-defined delegate is set + // to allow it to extract the actual token type from a different location (e.g from the claims). + if (string.IsNullOrEmpty(type)) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidTypeException(LogMessages.IDX10256) { InvalidType = null }); + + if (!validationParameters.ValidTypes.Contains(type, StringComparer.Ordinal)) + { + throw LogHelper.LogExceptionMessage( + new SecurityTokenInvalidTypeException(LogHelper.FormatInvariant(LogMessages.IDX10257, LogHelper.MarkAsNonPII(type), Utility.SerializeAsSingleCommaDelimitedString(validationParameters.ValidTypes))) + { InvalidType = type }); + } + + // if it reaches here, token type was succcessfully validated. + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX10258, LogHelper.MarkAsNonPII(type)); + + return type; + } + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/Validators.cs b/src/Microsoft.IdentityModel.Tokens/Validators.cs deleted file mode 100644 index 000ba4c617..0000000000 --- a/src/Microsoft.IdentityModel.Tokens/Validators.cs +++ /dev/null @@ -1,262 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Linq; -using System.Security.Cryptography.X509Certificates; -using Microsoft.IdentityModel.Abstractions; -using Microsoft.IdentityModel.Logging; - -namespace Microsoft.IdentityModel.Tokens -{ - /// - /// AudienceValidator - /// - public static partial class Validators - { - /// - /// Validates if a given algorithm for a is valid. - /// - /// The algorithm to be validated. - /// The that signed the . - /// The being validated. - /// required for validation. - public static void ValidateAlgorithm(string algorithm, SecurityKey securityKey, SecurityToken securityToken, TokenValidationParameters validationParameters) - { - if (validationParameters == null) - throw LogHelper.LogArgumentNullException(nameof(validationParameters)); - - if (validationParameters.AlgorithmValidator != null) - { - if (!validationParameters.AlgorithmValidator(algorithm, securityKey, securityToken, validationParameters)) - { - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidAlgorithmException(LogHelper.FormatInvariant(LogMessages.IDX10697, LogHelper.MarkAsNonPII(algorithm), securityKey)) - { - InvalidAlgorithm = algorithm, - }); - } - - return; - } - - if (validationParameters.ValidAlgorithms != null && validationParameters.ValidAlgorithms.Any() && !validationParameters.ValidAlgorithms.Contains(algorithm, StringComparer.Ordinal)) - { - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidAlgorithmException(LogHelper.FormatInvariant(LogMessages.IDX10696, LogHelper.MarkAsNonPII(algorithm))) - { - InvalidAlgorithm = algorithm, - }); - } - } - - /// - /// Validates the that signed a . - /// - /// The that signed the . - /// The being validated. - /// required for validation. - /// if 'securityKey' is null and ValidateIssuerSigningKey is true. - /// if 'securityToken' is null and ValidateIssuerSigningKey is true. - /// if 'validationParameters' is null. - public static void ValidateIssuerSecurityKey(SecurityKey securityKey, SecurityToken securityToken, TokenValidationParameters validationParameters) - { - ValidateIssuerSecurityKey(securityKey, securityToken, validationParameters, null); - } - - /// - /// Validates the that signed a . - /// - /// The that signed the . - /// The being validated. - /// required for validation. - /// The required for issuer and signing key validation. - /// if 'securityKey' is null and ValidateIssuerSigningKey is true. - /// if 'securityToken' is null and ValidateIssuerSigningKey is true. - /// if 'validationParameters' is null. - internal static void ValidateIssuerSecurityKey(SecurityKey securityKey, SecurityToken securityToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) - { - if (validationParameters == null) - throw LogHelper.LogArgumentNullException(nameof(validationParameters)); - - if (validationParameters.IssuerSigningKeyValidatorUsingConfiguration != null) - { - if (!validationParameters.IssuerSigningKeyValidatorUsingConfiguration(securityKey, securityToken, validationParameters, configuration)) - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSigningKeyException(LogHelper.FormatInvariant(LogMessages.IDX10232, securityKey)) { SigningKey = securityKey }); - - return; - } - - if (validationParameters.IssuerSigningKeyValidator != null) - { - if (!validationParameters.IssuerSigningKeyValidator(securityKey, securityToken, validationParameters)) - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSigningKeyException(LogHelper.FormatInvariant(LogMessages.IDX10232, securityKey)) { SigningKey = securityKey }); - - return; - } - - if (!validationParameters.ValidateIssuerSigningKey) - { - LogHelper.LogVerbose(LogMessages.IDX10237); - return; - } - - if (!validationParameters.RequireSignedTokens && securityKey == null) - { - LogHelper.LogInformation(LogMessages.IDX10252); - return; - } - else if (securityKey == null) - { - throw LogHelper.LogExceptionMessage(new ArgumentNullException(nameof(securityKey), LogMessages.IDX10253)); - } - - if (securityToken == null) - throw LogHelper.LogArgumentNullException(nameof(securityToken)); - - ValidateIssuerSigningKeyLifeTime(securityKey, validationParameters); - } - - /// - /// Given a signing key, when it's derived from a certificate, validates that the certificate is already active and non-expired - /// - /// The that signed the . - /// The that are used to validate the token. - internal static void ValidateIssuerSigningKeyLifeTime(SecurityKey securityKey, TokenValidationParameters validationParameters) - { - X509SecurityKey x509SecurityKey = securityKey as X509SecurityKey; - if (x509SecurityKey?.Certificate is X509Certificate2 cert) - { - DateTime utcNow = DateTime.UtcNow; - var notBeforeUtc = cert.NotBefore.ToUniversalTime(); - var notAfterUtc = cert.NotAfter.ToUniversalTime(); - - if (notBeforeUtc > DateTimeUtil.Add(utcNow, validationParameters.ClockSkew)) - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSigningKeyException(LogHelper.FormatInvariant(LogMessages.IDX10248, LogHelper.MarkAsNonPII(notBeforeUtc), LogHelper.MarkAsNonPII(utcNow)))); - - if (LogHelper.IsEnabled(EventLogLevel.Informational)) - LogHelper.LogInformation(LogMessages.IDX10250, LogHelper.MarkAsNonPII(notBeforeUtc), LogHelper.MarkAsNonPII(utcNow)); - - if (notAfterUtc < DateTimeUtil.Add(utcNow, validationParameters.ClockSkew.Negate())) - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSigningKeyException(LogHelper.FormatInvariant(LogMessages.IDX10249, LogHelper.MarkAsNonPII(notAfterUtc), LogHelper.MarkAsNonPII(utcNow)))); - - if (LogHelper.IsEnabled(EventLogLevel.Informational)) - LogHelper.LogInformation(LogMessages.IDX10251, LogHelper.MarkAsNonPII(notAfterUtc), LogHelper.MarkAsNonPII(utcNow)); - } - } - - /// - /// Validates if a token has been replayed. - /// - /// When does the security token expire. - /// The being validated. - /// required for validation. - /// If 'securityToken' is null or whitespace. - /// If 'validationParameters' is null or whitespace. - /// If is not null and expirationTime.HasValue is false. When a TokenReplayCache is set, tokens require an expiration time. - /// If the 'securityToken' is found in the cache. - /// If the 'securityToken' could not be added to the . - public static void ValidateTokenReplay(DateTime? expirationTime, string securityToken, TokenValidationParameters validationParameters) - { - if (string.IsNullOrWhiteSpace(securityToken)) - throw LogHelper.LogArgumentNullException(nameof(securityToken)); - - if (validationParameters == null) - throw LogHelper.LogArgumentNullException(nameof(validationParameters)); - - if (validationParameters.TokenReplayValidator != null) - { - if (!validationParameters.TokenReplayValidator(expirationTime, securityToken, validationParameters)) - throw LogHelper.LogExceptionMessage(new SecurityTokenReplayDetectedException( - LogHelper.FormatInvariant( - LogMessages.IDX10228, - LogHelper.MarkAsUnsafeSecurityArtifact(securityToken, t => t.ToString())))); - return; - } - - if (!validationParameters.ValidateTokenReplay) - { - LogHelper.LogVerbose(LogMessages.IDX10246); - return; - } - - // check if token if replay cache is set, then there must be an expiration time. - if (validationParameters.TokenReplayCache != null) - { - if (!expirationTime.HasValue) - throw LogHelper.LogExceptionMessage(new SecurityTokenNoExpirationException(LogHelper.FormatInvariant(LogMessages.IDX10227, securityToken))); - - if (validationParameters.TokenReplayCache.TryFind(securityToken)) - throw LogHelper.LogExceptionMessage(new SecurityTokenReplayDetectedException(LogHelper.FormatInvariant(LogMessages.IDX10228, securityToken))); - - if (!validationParameters.TokenReplayCache.TryAdd(securityToken, expirationTime.Value)) - throw LogHelper.LogExceptionMessage(new SecurityTokenReplayAddFailedException(LogHelper.FormatInvariant(LogMessages.IDX10229, securityToken))); - } - - // if it reaches here, that means no token replay is detected. - LogHelper.LogInformation(LogMessages.IDX10240); - } - - /// - /// Validates if a token has been replayed. - /// - /// The being validated. - /// When does the security token expire. - /// required for validation. - /// If 'securityToken' is null or whitespace. - /// If 'validationParameters' is null or whitespace. - /// If is not null and expirationTime.HasValue is false. When a TokenReplayCache is set, tokens require an expiration time. - /// If the 'securityToken' is found in the cache. - /// If the 'securityToken' could not be added to the . - public static void ValidateTokenReplay(string securityToken, DateTime? expirationTime, TokenValidationParameters validationParameters) - { - ValidateTokenReplay(expirationTime, securityToken, validationParameters); - } - - /// - /// Validates the type of the token. - /// - /// The token type or null if it couldn't be resolved (e.g from the 'typ' header for a JWT). - /// The that is being validated. - /// required for validation. - /// If is null. - /// If is null. - /// If is null or whitespace and is not null. - /// If failed to match . - /// An EXACT match is required. (case sensitive) is used for comparing against . - /// The actual token type, that may be the same as or a different value if the token type was resolved from a different location. - public static string ValidateTokenType(string type, SecurityToken securityToken, TokenValidationParameters validationParameters) - { - if (securityToken == null) - throw new ArgumentNullException(nameof(securityToken)); - - if (validationParameters == null) - throw LogHelper.LogArgumentNullException(nameof(validationParameters)); - - if (validationParameters.TypeValidator == null && (validationParameters.ValidTypes == null || !validationParameters.ValidTypes.Any())) - { - LogHelper.LogVerbose(LogMessages.IDX10255); - return type; - } - - if (validationParameters.TypeValidator != null) - return validationParameters.TypeValidator(type, securityToken, validationParameters); - - // Note: don't throw an exception for a null or empty token type when a user-defined delegate is set - // to allow it to extract the actual token type from a different location (e.g from the claims). - if (string.IsNullOrEmpty(type)) - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidTypeException(LogMessages.IDX10256) { InvalidType = null }); - - if (!validationParameters.ValidTypes.Contains(type, StringComparer.Ordinal)) - { - throw LogHelper.LogExceptionMessage( - new SecurityTokenInvalidTypeException(LogHelper.FormatInvariant(LogMessages.IDX10257, LogHelper.MarkAsNonPII(type), Utility.SerializeAsSingleCommaDelimitedString(validationParameters.ValidTypes))) - { InvalidType = type }); - } - - // if it reaches here, token type was succcessfully validated. - if (LogHelper.IsEnabled(EventLogLevel.Informational)) - LogHelper.LogInformation(LogMessages.IDX10258, LogHelper.MarkAsNonPII(type)); - - return type; - } - } -} From 279ae11c55b807ab5dbb62205d4d7904d54c95b9 Mon Sep 17 00:00:00 2001 From: joegoldman2 <147369450+joegoldman2@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:58:51 +0000 Subject: [PATCH 18/22] Add a constant for the create prompt in OpenIdConnectPrompt class (#2658) Co-authored-by: joegoldman2 <147369450+joegoldman@users.noreply.github.com> --- .../OpenIdConnectPrompt.cs | 6 +++++- .../OpenIdConnectRequestType.cs | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdConnectPrompt.cs b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdConnectPrompt.cs index 537053d1e0..8d57176929 100644 --- a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdConnectPrompt.cs +++ b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdConnectPrompt.cs @@ -13,6 +13,11 @@ public static class OpenIdConnectPrompt /// public const string None = "none"; + /// + /// Indicates 'create' prompt type see: https://openid.net/specs/openid-connect-prompt-create-1_0.html. + /// + public const string Create = "create"; + /// /// Indicates 'login' prompt type see: http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest. /// @@ -29,4 +34,3 @@ public static class OpenIdConnectPrompt public const string SelectAccount = "select_account"; } } - diff --git a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdConnectRequestType.cs b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdConnectRequestType.cs index 3da8bda4ab..e212fb4909 100644 --- a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdConnectRequestType.cs +++ b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdConnectRequestType.cs @@ -17,7 +17,7 @@ public enum OpenIdConnectRequestType Authentication, /// - /// Indicates a Logout Request see:http://openid.net/specs/openid-connect-frontchannel-1_0.html#RPLogout. + /// Indicates a Logout Request see: http://openid.net/specs/openid-connect-frontchannel-1_0.html#RPLogout. /// Logout, From bd81097fef0596b07fc5ee31ff459a4f90137863 Mon Sep 17 00:00:00 2001 From: JoshLozensky <103777376+JoshLozensky@users.noreply.github.com> Date: Fri, 28 Jun 2024 14:35:24 -0700 Subject: [PATCH 19/22] Remove if that targets all supported .Net versions (#2673) --- src/Microsoft.IdentityModel.Tokens/RsaSecurityKey.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Microsoft.IdentityModel.Tokens/RsaSecurityKey.cs b/src/Microsoft.IdentityModel.Tokens/RsaSecurityKey.cs index 7b3fa3ef05..11fe431374 100644 --- a/src/Microsoft.IdentityModel.Tokens/RsaSecurityKey.cs +++ b/src/Microsoft.IdentityModel.Tokens/RsaSecurityKey.cs @@ -76,14 +76,7 @@ public override bool HasPrivateKey { // imitate signing byte[] hash = new byte[20]; -#if NET462 || NET472 || NETSTANDARD2_0 || NET6_0_OR_GREATER Rsa.SignData(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); -#else - if (Rsa is RSACryptoServiceProvider rsaCryptoServiceProvider) - rsaCryptoServiceProvider.SignData(hash, SecurityAlgorithms.Sha256); - else - Rsa.DecryptValue(hash); -#endif _hasPrivateKey = true; } catch (CryptographicException) From cb39931e2bc16d66c197b413faaf7f2fe219169a Mon Sep 17 00:00:00 2001 From: JoshLozensky <103777376+JoshLozensky@users.noreply.github.com> Date: Fri, 28 Jun 2024 15:38:36 -0700 Subject: [PATCH 20/22] added the intent of the variables to their name (#2674) --- .../JsonWebTokenHandler.CreateToken.cs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index 0ceee47460..4b31b531c8 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -697,15 +697,15 @@ internal static void WriteJwsPayload( bool setDefaultTimesOnTokenCreation, int tokenLifetimeInMinutes) { - bool audienceChecked = false; + bool descriptorClaimsAudienceChecked = false; bool audienceSet = false; - bool issuerChecked = false; + bool descriptorClaimsIssuerChecked = false; bool issuerSet = false; - bool expChecked = false; + bool descriptorClaimsExpChecked = false; bool expSet = false; - bool iatChecked = false; + bool descriptorClaimsIatChecked = false; bool iatSet = false; - bool nbfChecked = false; + bool descriptorClaimsNbfChecked = false; bool nbfSet = false; writer.WriteStartObject(); @@ -762,9 +762,9 @@ internal static void WriteJwsPayload( { foreach (KeyValuePair kvp in tokenDescriptor.Claims) { - if (!audienceChecked && kvp.Key.Equals(JwtRegisteredClaimNames.Aud, StringComparison.Ordinal)) + if (!descriptorClaimsAudienceChecked && kvp.Key.Equals(JwtRegisteredClaimNames.Aud, StringComparison.Ordinal)) { - audienceChecked = true; + descriptorClaimsAudienceChecked = true; if (audienceSet) { if (LogHelper.IsEnabled(EventLogLevel.Informational)) @@ -784,9 +784,9 @@ internal static void WriteJwsPayload( audienceSet = true; } - if (!issuerChecked && kvp.Key.Equals(JwtRegisteredClaimNames.Iss, StringComparison.Ordinal)) + if (!descriptorClaimsIssuerChecked && kvp.Key.Equals(JwtRegisteredClaimNames.Iss, StringComparison.Ordinal)) { - issuerChecked = true; + descriptorClaimsIssuerChecked = true; if (issuerSet) { if (LogHelper.IsEnabled(EventLogLevel.Informational)) @@ -798,9 +798,9 @@ internal static void WriteJwsPayload( issuerSet = true; } - if (!expChecked && kvp.Key.Equals(JwtRegisteredClaimNames.Exp, StringComparison.Ordinal)) + if (!descriptorClaimsExpChecked && kvp.Key.Equals(JwtRegisteredClaimNames.Exp, StringComparison.Ordinal)) { - expChecked = true; + descriptorClaimsExpChecked = true; if (expSet) { if (LogHelper.IsEnabled(EventLogLevel.Informational)) @@ -812,9 +812,9 @@ internal static void WriteJwsPayload( expSet = true; } - if (!iatChecked && kvp.Key.Equals(JwtRegisteredClaimNames.Iat, StringComparison.Ordinal)) + if (!descriptorClaimsIatChecked && kvp.Key.Equals(JwtRegisteredClaimNames.Iat, StringComparison.Ordinal)) { - iatChecked = true; + descriptorClaimsIatChecked = true; if (iatSet) { if (LogHelper.IsEnabled(EventLogLevel.Informational)) @@ -826,9 +826,9 @@ internal static void WriteJwsPayload( iatSet = true; } - if (!nbfChecked && kvp.Key.Equals(JwtRegisteredClaimNames.Nbf, StringComparison.Ordinal)) + if (!descriptorClaimsNbfChecked && kvp.Key.Equals(JwtRegisteredClaimNames.Nbf, StringComparison.Ordinal)) { - nbfChecked = true; + descriptorClaimsNbfChecked = true; if (nbfSet) { if (LogHelper.IsEnabled(EventLogLevel.Informational)) From 24411fd4c885f7dcf84ef580d7e518fa1a659dff Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Sat, 29 Jun 2024 01:58:23 +0100 Subject: [PATCH 21/22] Lifetime validation: Remove exceptions (#2669) * Added LifetimeValidationResult. Added nullability annotations to ValidationResult * Added LifetimeValidationResult comparer * Added ValidateLifetime new version returning LifetimeValidationResult and removing all exception throwing. Updated documentation to disambiguate * Added ValidateLifetime tests --- .../Saml/SamlSecurityTokenHandler.cs | 2 +- .../Saml2/Saml2SecurityTokenHandler.cs | 2 +- .../Validation/LifetimeValidationResult.cs | 78 +++++ .../Validation/ValidationFailureType.cs | 6 + .../Validation/ValidationResult.cs | 8 +- .../Validation/Validators.Lifetime.cs | 157 +++++++++ .../JwtSecurityTokenHandler.cs | 2 +- .../IdentityComparer.cs | 69 ++++ .../LifetimeValidationResultTests.cs | 303 ++++++++++++++++++ 9 files changed, 621 insertions(+), 6 deletions(-) create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/LifetimeValidationResult.cs create mode 100644 test/Microsoft.IdentityModel.Tokens.Tests/Validation/LifetimeValidationResultTests.cs diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs index 5ec1f7b90c..e878f9ce19 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs @@ -992,7 +992,7 @@ protected virtual void ValidateIssuerSecurityKey(SecurityKey securityKey, Securi /// The value found in the . /// The being validated. /// required for validation. - /// for additional details. + /// for additional details. protected virtual void ValidateLifetime(DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters) { Validators.ValidateLifetime(notBefore, expires, securityToken, validationParameters); diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs index 171b0a7a5f..5c8eaecf2b 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs @@ -335,7 +335,7 @@ protected virtual void ValidateIssuerSecurityKey(SecurityKey key, Saml2SecurityT /// The value found in the . /// The being validated. /// required for validation. - /// for additional details. + /// for additional details. protected virtual void ValidateLifetime(DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters) { Validators.ValidateLifetime(notBefore, expires, securityToken, validationParameters); diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/LifetimeValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/LifetimeValidationResult.cs new file mode 100644 index 0000000000..f40608a574 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/LifetimeValidationResult.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +#nullable enable +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// Contains the result of validating the lifetime of a . + /// The contains a collection of for each step in the token validation. + /// + internal class LifetimeValidationResult : ValidationResult + { + private Exception? _exception; + + /// + /// Creates an instance of + /// + /// is the date from which the token that was validated successfully is valid. + /// is the expiration date for the token that was validated successfully. + public LifetimeValidationResult(DateTime? notBefore, DateTime? expires) + : base(ValidationFailureType.ValidationSucceeded) + { + NotBefore = notBefore; + Expires = expires; + IsValid = true; + } + + /// + /// Creates an instance of + /// + /// is the date from which the token is valid. + /// is the expiration date for the token. + /// is the that occurred during validation. + /// is the that occurred during validation. + public LifetimeValidationResult(DateTime? notBefore, DateTime? expires, ValidationFailureType validationFailure, ExceptionDetail exceptionDetail) + : base(validationFailure, exceptionDetail) + { + NotBefore = notBefore; + Expires = expires; + IsValid = false; + } + + /// + /// Gets the that occurred during validation. + /// + public override Exception? Exception + { + get + { + if (_exception != null || ExceptionDetail == null) + return _exception; + + HasValidOrExceptionWasRead = true; + _exception = ExceptionDetail.GetException(); + if (_exception is SecurityTokenInvalidLifetimeException securityTokenInvalidLifetimeException) + { + securityTokenInvalidLifetimeException.NotBefore = NotBefore; + securityTokenInvalidLifetimeException.Expires = Expires; + securityTokenInvalidLifetimeException.Source = "Microsoft.IdentityModel.Tokens"; + } + + return _exception; + } + } + + /// + /// Gets the date from which the token is valid. + /// + public DateTime? NotBefore { get; } + + /// + /// Gets the expiration date for the token. + /// + public DateTime? Expires { get; } + } +} +#nullable restore diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs index 4c561994fb..3c4a2ec3b8 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs @@ -39,6 +39,12 @@ private class IssuerValidationFailure : ValidationFailureType { internal IssuerV public static readonly ValidationFailureType AudienceValidationFailed = new AudienceValidationFailure("AudienceValidationFailed"); private class AudienceValidationFailure : ValidationFailureType { internal AudienceValidationFailure(string name) : base(name) { } } + /// + /// Defines a type that represents that lifetime validation failed. + /// + public static readonly ValidationFailureType LifetimeValidationFailed = new LifetimeValidationFailure("LifetimeValidationFailure"); + private class LifetimeValidationFailure : ValidationFailureType { internal LifetimeValidationFailure(string name) : base(name) { } } + /// /// Defines a type that represents that no evaluation has taken place. /// diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationResult.cs index f4d8ce0dc0..fe684d3b34 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationResult.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationResult.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics; +#nullable enable namespace Microsoft.IdentityModel.Tokens { /// @@ -49,18 +50,18 @@ protected ValidationResult(ValidationFailureType validationFailureType, Exceptio /// public void AddStackFrame(StackFrame stackFrame) { - ExceptionDetail.StackFrames.Add(stackFrame); + ExceptionDetail?.StackFrames.Add(stackFrame); } /// /// Gets the that occurred during validation. /// - public abstract Exception Exception { get; } + public abstract Exception? Exception { get; } /// /// Gets the that occurred during validation. /// - public ExceptionDetail ExceptionDetail { get; } + public ExceptionDetail? ExceptionDetail { get; } /// /// True if the token was successfully validated, false otherwise. @@ -108,3 +109,4 @@ public ValidationFailureType ValidationFailureType } = ValidationFailureType.ValidationNotEvaluated; } } +#nullable disable diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Lifetime.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Lifetime.cs index 77c553c4db..21b33a55f7 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Lifetime.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Lifetime.cs @@ -2,10 +2,30 @@ // Licensed under the MIT License. using System; +using System.Diagnostics; +using Microsoft.IdentityModel.Abstractions; using Microsoft.IdentityModel.Logging; +#nullable enable namespace Microsoft.IdentityModel.Tokens { + /// + /// Definition for delegate that will validate the lifetime of a . + /// + /// The 'notBefore' time found in the . + /// The 'expiration' time found in the . + /// The that is being validated. + /// required for validation. + /// + /// A that contains the results of validating the issuer. + /// This delegate is not expected to throw. + internal delegate LifetimeValidationResult LifetimeValidationDelegate( + DateTime? notBefore, + DateTime? expires, + SecurityToken? securityToken, + TokenValidationParameters validationParameters, + CallContext callContext); + /// /// IssuerValidation /// @@ -46,5 +66,142 @@ public static void ValidateLifetime(DateTime? notBefore, DateTime? expires, Secu ValidatorUtilities.ValidateLifetime(notBefore, expires, securityToken, validationParameters); } + + /// + /// Validates the lifetime of a . + /// + /// The 'notBefore' time found in the . + /// The 'expiration' time found in the . + /// The being validated. + /// required for validation. + /// + /// A indicating whether validation was successful, and providing a if it was not. + /// If 'validationParameters' is null. + /// If 'expires.HasValue' is false and is true. + /// If 'notBefore' is > 'expires'. + /// If 'notBefore' is > DateTime.UtcNow. + /// If 'expires' is < DateTime.UtcNow. + /// All time comparisons apply . + /// Exceptions are not thrown, but embedded in . +#pragma warning disable CA1801 // TODO: remove pragma disable once callContext is used for logging + internal static LifetimeValidationResult ValidateLifetime(DateTime? notBefore, DateTime? expires, SecurityToken? securityToken, TokenValidationParameters validationParameters, CallContext callContext) +#pragma warning restore CA1801 + { + if (validationParameters == null) + return new LifetimeValidationResult( + notBefore, + expires, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10000, + LogHelper.MarkAsNonPII(nameof(validationParameters))), + typeof(ArgumentNullException), + new StackFrame(true))); + + if (validationParameters.LifetimeValidator != null) + return ValidateLifetimeUsingDelegate(notBefore, expires, securityToken, validationParameters); + + if (!validationParameters.ValidateLifetime) + { + LogHelper.LogInformation(LogMessages.IDX10238); + return new LifetimeValidationResult(notBefore, expires); + } + + if (!expires.HasValue && validationParameters.RequireExpirationTime) + return new LifetimeValidationResult( + notBefore, + expires, + ValidationFailureType.LifetimeValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10225, + LogHelper.MarkAsNonPII(securityToken == null ? "null" : securityToken.GetType().ToString())), + typeof(SecurityTokenNoExpirationException), + new StackFrame(true))); + + if (notBefore.HasValue && expires.HasValue && (notBefore.Value > expires.Value)) + return new LifetimeValidationResult( + notBefore, + expires, + ValidationFailureType.LifetimeValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10224, + LogHelper.MarkAsNonPII(notBefore.Value), + LogHelper.MarkAsNonPII(expires.Value)), + typeof(SecurityTokenInvalidLifetimeException), + new StackFrame(true))); + + DateTime utcNow = DateTime.UtcNow; + if (notBefore.HasValue && (notBefore.Value > DateTimeUtil.Add(utcNow, validationParameters.ClockSkew))) + return new LifetimeValidationResult( + notBefore, + expires, + ValidationFailureType.LifetimeValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10222, + LogHelper.MarkAsNonPII(notBefore.Value), + LogHelper.MarkAsNonPII(utcNow)), + typeof(SecurityTokenNotYetValidException), + new StackFrame(true))); + + if (expires.HasValue && (expires.Value < DateTimeUtil.Add(utcNow, validationParameters.ClockSkew.Negate()))) + return new LifetimeValidationResult( + notBefore, + expires, + ValidationFailureType.LifetimeValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10223, + LogHelper.MarkAsNonPII(expires.Value), + LogHelper.MarkAsNonPII(utcNow)), + typeof(SecurityTokenExpiredException), + new StackFrame(true))); + + // if it reaches here, that means lifetime of the token is valid + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX10239); + + return new LifetimeValidationResult(notBefore, expires); + } + + private static LifetimeValidationResult ValidateLifetimeUsingDelegate(DateTime? notBefore, DateTime? expires, SecurityToken? securityToken, TokenValidationParameters validationParameters) + { + try + { + if (!validationParameters.LifetimeValidator(notBefore, expires, securityToken, validationParameters)) + return new LifetimeValidationResult( + notBefore, + expires, + ValidationFailureType.LifetimeValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10230, + securityToken), + typeof(SecurityTokenInvalidLifetimeException), + new StackFrame(true))); + + return new LifetimeValidationResult(notBefore, expires); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception delegateException) +#pragma warning restore CA1031 // Do not catch general exception types + { + return new LifetimeValidationResult( + notBefore, + expires, + ValidationFailureType.LifetimeValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10230, + securityToken), + delegateException.GetType(), + new StackFrame(true), + delegateException)); + } + } } } +#nullable restore diff --git a/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs b/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs index 0d3d48d65a..afc7ea669c 100644 --- a/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs +++ b/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs @@ -1674,7 +1674,7 @@ protected virtual void ValidateAudience(IEnumerable audiences, JwtSecuri /// The value of the 'exp' claim if it exists in the 'jwtToken'. /// The being validated. /// required for validation. - /// for additional details. + /// for additional details. protected virtual void ValidateLifetime(DateTime? notBefore, DateTime? expires, JwtSecurityToken jwtToken, TokenValidationParameters validationParameters) { Validators.ValidateLifetime(notBefore, expires, jwtToken, validationParameters); diff --git a/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs b/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs index c8caa5156d..d6955070f7 100644 --- a/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs +++ b/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs @@ -673,6 +673,75 @@ internal static bool AreAudienceValidationResultsEqual( return context.Merge(localContext); } + public static bool AreLifetimeValidationResultsEqual(object object1, object object2, CompareContext context) + { + var localContext = new CompareContext(context); + if (!ContinueCheckingEquality(object1, object2, context)) + return context.Merge(localContext); + + return AreLifetimeValidationResultsEqual( + object1 as LifetimeValidationResult, + object2 as LifetimeValidationResult, + "LifetimeValidationResult1", + "LifetimeValidationResult2", + null, + context); + } + + internal static bool AreLifetimeValidationResultsEqual( + LifetimeValidationResult lifetimeValidationResult1, + LifetimeValidationResult lifetimeValidationResult2, + string name1, + string name2, + string stackPrefix, + CompareContext context) + { + var localContext = new CompareContext(context); + if (!ContinueCheckingEquality(lifetimeValidationResult1, lifetimeValidationResult2, localContext)) + return context.Merge(localContext); + + if (lifetimeValidationResult1.NotBefore != lifetimeValidationResult2.NotBefore) + localContext.Diffs.Add($"LifetimeValidationResult1.NotBefore: '{lifetimeValidationResult1.NotBefore}' != LifetimeValidationResult2.NotBefore: '{lifetimeValidationResult2.NotBefore}'"); + + if (lifetimeValidationResult1.Expires != lifetimeValidationResult2.Expires) + localContext.Diffs.Add($"LifetimeValidationResult1.Expires: '{lifetimeValidationResult1.Expires}' != LifetimeValidationResult2.Expires: '{lifetimeValidationResult2.Expires}'"); + + if (lifetimeValidationResult1.IsValid != lifetimeValidationResult2.IsValid) + localContext.Diffs.Add($"LifetimeValidationResult1.IsValid: {lifetimeValidationResult1.IsValid} != LifetimeValidationResult2.IsValid: {lifetimeValidationResult2.IsValid}"); + + if (lifetimeValidationResult1.ValidationFailureType != lifetimeValidationResult2.ValidationFailureType) + localContext.Diffs.Add($"LifetimeValidationResult1.ValidationFailureType: {lifetimeValidationResult1.ValidationFailureType} != LifetimeValidationResult2.ValidationFailureType: {lifetimeValidationResult2.ValidationFailureType}"); + + // true => both are not null. + if (ContinueCheckingEquality(lifetimeValidationResult1.Exception, lifetimeValidationResult2.Exception, localContext)) + { + AreStringsEqual( + lifetimeValidationResult1.Exception.Message, + lifetimeValidationResult2.Exception.Message, + $"({name1})lifetimeValidationResult1.Exception.Message", + $"({name2})lifetimeValidationResult2.Exception.Message", + localContext); + + AreStringsEqual( + lifetimeValidationResult1.Exception.Source, + lifetimeValidationResult2.Exception.Source, + $"({name1})lifetimeValidationResult1.Exception.Source", + $"({name2})lifetimeValidationResult2.Exception.Source", + localContext); + + if (!string.IsNullOrEmpty(stackPrefix)) + AreStringPrefixesEqual( + lifetimeValidationResult1.Exception.StackTrace.Trim(), + lifetimeValidationResult2.Exception.StackTrace.Trim(), + $"({name1})lifetimeValidationResult1.Exception.StackTrace", + $"({name2})lifetimeValidationResult2.Exception.StackTrace", + stackPrefix.Trim(), + localContext); + } + + return context.Merge(localContext); + } + public static bool AreJArraysEqual(object object1, object object2, CompareContext context) { var localContext = new CompareContext(context); diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/LifetimeValidationResultTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/LifetimeValidationResultTests.cs new file mode 100644 index 0000000000..49ab54db6d --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/LifetimeValidationResultTests.cs @@ -0,0 +1,303 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.TestUtils; +using Xunit; + +namespace Microsoft.IdentityModel.Tokens.Validation.Tests +{ + public class LifetimeValidationResultTests + { + [Theory, MemberData(nameof(ValidateLifetimeTestCases), DisableDiscoveryEnumeration = true)] + public void ValidateLifetime(ValidateLifetimeTheoryData theoryData) + { + CompareContext context = TestUtilities.WriteHeader($"{this}.LifetimeValidatorTests", theoryData); + + LifetimeValidationResult lifetimeValidationResult = Validators.ValidateLifetime( + theoryData.NotBefore, + theoryData.Expires, + theoryData.SecurityToken, + theoryData.ValidationParameters, + new CallContext()); + + if (lifetimeValidationResult.Exception == null) + theoryData.ExpectedException.ProcessNoException(); + else + theoryData.ExpectedException.ProcessException(lifetimeValidationResult.Exception, context); + + IdentityComparer.AreLifetimeValidationResultsEqual( + lifetimeValidationResult, + theoryData.LifetimeValidationResult, + context); + + TestUtilities.AssertFailIfErrors(context); + } + + public static TheoryData ValidateLifetimeTestCases + { + get + { + DateTime now = DateTime.UtcNow; + DateTime oneHourFromNow = DateTime.UtcNow.AddHours(1); + DateTime twoHoursFromNow = DateTime.UtcNow.AddHours(2); + DateTime twoMinutesFromNow = DateTime.UtcNow.AddMinutes(2); + DateTime sixMinutesFromNow = DateTime.UtcNow.AddMinutes(6); + DateTime oneHourAgo = DateTime.UtcNow.AddHours(-1); + DateTime twoHoursAgo = DateTime.UtcNow.AddHours(-2); + DateTime twoMinutesAgo = DateTime.UtcNow.AddMinutes(-2); + DateTime oneMinuteAgo = DateTime.UtcNow.AddMinutes(-1); + DateTime sixMinutesAgo = DateTime.UtcNow.AddMinutes(-6); + + return new TheoryData + { + new ValidateLifetimeTheoryData("Valid") + { + Expires = oneHourFromNow, + NotBefore = oneHourAgo, + LifetimeValidationResult = new LifetimeValidationResult(oneHourAgo, oneHourFromNow), + ValidationParameters = new TokenValidationParameters() + }, + new ValidateLifetimeTheoryData("Valid_ValidateLifetimeIsFalse_DatesAreNotNull") + { + Expires = oneHourFromNow, + NotBefore = oneHourAgo, + LifetimeValidationResult = new LifetimeValidationResult(oneHourAgo, oneHourFromNow), + ValidationParameters = new TokenValidationParameters { ValidateLifetime = false } + }, + new ValidateLifetimeTheoryData("Valid_ValidateLifetimeIsFalse_DatesAreNull") + { + Expires = null, + NotBefore = null, + LifetimeValidationResult = new LifetimeValidationResult(null, null), + ValidationParameters = new TokenValidationParameters { ValidateLifetime = false } + }, + new ValidateLifetimeTheoryData("Valid_NotBeforeIsNull") + { + Expires = oneHourFromNow, + NotBefore = null, + LifetimeValidationResult = new LifetimeValidationResult(null, oneHourFromNow), + ValidationParameters = new TokenValidationParameters() + }, + new ValidateLifetimeTheoryData("Valid_SkewForward") + { + Expires = oneHourFromNow, + NotBefore = twoMinutesFromNow, + ValidationParameters = new TokenValidationParameters { ClockSkew = TimeSpan.FromMinutes(5) }, + LifetimeValidationResult = new LifetimeValidationResult(twoMinutesFromNow, oneHourFromNow), + }, + new ValidateLifetimeTheoryData("Valid_SkewBackward") + { + Expires = oneMinuteAgo, + NotBefore = twoMinutesAgo, + ValidationParameters = new TokenValidationParameters { ClockSkew = TimeSpan.FromMinutes(5) }, + LifetimeValidationResult = new LifetimeValidationResult(twoMinutesAgo, oneMinuteAgo), + }, + new ValidateLifetimeTheoryData("Valid_DelegateReturnsTrue") + { + Expires = oneHourFromNow, + NotBefore = oneHourAgo, + ValidationParameters = new TokenValidationParameters + { + LifetimeValidator = (DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters) => true + }, + LifetimeValidationResult = new LifetimeValidationResult(oneHourAgo, oneHourFromNow), + }, + new ValidateLifetimeTheoryData("Invalid_ValidationParametersIsNull") + { + Expires = oneHourFromNow, + NotBefore = oneHourAgo, + ValidationParameters = null, + ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), + LifetimeValidationResult = new LifetimeValidationResult( + oneHourAgo, + oneHourFromNow, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10000, + "validationParameters"), + typeof(ArgumentNullException), + new StackFrame(true), + null)), + }, + new ValidateLifetimeTheoryData("Invalid_ExpiresIsNull") + { + NotBefore = oneHourAgo, + ValidationParameters = new TokenValidationParameters(), + ExpectedException = ExpectedException.SecurityTokenNoExpirationException("IDX10225:"), + LifetimeValidationResult = new LifetimeValidationResult( + oneHourAgo, + null, + ValidationFailureType.LifetimeValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10225, + "null"), + typeof(SecurityTokenNoExpirationException), + new StackFrame(true), + null)), + }, + new ValidateLifetimeTheoryData("Invalid_NotBeforeIsAfterExpires") + { + Expires = oneHourAgo, + NotBefore = oneHourFromNow, + ValidationParameters = new TokenValidationParameters(), + ExpectedException = ExpectedException.SecurityTokenInvalidLifetimeException("IDX10224:"), + LifetimeValidationResult = new LifetimeValidationResult( + oneHourFromNow, // notBefore + oneHourAgo, // expires + ValidationFailureType.LifetimeValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10224, + LogHelper.MarkAsNonPII(oneHourFromNow), + LogHelper.MarkAsNonPII(oneHourAgo)), + typeof(SecurityTokenInvalidLifetimeException), + new StackFrame(true), + null)), + }, + new ValidateLifetimeTheoryData("Invalid_NotYetValid") + { + Expires = twoHoursFromNow, + NotBefore = oneHourFromNow, + ValidationParameters = new TokenValidationParameters(), + ExpectedException = ExpectedException.SecurityTokenNotYetValidException("IDX10222:"), + LifetimeValidationResult = new LifetimeValidationResult( + oneHourFromNow, + twoHoursFromNow, + ValidationFailureType.LifetimeValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10222, + LogHelper.MarkAsNonPII(oneHourFromNow), + LogHelper.MarkAsNonPII(now)), + typeof(SecurityTokenNotYetValidException), + new StackFrame(true), + null)), + }, + new ValidateLifetimeTheoryData("Invalid_Expired") + { + Expires = oneHourAgo, + NotBefore = twoHoursAgo, + ValidationParameters = new TokenValidationParameters(), + ExpectedException = ExpectedException.SecurityTokenExpiredException("IDX10223:"), + LifetimeValidationResult = new LifetimeValidationResult( + twoHoursAgo, + oneHourAgo, + ValidationFailureType.LifetimeValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10223, + LogHelper.MarkAsNonPII(oneHourAgo), + LogHelper.MarkAsNonPII(now)), + typeof(SecurityTokenExpiredException), + new StackFrame(true), + null)), + }, + new ValidateLifetimeTheoryData("Invalid_NotYetValid_SkewForward") + { + Expires = oneHourFromNow, + NotBefore = sixMinutesFromNow, + ValidationParameters = new TokenValidationParameters { ClockSkew = TimeSpan.FromMinutes(5) }, + ExpectedException = ExpectedException.SecurityTokenNotYetValidException("IDX10222:"), + LifetimeValidationResult = new LifetimeValidationResult( + sixMinutesFromNow, + oneHourFromNow, + ValidationFailureType.LifetimeValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10222, + LogHelper.MarkAsNonPII(sixMinutesFromNow), + LogHelper.MarkAsNonPII(now)), + typeof(SecurityTokenNotYetValidException), + new StackFrame(true), + null)), + }, + new ValidateLifetimeTheoryData("Invalid_Expired_SkewBackward") + { + Expires = sixMinutesAgo, + NotBefore = twoHoursAgo, + ValidationParameters = new TokenValidationParameters { ClockSkew = TimeSpan.FromMinutes(5) }, + ExpectedException = ExpectedException.SecurityTokenExpiredException("IDX10223:"), + LifetimeValidationResult = new LifetimeValidationResult( + twoHoursAgo, + sixMinutesAgo, + ValidationFailureType.LifetimeValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10223, + LogHelper.MarkAsNonPII(sixMinutesAgo), + LogHelper.MarkAsNonPII(now)), + typeof(SecurityTokenExpiredException), + new StackFrame(true), + null)), + }, + new ValidateLifetimeTheoryData("Invalid_DelegateReturnsFalse") + { + Expires = oneHourFromNow, + NotBefore = oneHourAgo, + ValidationParameters = new TokenValidationParameters + { + LifetimeValidator = (DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters) => false + }, + ExpectedException = ExpectedException.SecurityTokenInvalidLifetimeException("IDX10230:"), + LifetimeValidationResult = new LifetimeValidationResult( + oneHourAgo, + oneHourFromNow, + ValidationFailureType.LifetimeValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10230, + "null"), + typeof(SecurityTokenInvalidLifetimeException), + new StackFrame(true), + null)), + }, + new ValidateLifetimeTheoryData("Invalid_DelegateThrows") + { + Expires = oneHourFromNow, + NotBefore = oneHourAgo, + ValidationParameters = new TokenValidationParameters + { + LifetimeValidator = (DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters) => throw new SecurityTokenInvalidLifetimeException() + }, + ExpectedException = ExpectedException.SecurityTokenInvalidLifetimeException("IDX10230:", innerTypeExpected: typeof(SecurityTokenInvalidLifetimeException)), + LifetimeValidationResult = new LifetimeValidationResult( + oneHourAgo, + oneHourFromNow, + ValidationFailureType.LifetimeValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10230, + "null"), + typeof(SecurityTokenInvalidLifetimeException), + new StackFrame(true), + null)), + }, + }; + } + } + } + + public class ValidateLifetimeTheoryData : TheoryDataBase + { + public ValidateLifetimeTheoryData(string testId) : base(testId) + { + } + + public DateTime? NotBefore { get; set; } + + public DateTime? Expires { get; set; } + + public SecurityToken SecurityToken { get; set; } + + public TokenValidationParameters ValidationParameters { get; set; } + + internal LifetimeValidationResult LifetimeValidationResult { get; set; } + + internal ValidationFailureType ValidationFailureType { get; set; } + } +} From 0c5172caf95bf64748c0a0168fb1c56c2bc127f0 Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Sat, 29 Jun 2024 02:31:09 +0100 Subject: [PATCH 22/22] Issuer signing key validation: Remove exceptions (#2672) * Added SigningKeyValidationResult * Added validation failure type for signing key, removed exception throwing from ValidateIssuerSigningKey, and added tests --- .../Validation/SigningKeyValidationResult.cs | 69 ++++ .../Validation/ValidationFailureType.cs | 6 + .../Validators.IssuerSecurityKey.cs | 212 ++++++++++- .../IdentityComparer.cs | 65 ++++ .../SigningKeyValidationResultTests.cs | 333 ++++++++++++++++++ 5 files changed, 682 insertions(+), 3 deletions(-) create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/SigningKeyValidationResult.cs create mode 100644 test/Microsoft.IdentityModel.Tokens.Tests/Validation/SigningKeyValidationResultTests.cs diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/SigningKeyValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/SigningKeyValidationResult.cs new file mode 100644 index 0000000000..6c3905c1cf --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/SigningKeyValidationResult.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +#nullable enable +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// Contains the result of validating the used to sign a . + /// The contains a collection of for each step in the token validation. + /// + internal class SigningKeyValidationResult : ValidationResult + { + private Exception? _exception; + + /// + /// Creates an instance of + /// + /// is the security key that was validated successfully. + public SigningKeyValidationResult(SecurityKey? signingKey) + : base(ValidationFailureType.ValidationSucceeded) + { + SigningKey = signingKey; + IsValid = true; + } + + /// + /// Creates an instance of + /// + /// is the security key that was intended to be validated. + /// is the that occurred during validation. + /// is the that occurred during validation. + public SigningKeyValidationResult(SecurityKey? signingKey, ValidationFailureType validationFailure, ExceptionDetail exceptionDetail) + : base(validationFailure, exceptionDetail) + { + SigningKey = signingKey; + IsValid = false; + } + + /// + /// Gets the that occurred during validation. + /// + public override Exception? Exception + { + get + { + if (_exception != null || ExceptionDetail == null) + return _exception; + + HasValidOrExceptionWasRead = true; + _exception = ExceptionDetail.GetException(); + if (_exception is SecurityTokenInvalidSigningKeyException securityTokenInvalidSigningKeyException) + { + securityTokenInvalidSigningKeyException.SigningKey = SigningKey; + securityTokenInvalidSigningKeyException.ExceptionDetail = ExceptionDetail; + securityTokenInvalidSigningKeyException.Source = "Microsoft.IdentityModel.Tokens"; + } + + return _exception; + } + } + + /// + /// Gets the security key that was validated or intended to be validated. + /// + public SecurityKey? SigningKey { get; } + } +} +#nullable restore diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs index 3c4a2ec3b8..11a9f7c482 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs @@ -39,6 +39,12 @@ private class IssuerValidationFailure : ValidationFailureType { internal IssuerV public static readonly ValidationFailureType AudienceValidationFailed = new AudienceValidationFailure("AudienceValidationFailed"); private class AudienceValidationFailure : ValidationFailureType { internal AudienceValidationFailure(string name) : base(name) { } } + /// + /// Defines a type that represents that signing key validation failed. + /// + public static readonly ValidationFailureType SigningKeyValidationFailed = new SigningKeyValidationFailure("SigningKeyValidationFailed"); + private class SigningKeyValidationFailure : ValidationFailureType { internal SigningKeyValidationFailure(string name) : base(name) { } } + /// /// Defines a type that represents that lifetime validation failed. /// diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.IssuerSecurityKey.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.IssuerSecurityKey.cs index 1a50cb9d45..e5595a178a 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.IssuerSecurityKey.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.IssuerSecurityKey.cs @@ -2,12 +2,37 @@ // Licensed under the MIT License. using System; +using System.Diagnostics; using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; using Microsoft.IdentityModel.Abstractions; using Microsoft.IdentityModel.Logging; +#nullable enable namespace Microsoft.IdentityModel.Tokens { + /// + /// Definition for delegate that will validate the that signed a . + /// + /// The security key to validate. + /// The that is being validated. + /// required for validation. + /// + /// + /// A that contains the results of validating the issuer. + /// This delegate is not expected to throw. + internal delegate Task IssuerSecurityKeyValidationDelegate( + SecurityKey signingKey, + SecurityToken securityToken, + TokenValidationParameters validationParameters, + CallContext callContext, + CancellationToken cancellationToken); + + /// + /// SigningKeyValidation + /// + public static partial class Validators { /// @@ -21,7 +46,7 @@ public static partial class Validators /// if 'validationParameters' is null. public static void ValidateIssuerSecurityKey(SecurityKey securityKey, SecurityToken securityToken, TokenValidationParameters validationParameters) { - ValidateIssuerSecurityKey(securityKey, securityToken, validationParameters, null); + ValidateIssuerSecurityKey(securityKey, securityToken, validationParameters, configuration: null); } /// @@ -34,7 +59,7 @@ public static void ValidateIssuerSecurityKey(SecurityKey securityKey, SecurityTo /// if 'securityKey' is null and ValidateIssuerSigningKey is true. /// if 'securityToken' is null and ValidateIssuerSigningKey is true. /// if 'validationParameters' is null. - internal static void ValidateIssuerSecurityKey(SecurityKey securityKey, SecurityToken securityToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) + internal static void ValidateIssuerSecurityKey(SecurityKey securityKey, SecurityToken securityToken, TokenValidationParameters validationParameters, BaseConfiguration? configuration) { if (validationParameters == null) throw LogHelper.LogArgumentNullException(nameof(validationParameters)); @@ -84,7 +109,7 @@ internal static void ValidateIssuerSecurityKey(SecurityKey securityKey, Security /// The that are used to validate the token. internal static void ValidateIssuerSigningKeyLifeTime(SecurityKey securityKey, TokenValidationParameters validationParameters) { - X509SecurityKey x509SecurityKey = securityKey as X509SecurityKey; + X509SecurityKey? x509SecurityKey = securityKey as X509SecurityKey; if (x509SecurityKey?.Certificate is X509Certificate2 cert) { DateTime utcNow = DateTime.UtcNow; @@ -104,5 +129,186 @@ internal static void ValidateIssuerSigningKeyLifeTime(SecurityKey securityKey, T LogHelper.LogInformation(LogMessages.IDX10251, LogHelper.MarkAsNonPII(notAfterUtc), LogHelper.MarkAsNonPII(utcNow)); } } + + /// + /// Validates the that signed a . + /// + /// The that signed the . + /// The being validated. + /// required for validation. + /// + /// if 'securityKey' is null and ValidateIssuerSigningKey is true. + /// if 'securityToken' is null and ValidateIssuerSigningKey is true. + /// if 'validationParameters' is null. + internal static SigningKeyValidationResult ValidateIssuerSecurityKey(SecurityKey securityKey, SecurityToken securityToken, TokenValidationParameters validationParameters, CallContext callContext) + { + return ValidateIssuerSecurityKey(securityKey, securityToken, validationParameters, null, callContext); + } + + /// + /// Validates the that signed a . + /// + /// The that signed the . + /// The being validated. + /// required for validation. + /// The required for issuer and signing key validation. + /// + /// if 'securityKey' is null and ValidateIssuerSigningKey is true. + /// if 'securityToken' is null and ValidateIssuerSigningKey is true. + /// if 'validationParameters' is null. + internal static SigningKeyValidationResult ValidateIssuerSecurityKey(SecurityKey securityKey, SecurityToken securityToken, TokenValidationParameters validationParameters, BaseConfiguration? configuration, CallContext callContext) + { + if (validationParameters == null) + return new SigningKeyValidationResult( + securityKey, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10000, + LogHelper.MarkAsNonPII(nameof(validationParameters))), + typeof(ArgumentNullException), + new StackFrame(true))); + + if (validationParameters.IssuerSigningKeyValidatorUsingConfiguration != null) + { + return ValidateSigningKeyUsingDelegateAndConfiguration(securityKey, securityToken, validationParameters, configuration); + } + + if (validationParameters.IssuerSigningKeyValidator != null) + { + return ValidateSigningKeyUsingDelegateAndConfiguration(securityKey, securityToken, validationParameters, null); + } + + if (!validationParameters.ValidateIssuerSigningKey) + { + LogHelper.LogVerbose(LogMessages.IDX10237); + return new SigningKeyValidationResult(securityKey); + } + + if (!validationParameters.RequireSignedTokens && securityKey == null) + { + LogHelper.LogInformation(LogMessages.IDX10252); + return new SigningKeyValidationResult(securityKey); + } + else if (securityKey == null) + { + return new SigningKeyValidationResult( + securityKey, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10253, + LogHelper.MarkAsNonPII(nameof(securityKey))), + typeof(ArgumentNullException), + new StackFrame(true))); + } + + if (securityToken == null) + return new SigningKeyValidationResult( + securityKey, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10000, + LogHelper.MarkAsNonPII(nameof(securityToken))), + typeof(ArgumentNullException), + new StackFrame(true))); + + return ValidateIssuerSigningKeyLifeTime(securityKey, validationParameters, callContext); + } + + /// + /// Given a signing key, when it's derived from a certificate, validates that the certificate is already active and non-expired + /// + /// The that signed the . + /// The that are used to validate the token. + /// +#pragma warning disable CA1801 // Review unused parameters + internal static SigningKeyValidationResult ValidateIssuerSigningKeyLifeTime(SecurityKey securityKey, TokenValidationParameters validationParameters, CallContext callContext) +#pragma warning restore CA1801 // Review unused parameters + { + X509SecurityKey? x509SecurityKey = securityKey as X509SecurityKey; + if (x509SecurityKey?.Certificate is X509Certificate2 cert) + { + DateTime utcNow = DateTime.UtcNow; + var notBeforeUtc = cert.NotBefore.ToUniversalTime(); + var notAfterUtc = cert.NotAfter.ToUniversalTime(); + + if (notBeforeUtc > DateTimeUtil.Add(utcNow, validationParameters.ClockSkew)) + return new SigningKeyValidationResult( + securityKey, + ValidationFailureType.SigningKeyValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogHelper.FormatInvariant( + LogMessages.IDX10248, + LogHelper.MarkAsNonPII(notBeforeUtc), + LogHelper.MarkAsNonPII(utcNow))), + typeof(SecurityTokenInvalidSigningKeyException), + new StackFrame(true))); + + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX10250, LogHelper.MarkAsNonPII(notBeforeUtc), LogHelper.MarkAsNonPII(utcNow)); + + if (notAfterUtc < DateTimeUtil.Add(utcNow, validationParameters.ClockSkew.Negate())) + return new SigningKeyValidationResult( + securityKey, + ValidationFailureType.SigningKeyValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogHelper.FormatInvariant( + LogMessages.IDX10249, + LogHelper.MarkAsNonPII(notAfterUtc), + LogHelper.MarkAsNonPII(utcNow))), + typeof(SecurityTokenInvalidSigningKeyException), + new StackFrame(true))); + + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX10251, LogHelper.MarkAsNonPII(notAfterUtc), LogHelper.MarkAsNonPII(utcNow)); + } + + return new SigningKeyValidationResult(securityKey); + } + + private static SigningKeyValidationResult ValidateSigningKeyUsingDelegateAndConfiguration(SecurityKey securityKey, SecurityToken securityToken, TokenValidationParameters validationParameters, BaseConfiguration? configuration) + { + try + { + bool success; + if (configuration != null) + success = validationParameters.IssuerSigningKeyValidatorUsingConfiguration(securityKey, securityToken, validationParameters, configuration); + else + success = validationParameters.IssuerSigningKeyValidator(securityKey, securityToken, validationParameters); + + if (!success) + return new SigningKeyValidationResult( + securityKey, + ValidationFailureType.SigningKeyValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10232, + securityKey), + typeof(SecurityTokenInvalidSigningKeyException), + new StackFrame(true))); + + return new SigningKeyValidationResult(securityKey); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception exception) +#pragma warning restore CA1031 // Do not catch general exception types + { + return new SigningKeyValidationResult( + securityKey, + ValidationFailureType.SigningKeyValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10232, + securityKey), + exception.GetType(), + new StackFrame(true), + exception)); + } + } } } +#nullable restore diff --git a/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs b/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs index d6955070f7..1644fa856b 100644 --- a/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs +++ b/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs @@ -673,6 +673,71 @@ internal static bool AreAudienceValidationResultsEqual( return context.Merge(localContext); } + public static bool AreSigningKeyValidationResultsEqual(object object1, object object2, CompareContext context) + { + var localContext = new CompareContext(context); + if (!ContinueCheckingEquality(object1, object2, context)) + return context.Merge(localContext); + + return AreSigningKeyValidationResultsEqual( + object1 as SigningKeyValidationResult, + object2 as SigningKeyValidationResult, + "SigningKeyValidationResult1", + "SigningKeyValidationResult2", + null, + context); + } + + internal static bool AreSigningKeyValidationResultsEqual( + SigningKeyValidationResult signingKeyValidationResult1, + SigningKeyValidationResult signingKeyValidationResult2, + string name1, + string name2, + string stackPrefix, + CompareContext context) + { + var localContext = new CompareContext(context); + AreSecurityKeysEqual(signingKeyValidationResult1.SigningKey, signingKeyValidationResult2.SigningKey, localContext); + + if (!ContinueCheckingEquality(signingKeyValidationResult1, signingKeyValidationResult2, localContext)) + return context.Merge(localContext); + + if (signingKeyValidationResult1.IsValid != signingKeyValidationResult2.IsValid) + localContext.Diffs.Add($"SigningKeyValidationResult1.IsValid: {signingKeyValidationResult2.IsValid} != SigningKeyValidationResult2.IsValid: {signingKeyValidationResult2.IsValid}"); + + if (signingKeyValidationResult1.ValidationFailureType != signingKeyValidationResult2.ValidationFailureType) + localContext.Diffs.Add($"SigningKeyValidationResult1.ValidationFailureType: {signingKeyValidationResult1.ValidationFailureType} != SigningKeyValidationResult2.ValidationFailureType: {signingKeyValidationResult2.ValidationFailureType}"); + + // true => both are not null. + if (ContinueCheckingEquality(signingKeyValidationResult1.Exception, signingKeyValidationResult2.Exception, localContext)) + { + AreStringsEqual( + signingKeyValidationResult1.Exception.Message, + signingKeyValidationResult2.Exception.Message, + $"({name1})signingKeyValidationResult1.Exception.Message", + $"({name2})signingKeyValidationResult2.Exception.Message", + localContext); + + AreStringsEqual( + signingKeyValidationResult1.Exception.Source, + signingKeyValidationResult2.Exception.Source, + $"({name1})signingKeyValidationResult1.Exception.Source", + $"({name2})signingKeyValidationResult2.Exception.Source", + localContext); + + if (!string.IsNullOrEmpty(stackPrefix)) + AreStringPrefixesEqual( + signingKeyValidationResult1.Exception.StackTrace.Trim(), + signingKeyValidationResult2.Exception.StackTrace.Trim(), + $"({name1})signingKeyValidationResult1.Exception.StackTrace", + $"({name2})signingKeyValidationResult2.Exception.StackTrace", + stackPrefix.Trim(), + localContext); + } + + return context.Merge(localContext); + } + public static bool AreLifetimeValidationResultsEqual(object object1, object object2, CompareContext context) { var localContext = new CompareContext(context); diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/SigningKeyValidationResultTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/SigningKeyValidationResultTests.cs new file mode 100644 index 0000000000..43a6d3e313 --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/SigningKeyValidationResultTests.cs @@ -0,0 +1,333 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.IdentityModel.Tokens.Jwt; +using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.TestUtils; +using Xunit; + +namespace Microsoft.IdentityModel.Tokens.Validation.Tests +{ + public class SigningKeyValidationResultTests + { + [Theory, MemberData(nameof(SigningKeyValidationTestCases), DisableDiscoveryEnumeration = true)] + public void SecurityKey(SigningKeyValidationTheoryData theoryData) + { + CompareContext context = TestUtilities.WriteHeader($"{this}.SigningKeyValidationResultTests", theoryData); + + SigningKeyValidationResult signingKeyValidationResult = Validators.ValidateIssuerSecurityKey( + theoryData.SecurityKey, + theoryData.SecurityToken, + theoryData.ValidationParameters, + theoryData.BaseConfiguration, + new CallContext()); + + if (signingKeyValidationResult.Exception != null) + theoryData.ExpectedException.ProcessException(signingKeyValidationResult.Exception); + else + theoryData.ExpectedException.ProcessNoException(); + + IdentityComparer.AreSigningKeyValidationResultsEqual( + signingKeyValidationResult, + theoryData.SigningKeyValidationResult, + context); + + TestUtilities.AssertFailIfErrors(context); + } + + public static TheoryData SigningKeyValidationTestCases + { + get + { + DateTime utcNow = DateTime.UtcNow; + DateTime utcExpired = KeyingMaterial.ExpiredX509SecurityKey_Public.Certificate.NotAfter.ToUniversalTime(); + DateTime utcNotYetValid = KeyingMaterial.NotYetValidX509SecurityKey_Public.Certificate.NotBefore.ToUniversalTime(); + + return new TheoryData + { + new SigningKeyValidationTheoryData + { + TestId = "Valid_SecurityTokenIsPresent_ValidateIssuerSigningKeyIsTrue", + ExpectedException = ExpectedException.NoExceptionExpected, + SecurityKey = KeyingMaterial.SymmetricSecurityKey2_256, + SecurityToken = new JwtSecurityToken(), + ValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true }, + SigningKeyValidationResult = new SigningKeyValidationResult(KeyingMaterial.SymmetricSecurityKey2_256) + }, + new SigningKeyValidationTheoryData + { + TestId = "Valid_SecurityKeyIsNull_ValidateIssuerSigningKeyIsFalse", + ExpectedException = ExpectedException.NoExceptionExpected, + SecurityKey = null, + SecurityToken = new JwtSecurityToken(), + ValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = false }, + SigningKeyValidationResult = new SigningKeyValidationResult(null) + }, + new SigningKeyValidationTheoryData + { + TestId = "Valid_SecurityTokenIsNull_ValidateIssuerSigningKeyIsFalse", + ExpectedException = ExpectedException.NoExceptionExpected, + SecurityKey = KeyingMaterial.SymmetricSecurityKey2_256, + SecurityToken = null, + ValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = false }, + SigningKeyValidationResult = new SigningKeyValidationResult(KeyingMaterial.SymmetricSecurityKey2_256) + }, + new SigningKeyValidationTheoryData + { + TestId = "Valid_SecurityKeyIsNull_RequireSignedTokensIsFalse", + ExpectedException = ExpectedException.NoExceptionExpected, + SecurityKey = null, + SecurityToken = new JwtSecurityToken(), + ValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, RequireSignedTokens = false }, + SigningKeyValidationResult = new SigningKeyValidationResult(null) + }, + new SigningKeyValidationTheoryData + { + TestId = "Valid_SecurityKeyIsPresent_RequireSignedTokensIsTrue", + ExpectedException = ExpectedException.NoExceptionExpected, + SecurityKey = KeyingMaterial.SymmetricSecurityKey2_256, + SecurityToken = new JwtSecurityToken(), + ValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, RequireSignedTokens = true }, + SigningKeyValidationResult = new SigningKeyValidationResult(KeyingMaterial.SymmetricSecurityKey2_256) + }, + new SigningKeyValidationTheoryData + { + TestId = "Valid_SecurityKeyIsPresent_RequireSignedTokensIsFalse", + ExpectedException = ExpectedException.NoExceptionExpected, + SecurityKey = KeyingMaterial.SymmetricSecurityKey2_256, + SecurityToken = new JwtSecurityToken(), + ValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, RequireSignedTokens = false }, + SigningKeyValidationResult = new SigningKeyValidationResult(KeyingMaterial.SymmetricSecurityKey2_256) + }, + new SigningKeyValidationTheoryData + { + TestId = "Valid_DelegateSet_ReturnsTrue", + ExpectedException = ExpectedException.NoExceptionExpected, + SecurityKey = KeyingMaterial.SymmetricSecurityKey2_256, + SecurityToken = new JwtSecurityToken(), + ValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKeyValidator = (SecurityKey securityKey, SecurityToken token, TokenValidationParameters validationParameters) => true + }, + SigningKeyValidationResult = new SigningKeyValidationResult(KeyingMaterial.SymmetricSecurityKey2_256) + }, + new SigningKeyValidationTheoryData + { + TestId = "Valid_DelegateUsingConfigurationSet_ReturnsTrue", + ExpectedException = ExpectedException.NoExceptionExpected, + SecurityKey = KeyingMaterial.SymmetricSecurityKey2_256, + SecurityToken = new JwtSecurityToken(), + ValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKeyValidatorUsingConfiguration = (SecurityKey securityKey, SecurityToken token, TokenValidationParameters validationParameters, BaseConfiguration configuration) => true + }, + BaseConfiguration = new OpenIdConnectConfiguration(), + SigningKeyValidationResult = new SigningKeyValidationResult(KeyingMaterial.SymmetricSecurityKey2_256) + }, + new SigningKeyValidationTheoryData + { + TestId = "Invalid_SecurityKeyIsNull", + ExpectedException = ExpectedException.ArgumentNullException(substringExpected: "IDX10253:"), + SecurityKey = null, + SecurityToken = new JwtSecurityToken(), + ValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true }, + SigningKeyValidationResult = new SigningKeyValidationResult( + null, // SecurityKey + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail(LogMessages.IDX10253), + typeof(ArgumentNullException), + new StackFrame(true))) + }, + new SigningKeyValidationTheoryData + { + TestId = "Invalid_SecurityTokenIsNullAndValidateIssuerSigningKeyTrue", + ExpectedException = ExpectedException.ArgumentNullException(), + SecurityKey = KeyingMaterial.SymmetricSecurityKey2_256, + SecurityToken = null, + ValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true }, + SigningKeyValidationResult = new SigningKeyValidationResult( + KeyingMaterial.SymmetricSecurityKey2_256, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10000, + LogHelper.MarkAsNonPII("securityToken")), + typeof(ArgumentNullException), + new StackFrame(true))) + }, + new SigningKeyValidationTheoryData + { + TestId = "Invalid_ValidationParametersIsNull", + ExpectedException = ExpectedException.ArgumentNullException(), + SecurityKey = KeyingMaterial.SymmetricSecurityKey2_256, + SecurityToken = new JwtSecurityToken(), + ValidationParameters = null, + SigningKeyValidationResult = new SigningKeyValidationResult( + KeyingMaterial.SymmetricSecurityKey2_256, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10000, + LogHelper.MarkAsNonPII("validationParameters")), + typeof(ArgumentNullException), + new StackFrame(true))) + }, + new SigningKeyValidationTheoryData + { + TestId = "Invalid_SecurityKeyIsExpired", + ExpectedException = ExpectedException.SecurityTokenInvalidSigningKeyException(substringExpected: "IDX10249:"), + SecurityKey = KeyingMaterial.ExpiredX509SecurityKey_Public, + SecurityToken = new JwtSecurityToken(), + ValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true }, + SigningKeyValidationResult = new SigningKeyValidationResult( + KeyingMaterial.ExpiredX509SecurityKey_Public, + ValidationFailureType.SigningKeyValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10249, + LogHelper.MarkAsNonPII(utcExpired), + LogHelper.MarkAsNonPII(utcNow)), + typeof(SecurityTokenInvalidSigningKeyException), + new StackFrame(true))) + }, + new SigningKeyValidationTheoryData + { + TestId = "Invalid_SecurityKeyIsNotYetValid", + ExpectedException = ExpectedException.SecurityTokenInvalidSigningKeyException(substringExpected: "IDX10248:"), + SecurityKey = KeyingMaterial.NotYetValidX509SecurityKey_Public, + SecurityToken = new JwtSecurityToken(), + ValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true }, + SigningKeyValidationResult = new SigningKeyValidationResult( + KeyingMaterial.NotYetValidX509SecurityKey_Public, + ValidationFailureType.SigningKeyValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10248, + LogHelper.MarkAsNonPII(utcNotYetValid), + LogHelper.MarkAsNonPII(utcNow)), + typeof(SecurityTokenInvalidSigningKeyException), + new StackFrame(true))) + }, + new SigningKeyValidationTheoryData + { + TestId = "Invalid_SecurityKeyIsNull_RequireSignedTokensIsTrue", + ExpectedException = ExpectedException.ArgumentNullException(substringExpected: "IDX10253:"), + SecurityKey = null, + SecurityToken = new JwtSecurityToken(), + ValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, RequireSignedTokens = true }, + SigningKeyValidationResult = new SigningKeyValidationResult( + null, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail(LogMessages.IDX10253), + typeof(ArgumentNullException), + new StackFrame(true))) + }, + new SigningKeyValidationTheoryData + { + TestId = "Invalid_DelegateIsSet_ReturnsFalse", + ExpectedException = ExpectedException.SecurityTokenInvalidSigningKeyException(substringExpected: "IDX10232:"), + SecurityKey = KeyingMaterial.SymmetricSecurityKey2_256, + SecurityToken = new JwtSecurityToken(), + ValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKeyValidator = (SecurityKey securityKey, SecurityToken token, TokenValidationParameters validationParameters) => false + }, + SigningKeyValidationResult = new SigningKeyValidationResult( + KeyingMaterial.SymmetricSecurityKey2_256, + ValidationFailureType.SigningKeyValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10232, + KeyingMaterial.SymmetricSecurityKey2_256), + typeof(SecurityTokenInvalidSigningKeyException), + new StackFrame(true))) + }, + new SigningKeyValidationTheoryData + { + TestId = "Invalid_DelegateUsingConfigurationSet_ReturnsFalse", + ExpectedException = ExpectedException.SecurityTokenInvalidSigningKeyException(substringExpected: "IDX10232:"), + SecurityKey = KeyingMaterial.SymmetricSecurityKey2_256, + SecurityToken = new JwtSecurityToken(), + ValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKeyValidatorUsingConfiguration = (SecurityKey securityKey, SecurityToken token, TokenValidationParameters validationParameters, BaseConfiguration configuration) => false + }, + BaseConfiguration = new OpenIdConnectConfiguration(), + SigningKeyValidationResult = new SigningKeyValidationResult( + KeyingMaterial.SymmetricSecurityKey2_256, + ValidationFailureType.SigningKeyValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10232, + KeyingMaterial.SymmetricSecurityKey2_256), + typeof(SecurityTokenInvalidSigningKeyException), + new StackFrame(true))) + }, + new SigningKeyValidationTheoryData + { + TestId = "Invalid_DelegateIsSet_Throws", + ExpectedException = ExpectedException.SecurityTokenInvalidSigningKeyException(substringExpected: "IDX10232:", innerTypeExpected: typeof(SecurityTokenInvalidSigningKeyException)), + SecurityKey = KeyingMaterial.SymmetricSecurityKey2_256, + SecurityToken = new JwtSecurityToken(), + ValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKeyValidator = (SecurityKey securityKey, SecurityToken token, TokenValidationParameters validationParameters) => throw new SecurityTokenInvalidSigningKeyException() + }, + SigningKeyValidationResult = new SigningKeyValidationResult( + KeyingMaterial.SymmetricSecurityKey2_256, + ValidationFailureType.SigningKeyValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10232, + KeyingMaterial.SymmetricSecurityKey2_256), + typeof(SecurityTokenInvalidSigningKeyException), + new StackFrame(true), + new SecurityTokenInvalidSigningKeyException())) + }, + new SigningKeyValidationTheoryData + { + TestId = "Invalid_DelegateUsingConfigurationSet_Throws", + ExpectedException = ExpectedException.SecurityTokenInvalidSigningKeyException(substringExpected: "IDX10232:", innerTypeExpected: typeof(SecurityTokenInvalidSigningKeyException)), + SecurityKey = KeyingMaterial.SymmetricSecurityKey2_256, + SecurityToken = new JwtSecurityToken(), + ValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKeyValidatorUsingConfiguration = (SecurityKey securityKey, SecurityToken token, TokenValidationParameters validationParameters, BaseConfiguration configuration) => throw new SecurityTokenInvalidSigningKeyException() + }, + BaseConfiguration = new OpenIdConnectConfiguration(), + SigningKeyValidationResult = new SigningKeyValidationResult( + KeyingMaterial.SymmetricSecurityKey2_256, + ValidationFailureType.SigningKeyValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10232, + KeyingMaterial.SymmetricSecurityKey2_256), + typeof(SecurityTokenInvalidSigningKeyException), + new StackFrame(true), + new SecurityTokenInvalidSigningKeyException())) + }, + }; + } + } + } + + public class SigningKeyValidationTheoryData: TheoryDataBase + { + public SecurityKey SecurityKey { get; set; } + public SecurityToken SecurityToken { get; set; } + public TokenValidationParameters ValidationParameters { get; set; } + public BaseConfiguration BaseConfiguration { get; set; } + internal SigningKeyValidationResult SigningKeyValidationResult { get; set; } + } + }