-
Notifications
You must be signed in to change notification settings - Fork 271
move obo call to earlier #383
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
Changes from 4 commits
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 | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -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(); | ||||||||
|
Collaborator
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.
Suggested change
Collaborator
Author
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. |
||||||||
|
|
||||||||
| // 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> | ||||||||
|
|
@@ -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, | ||||||||
|
|
@@ -481,7 +470,7 @@ private async Task<string> GetAccessTokenOnBehalfOfUserFromCacheAsync( | |||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| return await GetAccessTokenOnBehalfOfUserFromCacheAsync( | ||||||||
| return await GetAccessTokenForWebAppWithAccountFromCacheAsync( | ||||||||
| application, | ||||||||
| account, | ||||||||
| scopes, | ||||||||
|
|
@@ -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, | ||||||||
|
|
@@ -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) | ||||||||
|
Collaborator
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.
Suggested change
Collaborator
Author
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. |
||||||||
| { | ||||||||
| 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; | ||||||||
| } | ||||||||
| } | ||||||||
| } | ||||||||
| 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)); | ||
| } | ||
| } | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
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.
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.
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)
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.
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. :)