/// <summary>
        /// Typically used from a Web App or WebAPI controller, this method retrieves an access token
        /// for a downstream API using;
        /// 1) the token cache (for Web Apps and Web APis) if a token exists in the cache
        /// 2) or the <a href='https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow'>on-behalf-of flow</a>
        /// in Web APIs, for the user account that is ascertained from claims are provided in the <see cref="HttpContext.User"/>
        /// instance of the current HttpContext.
        /// </summary>
        /// <param name="scopes">Scopes to request for the downstream API to call.</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>
        /// <param name="userFlow">Azure AD B2C user flow to target.</param>
        /// <param name="user">Optional claims principal representing the user. If not provided, will use the signed-in
        /// user (in a web app), or the user for which the token was received (in a Web API)
        /// 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 to call the downstream API and populated with this downstream API's scopes.</returns>
        /// <remarks>Calling this method from a web API supposes that you have previously called,
        /// in a method called by JwtBearerOptions.Events.OnTokenValidated, the HttpContextExtensions.StoreTokenUsedToCallWebAPI method
        /// passing the validated token (as a JwtSecurityToken). Calling it from a Web App supposes that
        /// you have previously called AddAccountToCacheFromAuthorizationCodeAsync from a method called by
        /// OpenIdConnectOptions.Events.OnAuthorizationCodeReceived.</remarks>
        public async Task <string> GetAccessTokenForUserAsync(
            IEnumerable <string> scopes,
            string?tenant        = null,
            string?userFlow      = null,
            ClaimsPrincipal?user = null)
        {
            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;

            try
            {
                accessToken = await GetAccessTokenOnBehalfOfUserFromCacheAsync(
                    _application,
                    user ?? CurrentHttpContext.User,
                    scopes,
                    tenant,
                    userFlow)
                              .ConfigureAwait(false);
            }
            catch (MsalUiRequiredException ex)
            {
                // 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 Web APIs: we need to do an on-behalf-of flow
                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
                                 .AcquireTokenOnBehalfOf(scopes.Except(_scopesRequestedByMsal), new UserAssertion(tokenUsedToCallTheWebApi))
                                 .WithSendX5C(_microsoftIdentityOptions.SendX5C)
                                 .ExecuteAsync()
                                 .ConfigureAwait(false);

                    accessToken = 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;
                }
            }

            return(accessToken);
        }
        private IConfidentialClientApplication GetOrCreateApplication()
        {
            if (_application == null)
            {
                lock (_applicationSyncObj)
                {
                    if (_application == null)
                    {
                        var options = new ConfidentialClientApplicationOptions()
                        {
                            ClientId     = AppServicesAuthenticationInformation.ClientId,
                            ClientSecret = AppServicesAuthenticationInformation.ClientSecret,
                            Instance     = AppServicesAuthenticationInformation.Issuer,
                        };
                        _application = ConfidentialClientApplicationBuilder.CreateWithApplicationOptions(options)
                                       .WithHttpClientFactory(_httpClientFactory)
                                       .Build();
                        _tokenCacheProvider.Initialize(_application.AppTokenCache);
                        _tokenCacheProvider.Initialize(_application.UserTokenCache);
                    }
                }
            }

            return(_application);
        }
Example #3
0
        /// <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="scope">The 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, cannot be overridden in the application, as you can request a token for only one resource at a time (use
        /// several calls to get tokens for other resources).</param>
        /// <param name="tenant">Enables overriding of the tenant/account for the same identity. This is useful
        /// for multi tenant apps or daemons.</param>
        /// <returns>An access token for the app itself, based on its scopes.</returns>
        public async Task <string> GetAccessTokenForAppAsync(string scope, string?tenant = null)
        {
            if (string.IsNullOrEmpty(scope))
            {
                throw new ArgumentNullException(nameof(scope));
            }

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

            if (!string.IsNullOrEmpty(tenant) && _metaTenantIdentifiers.Contains(tenant))
            {
                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(new string[] { scope }.Except(_scopesRequestedByMsal))
                     .WithSendX5C(_microsoftIdentityOptions.SendX5C)
                     .WithAuthority(authority)
                     .ExecuteAsync()
                     .ConfigureAwait(false);

            return(result.AccessToken);
        }
        /// <summary>
        /// Creates an MSAL confidential client application if needed.
        /// </summary>
        private async Task <IConfidentialClientApplication> GetOrBuildConfidentialClientApplicationAsync()
        {
            if (_application == null)
            {
                _application = await BuildConfidentialClientApplicationAsync().ConfigureAwait(false);
            }

            return(_application);
        }
        /// <summary>
        /// Typically used from a web app or web API controller, this method retrieves an access token
        /// for a downstream API using;
        /// 1) the token cache (for web apps and web APIs) if a token exists in the cache
        /// 2) or the <a href='https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow'>on-behalf-of flow</a>
        /// in web APIs, for the user account that is ascertained from claims provided in the <see cref="HttpContext.User"/>
        /// instance of the current HttpContext.
        /// </summary>
        /// <param name="scopes">Scopes to request for the downstream API to call.</param>
        /// <param name="tenantId">Enables overriding of the tenant/account for the same identity. This is useful in the
        /// cases where a given account is a guest in other tenants, and you want to acquire tokens for a specific tenant, like where the user is a guest.</param>
        /// <param name="userFlow">Azure AD B2C user flow to target.</param>
        /// <param name="user">Optional claims principal representing the user. If not provided, will use the signed-in
        /// user (in a web app), or the user for which the token was received (in a web API)
        /// cases where a given account is a guest in other tenants, and you want to acquire tokens for a specific tenant, like where the user is a guest.</param>
        /// <param name="tokenAcquisitionOptions">Options passed-in to create the token acquisition options object which calls into MSAL .NET.</param>
        /// <returns>An access token to call the downstream API and populated with this downstream API's scopes.</returns>
        /// <remarks>Calling this method from a web API supposes that you have previously called,
        /// in a method called by JwtBearerOptions.Events.OnTokenValidated, the HttpContextExtensions.StoreTokenUsedToCallWebAPI method
        /// passing the validated token (as a JwtSecurityToken). Calling it from a web app supposes that
        /// you have previously called AddAccountToCacheFromAuthorizationCodeAsync from a method called by
        /// OpenIdConnectOptions.Events.OnAuthorizationCodeReceived.</remarks>
        public async Task <AuthenticationResult> GetAuthenticationResultForUserAsync(
            IEnumerable <string> scopes,
            string?tenantId      = null,
            string?userFlow      = null,
            ClaimsPrincipal?user = null,
            TokenAcquisitionOptions?tokenAcquisitionOptions = null)
        {
            if (scopes == null)
            {
                throw new ArgumentNullException(nameof(scopes));
            }

            user = await GetAuthenticatedUserAsync(user).ConfigureAwait(false);

            _application = await GetOrBuildConfidentialClientApplicationAsync().ConfigureAwait(false);

            string authority = CreateAuthorityBasedOnTenantIfProvided(_application, tenantId);

            AuthenticationResult?authenticationResult;

            try
            {
                // Access token will return if call is from a web API
                authenticationResult = await GetAuthenticationResultForWebApiToCallDownstreamApiAsync(
                    _application,
                    authority,
                    scopes,
                    tokenAcquisitionOptions).ConfigureAwait(false);

                if (authenticationResult != null)
                {
                    return(authenticationResult);
                }

                // If access token is null, this is a web app
                return(await GetAuthenticationResultForWebAppWithAccountFromCacheAsync(
                           _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
                _logger.LogInformation(ex.Message);

                // 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(), userFlow);
            }
        }
Example #6
0
        internal /* for testing */ IConfidentialClientApplication GetOrBuildConfidentialClientApplication()
        {
            if (_application == null)
            {
                lock (_applicationSyncObj)
                {
                    if (_application == null)
                    {
                        _application = BuildConfidentialClientApplication();
                    }
                }
            }

            return(_application);
        }
        /// <inheritdoc/>
        public async Task <string?> RequestTokenAsync()
        {
            this.logger?.LogInformation("Requesting token from identity provider.");

            if (this.confidentialClientApplication == null)
            {
                ValidationResult validationResult = this.aadAppConfiguration.ValidationResult();
                if (!validationResult.Success)
                {
                    throw new ArgumentException(validationResult.Message);
                }

                if (!string.IsNullOrEmpty(this.aadAppConfiguration.CertificateSubjectName))
                {
                    this.confidentialClientApplication = ConfidentialClientApplicationBuilder.Create(this.aadAppConfiguration.ClientId)
                                                         .WithCertificate(CertificateFinder.FindBySubjectName(this.aadAppConfiguration.CertificateSubjectName, DateTime.UtcNow))
                                                         .WithAuthority(AzureCloudInstance.AzurePublic, this.aadAppConfiguration.TenantId)
                                                         .Build();
                }
                else
                {
                    this.confidentialClientApplication = ConfidentialClientApplicationBuilder.Create(this.aadAppConfiguration.ClientId)
                                                         .WithClientSecret(this.aadAppConfiguration.ClientSecret)
                                                         .WithAuthority(AzureCloudInstance.AzurePublic, this.aadAppConfiguration.TenantId)
                                                         .Build();
                }
            }

            AuthenticationResult?result;

            try
            {
                result = await this.confidentialClientApplication.AcquireTokenForClient(this.aadAppConfiguration.Scopes)
                         .ExecuteAsync();

                this.logger?.LogInformation("Token requested successfully.");
                this.logger?.LogDebug($"Access token: {result.AccessToken}");
                this.logger?.LogDebug($"Expires on  : {result.ExpiresOn}");
                this.logger?.LogDebug($"Scopes      : {string.Join(";", result.Scopes)}");
            }
            catch (MsalException e)
            {
                this.logger?.LogError(e, "Requesting token failed");
                throw;
            }

            return(result.AccessToken);
        }
        /// <summary>
        /// Acquires an authentication result 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="scope">The 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, as you can request a token for only one resource at a time (use
        /// several calls to get tokens for other resources).</param>
        /// <param name="tenant">Enables overriding of the tenant/account for the same identity. This is useful
        /// for multi tenant apps or daemons.</param>
        /// <param name="tokenAcquisitionOptions">Options passed-in to create the token acquisition object which calls into MSAL .NET.</param>
        /// <returns>An authentication result for the app itself, based on its scopes.</returns>
        public Task <AuthenticationResult> GetAuthenticationResultForAppAsync(
            string scope,
            string?tenant = null,
            TokenAcquisitionOptions?tokenAcquisitionOptions = null)
        {
            if (string.IsNullOrEmpty(scope))
            {
                throw new ArgumentNullException(nameof(scope));
            }

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

            if (string.IsNullOrEmpty(tenant))
            {
                tenant = _applicationOptions.TenantId;
            }

            if (!string.IsNullOrEmpty(tenant) && _metaTenantIdentifiers.Contains(tenant))
            {
                throw new ArgumentException(IDWebErrorMessage.ClientCredentialTenantShouldBeTenanted, nameof(tenant));
            }

            // Use MSAL to get the right token to call the API
            _application = GetOrBuildConfidentialClientApplication();
            string authority = CreateAuthorityBasedOnTenantIfProvided(_application, tenant);

            var builder = _application
                          .AcquireTokenForClient(new string[] { scope }.Except(_scopesRequestedByMsal))
                          .WithSendX5C(_microsoftIdentityOptions.SendX5C)
                          .WithAuthority(authority);

            if (tokenAcquisitionOptions != null)
            {
                builder.WithExtraQueryParameters(tokenAcquisitionOptions.ExtraQueryParameters);
                builder.WithCorrelationId(tokenAcquisitionOptions.CorrelationId);
                builder.WithForceRefresh(tokenAcquisitionOptions.ForceRefresh);
                builder.WithClaims(tokenAcquisitionOptions.Claims);
                if (tokenAcquisitionOptions.PoPConfiguration != null)
                {
                    builder.WithProofOfPossession(tokenAcquisitionOptions.PoPConfiguration);
                }
            }

            return(builder.ExecuteAsync());
        }
        /// <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.
        /// It uses the code to request an access token from the Microsoft Identity platform and caches the tokens and an entry about the signed-in user's account in the MSAL's token cache.
        /// The access token (and refresh token) provided in the <see cref="AuthorizationCodeReceivedContext"/>, once added to the cache, are then used to acquire more tokens using the
        /// <a href='https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow'>on-behalf-of flow</a> for the signed-in user's account,
        /// in order to call to downstream APIs.
        /// </summary>
        /// <param name="context">The context used when an 'AuthorizationCode' is received over the OpenIdConnect protocol.</param>
        /// <param name="scopes">scopes to request access to.</param>
        /// <example>
        /// From the configuration of the Authentication of the ASP.NET Core Web API:
        /// <code>OpenIdConnectOptions options;</code>
        ///
        /// Subscribe to the authorization code received event:
        /// <code>
        ///  options.Events = new OpenIdConnectEvents();
        ///  options.Events.OnAuthorizationCodeReceived = OnAuthorizationCodeReceived;
        /// }
        /// </code>
        ///
        /// And then in the OnAuthorizationCodeRecieved method, call <see cref="AddAccountToCacheFromAuthorizationCodeAsync"/>:
        /// <code>
        /// private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
        /// {
        ///   var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService&lt;ITokenAcquisition&gt;();
        ///    await _tokenAcquisition.AddAccountToCacheFromAuthorizationCode(context, new string[] { "user.read" });
        /// }
        /// </code>
        /// </example>
        public async Task AddAccountToCacheFromAuthorizationCodeAsync(
            AuthorizationCodeReceivedContext context,
            IEnumerable <string> scopes)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (scopes == null)
            {
                throw new ArgumentNullException(nameof(scopes));
            }

            try
            {
                string?userFlow = context.Principal?.Claims?.FirstOrDefault(c => c.Type == ClaimConstants.UserFlow)?.Value;
                _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 builder = _application
                              .AcquireTokenByAuthorizationCode(scopes.Except(_scopesRequestedByMsal), context.ProtocolMessage.Code)
                              .WithSendX5C(_microsoftIdentityOptions.SendX5C);

                if (_microsoftIdentityOptions.IsB2C)
                {
                    var authority = $"{_applicationOptions.Instance}{Constants.Tfp}/{_microsoftIdentityOptions.Domain}/{userFlow ?? _microsoftIdentityOptions.DefaultUserFlow}";
                    builder.WithB2CAuthority(authority);
                }

                var result = await builder.ExecuteAsync()
                             .ConfigureAwait(false);

                context.HandleCodeRedemption(null, result.IdToken);
            }
            catch (MsalException ex)
            {
                _logger.LogInformation(
                    ex,
                    string.Format(
                        CultureInfo.InvariantCulture,
                        LogMessages.ExceptionOccurredWhenAddingAnAccountToTheCacheFromAuthCode));
                throw;
            }
        }
Example #10
0
        /// <summary>
        /// Used in web APIs (which therefore cannot have an interaction with the user).
        /// Replies to the client through the HTTP response by sending a 403 (forbidden) and populating 'WWW-Authenticate' header so that
        /// the client can trigger an interaction with the user so that the user consents to more scopes.
        /// </summary>
        /// <param name="scopes">Scopes to consent to.</param>
        /// <param name="msalServiceException">The <see cref="MsalUiRequiredException"/> that triggered the challenge.</param>
        /// <param name="httpResponse">The <see cref="HttpResponse"/> to update.</param>
        /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
        public async Task ReplyForbiddenWithWwwAuthenticateHeaderAsync(IEnumerable <string> scopes, MsalUiRequiredException msalServiceException, HttpResponse?httpResponse = null)
        {
            // A user interaction is required, but we are in a web API, and therefore, we need to report back to the client through a 'WWW-Authenticate' header https://tools.ietf.org/html/rfc6750#section-3.1
            string proposedAction = Constants.Consent;

            if (msalServiceException.ErrorCode == MsalError.InvalidGrantError)
            {
                if (AcceptedTokenVersionMismatch(msalServiceException))
                {
                    throw msalServiceException;
                }
            }

            _application = await GetOrBuildConfidentialClientApplicationAsync().ConfigureAwait(false);

            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>()
            {
                { Constants.ConsentUrl, consentUrl },
                { Constants.Claims, msalServiceException.Claims },
                { Constants.Scopes, string.Join(",", scopes) },
                { Constants.ProposedAction, proposedAction },
            };

            string parameterString = string.Join(", ", parameters.Select(p => $"{p.Key}=\"{p.Value}\""));

            httpResponse ??= CurrentHttpContext?.Response;

            if (httpResponse == null)
            {
                throw new InvalidOperationException(IDWebErrorMessage.HttpContextAndHttpResponseAreNull);
            }

            var headers = httpResponse.Headers;

            httpResponse.StatusCode = (int)HttpStatusCode.Forbidden;

            headers[HeaderNames.WWWAuthenticate] = new StringValues($"{Constants.Bearer} {parameterString}");
        }
        /// <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 overridden in the application.</param>
        /// <returns>An access token for the app itself, based on its scopes.</returns>
        public async Task <string> GetAccessTokenForAppAsync(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))
                     .WithSendX5C(_microsoftIdentityOptions.SendX5C)
                     .ExecuteAsync()
                     .ConfigureAwait(false);

            return(result.AccessToken);
        }
Example #12
0
        private async Task <IConfidentialClientApplication> GetOrCreateApplication()
        {
            if (_confidentialClientApplication == null)
            {
                ConfidentialClientApplicationOptions options = new ConfidentialClientApplicationOptions()
                {
                    ClientId     = AppServicesAuthenticationInformation.ClientId,
                    ClientSecret = AppServicesAuthenticationInformation.ClientSecret,
                    Instance     = AppServicesAuthenticationInformation.Issuer,
                };
                _confidentialClientApplication = ConfidentialClientApplicationBuilder.CreateWithApplicationOptions(options)
                                                 .WithHttpClientFactory(_httpClientFactory)
                                                 .Build();
                await _tokenCacheProvider.InitializeAsync(_confidentialClientApplication.AppTokenCache).ConfigureAwait(false);

                await _tokenCacheProvider.InitializeAsync(_confidentialClientApplication.UserTokenCache).ConfigureAwait(false);
            }

            return(_confidentialClientApplication);
        }
Example #13
0
        /// <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.
        /// It uses the code to request an access token from the Microsoft Identity platform and caches the tokens and an entry about the signed-in user's account in the MSAL's token cache.
        /// The access token (and refresh token) provided in the <see cref="AuthorizationCodeReceivedContext"/>, once added to the cache, are then used to acquire more tokens using the
        /// <a href='https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow'>on-behalf-of flow</a> for the signed-in user's account,
        /// in order to call to downstream APIs.
        /// </summary>
        /// <param name="context">The context used when an 'AuthorizationCode' is received over the OpenIdConnect protocol.</param>
        /// <param name="scopes">scopes to request access to.</param>
        /// <example>
        /// From the configuration of the Authentication of the ASP.NET Core Web API:
        /// <code>OpenIdConnectOptions options;</code>
        ///
        /// Subscribe to the authorization code received event:
        /// <code>
        ///  options.Events = new OpenIdConnectEvents();
        ///  options.Events.OnAuthorizationCodeReceived = OnAuthorizationCodeReceived;
        /// }
        /// </code>
        ///
        /// And then in the OnAuthorizationCodeRecieved method, call <see cref="AddAccountToCacheFromAuthorizationCodeAsync"/>:
        /// <code>
        /// private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
        /// {
        ///   var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService&lt;ITokenAcquisition&gt;();
        ///    await _tokenAcquisition.AddAccountToCacheFromAuthorizationCode(context, new string[] { "user.read" });
        /// }
        /// </code>
        /// </example>
        public async Task AddAccountToCacheFromAuthorizationCodeAsync(
            AuthorizationCodeReceivedContext context,
            IEnumerable <string> scopes)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (scopes == null)
            {
                throw new ArgumentNullException(nameof(scopes));
            }

            try
            {
                _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
                             .AcquireTokenByAuthorizationCode(scopes.Except(_scopesRequestedByMsal), context.ProtocolMessage.Code)
                             .WithSendX5C(_microsoftIdentityOptions.SendX5C)
                             .ExecuteAsync()
                             .ConfigureAwait(false);

                context.HandleCodeRedemption(null, result.IdToken);
            }
            catch (MsalException ex)
            {
                _logger.LogInformation(
                    ex,
                    string.Format(
                        CultureInfo.InvariantCulture,
                        "Exception occurred while adding an account to the cache from the auth code. "));
                throw;
            }
        }
        /// <summary>
        /// Creates an MSAL confidential client application.
        /// </summary>
        private async Task <IConfidentialClientApplication> BuildConfidentialClientApplicationAsync()
        {
            var    request    = CurrentHttpContext?.Request;
            string?currentUri = null;

            if (!string.IsNullOrEmpty(_applicationOptions.RedirectUri))
            {
                currentUri = _applicationOptions.RedirectUri;
            }

            if (request != null && string.IsNullOrEmpty(currentUri))
            {
                currentUri = UriHelper.BuildAbsolute(
                    request.Scheme,
                    request.Host,
                    request.PathBase,
                    _microsoftIdentityOptions.CallbackPath.Value ?? string.Empty);
            }

            PrepareAuthorityInstanceForMsal();

            MicrosoftIdentityOptionsValidation.ValidateEitherClientCertificateOrClientSecret(
                _applicationOptions.ClientSecret,
                _microsoftIdentityOptions.ClientCertificates);

            try
            {
                var builder = ConfidentialClientApplicationBuilder
                              .CreateWithApplicationOptions(_applicationOptions)
                              .WithHttpClientFactory(_httpClientFactory)
                              .WithLogging(
                    Log,
                    ConvertMicrosoftExtensionsLogLevelToMsal(_logger),
                    enablePiiLogging: _applicationOptions.EnablePiiLogging);

                // The redirect URI is not needed for OBO
                if (!string.IsNullOrEmpty(currentUri))
                {
                    builder.WithRedirectUri(currentUri);
                }

                string authority;

                if (_microsoftIdentityOptions.IsB2C)
                {
                    authority = $"{_applicationOptions.Instance}{ClaimConstants.Tfp}/{_microsoftIdentityOptions.Domain}/{_microsoftIdentityOptions.DefaultUserFlow}";
                    builder.WithB2CAuthority(authority);
                }
                else
                {
                    authority = $"{_applicationOptions.Instance}{_applicationOptions.TenantId}/";
                    builder.WithAuthority(authority);
                }

                if (_microsoftIdentityOptions.ClientCertificates != null)
                {
                    X509Certificate2?certificate = DefaultCertificateLoader.LoadFirstCertificate(_microsoftIdentityOptions.ClientCertificates);
                    builder.WithCertificate(certificate);
                }

                IConfidentialClientApplication app = builder.Build();
                _application = app;
                // Initialize token cache providers
                await _tokenCacheProvider.InitializeAsync(app.AppTokenCache).ConfigureAwait(false);

                await _tokenCacheProvider.InitializeAsync(app.UserTokenCache).ConfigureAwait(false);

                return(app);
            }
            catch (Exception ex)
            {
                _logger.LogInformation(
                    ex,
                    IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient);
                throw;
            }
        }
Example #15
0
        /// <summary>
        /// Creates an MSAL confidential client application.
        /// </summary>
        private IConfidentialClientApplication BuildConfidentialClientApplication()
        {
            var    httpContext = CurrentHttpContext;
            var    request     = httpContext?.Request;
            string?currentUri  = null;

            if (!string.IsNullOrEmpty(_applicationOptions.RedirectUri))
            {
                currentUri = _applicationOptions.RedirectUri;
            }

            if (request != null && string.IsNullOrEmpty(currentUri))
            {
                currentUri = BuildCurrentUriFromRequest(httpContext !, request);
            }

            PrepareAuthorityInstanceForMsal();

            MicrosoftIdentityOptionsValidation.ValidateEitherClientCertificateOrClientSecret(
                _applicationOptions.ClientSecret,
                _microsoftIdentityOptions.ClientCertificates);

            try
            {
                var builder = ConfidentialClientApplicationBuilder
                              .CreateWithApplicationOptions(_applicationOptions)
                              .WithHttpClientFactory(_httpClientFactory)
                              .WithLogging(
                    Log,
                    ConvertMicrosoftExtensionsLogLevelToMsal(_logger),
                    enablePiiLogging: _applicationOptions.EnablePiiLogging)
                              .WithExperimentalFeatures();

                // The redirect URI is not needed for OBO
                if (!string.IsNullOrEmpty(currentUri))
                {
                    builder.WithRedirectUri(currentUri);
                }

                string authority;

                if (_microsoftIdentityOptions.IsB2C)
                {
                    authority = $"{_applicationOptions.Instance}{ClaimConstants.Tfp}/{_microsoftIdentityOptions.Domain}/{_microsoftIdentityOptions.DefaultUserFlow}";
                    builder.WithB2CAuthority(authority);
                }
                else
                {
                    authority = $"{_applicationOptions.Instance}{_applicationOptions.TenantId}/";
                    builder.WithAuthority(authority);
                }

                if (_microsoftIdentityOptions.ClientCertificates != null)
                {
                    X509Certificate2?certificate = DefaultCertificateLoader.LoadFirstCertificate(_microsoftIdentityOptions.ClientCertificates);
                    builder.WithCertificate(certificate);
                }

                IConfidentialClientApplication app = builder.Build();
                _application = app;
                // Initialize token cache providers
                _tokenCacheProvider.Initialize(app.AppTokenCache);
                _tokenCacheProvider.Initialize(app.UserTokenCache);
                return(app);
            }
            catch (Exception ex)
            {
                Logger.TokenAcquisitionError(
                    _logger,
                    IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient,
                    ex);
                throw;
            }
        }