Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Microsoft.Identity.Web/Constants/LogMessages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,7 @@ internal static class LogMessages
public const string SessionCacheKeyNotFound = "Cache key {0} not found in session {1}. ";
public const string SerializingSessionCache = "Serializing session {0}, cache key {1}. ";
public const string ClearingSessionCache = "Clearing session {0}, cache key {1}. ";

public const string ErrorAcquiringTokenForDownstreamWebApi = "Error acquiring a token for a downstream web API - MsalUiRequiredException message is: {0} .";
}
}
165 changes: 102 additions & 63 deletions src/Microsoft.Identity.Web/TokenAcquisition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -211,93 +211,82 @@ public async Task<string> GetAccessTokenForUserAsync(
string? userFlow = null,
ClaimsPrincipal? user = null)
{
if (user == null && _httpContextAccessor.HttpContext != null)
{
user = _httpContextAccessor.HttpContext.User;
}

if (user == null)
{
try
{
AuthenticationStateProvider? authenticationStateProvider =
_serviceProvider.GetService(typeof(AuthenticationStateProvider))
as AuthenticationStateProvider;

if (authenticationStateProvider != null)
{
// AuthenticationState provider is only available in Blazor
AuthenticationState state = await authenticationStateProvider.GetAuthenticationStateAsync().ConfigureAwait(false);
user = state.User;
}
}
catch
{
}
}

if (scopes == null)
{
throw new ArgumentNullException(nameof(scopes));
}

// Use MSAL to get the right token to call the API
_application = await GetOrBuildConfidentialClientApplicationAsync().ConfigureAwait(false);
string accessToken;
string authority;
user = await GetAuthenticatedUserAsync(user).ConfigureAwait(false);

if (!string.IsNullOrEmpty(tenant))
{
authority = _application.Authority.Replace(new Uri(_application.Authority).PathAndQuery, $"/{tenant}/");
}
else
{
authority = _application.Authority;
}
_application = await GetOrBuildConfidentialClientApplicationAsync().ConfigureAwait(false);
string authority = CreateAuthorityBasedOnTenantIfProvided(_application, tenant);
string? accessToken;

try
{
accessToken = await GetAccessTokenOnBehalfOfUserFromCacheAsync(
// Access token will return if call is from a web API
accessToken = await GetTokenForWebApiToCallDownstreamApiAsync(
_application,
user,
scopes,
authority,
userFlow)
.ConfigureAwait(false);
scopes).ConfigureAwait(false);

if (!string.IsNullOrEmpty(accessToken))
{
return accessToken;
}

// If access token is null, this is a web app
return await GetAccessTokenForWebAppWithAccountFromCacheAsync(
_application,
user,
scopes,
authority,
userFlow)
.ConfigureAwait(false);
}
catch (MsalUiRequiredException ex)
{
// GetAccessTokenForUserAsync is an abstraction that can be called from a web app or a web API
// GetAccessTokenForUserAsync is an abstraction that can be called from a Web App or a Web API
_logger.LogInformation(ex.Message);

// to get a token for a Web API on behalf of the user, but not necessarily with the on behalf of OAuth2.0
// flow as this one only applies to Web APIs.
// Case of the Web App: we let the MsalUiRequiredException be caught by the
// AuthorizeForScopesAttribute exception filter so that the user can consent, do 2FA, etc ...
throw new MicrosoftIdentityWebChallengeUserException(ex, scopes.ToArray());
}
}

private async Task<string?> GetTokenForWebApiToCallDownstreamApiAsync(
IConfidentialClientApplication application,
string authority,
IEnumerable<string> scopes)
{
try
{
// In web API, validatedToken will not be null
JwtSecurityToken? validatedToken = CurrentHttpContext?.GetTokenUsedToCallWebAPI();

// Case of web APIs: we need to do an on-behalf-of flow
// Case of web APIs: we need to do an on-behalf-of flow, with the token used to call the API
if (validatedToken != null)
{
// In the case the token is a JWE (encrypted token), we use the decrypted token.
string tokenUsedToCallTheWebApi = validatedToken.InnerToken == null ? validatedToken.RawData
: validatedToken.InnerToken.RawData;
var result = await _application
var result = await application
.AcquireTokenOnBehalfOf(scopes.Except(_scopesRequestedByMsal), new UserAssertion(tokenUsedToCallTheWebApi))
.WithSendX5C(_microsoftIdentityOptions.SendX5C)
.WithAuthority(authority)
.ExecuteAsync()
.ConfigureAwait(false);
accessToken = result.AccessToken;
return result.AccessToken;
}

// Case of the Web App: we let the MsalUiRequiredException be caught by the
// AuthorizeForScopesAttribute exception filter so that the user can consent, do 2FA, etc ...
else
{
throw new MicrosoftIdentityWebChallengeUserException(ex, scopes.ToArray());
}
return null;
}
catch (MsalUiRequiredException ex)
{
_logger.LogInformation(string.Format(CultureInfo.InvariantCulture, LogMessages.ErrorAcquiringTokenForDownstreamWebApi, ex.Message));
throw ex;
}

return accessToken;
}

/// <summary>
Expand Down Expand Up @@ -456,7 +445,7 @@ private async Task<IConfidentialClientApplication> BuildConfidentialClientApplic
/// <param name="authority">(optional) Authority based on a specific tenant for which to acquire a token to access the scopes
/// on behalf of the user described in the claimsPrincipal.</param>
/// <param name="userFlow">Azure AD B2C user flow to target.</param>
private async Task<string> GetAccessTokenOnBehalfOfUserFromCacheAsync(
private async Task<string> GetAccessTokenForWebAppWithAccountFromCacheAsync(
IConfidentialClientApplication application,
ClaimsPrincipal? claimsPrincipal,
IEnumerable<string> scopes,
Expand All @@ -481,7 +470,7 @@ private async Task<string> GetAccessTokenOnBehalfOfUserFromCacheAsync(
}
}

return await GetAccessTokenOnBehalfOfUserFromCacheAsync(
return await GetAccessTokenForWebAppWithAccountFromCacheAsync(
application,
account,
scopes,
Expand All @@ -498,7 +487,7 @@ private async Task<string> GetAccessTokenOnBehalfOfUserFromCacheAsync(
/// <param name="authority">Authority based on a specific tenant for which to acquire a token to access the scopes
/// on behalf of the user.</param>
/// <param name="userFlow">Azure AD B2C user flow.</param>
private async Task<string> GetAccessTokenOnBehalfOfUserFromCacheAsync(
private async Task<string> GetAccessTokenForWebAppWithAccountFromCacheAsync(
IConfidentialClientApplication application,
IAccount? account,
IEnumerable<string> scopes,
Expand Down Expand Up @@ -573,11 +562,14 @@ public async Task ReplyForbiddenWithWwwAuthenticateHeaderAsync(IEnumerable<strin

string parameterString = string.Join(", ", parameters.Select(p => $"{p.Key}=\"{p.Value}\""));

var httpResponse = CurrentHttpContext.Response;
var headers = httpResponse.Headers;
httpResponse.StatusCode = (int)HttpStatusCode.Forbidden;
if (CurrentHttpContext != null)
{
var httpResponse = CurrentHttpContext.Response;
var headers = httpResponse.Headers;
httpResponse.StatusCode = (int)HttpStatusCode.Forbidden;

headers[HeaderNames.WWWAuthenticate] = new StringValues($"{Constants.Bearer} {parameterString}");
headers[HeaderNames.WWWAuthenticate] = new StringValues($"{Constants.Bearer} {parameterString}");
}
}

private static bool AcceptedTokenVersionMismatch(MsalUiRequiredException msalSeviceException)
Expand All @@ -588,5 +580,52 @@ private static bool AcceptedTokenVersionMismatch(MsalUiRequiredException msalSev
// This is subject to change in the future
return msalSeviceException.Message.Contains(ErrorCodes.B2CPasswordResetErrorCode, StringComparison.InvariantCulture);
}

private async Task<ClaimsPrincipal?> GetAuthenticatedUserAsync(ClaimsPrincipal? user)
{
if (user == null && _httpContextAccessor.HttpContext?.User != null)
{
user = _httpContextAccessor.HttpContext.User;
}

if (user == null)
{
try
{
AuthenticationStateProvider? authenticationStateProvider =
_serviceProvider.GetService(typeof(AuthenticationStateProvider))
as AuthenticationStateProvider;

if (authenticationStateProvider != null)
{
// AuthenticationState provider is only available in Blazor
AuthenticationState state = await authenticationStateProvider.GetAuthenticationStateAsync().ConfigureAwait(false);
user = state.User;
}
}
catch
{
}
}

return user;
}

internal /*for tests*/ string CreateAuthorityBasedOnTenantIfProvided(
IConfidentialClientApplication application,
string? tenant)
{
string authority;
if (!string.IsNullOrEmpty(tenant))
{
authority = application.Authority.Replace(new Uri(application.Authority).PathAndQuery, $"/{tenant}/");
}
else
{
authority = application.Authority;
}

return authority;
}
}
}
3 changes: 2 additions & 1 deletion tests/Microsoft.Identity.Web.Test.Common/TestConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ public static class TestConstants
public const string UserTwo = "User Two";

public const string ClientId = "87f0ee88-8251-48b3-8825-e0c9563f5234";
public const string TenantId = "guest-tenant-id";
public const string GuestTenantId = "guest-tenant-id";
public const string HomeTenantId = "home-tenant-id";
public const string TenantIdAsGuid = "da41245a5-11b3-996c-00a8-4d99re19f292";
public const string ObjectIdAsGuid = "6364bb70-9521-3fa8-989d-c2c19ff90223";
public const string Domain = "contoso.onmicrosoft.com";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,17 +129,17 @@ public void GetTenantId_WithTidOrTenantIdClaims_ReturnsTenantId()
var claimsPrincipalWithTenantId = new ClaimsPrincipal(
new ClaimsIdentity(new Claim[]
{
new Claim(ClaimConstants.TenantId, TestConstants.TenantId),
new Claim(ClaimConstants.TenantId, TestConstants.GuestTenantId),
}));
var claimsPrincipalWithTidAndTenantId = new ClaimsPrincipal(
new ClaimsIdentity(new Claim[]
{
new Claim(ClaimConstants.Tid, TestConstants.TenantIdAsGuid),
new Claim(ClaimConstants.TenantId, TestConstants.TenantId),
new Claim(ClaimConstants.TenantId, TestConstants.GuestTenantId),
}));

Assert.Equal(TestConstants.TenantIdAsGuid, claimsPrincipalWithTid.GetTenantId());
Assert.Equal(TestConstants.TenantId, claimsPrincipalWithTenantId.GetTenantId());
Assert.Equal(TestConstants.GuestTenantId, claimsPrincipalWithTenantId.GetTenantId());
Assert.Equal(TestConstants.TenantIdAsGuid, claimsPrincipalWithTidAndTenantId.GetTenantId());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ public void IsB2C_NullOrEmptyUserFlow_ReturnsFalse()
}

[Theory]
[InlineData(TestConstants.ClientId, TestConstants.AadInstance, TestConstants.TenantId, null, AzureAd, null)]
[InlineData(null, TestConstants.AadInstance, TestConstants.TenantId, null, null, AzureAd, MissingParam.ClientId)]
[InlineData("", TestConstants.AadInstance, TestConstants.TenantId, null, null, AzureAd, MissingParam.ClientId)]
[InlineData(TestConstants.ClientId, null, TestConstants.TenantId, null, null, AzureAd, MissingParam.Instance)]
[InlineData(TestConstants.ClientId, "", TestConstants.TenantId, null, null, AzureAd, MissingParam.Instance)]
[InlineData(TestConstants.ClientId, TestConstants.AadInstance, TestConstants.GuestTenantId, null, AzureAd, null)]
[InlineData(null, TestConstants.AadInstance, TestConstants.GuestTenantId, null, null, AzureAd, MissingParam.ClientId)]
[InlineData("", TestConstants.AadInstance, TestConstants.GuestTenantId, null, null, AzureAd, MissingParam.ClientId)]
[InlineData(TestConstants.ClientId, null, TestConstants.GuestTenantId, null, null, AzureAd, MissingParam.Instance)]
[InlineData(TestConstants.ClientId, "", TestConstants.GuestTenantId, null, null, AzureAd, MissingParam.Instance)]
[InlineData(TestConstants.ClientId, TestConstants.AadInstance, null, null, null, AzureAd, MissingParam.TenantId)]
[InlineData(TestConstants.ClientId, TestConstants.AadInstance, "", null, null, AzureAd, MissingParam.TenantId)]
[InlineData(TestConstants.ClientId, TestConstants.B2CInstance, null, TestConstants.B2CSignUpSignInUserFlow, TestConstants.B2CTenant, AzureAdB2C)]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Globalization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.Identity.Client;
using Microsoft.Identity.Web.Test.Common;
using Xunit;

namespace Microsoft.Identity.Web.Test
{
public class TokenAcquisitionAuthorityTests
{
private TokenAcquisition _tokenAcquisition;
private ServiceProvider _provider;
private ConfidentialClientApplicationOptions _applicationOptions;

private void InitializeTokenAcquisitionObjects()
{
_tokenAcquisition = new TokenAcquisition(
null,
null,
_provider.GetService<IOptions<MicrosoftIdentityOptions>>(),
_provider.GetService<IOptions<ConfidentialClientApplicationOptions>>(),
null,
null,
_provider);
}

private void BuildTheRequiredServices()
{
var services = new ServiceCollection();

_applicationOptions = new ConfidentialClientApplicationOptions
{
Instance = TestConstants.AadInstance,
ClientId = TestConstants.ConfidentialClientId,
ClientSecret = "cats",
};

services.AddTokenAcquisition();
services.AddTransient(
provider => Options.Create(new MicrosoftIdentityOptions
{
Authority = TestConstants.AuthorityCommonTenant,
ClientId = TestConstants.ConfidentialClientId,
CallbackPath = string.Empty,
}));
services.AddTransient(
provider => Options.Create(_applicationOptions));
_provider = services.BuildServiceProvider();
}

[Theory]
[InlineData(TestConstants.GuestTenantId)]
[InlineData(TestConstants.HomeTenantId)]
[InlineData(null)]
[InlineData("")]
public void VerifyCorrectAuthorityUsedInTokenAcquisitionTests(string tenant)
{
BuildTheRequiredServices();
InitializeTokenAcquisitionObjects();
IConfidentialClientApplication app = ConfidentialClientApplicationBuilder
.CreateWithApplicationOptions(_applicationOptions)
.WithAuthority(TestConstants.AuthorityCommonTenant).Build();

if (!string.IsNullOrEmpty(tenant))
{
Assert.Equal(
string.Format(
CultureInfo.InvariantCulture, "{0}/{1}/", TestConstants.AadInstance, tenant),
_tokenAcquisition.CreateAuthorityBasedOnTenantIfProvided(
app,
tenant));
}
else
{
Assert.Equal(app.Authority, _tokenAcquisition.CreateAuthorityBasedOnTenantIfProvided(app, tenant));
}
}
}
}