Skip to content

Commit

Permalink
Brokered Hybrid Spa (#4020)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
3 people authored Mar 30, 2023
1 parent 8682dd6 commit 218643b
Show file tree
Hide file tree
Showing 13 changed files with 227 additions and 30 deletions.
20 changes: 16 additions & 4 deletions src/client/Microsoft.Identity.Client/AuthenticationResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ public partial class AuthenticationResult
/// <param name="tokenType">The token type, defaults to Bearer. Note: this property is experimental and may change in future versions of the library.</param>
/// <param name="authenticationResultMetadata">Contains metadata related to the Authentication Result.</param>
/// <param name="claimsPrincipal">Claims from the ID token</param>
/// <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>
/// <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>
/// <param name="additionalResponseParameters">Other properties from the token response.</param>
public AuthenticationResult( // for backwards compat with 4.16-
string accessToken,
bool isExtendedLifeTimeToken,
Expand All @@ -54,7 +55,8 @@ public partial class AuthenticationResult
string tokenType = "Bearer",
AuthenticationResultMetadata authenticationResultMetadata = null,
ClaimsPrincipal claimsPrincipal = null,
string spaAuthCode = null)
string spaAuthCode = null,
IReadOnlyDictionary<string, string> additionalResponseParameters = null)
{
AccessToken = accessToken;
#pragma warning disable CS0618 // Type or member is obsolete
Expand All @@ -72,6 +74,7 @@ public partial class AuthenticationResult
AuthenticationResultMetadata = authenticationResultMetadata;
ClaimsPrincipal = claimsPrincipal;
SpaAuthCode = spaAuthCode;
AdditionalResponseParameters = additionalResponseParameters;
}

/// <summary>
Expand Down Expand Up @@ -130,7 +133,8 @@ internal AuthenticationResult(
TokenSource tokenSource,
ApiEvent apiEvent,
Account account,
string spaAuthCode = null)
string spaAuthCode,
IReadOnlyDictionary<string, string> additionalResponseParameters)
{
_authenticationScheme = authenticationScheme ?? throw new ArgumentNullException(nameof(authenticationScheme));

Expand Down Expand Up @@ -162,7 +166,7 @@ internal AuthenticationResult(
CorrelationId = correlationID;
ApiEvent = apiEvent;
AuthenticationResultMetadata = new AuthenticationResultMetadata(tokenSource);

AdditionalResponseParameters = additionalResponseParameters;
if (msalAccessTokenCacheItem != null)
{
AccessToken = authenticationScheme.FormatAccessToken(msalAccessTokenCacheItem);
Expand Down Expand Up @@ -268,6 +272,14 @@ internal AuthenticationResult() { }
/// </summary>
public string SpaAuthCode { get; }

/// <summary>
/// Exposes additional response parameters returned by the token issuer (AAD).
/// </summary>
/// <remarks>
/// Not all parameters are added here, only the ones that MSAL doesn't interpret itself and only scalars.
/// </remarks>
public IReadOnlyDictionary<string, string> AdditionalResponseParameters { get; }

/// <summary>
/// All the claims present in the ID token.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ protected override async Task<AuthenticationResult> ExecuteAsync(CancellationTok
AuthenticationRequestParameters.RequestContext.CorrelationId,
TokenSource.Cache,
AuthenticationRequestParameters.RequestContext.ApiEvent,
null);
account: null,
spaAuthCode: null,
additionalResponseParameters: null);
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ protected override async Task<AuthenticationResult> ExecuteAsync(CancellationTok
AuthenticationRequestParameters.RequestContext.CorrelationId,
TokenSource.Cache,
AuthenticationRequestParameters.RequestContext.ApiEvent,
null);
account: null,
spaAuthCode: null,
additionalResponseParameters: null);
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ protected override async Task<AuthenticationResult> ExecuteAsync(CancellationTok
AuthenticationRequestParameters.RequestContext.CorrelationId,
TokenSource.Cache,
AuthenticationRequestParameters.RequestContext.ApiEvent,
account);
account,
spaAuthCode: null,
additionalResponseParameters: null);
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,8 @@ protected async Task<AuthenticationResult> CacheTokenResponseAndCreateAuthentica
msalTokenResponse.TokenSource,
AuthenticationRequestParameters.RequestContext.ApiEvent,
account,
msalTokenResponse.SpaAuthCode);
msalTokenResponse.SpaAuthCode,
msalTokenResponse.CreateExtensionDataStringMap());
}

private void ValidateAccountIdentifiers(ClientInfo fromServer)
Expand Down Expand Up @@ -452,7 +453,9 @@ internal async Task<AuthenticationResult> HandleTokenRefreshErrorAsync(MsalServi
AuthenticationRequestParameters.RequestContext.CorrelationId,
TokenSource.Cache,
AuthenticationRequestParameters.RequestContext.ApiEvent,
account);
account,
spaAuthCode: null,
additionalResponseParameters: null);
}

logger.Warning("Either the exception does not indicate a problem with AAD or the token cache does not have an AT that is usable. ");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,9 @@ private async Task<AuthenticationResult> CreateAuthenticationResultAsync(MsalAcc
AuthenticationRequestParameters.RequestContext.CorrelationId,
TokenSource.Cache,
AuthenticationRequestParameters.RequestContext.ApiEvent,
account);
account,
spaAuthCode: null,
additionalResponseParameters: null);
}

private async Task<MsalTokenResponse> TryGetTokenUsingFociAsync(CancellationToken cancellationToken)
Expand Down
59 changes: 58 additions & 1 deletion src/client/Microsoft.Identity.Client/OAuth2/MsalTokenResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using JsonProperty = System.Text.Json.Serialization.JsonPropertyNameAttribute;
#else
using Microsoft.Identity.Json;
using Microsoft.Identity.Json.Linq;
#endif

namespace Microsoft.Identity.Client.OAuth2
Expand All @@ -39,13 +40,16 @@ internal class TokenResponseClaim : OAuth2ResponseBaseClaim
public const string Authority = "authority";
public const string FamilyId = "foci";
public const string RefreshIn = "refresh_in";
public const string SpaCode = "spa_code";
public const string ErrorSubcode = "error_subcode";
public const string ErrorSubcodeCancel = "cancel";

public const string TenantId = "tenant_id";
public const string Upn = "username";
public const string LocalAccountId = "local_account_id";

// Hybrid SPA - see https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/3994
public const string SpaCode = "spa_code";

}

[JsonObject]
Expand All @@ -59,6 +63,59 @@ public MsalTokenResponse()

private const string iOSBrokerErrorMetadata = "error_metadata";
private const string iOSBrokerHomeAccountId = "home_account_id";

// All properties not explicitly defined are added to this dictionary
// See JSON overflow https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/handle-overflow?pivots=dotnet-7-0
#if SUPPORTS_SYSTEM_TEXT_JSON
[JsonExtensionData]
public Dictionary<string, JsonElement> ExtensionData { get; set; }
#else
[JsonExtensionData]
public Dictionary<string, JToken> ExtensionData { get; set; }
#endif

// Exposes only scalar properties from ExtensionData
public Dictionary<string, string> CreateExtensionDataStringMap()
{
if (ExtensionData == null || ExtensionData.Count == 0)
{
return null;
}

Dictionary<string, string> stringExtensionData = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

#if SUPPORTS_SYSTEM_TEXT_JSON
foreach (KeyValuePair<string, JsonElement> item in ExtensionData)
{
if (item.Value.ValueKind == JsonValueKind.String ||
item.Value.ValueKind == JsonValueKind.Number ||
item.Value.ValueKind == JsonValueKind.True ||
item.Value.ValueKind == JsonValueKind.False ||
item.Value.ValueKind == JsonValueKind.Null)
{
stringExtensionData.Add(item.Key, item.Value.ToString());
}
}
#else
foreach (KeyValuePair<string, JToken> item in ExtensionData)
{
if (item.Value.Type == JTokenType.String ||
item.Value.Type == JTokenType.Uri ||
item.Value.Type == JTokenType.Boolean ||
item.Value.Type == JTokenType.Date ||
item.Value.Type == JTokenType.Float ||
item.Value.Type == JTokenType.Guid ||
item.Value.Type == JTokenType.Integer ||
item.Value.Type == JTokenType.TimeSpan ||
item.Value.Type == JTokenType.Null)
{
stringExtensionData.Add(item.Key, item.Value.ToString());
}
}
#endif
return stringExtensionData;
}

[JsonProperty(TokenResponseClaim.TokenType)]
public string TokenType { get; set; }

Expand Down
5 changes: 4 additions & 1 deletion src/client/Microsoft.Identity.Client/Utils/JsonHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ internal static T DeserializeFromJson<T>(string json)
#if SUPPORTS_SYSTEM_TEXT_JSON
return (T)JsonSerializer.Deserialize(json, typeof(T), MsalJsonSerializerContext.Custom);
#else
return JsonConvert.DeserializeObject<T>(json);

return JsonConvert.DeserializeObject<T>(json, new JsonSerializerSettings() {
DateParseHandling = DateParseHandling.None, // Newtonsoft tries to be smart about dates, but System.Text.Json does not
});
#endif
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ internal class EnumerableDictionaryWrapper<TEnumeratorKey, TEnumeratorValue> : I
{
private readonly IEnumerable<KeyValuePair<TEnumeratorKey, TEnumeratorValue>> _e;

internal EnumerableDictionaryWrapper(IEnumerable<KeyValuePair<TEnumeratorKey, TEnumeratorValue>> e)
public EnumerableDictionaryWrapper(IEnumerable<KeyValuePair<TEnumeratorKey, TEnumeratorValue>> e)
{
ValidationUtils.ArgumentNotNull(e, nameof(e));
_e = e;
Expand Down
37 changes: 31 additions & 6 deletions tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Xml;
using Microsoft.Identity.Client.Utils;
using Microsoft.Identity.Test.Unit;

Expand Down Expand Up @@ -60,8 +61,7 @@ public static string GetDefaultTokenResponse(string accessToken = TestConstants.
"\"r1/scope1 r1/scope2\",\"access_token\":\"" + accessToken + "\"" +
",\"refresh_token\":\"" + Guid.NewGuid() + "\",\"client_info\"" +
":\"" + CreateClientInfo() + "\",\"id_token\"" +
":\"" + CreateIdToken(TestConstants.UniqueId, TestConstants.DisplayableId) +
"\",\"id_token_expires_in\":\"3600\"}";
":\"" + CreateIdToken(TestConstants.UniqueId, TestConstants.DisplayableId) + "\"}";
}

public static string GetPopTokenResponse()
Expand All @@ -87,6 +87,18 @@ public static string GetHybridSpaTokenResponse(string spaCode)
",\"id_token_expires_in\":\"3600\"}";
}

public static string GetBridgedHybridSpaTokenResponse(string spaAccountId)
{
return
"{\"token_type\":\"Bearer\",\"expires_in\":\"3599\",\"refresh_in\":\"2400\",\"scope\":" +
"\"r1/scope1 r1/scope2\",\"access_token\":\"" + TestConstants.ATSecret + "\"" +
",\"refresh_token\":\"" + Guid.NewGuid() + "\",\"client_info\"" +
":\"" + CreateClientInfo() + "\",\"id_token\"" +
":\"" + CreateIdToken(TestConstants.UniqueId, TestConstants.DisplayableId) +
"\",\"spa_accountId\":\"" + spaAccountId + "\"" +
",\"id_token_expires_in\":\"3600\"}";
}

public static string GetMsiSuccessfulResponse()
{
string expiresOn = DateTimeHelpers.DateTimeToUnixTimestamp(DateTime.UtcNow.AddHours(1));
Expand Down Expand Up @@ -311,17 +323,30 @@ public static HttpResponseMessage CreateSuccessTokenResponseMessage(
string accessToken = "some-access-token",
string refreshToken = "OAAsomethingencrypedQwgAA")
{
string idToken = CreateIdToken(uniqueId, displayableId, TestConstants.Utid);
HttpResponseMessage responseMessage = new HttpResponseMessage(HttpStatusCode.OK);
string stringContent = CreateSuccessTokenResponseString(uniqueId, displayableId, scope, foci, utid, accessToken, refreshToken);
HttpContent content = new StringContent(stringContent);
responseMessage.Content = content;
return responseMessage;
}

public static string CreateSuccessTokenResponseString(string uniqueId,
string displayableId,
string[] scope,
bool foci = false,
string utid = TestConstants.Utid,
string accessToken = "some-access-token",
string refreshToken = "OAAsomethingencrypedQwgAA")
{
string idToken = CreateIdToken(uniqueId, displayableId, TestConstants.Utid);
string stringContent = "{\"token_type\":\"Bearer\",\"expires_in\":\"3599\",\"refresh_in\":\"2400\",\"scope\":\"" +
scope.AsSingleString() +
"\",\"access_token\":\"" + accessToken + "\",\"refresh_token\":\"" + refreshToken + "\",\"id_token\":\"" +
idToken +
(foci ? "\",\"foci\":\"1" : "") +
"\",\"id_token_expires_in\":\"3600\",\"client_info\":\"" + CreateClientInfo(uniqueId, utid) + "\"}";
HttpContent content = new StringContent(stringContent);
responseMessage.Content = content;
return responseMessage;

return stringContent;
}

public static string CreateIdToken(string uniqueId, string displayableId)
Expand Down
Loading

0 comments on commit 218643b

Please sign in to comment.