Skip to content

Commit 218643b

Browse files
bgavrilMSgladjohnpmaytak
authored
Brokered Hybrid Spa (#4020)
* Brokered Hybrid Spa * wip * Change to extra params * Update src/client/Microsoft.Identity.Client/AuthenticationResult.cs Co-authored-by: Gladwin Johnson <[email protected]> * PR feedback. --------- Co-authored-by: Gladwin Johnson <[email protected]> Co-authored-by: pmaytak <[email protected]>
1 parent 8682dd6 commit 218643b

File tree

13 files changed

+227
-30
lines changed

13 files changed

+227
-30
lines changed

src/client/Microsoft.Identity.Client/AuthenticationResult.cs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ public partial class AuthenticationResult
3939
/// <param name="tokenType">The token type, defaults to Bearer. Note: this property is experimental and may change in future versions of the library.</param>
4040
/// <param name="authenticationResultMetadata">Contains metadata related to the Authentication Result.</param>
4141
/// <param name="claimsPrincipal">Claims from the ID token</param>
42-
/// <param name="spaAuthCode">Auth Code returned by the Microsoft identity platform when you use AcquireTokenByAuthorizeCode.WithSpaAuthorizationCode(). This auth code is meant to be redeemed by the frontend code.</param>
42+
/// <param name="spaAuthCode">Auth Code returned by the Microsoft identity platform when you use AcquireTokenByAuthorizationCode.WithSpaAuthorizationCode(). This auth code is meant to be redeemed by the frontend code. See https://aka.ms/msal-net/spa-auth-code</param>
43+
/// <param name="additionalResponseParameters">Other properties from the token response.</param>
4344
public AuthenticationResult( // for backwards compat with 4.16-
4445
string accessToken,
4546
bool isExtendedLifeTimeToken,
@@ -54,7 +55,8 @@ public partial class AuthenticationResult
5455
string tokenType = "Bearer",
5556
AuthenticationResultMetadata authenticationResultMetadata = null,
5657
ClaimsPrincipal claimsPrincipal = null,
57-
string spaAuthCode = null)
58+
string spaAuthCode = null,
59+
IReadOnlyDictionary<string, string> additionalResponseParameters = null)
5860
{
5961
AccessToken = accessToken;
6062
#pragma warning disable CS0618 // Type or member is obsolete
@@ -72,6 +74,7 @@ public partial class AuthenticationResult
7274
AuthenticationResultMetadata = authenticationResultMetadata;
7375
ClaimsPrincipal = claimsPrincipal;
7476
SpaAuthCode = spaAuthCode;
77+
AdditionalResponseParameters = additionalResponseParameters;
7578
}
7679

7780
/// <summary>
@@ -130,7 +133,8 @@ internal AuthenticationResult(
130133
TokenSource tokenSource,
131134
ApiEvent apiEvent,
132135
Account account,
133-
string spaAuthCode = null)
136+
string spaAuthCode,
137+
IReadOnlyDictionary<string, string> additionalResponseParameters)
134138
{
135139
_authenticationScheme = authenticationScheme ?? throw new ArgumentNullException(nameof(authenticationScheme));
136140

@@ -162,7 +166,7 @@ internal AuthenticationResult(
162166
CorrelationId = correlationID;
163167
ApiEvent = apiEvent;
164168
AuthenticationResultMetadata = new AuthenticationResultMetadata(tokenSource);
165-
169+
AdditionalResponseParameters = additionalResponseParameters;
166170
if (msalAccessTokenCacheItem != null)
167171
{
168172
AccessToken = authenticationScheme.FormatAccessToken(msalAccessTokenCacheItem);
@@ -268,6 +272,14 @@ internal AuthenticationResult() { }
268272
/// </summary>
269273
public string SpaAuthCode { get; }
270274

275+
/// <summary>
276+
/// Exposes additional response parameters returned by the token issuer (AAD).
277+
/// </summary>
278+
/// <remarks>
279+
/// Not all parameters are added here, only the ones that MSAL doesn't interpret itself and only scalars.
280+
/// </remarks>
281+
public IReadOnlyDictionary<string, string> AdditionalResponseParameters { get; }
282+
271283
/// <summary>
272284
/// All the claims present in the ID token.
273285
/// </summary>

src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ protected override async Task<AuthenticationResult> ExecuteAsync(CancellationTok
6565
AuthenticationRequestParameters.RequestContext.CorrelationId,
6666
TokenSource.Cache,
6767
AuthenticationRequestParameters.RequestContext.ApiEvent,
68-
null);
68+
account: null,
69+
spaAuthCode: null,
70+
additionalResponseParameters: null);
6971
}
7072
else
7173
{

src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ protected override async Task<AuthenticationResult> ExecuteAsync(CancellationTok
5656
AuthenticationRequestParameters.RequestContext.CorrelationId,
5757
TokenSource.Cache,
5858
AuthenticationRequestParameters.RequestContext.ApiEvent,
59-
null);
59+
account: null,
60+
spaAuthCode: null,
61+
additionalResponseParameters: null);
6062
}
6163
else
6264
{

src/client/Microsoft.Identity.Client/Internal/Requests/OnBehalfOfRequest.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,9 @@ protected override async Task<AuthenticationResult> ExecuteAsync(CancellationTok
8282
AuthenticationRequestParameters.RequestContext.CorrelationId,
8383
TokenSource.Cache,
8484
AuthenticationRequestParameters.RequestContext.ApiEvent,
85-
account);
85+
account,
86+
spaAuthCode: null,
87+
additionalResponseParameters: null);
8688
}
8789
else
8890
{

src/client/Microsoft.Identity.Client/Internal/Requests/RequestBase.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,8 @@ protected async Task<AuthenticationResult> CacheTokenResponseAndCreateAuthentica
235235
msalTokenResponse.TokenSource,
236236
AuthenticationRequestParameters.RequestContext.ApiEvent,
237237
account,
238-
msalTokenResponse.SpaAuthCode);
238+
msalTokenResponse.SpaAuthCode,
239+
msalTokenResponse.CreateExtensionDataStringMap());
239240
}
240241

241242
private void ValidateAccountIdentifiers(ClientInfo fromServer)
@@ -452,7 +453,9 @@ internal async Task<AuthenticationResult> HandleTokenRefreshErrorAsync(MsalServi
452453
AuthenticationRequestParameters.RequestContext.CorrelationId,
453454
TokenSource.Cache,
454455
AuthenticationRequestParameters.RequestContext.ApiEvent,
455-
account);
456+
account,
457+
spaAuthCode: null,
458+
additionalResponseParameters: null);
456459
}
457460

458461
logger.Warning("Either the exception does not indicate a problem with AAD or the token cache does not have an AT that is usable. ");

src/client/Microsoft.Identity.Client/Internal/Requests/Silent/CacheSilentStrategy.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,9 @@ private async Task<AuthenticationResult> CreateAuthenticationResultAsync(MsalAcc
160160
AuthenticationRequestParameters.RequestContext.CorrelationId,
161161
TokenSource.Cache,
162162
AuthenticationRequestParameters.RequestContext.ApiEvent,
163-
account);
163+
account,
164+
spaAuthCode: null,
165+
additionalResponseParameters: null);
164166
}
165167

166168
private async Task<MsalTokenResponse> TryGetTokenUsingFociAsync(CancellationToken cancellationToken)

src/client/Microsoft.Identity.Client/OAuth2/MsalTokenResponse.cs

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using JsonProperty = System.Text.Json.Serialization.JsonPropertyNameAttribute;
2020
#else
2121
using Microsoft.Identity.Json;
22+
using Microsoft.Identity.Json.Linq;
2223
#endif
2324

2425
namespace Microsoft.Identity.Client.OAuth2
@@ -39,13 +40,16 @@ internal class TokenResponseClaim : OAuth2ResponseBaseClaim
3940
public const string Authority = "authority";
4041
public const string FamilyId = "foci";
4142
public const string RefreshIn = "refresh_in";
42-
public const string SpaCode = "spa_code";
4343
public const string ErrorSubcode = "error_subcode";
4444
public const string ErrorSubcodeCancel = "cancel";
4545

4646
public const string TenantId = "tenant_id";
4747
public const string Upn = "username";
4848
public const string LocalAccountId = "local_account_id";
49+
50+
// Hybrid SPA - see https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/3994
51+
public const string SpaCode = "spa_code";
52+
4953
}
5054

5155
[JsonObject]
@@ -59,6 +63,59 @@ public MsalTokenResponse()
5963

6064
private const string iOSBrokerErrorMetadata = "error_metadata";
6165
private const string iOSBrokerHomeAccountId = "home_account_id";
66+
67+
// All properties not explicitly defined are added to this dictionary
68+
// See JSON overflow https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/handle-overflow?pivots=dotnet-7-0
69+
#if SUPPORTS_SYSTEM_TEXT_JSON
70+
[JsonExtensionData]
71+
public Dictionary<string, JsonElement> ExtensionData { get; set; }
72+
#else
73+
[JsonExtensionData]
74+
public Dictionary<string, JToken> ExtensionData { get; set; }
75+
#endif
76+
77+
// Exposes only scalar properties from ExtensionData
78+
public Dictionary<string, string> CreateExtensionDataStringMap()
79+
{
80+
if (ExtensionData == null || ExtensionData.Count == 0)
81+
{
82+
return null;
83+
}
84+
85+
Dictionary<string, string> stringExtensionData = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
86+
87+
#if SUPPORTS_SYSTEM_TEXT_JSON
88+
foreach (KeyValuePair<string, JsonElement> item in ExtensionData)
89+
{
90+
if (item.Value.ValueKind == JsonValueKind.String ||
91+
item.Value.ValueKind == JsonValueKind.Number ||
92+
item.Value.ValueKind == JsonValueKind.True ||
93+
item.Value.ValueKind == JsonValueKind.False ||
94+
item.Value.ValueKind == JsonValueKind.Null)
95+
{
96+
stringExtensionData.Add(item.Key, item.Value.ToString());
97+
}
98+
}
99+
#else
100+
foreach (KeyValuePair<string, JToken> item in ExtensionData)
101+
{
102+
if (item.Value.Type == JTokenType.String ||
103+
item.Value.Type == JTokenType.Uri ||
104+
item.Value.Type == JTokenType.Boolean ||
105+
item.Value.Type == JTokenType.Date ||
106+
item.Value.Type == JTokenType.Float ||
107+
item.Value.Type == JTokenType.Guid ||
108+
item.Value.Type == JTokenType.Integer ||
109+
item.Value.Type == JTokenType.TimeSpan ||
110+
item.Value.Type == JTokenType.Null)
111+
{
112+
stringExtensionData.Add(item.Key, item.Value.ToString());
113+
}
114+
}
115+
#endif
116+
return stringExtensionData;
117+
}
118+
62119
[JsonProperty(TokenResponseClaim.TokenType)]
63120
public string TokenType { get; set; }
64121

src/client/Microsoft.Identity.Client/Utils/JsonHelper.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ internal static T DeserializeFromJson<T>(string json)
4040
#if SUPPORTS_SYSTEM_TEXT_JSON
4141
return (T)JsonSerializer.Deserialize(json, typeof(T), MsalJsonSerializerContext.Custom);
4242
#else
43-
return JsonConvert.DeserializeObject<T>(json);
43+
44+
return JsonConvert.DeserializeObject<T>(json, new JsonSerializerSettings() {
45+
DateParseHandling = DateParseHandling.None, // Newtonsoft tries to be smart about dates, but System.Text.Json does not
46+
});
4447
#endif
4548
}
4649

src/client/Microsoft.Identity.Client/json/Serialization/DefaultContractResolver.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -582,7 +582,7 @@ internal class EnumerableDictionaryWrapper<TEnumeratorKey, TEnumeratorValue> : I
582582
{
583583
private readonly IEnumerable<KeyValuePair<TEnumeratorKey, TEnumeratorValue>> _e;
584584

585-
internal EnumerableDictionaryWrapper(IEnumerable<KeyValuePair<TEnumeratorKey, TEnumeratorValue>> e)
585+
public EnumerableDictionaryWrapper(IEnumerable<KeyValuePair<TEnumeratorKey, TEnumeratorValue>> e)
586586
{
587587
ValidationUtils.ArgumentNotNull(e, nameof(e));
588588
_e = e;

tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Net;
88
using System.Net.Http;
99
using System.Net.Http.Headers;
10+
using System.Xml;
1011
using Microsoft.Identity.Client.Utils;
1112
using Microsoft.Identity.Test.Unit;
1213

@@ -60,8 +61,7 @@ public static string GetDefaultTokenResponse(string accessToken = TestConstants.
6061
"\"r1/scope1 r1/scope2\",\"access_token\":\"" + accessToken + "\"" +
6162
",\"refresh_token\":\"" + Guid.NewGuid() + "\",\"client_info\"" +
6263
":\"" + CreateClientInfo() + "\",\"id_token\"" +
63-
":\"" + CreateIdToken(TestConstants.UniqueId, TestConstants.DisplayableId) +
64-
"\",\"id_token_expires_in\":\"3600\"}";
64+
":\"" + CreateIdToken(TestConstants.UniqueId, TestConstants.DisplayableId) + "\"}";
6565
}
6666

6767
public static string GetPopTokenResponse()
@@ -87,6 +87,18 @@ public static string GetHybridSpaTokenResponse(string spaCode)
8787
",\"id_token_expires_in\":\"3600\"}";
8888
}
8989

90+
public static string GetBridgedHybridSpaTokenResponse(string spaAccountId)
91+
{
92+
return
93+
"{\"token_type\":\"Bearer\",\"expires_in\":\"3599\",\"refresh_in\":\"2400\",\"scope\":" +
94+
"\"r1/scope1 r1/scope2\",\"access_token\":\"" + TestConstants.ATSecret + "\"" +
95+
",\"refresh_token\":\"" + Guid.NewGuid() + "\",\"client_info\"" +
96+
":\"" + CreateClientInfo() + "\",\"id_token\"" +
97+
":\"" + CreateIdToken(TestConstants.UniqueId, TestConstants.DisplayableId) +
98+
"\",\"spa_accountId\":\"" + spaAccountId + "\"" +
99+
",\"id_token_expires_in\":\"3600\"}";
100+
}
101+
90102
public static string GetMsiSuccessfulResponse()
91103
{
92104
string expiresOn = DateTimeHelpers.DateTimeToUnixTimestamp(DateTime.UtcNow.AddHours(1));
@@ -311,17 +323,30 @@ public static HttpResponseMessage CreateSuccessTokenResponseMessage(
311323
string accessToken = "some-access-token",
312324
string refreshToken = "OAAsomethingencrypedQwgAA")
313325
{
314-
string idToken = CreateIdToken(uniqueId, displayableId, TestConstants.Utid);
315326
HttpResponseMessage responseMessage = new HttpResponseMessage(HttpStatusCode.OK);
327+
string stringContent = CreateSuccessTokenResponseString(uniqueId, displayableId, scope, foci, utid, accessToken, refreshToken);
328+
HttpContent content = new StringContent(stringContent);
329+
responseMessage.Content = content;
330+
return responseMessage;
331+
}
332+
333+
public static string CreateSuccessTokenResponseString(string uniqueId,
334+
string displayableId,
335+
string[] scope,
336+
bool foci = false,
337+
string utid = TestConstants.Utid,
338+
string accessToken = "some-access-token",
339+
string refreshToken = "OAAsomethingencrypedQwgAA")
340+
{
341+
string idToken = CreateIdToken(uniqueId, displayableId, TestConstants.Utid);
316342
string stringContent = "{\"token_type\":\"Bearer\",\"expires_in\":\"3599\",\"refresh_in\":\"2400\",\"scope\":\"" +
317343
scope.AsSingleString() +
318344
"\",\"access_token\":\"" + accessToken + "\",\"refresh_token\":\"" + refreshToken + "\",\"id_token\":\"" +
319345
idToken +
320346
(foci ? "\",\"foci\":\"1" : "") +
321347
"\",\"id_token_expires_in\":\"3600\",\"client_info\":\"" + CreateClientInfo(uniqueId, utid) + "\"}";
322-
HttpContent content = new StringContent(stringContent);
323-
responseMessage.Content = content;
324-
return responseMessage;
348+
349+
return stringContent;
325350
}
326351

327352
public static string CreateIdToken(string uniqueId, string displayableId)

0 commit comments

Comments
 (0)