Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make GetTokenForAppAsync less confusing and allow to pass tenantId #413 #424

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions src/Microsoft.Identity.Web/Constants/IDWebErrorMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ internal static class IDWebErrorMessage
public const string TenantIdClaimNotPresentInToken = "IDW10401: Neither `tid` nor `tenantId` claim is present in the token obtained from Microsoft identity platform. ";
public const string ClientInfoReturnedFromServerIsNull = "IDW10402: Client info returned from the server is null. ";
public const string TokenIsNotJwtToken = "IDW10403: Token is not JWT token. ";
public const string ClientCredentialScopeParameterShouldEndInDotDefault =
"IDW10404: 'scope' parameter should be of the form 'AppIdUri/.default'.";
public const string ClientCredentialTenantShouldBeTenanted =
"IDW10405: 'tenant' parameter should be a tenant ID or domain name, not 'common', 'organizations' or 'consumers'.";

// MSAL IDW10500 = "IDW10500:"
public const string ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client. ";
Expand Down
9 changes: 6 additions & 3 deletions src/Microsoft.Identity.Web/ITokenAcquisition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,15 @@ Task<string> GetAccessTokenForUserAsync(
/// Acquires a token from the authority configured in the app, for the confidential client itself (not on behalf of a user)
/// using the client credentials flow. See https://aka.ms/msal-net-client-credentials.
/// </summary>
/// <param name="scopes">scopes requested to access a protected API. For this flow (client credentials), the scopes
/// <param name="scope">scope requested to access a protected API. For this flow (client credentials), the scope
/// should be of the form "{ResourceIdUri/.default}" for instance <c>https://management.azure.net/.default</c> or, for Microsoft
/// Graph, <c>https://graph.microsoft.com/.default</c> as the requested scopes are defined statically with the application registration
/// in the portal, and cannot be overridden in the application.</param>
/// in the portal, cannot be overridden in the application, and you can request a token for only one resource at a time (use
/// several call to get tokens for other resources).</param>
/// <param name="tenant">Enables overriding of the tenant/account for the same identity. This is useful in the
/// cases where a given account is guest in other tenants, and you want to acquire tokens for a specific tenant, like where the user is a guest in.</param>
/// <returns>An access token for the app itself, based on its scopes.</returns>
Task<string> GetAccessTokenForAppAsync(IEnumerable<string> scopes);
Task<string> GetAccessTokenForAppAsync(string scope, string? tenant);

/// <summary>
/// Used in Web APIs (which therefore cannot have an interaction with the user).
Expand Down
18 changes: 12 additions & 6 deletions src/Microsoft.Identity.Web/Microsoft.Identity.Web.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 32 additions & 6 deletions src/Microsoft.Identity.Web/TokenAcquisition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ public TokenAcquisition(
OidcConstants.ScopeOfflineAccess,
};

/// <summary>
/// meta-tenant identifiers which are not allowed in client credentials.
/// </summary>
private readonly string[] unwantedTenantIdentifiersInClientCredentials = new string[]
{
"common",
"organizations",
"consumers",
};

/// <summary>
/// This handler is executed after the authorization code is received (once the user signs-in and consents) during the
/// <a href='https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow'>Authorization code flow grant flow</a> in a web app.
Expand Down Expand Up @@ -293,25 +303,41 @@ public async Task<string> GetAccessTokenForUserAsync(
/// Acquires a token from the authority configured in the app, for the confidential client itself (not on behalf of a user)
/// using the client credentials flow. See https://aka.ms/msal-net-client-credentials.
/// </summary>
/// <param name="scopes">scopes requested to access a protected API. For this flow (client credentials), the scopes
/// <param name="scope">scope requested to access a protected API. For this flow (client credentials), the scope
/// should be of the form "{ResourceIdUri/.default}" for instance <c>https://management.azure.net/.default</c> or, for Microsoft
/// Graph, <c>https://graph.microsoft.com/.default</c> as the requested scopes are defined statically with the application registration
/// in the portal, and cannot be overridden in the application.</param>
/// in the portal, cannot be overridden in the application, and you can request a token for only one resource at a time (use
/// several call to get tokens for other resources).</param>
/// <param name="tenant">Enables overriding of the tenant/account for the same identity. This is useful in the
/// cases where a given account is guest in other tenants, and you want to acquire tokens for a specific tenant, like where the user is a guest in.</param>
/// <returns>An access token for the app itself, based on its scopes.</returns>
public async Task<string> GetAccessTokenForAppAsync(IEnumerable<string> scopes)
public async Task<string> GetAccessTokenForAppAsync(string scope, string? tenant = null)
{
if (scopes == null)
if (scope == null)
{
throw new ArgumentNullException(nameof(scopes));
throw new ArgumentNullException(nameof(scope));
}

if (!scope.EndsWith("/.default", true, CultureInfo.InvariantCulture))
{
throw new ArgumentException(IDWebErrorMessage.ClientCredentialScopeParameterShouldEndInDotDefault, nameof(scope));
}

if (!string.IsNullOrEmpty(tenant)
&& unwantedTenantIdentifiersInClientCredentials.Any(u => u.Equals(tenant, StringComparison.InvariantCultureIgnoreCase)))
{
throw new ArgumentException(IDWebErrorMessage.ClientCredentialTenantShouldBeTenanted, nameof(tenant));
}

// Use MSAL to get the right token to call the API
_application = await GetOrBuildConfidentialClientApplicationAsync().ConfigureAwait(false);
string authority = CreateAuthorityBasedOnTenantIfProvided(_application, tenant);

AuthenticationResult result;
result = await _application
.AcquireTokenForClient(scopes.Except(_scopesRequestedByMsal))
.AcquireTokenForClient(new string[] { scope }.Except(_scopesRequestedByMsal))
.WithSendX5C(_microsoftIdentityOptions.SendX5C)
.WithAuthority(authority)
.ExecuteAsync()
.ConfigureAwait(false);

Expand Down
5 changes: 1 addition & 4 deletions tests/Microsoft.Identity.Web.Test.Common/TestConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,7 @@ public static class TestConstants
ProductionNotPrefEnvironmentAlias,
};

public static readonly IEnumerable<string> s_scopesForApp = new[]
{
"https://graph.microsoft.com/.default",
};
public static readonly string s_scopeForApp = "https://graph.microsoft.com/.default";

public static readonly IEnumerable<string> s_scopesForUser = new[]
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
Expand Down Expand Up @@ -61,14 +62,30 @@ public async Task GetAccessTokenForApp_ReturnsAccessTokenAsync()

// Act
string token =
await _tokenAcquisition.GetAccessTokenForAppAsync(TestConstants.s_scopesForApp).ConfigureAwait(false);
await _tokenAcquisition.GetAccessTokenForAppAsync(TestConstants.s_scopeForApp).ConfigureAwait(false);

// Assert
Assert.NotNull(token);

AssertAppTokenInMemoryCache(TestConstants.ConfidentialClientId, 1);
}

[Fact]
public async Task GetAccessTokenForApp_WithMetaTenant()
{
// Arrange
InitializeTokenAcquisitionObjects();

Assert.Equal(0, _msalTestTokenCacheProvider.Count);

async Task result() =>
await _tokenAcquisition.GetAccessTokenForAppAsync(TestConstants.s_scopeForApp, "organizations").ConfigureAwait(false);

ArgumentException ex = await Assert.ThrowsAsync<ArgumentException>(result).ConfigureAwait(false);
Assert.Contains(IDWebErrorMessage.ClientCredentialTenantShouldBeTenanted, ex.Message);
Assert.Equal(0, _msalTestTokenCacheProvider.Count);
}

[Fact]
public async Task GetAccessTokenForApp_WithUserScope_MsalServiceExceptionThrownAsync()
{
Expand All @@ -77,13 +94,11 @@ public async Task GetAccessTokenForApp_WithUserScope_MsalServiceExceptionThrownA

// Act & Assert
async Task result() =>
await _tokenAcquisition.GetAccessTokenForAppAsync(TestConstants.s_scopesForUser).ConfigureAwait(false);
await _tokenAcquisition.GetAccessTokenForAppAsync(TestConstants.s_scopesForUser.FirstOrDefault()).ConfigureAwait(false);

MsalServiceException ex = await Assert.ThrowsAsync<MsalServiceException>(result).ConfigureAwait(false);
ArgumentException ex = await Assert.ThrowsAsync<ArgumentException>(result).ConfigureAwait(false);

Assert.Contains(TestConstants.InvalidScopeError, ex.Message);
Assert.Equal(TestConstants.InvalidScope, ex.ErrorCode);
Assert.StartsWith(TestConstants.InvalidScopeErrorcode, ex.Message);
Assert.Contains(IDWebErrorMessage.ClientCredentialScopeParameterShouldEndInDotDefault, ex.Message);
Assert.Equal(0, _msalTestTokenCacheProvider.Count);
}

Expand Down