Skip to content

Commit be1c621

Browse files
authored
Integrate ServerNonce validation (#1704)
1 parent e42679d commit be1c621

File tree

6 files changed

+166
-2
lines changed

6 files changed

+166
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//------------------------------------------------------------------------------
2+
//
3+
// Copyright (c) Microsoft Corporation.
4+
// All rights reserved.
5+
//
6+
// This code is licensed under the MIT License.
7+
//
8+
// Permission is hereby granted, free of charge, to any person obtaining a copy
9+
// of this software and associated documentation files(the "Software"), to deal
10+
// in the Software without restriction, including without limitation the rights
11+
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
12+
// copies of the Software, and to permit persons to whom the Software is
13+
// furnished to do so, subject to the following conditions :
14+
//
15+
// The above copyright notice and this permission notice shall be included in
16+
// all copies or substantial portions of the Software.
17+
//
18+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
21+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24+
// THE SOFTWARE.
25+
//
26+
//------------------------------------------------------------------------------
27+
28+
using System;
29+
using System.Collections.Generic;
30+
using System.Runtime.Serialization;
31+
32+
namespace Microsoft.IdentityModel.Protocols.SignedHttpRequest
33+
{
34+
/// <summary>
35+
/// This exception is thrown when a SignedHttpRequest handler encounters an error during 'Nonce' claim resolution.
36+
/// </summary>
37+
[Serializable]
38+
public class SignedHttpRequestInvalidNonceClaimException : SignedHttpRequestValidationException
39+
{
40+
/// <summary>
41+
/// Initializes a new instance of the <see cref="SignedHttpRequestInvalidNonceClaimException"/> class.
42+
/// </summary>
43+
public SignedHttpRequestInvalidNonceClaimException()
44+
{
45+
}
46+
47+
/// <summary>
48+
/// Initializes a new instance of the <see cref="SignedHttpRequestInvalidNonceClaimException"/> class.
49+
/// </summary>
50+
/// <param name="message">Additional information to be included in the exception and displayed to user.</param>
51+
public SignedHttpRequestInvalidNonceClaimException(string message)
52+
: base(message)
53+
{
54+
}
55+
56+
/// <summary>
57+
/// Initializes a new instance of the <see cref="SignedHttpRequestInvalidNonceClaimException"/> class.
58+
/// </summary>
59+
/// <param name="message">Additional information to be included in the exception and displayed to user.</param>
60+
/// <param name="innerException">A <see cref="Exception"/> that represents the root cause of the exception.</param>
61+
public SignedHttpRequestInvalidNonceClaimException(string message, Exception innerException)
62+
: base(message, innerException)
63+
{
64+
}
65+
66+
/// <summary>
67+
/// Initializes a new instance of the <see cref="SignedHttpRequestInvalidNonceClaimException"/> class.
68+
/// </summary>
69+
/// <param name="info">the <see cref="SerializationInfo"/> that holds the serialized object data.</param>
70+
/// <param name="context">The contextual information about the source or destination.</param>
71+
protected SignedHttpRequestInvalidNonceClaimException(SerializationInfo info, StreamingContext context)
72+
: base(info, context)
73+
{
74+
}
75+
76+
/// <summary>
77+
/// Gets or sets an <see cref="IDictionary{String, Object}"/> that enables custom extensibility scenarios.
78+
/// </summary>
79+
public IDictionary<string, object> PropertyBag { get; set; }
80+
}
81+
}

src/Microsoft.IdentityModel.Protocols.SignedHttpRequest/GlobalSuppressions.cs

+1
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@
1111
[assembly: SuppressMessage("Performance", "CA1825: Avoid zero-length array allocations", Justification = "net45 target doesn't support Array.Empty")]
1212
[assembly: SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "Headers need to be lowercase to calcuate appropriate hash", Scope = "type", Target = "~T:Microsoft.IdentityModel.Protocols.SignedHttpRequest.SignedHttpRequestHandler")]
1313
[assembly: SuppressMessage("Design", "CA1001:Types that own disposable fields should be disposable", Justification = "Breaking change", Scope = "type", Target = "~T:Microsoft.IdentityModel.Protocols.SignedHttpRequest.SignedHttpRequestHandler")]
14+
[assembly: SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "<Pending>", Scope = "member", Target = "~P:Microsoft.IdentityModel.Protocols.SignedHttpRequest.SignedHttpRequestInvalidNonceClaimException.PropertyBag")]

src/Microsoft.IdentityModel.Protocols.SignedHttpRequest/LogMessages.cs

+1
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,6 @@ internal static class LogMessages
6969
public const string IDX23033 = "IDX23033: Unable to validate the 'cnf' claim reference. Thumbprint of the JWK used to sign the SignedHttpRequest (root 'cnf' claim) does not match the expected thumbprint ('at' -> 'cnf' -> 'kid'). Expected value: '{0}', actual value: '{1}'. Root 'cnf' claim value: '{2}'. For more details, see https://aka.ms/IdentityModel/SignedHttpRequest.";
7070
public const string IDX23034 = "IDX23034: Signed http request signature validation failed. SignedHttpRequest: '{0}'";
7171
public const string IDX23035 = "IDX23035: Unable to resolve a PoP key from the 'jku' claim. Multiple keys are found in the referenced JWK Set document and the 'cnf' claim doesn't contain a 'kid' value.";
72+
public const string IDX23036 = "IDX23036: Signed http request nonce validation failed. Exceptions caught: '{0}'.";
7273
}
7374
}

src/Microsoft.IdentityModel.Protocols.SignedHttpRequest/SignedHttpRequestHandler.cs

+24
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,9 @@ public async Task<SignedHttpRequestValidationResult> ValidateSignedHttpRequestAs
483483
// validate signed http request signature
484484
signedHttpRequest.SigningKey = await ValidateSignatureAsync(signedHttpRequest, popKey, signedHttpRequestValidationContext, cancellationToken).ConfigureAwait(false);
485485

486+
// validate nonce claim
487+
ValidateNonceAsync(signedHttpRequest, popKey, signedHttpRequestValidationContext, cancellationToken);
488+
486489
// validate signed http request payload
487490
var validatedSignedHttpRequest = await ValidateSignedHttpRequestPayloadAsync(signedHttpRequest, signedHttpRequestValidationContext, cancellationToken).ConfigureAwait(false);
488491

@@ -648,6 +651,27 @@ internal virtual async Task<SecurityKey> ValidateSignatureAsync(JsonWebToken sig
648651
throw LogHelper.LogExceptionMessage(new SignedHttpRequestInvalidSignatureException(LogHelper.FormatInvariant(LogMessages.IDX23034, signedHttpRequest.EncodedToken)));
649652
}
650653

654+
/// <summary>
655+
/// Validates the nonce claim of the signed http request.
656+
/// </summary>
657+
/// <param name="signedHttpRequest">A SignedHttpRequest.</param>
658+
/// <param name="popKey">A Pop key used to validate the signed http request nonce signature.</param>
659+
/// <param name="signedHttpRequestValidationContext">A structure that wraps parameters needed for SignedHttpRequest validation.</param>
660+
/// <param name="cancellationToken">Propagates notification that operations should be canceled.</param>
661+
internal virtual void ValidateNonceAsync(JsonWebToken signedHttpRequest, SecurityKey popKey, SignedHttpRequestValidationContext signedHttpRequestValidationContext, CancellationToken cancellationToken)
662+
{
663+
try
664+
{
665+
if (signedHttpRequestValidationContext.SignedHttpRequestValidationParameters.NonceValidatorAsync != null)
666+
if (!signedHttpRequestValidationContext.SignedHttpRequestValidationParameters.NonceValidatorAsync(popKey, signedHttpRequest, signedHttpRequestValidationContext, cancellationToken))
667+
throw LogHelper.LogExceptionMessage(new SignedHttpRequestInvalidNonceClaimException("SignedHttpRequest nonce validation failed."));
668+
}
669+
catch (Exception ex)
670+
{
671+
throw LogHelper.LogExceptionMessage(new SignedHttpRequestInvalidNonceClaimException(LogHelper.FormatInvariant(LogMessages.IDX23036, ex.ToString()), ex));
672+
}
673+
}
674+
651675
/// <summary>
652676
/// Validates the signed http request lifetime ("ts").
653677
/// </summary>

src/Microsoft.IdentityModel.Protocols.SignedHttpRequest/SignedHttpRequestValidationParameters.cs

+16-1
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,22 @@ namespace Microsoft.IdentityModel.Protocols.SignedHttpRequest
7474
/// <summary>
7575
/// A delegate that will be called to check if SignedHttpRequest is replayed, if set.
7676
/// </summary>
77-
/// <param name="signedHttpRequest">A SignedHttpRequest.</param>
77+
/// <param name="signedHttpRequest">A SignedHttpRequest which contains the 'nonce' claim to validate.</param>
7878
/// <param name="signedHttpRequestValidationContext">A structure that wraps parameters needed for SignedHttpRequest validation.</param>
7979
/// <param name="cancellationToken">Propagates notification that operations should be canceled.</param>
8080
/// <returns>Expected to throw an appropriate exception if SignedHttpRequest replay is detected.</returns>
8181
public delegate Task ReplayValidatorAsync(SecurityToken signedHttpRequest, SignedHttpRequestValidationContext signedHttpRequestValidationContext, CancellationToken cancellationToken);
8282

83+
/// <summary>
84+
/// A delegate that will take control over SignedHttpRequest nonce validation, if set.
85+
/// </summary>
86+
/// <param name="key">the key use to validate server nonce.</param>
87+
/// <param name="signedHttpRequest">A SignedHttpRequest.</param>
88+
/// <param name="signedHttpRequestValidationContext">A structure that wraps parameters needed for SignedHttpRequest validation.</param>
89+
/// <param name="cancellationToken">Propagates notification that operations should be canceled.</param>
90+
/// <returns>Expected to throw an appropriate exception if SignedHttpRequest replay is detected.</returns>
91+
public delegate bool NonceValidatorAsync(SecurityKey key, SecurityToken signedHttpRequest, SignedHttpRequestValidationContext signedHttpRequestValidationContext, CancellationToken cancellationToken);
92+
8393
/// <summary>
8494
/// A delegate that will take control over SignedHttpRequest signature validation, if set.
8595
/// </summary>
@@ -142,6 +152,11 @@ public class SignedHttpRequestValidationParameters
142152
/// <remarks>https://tools.ietf.org/html/rfc7800#section-3.5</remarks>
143153
public HttpClientProvider HttpClientProvider { get; set; }
144154

155+
/// <summary>
156+
/// Gets or sets the <see cref="NonceValidatorAsync"/> delegate.
157+
/// </summary>
158+
public NonceValidatorAsync NonceValidatorAsync { get; set; }
159+
145160
/// <summary>
146161
/// Gets or sets the <see cref="PopKeyResolverAsync"/> delegate.
147162
/// </summary>

test/Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests/SignedHttpRequestValidationTests.cs

+43-1
Original file line numberDiff line numberDiff line change
@@ -1683,8 +1683,50 @@ public static TheoryData<ValidateSignedHttpRequestTheoryData> ValidateSignedHttp
16831683
ValidatedSignedHttpRequest = signedHttpRequest,
16841684
},
16851685
TestId = "ValidTest",
1686+
},
1687+
new ValidateSignedHttpRequestTheoryData
1688+
{
1689+
SignedHttpRequestToken = signedHttpRequest,
1690+
SignedHttpRequestValidationParameters = new SignedHttpRequestValidationParameters()
1691+
{
1692+
NonceValidatorAsync = (popKey, signedHttpRequestToken, signedHttpRequestValidationContext, cancellationToken) => false
1693+
},
1694+
ExpectedException = new ExpectedException(typeof(SignedHttpRequestInvalidNonceClaimException), "IDX23036", typeof(SignedHttpRequestInvalidNonceClaimException)),
1695+
TestId = "InValidNonceValidationFailed"
1696+
},
1697+
new ValidateSignedHttpRequestTheoryData
1698+
{
1699+
SignedHttpRequestToken = signedHttpRequest,
1700+
SignedHttpRequestValidationParameters = new SignedHttpRequestValidationParameters()
1701+
{
1702+
ValidateB = false,
1703+
ValidateH = false,
1704+
ValidateM = false,
1705+
ValidateP = false,
1706+
ValidateQ = false,
1707+
ValidateTs = false,
1708+
ValidateU = false,
1709+
NonceValidatorAsync = (popKey, signedHttpRequestToken, signedHttpRequestValidationContext, cancellationToken) =>
1710+
{
1711+
var jwtSignedHttpRequest = signedHttpRequestToken as JsonWebToken;
1712+
var nonce = jwtSignedHttpRequest.GetPayloadValue<string>(SignedHttpRequestClaimTypes.Nonce);
1713+
return nonce == SignedHttpRequestTestUtils.DefaultSignedHttpRequestPayload.GetValue(SignedHttpRequestClaimTypes.Nonce).ToString();
1714+
}
1715+
},
1716+
ExpectedSignedHttpRequestValidationResult = new SignedHttpRequestValidationResult()
1717+
{
1718+
AccessTokenValidationResult = new TokenValidationResult()
1719+
{
1720+
IsValid = true,
1721+
SecurityToken = validatedToken,
1722+
ClaimsIdentity = resultingClaimsIdentity
1723+
},
1724+
IsValid = true,
1725+
SignedHttpRequest = signedHttpRequest.EncodedToken,
1726+
ValidatedSignedHttpRequest = signedHttpRequest,
1727+
},
1728+
TestId = "ValidTestWithNonce"
16861729
}
1687-
16881730
};
16891731
}
16901732
}

0 commit comments

Comments
 (0)