Skip to content

Commit c9744b7

Browse files
trwalketrwalke
andauthored
Adding fix for OBO cache key error in long running OBO with DefaultAuthorizationHeaderProvider (#3381)
* Adding fix for OBO cache key error * Refactoring * Refactoring, updating test cases * Adding LRObo mock response * Fixing test issue * Adding new test --------- Co-authored-by: trwalke <[email protected]>
1 parent 56765f8 commit c9744b7

File tree

3 files changed

+299
-5
lines changed

3 files changed

+299
-5
lines changed

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

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,16 @@ public async Task<string> CreateAuthorizationHeaderForUserAsync(
2727
ClaimsPrincipal? claimsPrincipal = null,
2828
CancellationToken cancellationToken = default)
2929
{
30+
var newTokenAcquisitionOptions = CreateTokenAcquisitionOptionsFromApiOptions(downstreamApiOptions, cancellationToken);
3031
var result = await _tokenAcquisition.GetAuthenticationResultForUserAsync(
3132
scopes,
3233
downstreamApiOptions?.AcquireTokenOptions.AuthenticationOptionsName,
3334
downstreamApiOptions?.AcquireTokenOptions.Tenant,
3435
downstreamApiOptions?.AcquireTokenOptions.UserFlow,
3536
claimsPrincipal,
36-
CreateTokenAcquisitionOptionsFromApiOptions(downstreamApiOptions, cancellationToken)).ConfigureAwait(false);
37+
newTokenAcquisitionOptions).ConfigureAwait(false);
38+
39+
UpdateOriginalTokenAcquisitionOptions(downstreamApiOptions?.AcquireTokenOptions, newTokenAcquisitionOptions);
3740
return result.CreateAuthorizationHeader();
3841
}
3942

@@ -48,6 +51,7 @@ public async Task<string> CreateAuthorizationHeaderForAppAsync(
4851
downstreamApiOptions?.AcquireTokenOptions.AuthenticationOptionsName,
4952
downstreamApiOptions?.AcquireTokenOptions.Tenant,
5053
CreateTokenAcquisitionOptionsFromApiOptions(downstreamApiOptions, cancellationToken)).ConfigureAwait(false);
54+
5155
return result.CreateAuthorizationHeader();
5256
}
5357

@@ -59,6 +63,7 @@ public async Task<string> CreateAuthorizationHeaderAsync(
5963
CancellationToken cancellationToken = default)
6064
{
6165
Client.AuthenticationResult result;
66+
var newTokenAcquisitionOptions = CreateTokenAcquisitionOptionsFromApiOptions(downstreamApiOptions, cancellationToken);
6267

6368
// Previously, with the API name we were able to distinguish between app and user token acquisition
6469
// This context is missing in the new API, so can we enforce that downstreamApiOptions.RequestAppToken
@@ -75,8 +80,7 @@ public async Task<string> CreateAuthorizationHeaderAsync(
7580
scopes.FirstOrDefault()!,
7681
downstreamApiOptions?.AcquireTokenOptions.AuthenticationOptionsName,
7782
downstreamApiOptions?.AcquireTokenOptions.Tenant,
78-
CreateTokenAcquisitionOptionsFromApiOptions(downstreamApiOptions, cancellationToken)).ConfigureAwait(false);
79-
return result.CreateAuthorizationHeader();
83+
newTokenAcquisitionOptions).ConfigureAwait(false);
8084
}
8185
else
8286
{
@@ -86,9 +90,11 @@ public async Task<string> CreateAuthorizationHeaderAsync(
8690
downstreamApiOptions?.AcquireTokenOptions?.Tenant,
8791
downstreamApiOptions?.AcquireTokenOptions?.UserFlow,
8892
claimsPrincipal,
89-
CreateTokenAcquisitionOptionsFromApiOptions(downstreamApiOptions, cancellationToken)).ConfigureAwait(false);
90-
return result.CreateAuthorizationHeader();
93+
newTokenAcquisitionOptions).ConfigureAwait(false);
9194
}
95+
96+
UpdateOriginalTokenAcquisitionOptions(downstreamApiOptions?.AcquireTokenOptions, newTokenAcquisitionOptions);
97+
return result.CreateAuthorizationHeader();
9298
}
9399

94100
private static TokenAcquisitionOptions CreateTokenAcquisitionOptionsFromApiOptions(
@@ -113,5 +119,17 @@ private static TokenAcquisitionOptions CreateTokenAcquisitionOptionsFromApiOptio
113119
FmiPath = downstreamApiOptions?.AcquireTokenOptions.FmiPath,
114120
};
115121
}
122+
123+
/// <summary>
124+
/// Since AcquireTokenOptions is recreated, we need to update the original TokenAcquisitionOptions wth the parameters that were
125+
/// updated in the new TokenAcquisitionOptions.
126+
/// </summary>
127+
private void UpdateOriginalTokenAcquisitionOptions(AcquireTokenOptions? acquireTokenOptions, TokenAcquisitionOptions newTokenAcquisitionOptions)
128+
{
129+
if (acquireTokenOptions is not null && newTokenAcquisitionOptions is not null)
130+
{
131+
acquireTokenOptions.LongRunningWebApiSessionKey = newTokenAcquisitionOptions.LongRunningWebApiSessionKey;
132+
}
133+
}
116134
}
117135
}

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,38 @@ private static HttpResponseMessage CreateSuccessfulClientCredentialTokenResponse
2121
"{\"token_type\":\"" + tokenType + "\",\"expires_in\":" + expiry + ",\"client_info\":\"" + CreateClientInfo() + "\",\"access_token\":\"" + token + "\"}");
2222
}
2323

24+
public static HttpResponseMessage GetLrOboTokenResponse(string scopes, string accessToken = "header.payload.signature", string refreshToken = "header.payload.signatureRt")
25+
{
26+
return CreateSuccessResponseMessage(
27+
"{\"token_type\":\"Bearer\",\"expires_in\":\"3599\",\"refresh_in\":\"2400\",\"scope\":" +
28+
"\"" + scopes + "\",\"access_token\":\"" + accessToken + "\"" +
29+
",\"refresh_token\":\"" + refreshToken + "\",\"client_info\"" +
30+
":\"" + CreateClientInfo() + "\",\"id_token\"" +
31+
":\"" + CreateIdToken("UniqueId", "DisplayableId") + "\"}");
32+
}
33+
34+
public static string CreateIdToken(string uniqueId, string displayableId)
35+
{
36+
return CreateIdToken(uniqueId, displayableId, TestConstants.Utid);
37+
}
38+
39+
public static string CreateIdToken(string uniqueId, string displayableId, string tenantId)
40+
{
41+
string id = "{\"aud\": \"e854a4a7-6c34-449c-b237-fc7a28093d84\"," +
42+
"\"iss\": \"https://login.microsoftonline.com/6c3d51dd-f0e5-4959-b4ea-a80c4e36fe5e/v2.0/\"," +
43+
"\"iat\": 1455833828," +
44+
"\"nbf\": 1455833828," +
45+
"\"exp\": 1455837728," +
46+
"\"ipaddr\": \"131.107.159.117\"," +
47+
"\"name\": \"Marrrrrio Bossy\"," +
48+
"\"oid\": \"" + uniqueId + "\"," +
49+
"\"preferred_username\": \"" + displayableId + "\"," +
50+
"\"sub\": \"K4_SGGxKqW1SxUAmhg6C1F6VPiFzcx-Qd80ehIEdFus\"," +
51+
"\"tid\": \"" + tenantId + "\"," +
52+
"\"ver\": \"2.0\"}";
53+
return string.Format(CultureInfo.InvariantCulture, "someheader.{0}.somesignature", Base64UrlHelpers.Encode(id));
54+
}
55+
2456
public static HttpResponseMessage CreateSuccessResponseMessage(string successResponse)
2557
{
2658
HttpResponseMessage responseMessage = new HttpResponseMessage(HttpStatusCode.OK);
@@ -62,6 +94,18 @@ public static MockHttpMessageHandler CreateClientCredentialTokenHandler(
6294
return handler;
6395
}
6496

97+
public static MockHttpMessageHandler CreateLrOboTokenHandler(
98+
string scopes, string accessToken = "header.payload.signature", string refreshToken = "header.payload.signatureRt")
99+
{
100+
var handler = new MockHttpMessageHandler()
101+
{
102+
ExpectedMethod = HttpMethod.Post,
103+
ResponseMessage = GetLrOboTokenResponse(scopes, accessToken, refreshToken),
104+
};
105+
106+
return handler;
107+
}
108+
65109
public static MockHttpMessageHandler CreateHandlerToValidatePostData(
66110
HttpMethod expectedMethod,
67111
IDictionary<string, string> expectedPostData)
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Security.Claims;
8+
using System.Text;
9+
using System.Threading.Tasks;
10+
using Microsoft.Extensions.DependencyInjection;
11+
using Microsoft.Identity.Abstractions;
12+
using Microsoft.Identity.Client;
13+
using Microsoft.Identity.Web.Test.Common.Mocks;
14+
using Microsoft.Identity.Web.TestOnly;
15+
using Microsoft.IdentityModel.Tokens;
16+
using Xunit;
17+
18+
namespace Microsoft.Identity.Web.Test
19+
{
20+
public class AuthorizationHeaderProviderTests
21+
{
22+
[Fact]
23+
public async Task LongRunningSessionForDefaultAuthProviderForUserDefaultKeyTest()
24+
{
25+
// Arrange
26+
var tokenAcquirerFactory = InitTokenAcquirerFactoryForTest();
27+
IServiceProvider serviceProvider = tokenAcquirerFactory.Build();
28+
29+
IAuthorizationHeaderProvider authorizationHeaderProvider =
30+
serviceProvider.GetRequiredService<IAuthorizationHeaderProvider>();
31+
var mockHttpClient = serviceProvider.GetRequiredService<IMsalHttpClientFactory>() as MockHttpClientFactory;
32+
33+
using (mockHttpClient)
34+
{
35+
// Create a test ClaimsPrincipal
36+
var claims = new List<Claim>
37+
{
38+
new Claim(ClaimTypes.Name, "[email protected]")
39+
};
40+
41+
var identity = new CaseSensitiveClaimsIdentity(claims, "TestAuth");
42+
identity.BootstrapContext = CreateTestJwt();
43+
var claimsPrincipal = new ClaimsPrincipal(identity);
44+
45+
// Create options with LongRunningWebApiSessionKey
46+
var options = new AuthorizationHeaderProviderOptions
47+
{
48+
AcquireTokenOptions = new AcquireTokenOptions
49+
{
50+
//When this is set to Auto, the first call will set the LongRunningWebApiSessionKey in the options to
51+
//the hash of the ClaimsPrincipal's access token.
52+
LongRunningWebApiSessionKey = TokenAcquisitionOptions.LongRunningWebApiSessionKeyAuto
53+
}
54+
};
55+
56+
// Act & Assert
57+
58+
// Step 3: First call with ClaimsPrincipal to initiate LR session
59+
var scopes = new[] { "User.Read" };
60+
mockHttpClient!.AddMockHandler(MockHttpCreator.CreateLrOboTokenHandler("User.Read"));
61+
var result = await authorizationHeaderProvider.CreateAuthorizationHeaderForUserAsync(
62+
scopes,
63+
options,
64+
claimsPrincipal);
65+
66+
Assert.NotNull(result);
67+
Assert.NotEqual(options.AcquireTokenOptions.LongRunningWebApiSessionKey, TokenAcquisitionOptions.LongRunningWebApiSessionKeyAuto);
68+
string key1 = options.AcquireTokenOptions.LongRunningWebApiSessionKey;
69+
70+
// Step 4: Second call without ClaimsPrincipal should return the token from cache
71+
result = await authorizationHeaderProvider.CreateAuthorizationHeaderForUserAsync(
72+
scopes,
73+
options);
74+
75+
Assert.NotNull(result);
76+
Assert.NotEqual(options.AcquireTokenOptions.LongRunningWebApiSessionKey, TokenAcquisitionOptions.LongRunningWebApiSessionKeyAuto);
77+
Assert.Equal(key1, options.AcquireTokenOptions.LongRunningWebApiSessionKey);
78+
79+
// Step 5: First call with ClaimsPrincipal to initiate LR session for CreateAuthorizationHeaderAsync
80+
scopes = new[] { "User.Write" };
81+
mockHttpClient!.AddMockHandler(MockHttpCreator.CreateLrOboTokenHandler("User.Write"));
82+
result = await authorizationHeaderProvider.CreateAuthorizationHeaderAsync(
83+
scopes,
84+
options,
85+
claimsPrincipal);
86+
87+
Assert.NotNull(result);
88+
Assert.NotEqual(options.AcquireTokenOptions.LongRunningWebApiSessionKey, TokenAcquisitionOptions.LongRunningWebApiSessionKeyAuto);
89+
key1 = options.AcquireTokenOptions.LongRunningWebApiSessionKey;
90+
91+
// Step 6: Second call without ClaimsPrincipal should return the token from cache for CreateAuthorizationHeaderAsync
92+
result = await authorizationHeaderProvider.CreateAuthorizationHeaderAsync(
93+
scopes,
94+
options);
95+
96+
Assert.NotNull(result);
97+
Assert.NotEqual(options.AcquireTokenOptions.LongRunningWebApiSessionKey, TokenAcquisitionOptions.LongRunningWebApiSessionKeyAuto);
98+
Assert.Equal(key1, options.AcquireTokenOptions.LongRunningWebApiSessionKey);
99+
}
100+
}
101+
102+
[Fact]
103+
public async Task LongRunningSessionForDefaultAuthProviderForUserTest()
104+
{
105+
// Arrange
106+
var tokenAcquirerFactory = InitTokenAcquirerFactoryForTest();
107+
IServiceProvider serviceProvider = tokenAcquirerFactory.Build();
108+
109+
IAuthorizationHeaderProvider authorizationHeaderProvider =
110+
serviceProvider.GetRequiredService<IAuthorizationHeaderProvider>();
111+
var mockHttpClient = serviceProvider.GetRequiredService<IMsalHttpClientFactory>() as MockHttpClientFactory;
112+
113+
using (mockHttpClient)
114+
{
115+
// Create a test ClaimsPrincipal
116+
var claims = new List<Claim>
117+
{
118+
new Claim(ClaimTypes.Name, "[email protected]")
119+
};
120+
121+
var identity = new CaseSensitiveClaimsIdentity(claims, "TestAuth");
122+
identity.BootstrapContext = CreateTestJwt();
123+
var claimsPrincipal = new ClaimsPrincipal(identity);
124+
125+
// Create options with LongRunningWebApiSessionKey
126+
var options = new AuthorizationHeaderProviderOptions
127+
{
128+
AcquireTokenOptions = new AcquireTokenOptions
129+
{
130+
LongRunningWebApiSessionKey = "oboKey1"
131+
}
132+
};
133+
134+
// Act & Assert
135+
136+
// Step 3: First call with ClaimsPrincipal to initiate LR session
137+
var scopes = new[] { "User.Read" };
138+
mockHttpClient!.AddMockHandler(MockHttpCreator.CreateLrOboTokenHandler("User.Read"));
139+
var result = await authorizationHeaderProvider.CreateAuthorizationHeaderForUserAsync(
140+
scopes,
141+
options,
142+
claimsPrincipal);
143+
144+
Assert.NotNull(result);
145+
Assert.Equal("oboKey1", options.AcquireTokenOptions.LongRunningWebApiSessionKey);
146+
147+
// Step 4: Second call without ClaimsPrincipal should return the token from cache
148+
result = await authorizationHeaderProvider.CreateAuthorizationHeaderForUserAsync(
149+
scopes,
150+
options);
151+
152+
Assert.NotNull(result);
153+
Assert.Equal("oboKey1", options.AcquireTokenOptions.LongRunningWebApiSessionKey);
154+
155+
options = new AuthorizationHeaderProviderOptions
156+
{
157+
AcquireTokenOptions = new AcquireTokenOptions
158+
{
159+
LongRunningWebApiSessionKey = "oboKey2"
160+
}
161+
};
162+
163+
// Step 5: First call with ClaimsPrincipal to initiate LR session for CreateAuthorizationHeaderAsync
164+
scopes = new[] { "User.Write" };
165+
mockHttpClient!.AddMockHandler(MockHttpCreator.CreateLrOboTokenHandler("User.Write"));
166+
result = await authorizationHeaderProvider.CreateAuthorizationHeaderAsync(
167+
scopes,
168+
options,
169+
claimsPrincipal);
170+
171+
Assert.NotNull(result);
172+
Assert.Equal("oboKey2", options.AcquireTokenOptions.LongRunningWebApiSessionKey);
173+
174+
// Step 6: Second call without ClaimsPrincipal should return the token from cache for CreateAuthorizationHeaderAsync
175+
result = await authorizationHeaderProvider.CreateAuthorizationHeaderAsync(
176+
scopes,
177+
options);
178+
179+
Assert.NotNull(result);
180+
Assert.Equal("oboKey2", options.AcquireTokenOptions.LongRunningWebApiSessionKey);
181+
}
182+
}
183+
184+
private static string CreateTestJwt()
185+
{
186+
var header = new Dictionary<string, object>
187+
{
188+
{ "alg", "HS256" },
189+
{ "typ", "JWT" }
190+
};
191+
192+
var payload = new Dictionary<string, object>
193+
{
194+
{ "iss", "https://login.microsoftonline.com/test-tenant-id/v2.0" }
195+
};
196+
197+
string headerJson = System.Text.Json.JsonSerializer.Serialize(header);
198+
string payloadJson = System.Text.Json.JsonSerializer.Serialize(payload);
199+
200+
string headerBase64 = Base64UrlEncoder.Encode(headerJson);
201+
string payloadBase64 = Base64UrlEncoder.Encode(payloadJson);
202+
203+
// For testing purposes, we're using a fixed signature
204+
const string signature = "test_signature";
205+
string signatureBase64 = Base64UrlEncoder.Encode(signature);
206+
207+
return $"{headerBase64}.{payloadBase64}.{signatureBase64}";
208+
}
209+
210+
private TokenAcquirerFactory InitTokenAcquirerFactoryForTest()
211+
{
212+
TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest();
213+
TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance();
214+
tokenAcquirerFactory.Services.Configure<MicrosoftIdentityApplicationOptions>(options =>
215+
{
216+
options.Instance = "https://login.microsoftonline.com/";
217+
options.TenantId = "testTenantId";
218+
options.ClientId = "testClientId";
219+
options.ClientCredentials = [ new CredentialDescription() {
220+
SourceType = CredentialSource.ClientSecret,
221+
ClientSecret = "test-secret"
222+
}];
223+
});
224+
225+
// Add required services
226+
tokenAcquirerFactory.Services.AddSingleton<IMsalHttpClientFactory, MockHttpClientFactory>();
227+
tokenAcquirerFactory.Services.AddScoped<IAuthorizationHeaderProvider, DefaultAuthorizationHeaderProvider>();
228+
229+
return tokenAcquirerFactory;
230+
}
231+
}
232+
}

0 commit comments

Comments
 (0)