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

Add AcquireTokenForApp(scopes) to ITokenAcquisition #39

Merged
merged 7 commits into from
Mar 1, 2020
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: 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);
Copy link
Member

@bgavrilMS bgavrilMS Feb 29, 2020

Choose a reason for hiding this comment

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

Fancy new C# 8 feature you're using there :) #Resolved

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>
Copy link
Collaborator Author

@jennyf19 jennyf19 Feb 27, 2020

Choose a reason for hiding this comment

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

@jmprieur no idea what you want said here. please review. :) #Resolved

/// 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. ");
Copy link
Contributor

Choose a reason for hiding this comment

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

We should discuss putting all error strings into a separate class.

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>
Copy link
Collaborator Author

@jennyf19 jennyf19 Feb 27, 2020

Choose a reason for hiding this comment

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

@jmprieur please review comment #Resolved

/// 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()
{
Copy link
Contributor

Choose a reason for hiding this comment

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

We should discuss renaming this method (and any ones like it). Having 'or' or 'and' in the method name suggests that a method has more than one responsibility. In this case I would probably rename it to just 'GetConfidential...' since the caller only cares about getting an app instance; whether it's built or not is internal/abstracted detail.

Copy link
Member

Choose a reason for hiding this comment

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

GetOrCreate is a pretty standard method name. In any case, this is private method, so it does not matter that much. IMO the more explicit the better.

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
Copy link
Member

@bgavrilMS bgavrilMS Feb 28, 2020

Choose a reason for hiding this comment

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

what about ADFS? ADFS does not have tenant ID #WontFix

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

i made a separate issue for this.


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

{
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);
Copy link
Member

@bgavrilMS bgavrilMS Feb 29, 2020

Choose a reason for hiding this comment

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

+1 #Resolved

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