Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 ErrorAcquiringTokenForOboForWebApi = "Error acquiring a token for obo for a web API - MsalUiRequiredException message is: {0} .";

@pmaytak pmaytak Aug 4, 2020

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public const string ErrorAcquiringTokenForOboForWebApi = "Error acquiring a token for obo for a web API - MsalUiRequiredException message is: {0} .";
public const string ErrorAcquiringTokenForOboForWebApi = "Error acquiring a token for On-Behalf-Of flow for a web API - MsalUiRequiredException message is: {0} .";
``` #Resolved

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm going to call it downstream web api instead, as OBO is not a flow, and it is confusing for a customer. thanks, peter. :)


In reply to: 464802916 [](ancestors = 464802916)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, downstream API makes sense.

Yea, idk I got the phrase from MS docs: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow. :)

}
}
156 changes: 96 additions & 60 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.
JwtSecurityToken? validatedToken = CurrentHttpContext?.GetTokenUsedToCallWebAPI();
// 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();

@pmaytak pmaytak Aug 4, 2020

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
JwtSecurityToken? validatedToken = CurrentHttpContext.GetTokenUsedToCallWebAPI();
JwtSecurityToken? validatedToken = CurrentHttpContext?.GetTokenUsedToCallWebAPI();
``` #Resolved

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed. thanks


In reply to: 464810227 [](ancestors = 464810227)


// 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.ErrorAcquiringTokenForOboForWebApi, 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 @@ -588,5 +577,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 != null)

@pmaytak pmaytak Aug 4, 2020

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (user == null && _httpContextAccessor.HttpContext != null)
if (user == null && _httpContextAccessor.HttpContext?.User != null)
``` #Resolved

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it. thanks!


In reply to: 464811695 [](ancestors = 464811695)

{
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));
}
}
}
}