Skip to content

Commit

Permalink
Merge pull request #39 from AzureAD/jennyf/ATForApp
Browse files Browse the repository at this point in the history
Add `AcquireTokenForApp(scopes)` to `ITokenAcquisition`
  • Loading branch information
jennyf19 authored Mar 1, 2020
2 parents 88269aa + 0aca13a commit 277cdd1
Show file tree
Hide file tree
Showing 30 changed files with 1,419 additions and 64 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/dotnetcore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- name: Setup .NET Core
Expand Down
15 changes: 4 additions & 11 deletions src/Microsoft.Identity.Web/ClientInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,7 @@ public static ClientInfo CreateFromJson(string clientInfo)
throw new ArgumentNullException(nameof(clientInfo), $"client info returned from the server is null");
}

try
{
return DeserializeFromJson<ClientInfo>(Base64UrlHelpers.DecodeToBytes(clientInfo));
}
catch (Exception exc)
{
throw new ArgumentException($"Failed to parse the returned client info. ", nameof(clientInfo));
}
return DeserializeFromJson<ClientInfo>(Base64UrlHelpers.DecodeToBytes(clientInfo));
}

internal static T DeserializeFromJson<T>(byte[] jsonByteArray)
Expand All @@ -42,9 +35,9 @@ internal static T DeserializeFromJson<T>(byte[] jsonByteArray)
return default;
}

using (var stream = new MemoryStream(jsonByteArray))
using (var reader = new StreamReader(stream, Encoding.UTF8))
return (T)JsonSerializer.Create().Deserialize(reader, typeof(T));
using var stream = new MemoryStream(jsonByteArray);
using var reader = new StreamReader(stream, Encoding.UTF8);
return (T)JsonSerializer.Create().Deserialize(reader, typeof(T));
}
}
}
11 changes: 11 additions & 0 deletions src/Microsoft.Identity.Web/ITokenAcquisition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,16 @@ public interface ITokenAcquisition
void ReplyForbiddenWithWwwAuthenticateHeader(
IEnumerable<string> scopes,
MsalUiRequiredException msalSeviceException);

/// <summary>
/// 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
/// 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 overriden in the application.</param>
/// <returns>An access token for the app itself, based on its scopes</returns>
Task<string> AcquireTokenForAppAsync(IEnumerable<string> scopes);
}
}
2 changes: 1 addition & 1 deletion src/Microsoft.Identity.Web/Microsoft.Identity.Web.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<CodeAnalysisRuleSet>Microsoft.Identity.Web.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="3.1.1" />
Expand Down
21 changes: 21 additions & 0 deletions src/Microsoft.Identity.Web/Microsoft.Identity.Web.sln
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoListService", "..\..\te
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoListClient", "..\..\tests\WebAppCallsWebApiCallsGraph\Client\TodoListClient.csproj", "{829BA99E-0E31-4CEE-9A59-CD91531B5ED4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Web.Test.Integration", "..\..\tests\Microsoft.Identity.Web.Test.Integration\Microsoft.Identity.Web.Test.Integration.csproj", "{24DE8503-90EB-4788-A603-681DD4D7DB50}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Web.Test.Common", "..\..\tests\Microsoft.Identity.Web.Test.Common\Microsoft.Identity.Web.Test.Common.csproj", "{F37646FF-C346-4FF5-A111-2269952AB376}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Identity.Web.Test.LabInfrastructure", "..\..\tests\Microsoft.Identity.Web.Test.LabInfrastructure\Microsoft.Identity.Web.Test.LabInfrastructure.csproj", "{A9F4539F-51AB-4D98-9204-D76B336E0BE8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -57,6 +63,18 @@ Global
{829BA99E-0E31-4CEE-9A59-CD91531B5ED4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{829BA99E-0E31-4CEE-9A59-CD91531B5ED4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{829BA99E-0E31-4CEE-9A59-CD91531B5ED4}.Release|Any CPU.Build.0 = Release|Any CPU
{24DE8503-90EB-4788-A603-681DD4D7DB50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{24DE8503-90EB-4788-A603-681DD4D7DB50}.Debug|Any CPU.Build.0 = Debug|Any CPU
{24DE8503-90EB-4788-A603-681DD4D7DB50}.Release|Any CPU.ActiveCfg = Release|Any CPU
{24DE8503-90EB-4788-A603-681DD4D7DB50}.Release|Any CPU.Build.0 = Release|Any CPU
{F37646FF-C346-4FF5-A111-2269952AB376}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F37646FF-C346-4FF5-A111-2269952AB376}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F37646FF-C346-4FF5-A111-2269952AB376}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F37646FF-C346-4FF5-A111-2269952AB376}.Release|Any CPU.Build.0 = Release|Any CPU
{A9F4539F-51AB-4D98-9204-D76B336E0BE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A9F4539F-51AB-4D98-9204-D76B336E0BE8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A9F4539F-51AB-4D98-9204-D76B336E0BE8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A9F4539F-51AB-4D98-9204-D76B336E0BE8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -69,6 +87,9 @@ Global
{9EAE4CCD-EDEB-4FAE-B873-F24714F070A4} = {48D133A6-50D7-4783-9048-148E4B4E1353}
{A6254A6A-B560-4DC1-8348-DED4D947ADCE} = {DA125992-5622-4D9D-B7AB-79CDC45A66B0}
{829BA99E-0E31-4CEE-9A59-CD91531B5ED4} = {DA125992-5622-4D9D-B7AB-79CDC45A66B0}
{24DE8503-90EB-4788-A603-681DD4D7DB50} = {79310504-1334-4F14-93C4-1240913224BA}
{F37646FF-C346-4FF5-A111-2269952AB376} = {79310504-1334-4F14-93C4-1240913224BA}
{A9F4539F-51AB-4D98-9204-D76B336E0BE8} = {79310504-1334-4F14-93C4-1240913224BA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F4FA8C4C-3251-41CC-939B-7892F5798549}
Expand Down
121 changes: 77 additions & 44 deletions src/Microsoft.Identity.Web/TokenAcquisition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public class TokenAcquisition : ITokenAcquisition

private readonly IMsalTokenCacheProvider _tokenCacheProvider;

private IConfidentialClientApplication application;
private IConfidentialClientApplication _application;
private readonly IHttpContextAccessor _httpContextAccessor;
private HttpContext CurrentHttpContext => _httpContextAccessor.HttpContext;
private readonly ILogger _logger;
Expand Down Expand Up @@ -110,7 +110,7 @@ public async Task AddAccountToCacheFromAuthorizationCodeAsync(
{
throw new ArgumentNullException(nameof(scopes));
}

try
{
// As AcquireTokenByAuthorizationCodeAsync is asynchronous we want to tell ASP.NET core that we are handing the code
Expand All @@ -125,20 +125,20 @@ public async Task AddAccountToCacheFromAuthorizationCodeAsync(
(context.HttpContext.User.Identity as ClaimsIdentity).AddClaims(context.Principal.Claims);
}

var application = GetOrBuildConfidentialClientApplication();
_application = await GetOrBuildConfidentialClientApplicationAsync().ConfigureAwait(false);

// Do not share the access token with ASP.NET Core otherwise ASP.NET will cache it and will not send the OAuth 2.0 request in
// case a further call to AcquireTokenByAuthorizationCodeAsync in the future is required for incremental consent (getting a code requesting more scopes)
// Share the ID Token though
var result = await application
var result = await _application
.AcquireTokenByAuthorizationCode(scopes.Except(_scopesRequestedByMsal), context.ProtocolMessage.Code)
.ExecuteAsync()
.ConfigureAwait(false);
context.HandleCodeRedemption(null, result.IdToken);
}
catch (MsalException ex)
{
_logger.LogInformation(ex.Message);
_logger.LogInformation(ex, "Exception occured while adding an account to the cache from the auth code. ");
throw;
}
}
Expand Down Expand Up @@ -195,15 +195,14 @@ public async Task<string> GetAccessTokenForUserAsync(
}

// Use MSAL to get the right token to call the API
var application = GetOrBuildConfidentialClientApplication();
_application = await GetOrBuildConfidentialClientApplicationAsync().ConfigureAwait(false);
string accessToken;

try
{
accessToken = await GetAccessTokenOnBehalfOfUserFromCacheAsync(application, CurrentHttpContext.User, scopes, tenant)
accessToken = await GetAccessTokenOnBehalfOfUserFromCacheAsync(_application, CurrentHttpContext.User, scopes, tenant)
.ConfigureAwait(false);
}

catch (MsalUiRequiredException ex)
{
// GetAccessTokenForUserAsync is an abstraction that can be called from a Web App or a Web API
Expand All @@ -219,7 +218,7 @@ public async Task<string> GetAccessTokenForUserAsync(
// 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))
.ExecuteAsync()
Expand All @@ -238,6 +237,34 @@ public async Task<string> GetAccessTokenForUserAsync(
return accessToken;
}

/// <summary>
/// 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
/// 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 overriden in the application.</param>
/// <returns>An access token for the app itself, based on its scopes</returns>
public async Task<string> AcquireTokenForAppAsync(IEnumerable<string> scopes)
{
if (scopes == null)
{
throw new ArgumentNullException(nameof(scopes));
}

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

AuthenticationResult result;
result = await _application
.AcquireTokenForClient(scopes.Except(_scopesRequestedByMsal))
.ExecuteAsync()
.ConfigureAwait(false);

return result.AccessToken;
}

/// <summary>
/// Removes the account associated with context.HttpContext.User from the MSAL.NET cache
/// </summary>
Expand All @@ -247,7 +274,7 @@ public async Task<string> GetAccessTokenForUserAsync(
public async Task RemoveAccountAsync(RedirectContext context)
{
ClaimsPrincipal user = context.HttpContext.User;
IConfidentialClientApplication app = GetOrBuildConfidentialClientApplication();
IConfidentialClientApplication app = await GetOrBuildConfidentialClientApplicationAsync().ConfigureAwait(false);
IAccount account = null;

// For B2C, we should remove all accounts of the user regardless the user flow
Expand Down Expand Up @@ -287,61 +314,67 @@ public async Task RemoveAccountAsync(RedirectContext context)
/// </summary>
/// <param name="claimsPrincipal"></param>
/// <returns></returns>
private IConfidentialClientApplication GetOrBuildConfidentialClientApplication()
private async Task<IConfidentialClientApplication> GetOrBuildConfidentialClientApplicationAsync()
{
if (application == null)
if (_application == null)
{
application = BuildConfidentialClientApplication();
_application = await BuildConfidentialClientApplicationAsync().ConfigureAwait(false);
}
return application;
return _application;
}

/// <summary>
/// Creates an MSAL Confidential client application
/// </summary>
/// <param name="claimsPrincipal"></param>
/// <returns></returns>
private IConfidentialClientApplication BuildConfidentialClientApplication()
private async Task<IConfidentialClientApplication> BuildConfidentialClientApplicationAsync()
{
var request = CurrentHttpContext.Request;
var microsoftIdentityOptions = _microsoftIdentityOptions;
var applicationOptions = _applicationOptions;
string currentUri = UriHelper.BuildAbsolute(
request.Scheme,
request.Host,
request.PathBase,
microsoftIdentityOptions.CallbackPath.Value ?? string.Empty);
_microsoftIdentityOptions.CallbackPath.Value ?? string.Empty);

if (!applicationOptions.Instance.EndsWith("/"))
applicationOptions.Instance += "/";
if (!_applicationOptions.Instance.EndsWith("/"))
_applicationOptions.Instance += "/";

string authority ;
IConfidentialClientApplication app = null;
string authority;
IConfidentialClientApplication app;

if (microsoftIdentityOptions.IsB2C)
try
{
authority = $"{applicationOptions.Instance}tfp/{microsoftIdentityOptions.Domain}/{microsoftIdentityOptions.DefaultUserFlow}";
app = ConfidentialClientApplicationBuilder
.CreateWithApplicationOptions(applicationOptions)
.WithRedirectUri(currentUri)
.WithB2CAuthority(authority)
.Build();
if (_microsoftIdentityOptions.IsB2C)
{
authority = $"{ _applicationOptions.Instance}tfp/{_microsoftIdentityOptions.Domain}/{_microsoftIdentityOptions.DefaultUserFlow}";
app = ConfidentialClientApplicationBuilder
.CreateWithApplicationOptions(_applicationOptions)
.WithRedirectUri(currentUri)
.WithB2CAuthority(authority)
.Build();
}
else
{
authority = $"{ _applicationOptions.Instance}{_applicationOptions.TenantId}/";

app = ConfidentialClientApplicationBuilder
.CreateWithApplicationOptions(_applicationOptions)
.WithRedirectUri(currentUri)
.WithAuthority(authority)
.Build();
}

// Initialize token cache providers
await _tokenCacheProvider.InitializeAsync(app.AppTokenCache).ConfigureAwait(false);
await _tokenCacheProvider.InitializeAsync(app.UserTokenCache).ConfigureAwait(false);
return app;
}
else
catch (Exception ex)
{
authority = $"{applicationOptions.Instance}{applicationOptions.TenantId}/";
app = ConfidentialClientApplicationBuilder
.CreateWithApplicationOptions(applicationOptions)
.WithRedirectUri(currentUri)
.WithAuthority(authority)
.Build();
_logger.LogInformation(ex, "Exception acquiring token for a confidential client. ");
throw;
}

// Initialize token cache providers
_tokenCacheProvider?.InitializeAsync(app.AppTokenCache);
_tokenCacheProvider?.InitializeAsync(app.UserTokenCache);

return app;
}

/// <summary>
Expand Down Expand Up @@ -465,8 +498,8 @@ public void ReplyForbiddenWithWwwAuthenticateHeader(IEnumerable<string> scopes,
}
}

string consentUrl = $"{application.Authority}/oauth2/v2.0/authorize?client_id={_applicationOptions.ClientId}"
+ $"&response_type=code&redirect_uri={application.AppConfig.RedirectUri}"
string consentUrl = $"{_application.Authority}/oauth2/v2.0/authorize?client_id={_applicationOptions.ClientId}"
+ $"&response_type=code&redirect_uri={_application.AppConfig.RedirectUri}"
+ $"&response_mode=query&scope=offline_access%20{string.Join("%20", scopes)}";

IDictionary<string, string> parameters = new Dictionary<string, string>()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Identity.Web\Microsoft.Identity.Web.csproj" />
</ItemGroup>

</Project>
Loading

0 comments on commit 277cdd1

Please sign in to comment.