Skip to content

Commit 7a8237e

Browse files
committed
Finalize implementaton and ut
1 parent 3701e17 commit 7a8237e

File tree

3 files changed

+152
-25
lines changed

3 files changed

+152
-25
lines changed

src/Microsoft.Identity.Web.TokenAcquisition/MsAuth10AtPop.cs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.IdentityModel.Tokens.Jwt;
67
using System.Security.Cryptography.X509Certificates;
78
using System.Threading.Tasks;
89
using Microsoft.Identity.Client;
@@ -68,19 +69,19 @@ internal static AcquireTokenForClientParameterBuilder WithAtPop(
6869
SigningCredentials = signingCredentials
6970
};
7071

72+
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
73+
JwtSecurityToken token = (JwtSecurityToken)tokenHandler.CreateToken(securityTokenDescriptor);
74+
7175
if (sendX5C)
7276
{
73-
if (signingCredentials.Key is X509SecurityKey x509SecurityKey)
74-
{
75-
string x5cValue = Convert.ToBase64String(x509SecurityKey.Certificate.GetRawCertData());
76-
77-
// Does not seem to work?!
78-
securityTokenDescriptor.AdditionalHeaderClaims = new Dictionary<string, object>() { { "x5c", x5cValue } };
79-
}
77+
string exportedCertificate = Convert.ToBase64String(certificate.Export(X509ContentType.Cert));
78+
token.Header.Add(
79+
IdentityModel.JsonWebTokens.JwtHeaderParameterNames.X5c,
80+
new List<string> { exportedCertificate });
8081
}
8182

82-
var handler = new JsonWebTokenHandler();
83-
return handler.CreateToken(securityTokenDescriptor);
83+
string stringToken = tokenHandler.WriteToken(token);
84+
return stringToken;
8485
}
8586
}
8687
}

tests/Microsoft.Identity.Web.Test.Common/Mocks/MockHttpMessageHandler.cs

Lines changed: 108 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22
// Licensed under the MIT License.
33

44
using System;
5+
using System.Collections.Generic;
6+
using System.Globalization;
57
using System.Net;
68
using System.Net.Http;
79
using System.Threading;
810
using System.Threading.Tasks;
11+
using System.Web;
12+
using Azure;
913
using Xunit;
1014

1115
namespace Microsoft.Identity.Web.Test.Common.Mocks
@@ -32,8 +36,9 @@ public MockHttpMessageHandler()
3236
/// Once the http message is executed, this property holds the request message.
3337
/// </summary>
3438
public HttpRequestMessage ActualRequestMessage { get; private set; }
39+
public Dictionary<string, string> ActualRequestPostData { get; private set; }
3540

36-
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
41+
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
3742
{
3843
ActualRequestMessage = request;
3944

@@ -62,7 +67,7 @@ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage reques
6267
Content = new StringContent(TestConstants.DiscoveryJsonResponse),
6368
};
6469

65-
return Task.FromResult(responseMessage);
70+
return responseMessage;
6671
}
6772
}
6873

@@ -81,10 +86,109 @@ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage reques
8186

8287
if (request.Method != HttpMethod.Get && request.Content != null)
8388
{
84-
string postData = request.Content.ReadAsStringAsync().Result;
89+
string postData = await request.Content.ReadAsStringAsync();
90+
ActualRequestPostData = QueryStringParser.ParseKeyValueList(postData, '&', true, false);
91+
92+
}
93+
94+
return ResponseMessage;
95+
}
96+
}
97+
98+
public static class QueryStringParser
99+
{
100+
public static Dictionary<string, string> ParseKeyValueList(string input, char delimiter, bool urlDecode,
101+
bool lowercaseKeys)
102+
{
103+
var response = new Dictionary<string, string>();
104+
105+
var queryPairs = SplitWithQuotes(input, delimiter);
106+
107+
foreach (string queryPair in queryPairs)
108+
{
109+
var pair = SplitWithQuotes(queryPair, '=');
110+
111+
if (pair.Count == 2 && !string.IsNullOrWhiteSpace(pair[0]) && !string.IsNullOrWhiteSpace(pair[1]))
112+
{
113+
string key = pair[0];
114+
string value = pair[1];
115+
116+
// Url decoding is needed for parsing OAuth response, but not for parsing WWW-Authenticate header in 401 challenge
117+
if (urlDecode)
118+
{
119+
key = UrlDecode(key);
120+
value = UrlDecode(value);
121+
}
122+
123+
if (lowercaseKeys)
124+
{
125+
key = key.Trim().ToLowerInvariant();
126+
}
127+
128+
value = value.Trim().Trim('\"').Trim();
129+
130+
response[key] = value;
131+
}
132+
}
133+
134+
return response;
135+
}
136+
137+
public static Dictionary<string, string> ParseKeyValueList(string input, char delimiter, bool urlDecode)
138+
{
139+
return ParseKeyValueList(input, delimiter, urlDecode, true);
140+
}
141+
142+
private static string UrlDecode(string message)
143+
{
144+
if (string.IsNullOrEmpty(message))
145+
{
146+
return message;
147+
}
148+
149+
message = message.Replace("+", "%20");
150+
message = Uri.UnescapeDataString(message);
151+
152+
return message;
153+
}
154+
155+
internal static IReadOnlyList<string> SplitWithQuotes(string input, char delimiter)
156+
{
157+
if (string.IsNullOrWhiteSpace(input))
158+
{
159+
return Array.Empty<string>();
160+
}
161+
162+
var items = new List<string>();
163+
164+
int startIndex = 0;
165+
bool insideString = false;
166+
string item;
167+
for (int i = 0; i < input.Length; i++)
168+
{
169+
if (input[i] == delimiter && !insideString)
170+
{
171+
item = input.Substring(startIndex, i - startIndex);
172+
if (!string.IsNullOrWhiteSpace(item.Trim()))
173+
{
174+
items.Add(item);
175+
}
176+
177+
startIndex = i + 1;
178+
}
179+
else if (input[i] == '"')
180+
{
181+
insideString = !insideString;
182+
}
183+
}
184+
185+
item = input.Substring(startIndex);
186+
if (!string.IsNullOrWhiteSpace(item.Trim()))
187+
{
188+
items.Add(item);
85189
}
86190

87-
return Task.FromResult(ResponseMessage);
191+
return items;
88192
}
89193
}
90194
}

tests/Microsoft.Identity.Web.Test/MsAuth10AtPopTests.cs

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22
// Licensed under the MIT License.
33

44
using System;
5+
using System.Collections.Generic;
6+
using System.IdentityModel.Tokens.Jwt;
7+
using System.Linq;
58
using System.Security.Cryptography.X509Certificates;
69
using System.Threading.Tasks;
10+
using Microsoft.Graph;
711
using Microsoft.Identity.Client;
812
using Microsoft.Identity.Web.Test.Common;
913
using Microsoft.Identity.Web.Test.Common.Mocks;
@@ -17,14 +21,13 @@ public class MsAuth10AtPopTests
1721
public async Task MsAuth10AtPop_WithAtPop_ShouldPopulateBuilderWithProofOfPosessionKeyIdAndOnBeforeTokenRequestTestAsync()
1822
{
1923
// Arrange
20-
2124
MockHttpClientFactory mockHttpClientFactory = new MockHttpClientFactory();
22-
var tokenHandler = MockHttpCreator.CreateClientCredentialTokenHandler();
25+
var httpTokenRequest = MockHttpCreator.CreateClientCredentialTokenHandler();
2326
mockHttpClientFactory.AddMockHandler(MockHttpCreator.CreateInstanceDiscoveryMockHandler());
24-
mockHttpClientFactory.AddMockHandler(tokenHandler);
27+
mockHttpClientFactory.AddMockHandler(httpTokenRequest);
2528

2629
var certificateDescription = CertificateDescription.FromBase64Encoded(
27-
TestConstants.CertificateX5cWithPrivateKey,
30+
TestConstants.CertificateX5cWithPrivateKey,
2831
TestConstants.CertificateX5cWithPrivateKeyPassword);
2932
ICertificateLoader loader = new DefaultCertificateLoader();
3033
loader.LoadIfNeeded(certificateDescription);
@@ -37,18 +40,37 @@ public async Task MsAuth10AtPop_WithAtPop_ShouldPopulateBuilderWithProofOfPosess
3740
.WithHttpClientFactory(mockHttpClientFactory)
3841
.Build();
3942

40-
43+
4144
var popPublicKey = "pop_key";
4245
var jwkClaim = "jwk_claim";
43-
var clientId = "client_id";
4446

4547
// Act
4648
AuthenticationResult result = await app.AcquireTokenForClient(new[] { TestConstants.Scopes })
47-
.WithAtPop(certificateDescription.Certificate, popPublicKey, jwkClaim, clientId, true)
49+
.WithAtPop(certificateDescription.Certificate, popPublicKey, jwkClaim, TestConstants.ClientId, true)
4850
.ExecuteAsync();
4951

5052
// Assert
51-
Assert.NotNull(result);
53+
httpTokenRequest.ActualRequestPostData.TryGetValue("request", out string? request);
54+
Assert.NotNull(request);
55+
httpTokenRequest.ActualRequestPostData.TryGetValue("client_assertion", out string? clientAssertion);
56+
Assert.Null(clientAssertion);
57+
58+
JwtSecurityTokenHandler jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
59+
JwtSecurityToken assertion = jwtSecurityTokenHandler.ReadJwtToken(request);
60+
61+
Assert.Equal("https://login.microsoftonline.com/common/oauth2/v2.0/token", assertion.Claims.Single(c => c.Type == "aud").Value);
62+
Assert.Equal(TestConstants.ClientId, assertion.Claims.Single(c => c.Type == "iss").Value);
63+
Assert.Equal(TestConstants.ClientId, assertion.Claims.Single(c => c.Type == "sub").Value);
64+
Assert.NotEmpty(assertion.Claims.Single(c => c.Type == "jti").Value);
65+
Assert.Equal(jwkClaim, assertion.Claims.Single(c => c.Type == "pop_jwk").Value);
66+
67+
assertion.Header.TryGetValue("x5c", out var x5cClaimValue);
68+
Assert.NotNull(x5cClaimValue);
69+
string actualX5c = (string)((List<object>)x5cClaimValue).Single();
70+
71+
string expectedX5C= Convert.ToBase64String(certificateDescription.Certificate.RawData);
72+
73+
Assert.Equal(expectedX5C, actualX5c);
5274
}
5375

5476
[Fact]
@@ -62,11 +84,11 @@ public void MsAuth10AtPop_ThrowsWithNullPopKeyTest()
6284

6385
// Act & Assert
6486
Assert.Throws<ArgumentNullException>(() => MsAuth10AtPop.WithAtPop(
65-
app.AcquireTokenForClient(new[] { TestConstants.Scopes }),
66-
clientCertificate,
87+
app.AcquireTokenForClient(new[] { TestConstants.Scopes }),
88+
clientCertificate,
6789
string.Empty,
68-
jwkClaim,
69-
clientId,
90+
jwkClaim,
91+
clientId,
7092
true));
7193
}
7294

0 commit comments

Comments
 (0)