-
Notifications
You must be signed in to change notification settings - Fork 218
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
Changes from all commits
cfce5df
ad5648d
0b4ee95
82d36a6
8814b27
18e4af4
0aca13a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,7 @@ jobs: | |
build: | ||
|
||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- uses: actions/checkout@v2 | ||
- name: Setup .NET Core | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -88,5 +88,16 @@ public interface ITokenAcquisition | |
void ReplyForbiddenWithWwwAuthenticateHeader( | ||
IEnumerable<string> scopes, | ||
MsalUiRequiredException msalSeviceException); | ||
|
||
/// <summary> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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 | ||
|
@@ -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. "); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should discuss putting all error strings into a separate class. |
||
throw; | ||
} | ||
} | ||
|
@@ -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 | ||
|
@@ -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() | ||
|
@@ -238,6 +237,34 @@ public async Task<string> GetAccessTokenForUserAsync( | |
return accessToken; | ||
} | ||
|
||
/// <summary> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
|
@@ -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 | ||
|
@@ -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() | ||
{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what about ADFS? ADFS does not have tenant ID #WontFix There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
{ | ||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
|
@@ -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>() | ||
|
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> |
There was a problem hiding this comment.
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