From b9de3cc12a58864f24e03538279d5577185e6788 Mon Sep 17 00:00:00 2001 From: Jean-Marc Prieur Date: Tue, 20 Oct 2020 23:12:06 +0200 Subject: [PATCH] Jmprieur/easy auth without test project (#700) * First step of the integration of Easy-auth with Microsoft.Identity.Web * Updating the EasyAuth code and adding debugging capabilities * Improving the developer experience. Co-authored-by: jennyf19 * Improvements: - in the authentication handler, processes both v1.0 and v2.0 IdTokens - Fixing CA warnings - Adding a page (Debugging) which explains how to debug the EasyAuth integration locally. Co-authored-by: jennyf19 --- ProjectTemplates/configuration.json | 4 +- .../Controllers/AccountController.cs | 25 ++-- ...ServicesAuthenticationBuilderExtensions.cs | 34 +++++ .../AppServicesAuthenticationDefaults.cs | 16 ++ .../AppServicesAuthenticationHandler.cs | 94 ++++++++++++ .../AppServicesAuthenticationInformation.cs | 95 ++++++++++++ .../AppServicesAuthenticationOptions.cs | 14 ++ ...pServicesAuthenticationTokenAcquisition.cs | 141 ++++++++++++++++++ .../Constants/Constants.cs | 1 + .../Microsoft.Identity.Web.xml | 105 +++++++++++++ ...softIdentityWebAppAuthenticationBuilder.cs | 131 ++++++++-------- ...tyWebAppAuthenticationBuilderExtensions.cs | 17 ++- ...pAuthenticationBuilderWithConfiguration.cs | 13 +- .../Pages/Debugging.cshtml | 51 +++++++ .../Pages/Debugging.cshtml.cs | 16 ++ .../profile.arm.json | 113 ++++++++++++++ .../profile.arm.json | 119 +++++++++++++++ 17 files changed, 914 insertions(+), 75 deletions(-) create mode 100644 src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationBuilderExtensions.cs create mode 100644 src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationDefaults.cs create mode 100644 src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationHandler.cs create mode 100644 src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationInformation.cs create mode 100644 src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationOptions.cs create mode 100644 src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationTokenAcquisition.cs create mode 100644 tests/WebAppCallsMicrosoftGraph/Pages/Debugging.cshtml create mode 100644 tests/WebAppCallsMicrosoftGraph/Pages/Debugging.cshtml.cs create mode 100644 tests/WebAppCallsMicrosoftGraph/Properties/ServiceDependencies/MdIdWebTestForEasyAuthIntegration - Web Deploy/profile.arm.json create mode 100644 tests/WebAppCallsMicrosoftGraph/Properties/ServiceDependencies/MsIdWebLinuxAppServiceForTesting - Web Deploy/profile.arm.json diff --git a/ProjectTemplates/configuration.json b/ProjectTemplates/configuration.json index 688b48568..3884b59cd 100644 --- a/ProjectTemplates/configuration.json +++ b/ProjectTemplates/configuration.json @@ -5,7 +5,6 @@ "B2C_Authority": "https://fabrikamb2c.b2clogin.com/fabrikamb2c.onmicrosoft.com/b2c_1_susi", "B2C_Client_ClientId": "fdb91ff5-5ce6-41f3-bdbd-8267c817015d", - "B2C_Client_ClientSecret": "[Copy the client secret added to the app from the Azure portal]", "B2C_Client_Port": "44365", "B2C_WebApi_ClientId": "90c0fe63-bcf2-44d5-8fb7-b8bbc0b29dc6", @@ -19,11 +18,12 @@ "AAD_Authority": "https://login.microsoftonline.com/msidentitysamplestesting.onmicrosoft.com", "AAD_Client_ClientId": "86699d80-dd21-476a-bcd1-7c1a3d471f75", - "AAD_Client_ClientSecret": "[Copy the client secret added to the app from the Azure portal]", "AAD_Client_Port": "44357", "AAD_WebApi_ClientId": "a4c2469b-cf84-4145-8f5f-cb7bacf814bc", "AAD_WebApi_Port": "44351", + "B2C_Client_ClientSecret": "[Copy the client secret added to the app from the Azure portal]", + "AAD_Client_ClientSecret": "[Copy the client secret added to the app from the Azure portal]", "AAD_WebApi_ClientSecret": "[Copy the client secret added to the app from the Azure portal]" }, "Projects": [ diff --git a/src/Microsoft.Identity.Web.UI/Areas/MicrosoftIdentity/Controllers/AccountController.cs b/src/Microsoft.Identity.Web.UI/Areas/MicrosoftIdentity/Controllers/AccountController.cs index 9cec257fb..148391ddc 100644 --- a/src/Microsoft.Identity.Web.UI/Areas/MicrosoftIdentity/Controllers/AccountController.cs +++ b/src/Microsoft.Identity.Web.UI/Areas/MicrosoftIdentity/Controllers/AccountController.cs @@ -92,15 +92,22 @@ public IActionResult Challenge( [HttpGet("{scheme?}")] public IActionResult SignOut([FromRoute] string scheme) { - scheme ??= OpenIdConnectDefaults.AuthenticationScheme; - var callbackUrl = Url.Page("/Account/SignedOut", pageHandler: null, values: null, protocol: Request.Scheme); - return SignOut( - new AuthenticationProperties - { - RedirectUri = callbackUrl, - }, - CookieAuthenticationDefaults.AuthenticationScheme, - scheme); + if (AppServicesAuthenticationInformation.IsAppServicesAadAuthenticationEnabled) + { + return LocalRedirect(AppServicesAuthenticationInformation.LogoutUrl); + } + else + { + scheme ??= OpenIdConnectDefaults.AuthenticationScheme; + var callbackUrl = Url.Page("/Account/SignedOut", pageHandler: null, values: null, protocol: Request.Scheme); + return SignOut( + new AuthenticationProperties + { + RedirectUri = callbackUrl, + }, + CookieAuthenticationDefaults.AuthenticationScheme, + scheme); + } } /// diff --git a/src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationBuilderExtensions.cs b/src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationBuilderExtensions.cs new file mode 100644 index 000000000..0b0eb4a35 --- /dev/null +++ b/src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationBuilderExtensions.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Authentication; + +namespace Microsoft.Identity.Web +{ + /// + /// Extension methods related to App Services authentication (Easy Auth). + /// + public static class AppServicesAuthenticationBuilderExtensions + { + /// + /// Add authentication with App Services. + /// + /// Authentication builder. + /// The builder, to chain commands. + public static AuthenticationBuilder AddAppServicesAuthentication( + this AuthenticationBuilder builder) + { + if (builder is null) + { + throw new System.ArgumentNullException(nameof(builder)); + } + + builder.AddScheme( + AppServicesAuthenticationDefaults.AuthenticationScheme, + AppServicesAuthenticationDefaults.AuthenticationScheme, + options => { }); + + return builder; + } + } +} diff --git a/src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationDefaults.cs b/src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationDefaults.cs new file mode 100644 index 000000000..c437e98f9 --- /dev/null +++ b/src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationDefaults.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Identity.Web +{ + /// + /// Default values related to AppServiceAuthentication handler. + /// + public class AppServicesAuthenticationDefaults + { + /// + /// The default value used for AppServiceAuthenticationOptions.AuthenticationScheme. + /// + public const string AuthenticationScheme = "AppServicesAuthentication"; + } +} diff --git a/src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationHandler.cs b/src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationHandler.cs new file mode 100644 index 000000000..c1bbc3d66 --- /dev/null +++ b/src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationHandler.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Linq; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.JsonWebTokens; + +namespace Microsoft.Identity.Web +{ + /// + /// App service authentication handler. + /// + public class AppServicesAuthenticationHandler : AuthenticationHandler + { + /// + /// Constructor for the AppServiceAuthenticationHandler. + /// Note the parameters are required by the base class. + /// + /// App service authentication options. + /// Logger factory. + /// URL encoder. + /// System clock. + public AppServicesAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock) + : base(options, logger, encoder, clock) + { + } + + // Constants + private const string AppServicesAuthIdTokenHeader = "X-MS-TOKEN-AAD-ID-TOKEN"; + private const string AppServicesAuthIdpTokenHeader = "X-MS-CLIENT-PRINCIPAL-IDP"; + + /// + protected override Task HandleAuthenticateAsync() + { + if (AppServicesAuthenticationInformation.IsAppServicesAadAuthenticationEnabled) + { + string? idToken = GetIdToken(); + string? idp = GetIdp(); + + if (idToken != null && idp != null) + { + JsonWebToken jsonWebToken = new JsonWebToken(idToken); + bool isAadV1Token = jsonWebToken.Claims + .Any(c => c.Type == Constants.Version && c.Value == Constants.V1); + ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity( + jsonWebToken.Claims, + idp, + isAadV1Token ? Constants.NameClaim : Constants.PreferredUserName, + ClaimsIdentity.DefaultRoleClaimType)); + + AuthenticationTicket ticket = new AuthenticationTicket(claimsPrincipal, AppServicesAuthenticationDefaults.AuthenticationScheme); + AuthenticateResult success = AuthenticateResult.Success(ticket); + return Task.FromResult(success); + } + } + + // Try another handler + return Task.FromResult(AuthenticateResult.NoResult()); + } + + private string? GetIdp() + { + string? idp = Context.Request.Headers[AppServicesAuthIdpTokenHeader]; +#if DEBUG + if (string.IsNullOrEmpty(idp)) + { + idp = AppServicesAuthenticationInformation.SimulateGetttingHeaderFromDebugEnvironmentVariable(AppServicesAuthIdpTokenHeader); + } +#endif + return idp; + } + + private string? GetIdToken() + { + string? idToken = Context.Request.Headers[AppServicesAuthIdTokenHeader]; +#if DEBUG + if (string.IsNullOrEmpty(idToken)) + { + idToken = AppServicesAuthenticationInformation.SimulateGetttingHeaderFromDebugEnvironmentVariable(AppServicesAuthIdTokenHeader); + } +#endif + return idToken; + } + } +} diff --git a/src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationInformation.cs b/src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationInformation.cs new file mode 100644 index 000000000..39d3a25b9 --- /dev/null +++ b/src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationInformation.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Linq; + +namespace Microsoft.Identity.Web +{ + /// + /// Information about the App Services configuration on the host. + /// + public static class AppServicesAuthenticationInformation + { + // Environment variables. + private const string AppServicesAuthEnabledEnvironmentVariable = "WEBSITE_AUTH_ENABLED"; // True + private const string AppServicesAuthOpenIdIssuerEnvironmentVariable = "WEBSITE_AUTH_OPENID_ISSUER"; // for instance https://sts.windows.net// + private const string AppServicesAuthClientIdEnvironmentVariable = "WEBSITE_AUTH_CLIENT_ID"; // A GUID + private const string AppServicesAuthClientSecretEnvironmentVariable = "WEBSITE_AUTH_CLIENT_SECRET"; // A string + private const string AppServicesAuthLogoutPathEnvironmentVariable = "WEBSITE_AUTH_LOGOUT_PATH"; // /.auth/logout + private const string AppServicesAuthIdentityProviderEnvironmentVariable = "WEBSITE_AUTH_DEFAULT_PROVIDER"; // AzureActiveDirectory + private const string AppServicesAuthAzureActiveDirectory = "AzureActiveDirectory"; + + // Artificially added by Microsoft.Identity.Web to help debugging App Services. See the Debug controller of the test app + private const string AppServicesAuthDebugHeadersEnvironmentVariable = "APP_SERVICES_AUTH_LOCAL_DEBUG"; + + /// + /// Is App Services authentication enabled?. + /// + public static bool IsAppServicesAadAuthenticationEnabled + { + get + { + return (Environment.GetEnvironmentVariable(AppServicesAuthEnabledEnvironmentVariable) == Constants.True) + && Environment.GetEnvironmentVariable(AppServicesAuthIdentityProviderEnvironmentVariable) == AppServicesAuthAzureActiveDirectory; + } + } + + /// + /// Logout URL for App Services Auth web sites. + /// + public static string? LogoutUrl + { + get + { + return Environment.GetEnvironmentVariable(AppServicesAuthLogoutPathEnvironmentVariable); + } + } + + /// + /// ClientID of the App Services Auth web site. + /// + internal static string? ClientId + { + get + { + return Environment.GetEnvironmentVariable(AppServicesAuthClientIdEnvironmentVariable); + } + } + + /// + /// Client secret of the App Services Auth web site. + /// + internal static string? ClientSecret + { + get + { + return Environment.GetEnvironmentVariable(AppServicesAuthClientSecretEnvironmentVariable); + } + } + + /// + /// Issuer of the App Services Auth web site. + /// + internal static string? Issuer + { + get + { + return Environment.GetEnvironmentVariable(AppServicesAuthOpenIdIssuerEnvironmentVariable); + } + } + +#if DEBUG + /// + /// Get headers from environment to help debugging App Services authentication. + /// + internal static string? SimulateGetttingHeaderFromDebugEnvironmentVariable(string header) + { + string? headerPlusValue = Environment.GetEnvironmentVariable(AppServicesAuthDebugHeadersEnvironmentVariable) + ?.Split(';') + ?.FirstOrDefault(h => h.StartsWith(header)); + return headerPlusValue?.Substring(header.Length + 1); + } +#endif + } +} diff --git a/src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationOptions.cs b/src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationOptions.cs new file mode 100644 index 000000000..859988832 --- /dev/null +++ b/src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationOptions.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Authentication; + +namespace Microsoft.Identity.Web +{ + /// + /// Options for Azure App Services authentication. + /// + public class AppServicesAuthenticationOptions : AuthenticationSchemeOptions + { + } +} diff --git a/src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationTokenAcquisition.cs b/src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationTokenAcquisition.cs new file mode 100644 index 000000000..3531d34ac --- /dev/null +++ b/src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationTokenAcquisition.cs @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Identity.Client; +using Microsoft.Identity.Web.TokenCacheProviders; + +namespace Microsoft.Identity.Web +{ + /// + /// Implementation of ITokenAcquisition for App Services authentication (EasyAuth). + /// + public class AppServicesAuthenticationTokenAcquisition : ITokenAcquisition + { + private IConfidentialClientApplication? _confidentialClientApplication; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IMsalHttpClientFactory _httpClientFactory; + private readonly IMsalTokenCacheProvider _tokenCacheProvider; + + private HttpContext? CurrentHttpContext + { + get + { + return _httpContextAccessor.HttpContext; + } + } + + /// + /// Constructor of the AppServicesAuthenticationTokenAcquisition. + /// + /// The App token cache provider. + /// Access to the HttpContext of the request. + /// HTTP client factory. + public AppServicesAuthenticationTokenAcquisition( + IMsalTokenCacheProvider tokenCacheProvider, + IHttpContextAccessor httpContextAccessor, + IHttpClientFactory httpClientFactory) + { + _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); + _httpClientFactory = new MsalAspNetCoreHttpClientFactory(httpClientFactory); + _tokenCacheProvider = tokenCacheProvider; + } + + private async Task 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; + } + + /// + public async Task GetAccessTokenForAppAsync( + string scope, + string? tenant = null, + TokenAcquisitionOptions? tokenAcquisitionOptions = null) + { + // We could use MSI + if (scope is null) + { + throw new ArgumentNullException(nameof(scope)); + } + + var app = await GetOrCreateApplication().ConfigureAwait(false); + AuthenticationResult result = await app.AcquireTokenForClient(new string[] { scope }) + .ExecuteAsync() + .ConfigureAwait(false); + + return result.AccessToken; + } + + /// + public async Task GetAccessTokenForUserAsync( + IEnumerable scopes, + string? tenantId = null, + string? userFlow = null, + ClaimsPrincipal? user = null, + TokenAcquisitionOptions? tokenAcquisitionOptions = null) + { + string accessToken = GetAccessToken(CurrentHttpContext?.Request.Headers); + + return await Task.FromResult(accessToken).ConfigureAwait(false); + } + + private string GetAccessToken(IHeaderDictionary? headers) + { + const string AppServicesAuthAccessTokenHeader = "X-MS-TOKEN-AAD-ACCESS-TOKEN"; + + string? accessToken = null; + if (headers != null) + { + accessToken = headers[AppServicesAuthAccessTokenHeader]; + } +#if DEBUG + if (string.IsNullOrEmpty(accessToken)) + { + accessToken = AppServicesAuthenticationInformation.SimulateGetttingHeaderFromDebugEnvironmentVariable(AppServicesAuthAccessTokenHeader); + } +#endif + if (!string.IsNullOrEmpty(accessToken)) + { + return accessToken; + } + + return string.Empty; + } + + /// +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + public async Task GetAuthenticationResultForUserAsync(IEnumerable scopes, string? tenantId = null, string? userFlow = null, ClaimsPrincipal? user = null, TokenAcquisitionOptions? tokenAcquisitionOptions = null) + { + throw new NotImplementedException(); + } + + /// + public async Task ReplyForbiddenWithWwwAuthenticateHeaderAsync(IEnumerable scopes, MsalUiRequiredException msalServiceException, HttpResponse? httpResponse = null) + { + // Not implmented for the moment + throw new NotImplementedException(); + } +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + + } +} diff --git a/src/Microsoft.Identity.Web/Constants/Constants.cs b/src/Microsoft.Identity.Web/Constants/Constants.cs index 3eaa5f9fc..4724c65c3 100644 --- a/src/Microsoft.Identity.Web/Constants/Constants.cs +++ b/src/Microsoft.Identity.Web/Constants/Constants.cs @@ -94,6 +94,7 @@ public static class Constants internal const string Authorization = "Authorization"; internal const string ApplicationJson = "application/json"; internal const string ISessionStore = "ISessionStore"; + internal const string True = "True"; // Blazor challenge URI internal const string BlazorChallengeUri = "MicrosoftIdentity/Account/Challenge?redirectUri="; diff --git a/src/Microsoft.Identity.Web/Microsoft.Identity.Web.xml b/src/Microsoft.Identity.Web/Microsoft.Identity.Web.xml index 5307d05ed..26f4d7630 100644 --- a/src/Microsoft.Identity.Web/Microsoft.Identity.Web.xml +++ b/src/Microsoft.Identity.Web/Microsoft.Identity.Web.xml @@ -17,6 +17,111 @@ The instance. A built from . + + + Extension methods related to App Services authentication (Easy Auth). + + + + + Add authentication with App Services. + + Authentication builder. + The builder, to chain commands. + + + + Default values related to AppServiceAuthentication handler. + + + + + The default value used for AppServiceAuthenticationOptions.AuthenticationScheme. + + + + + App service authentication handler. + + + + + Constructor for the AppServiceAuthenticationHandler. + Note the parameters are required by the base class. + + App service authentication options. + Logger factory. + URL encoder. + System clock. + + + + + + + Information about the App Services configuration on the host. + + + + + Is App Services authentication enabled?. + + + + + Logout URL for App Services Auth web sites. + + + + + ClientID of the App Services Auth web site. + + + + + Client secret of the App Services Auth web site. + + + + + Issuer of the App Services Auth web site. + + + + + Get headers from environment to help debugging App Services authentication. + + + + + Options for Azure App Services authentication. + + + + + Implementation of ITokenAcquisition for App Services authentication (EasyAuth). + + + + + Constructor of the AppServicesAuthenticationTokenAcquisition. + + The App token cache provider. + Access to the HttpContext of the request. + HTTP client factory. + + + + + + + + + + + + + Filter used on a controller action to trigger incremental consent. diff --git a/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilder.cs b/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilder.cs index c94dd9e03..8db266378 100644 --- a/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilder.cs +++ b/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilder.cs @@ -88,68 +88,75 @@ internal static void WebAppCallsWebApiImplementation( services.AddHttpContextAccessor(); - services.AddTokenAcquisition(); - - services.AddOptions(openIdConnectScheme) - .Configure((options, serviceProvider) => - { - options.ResponseType = OpenIdConnectResponseType.Code; - options.UsePkce = false; - - // This scope is needed to get a refresh token when users sign-in with their Microsoft personal accounts - // It's required by MSAL.NET and automatically provided when users sign-in with work or school accounts - options.Scope.Add(OidcConstants.ScopeOfflineAccess); - if (initialScopes != null) - { - foreach (string scope in initialScopes) - { - if (!options.Scope.Contains(scope)) - { - options.Scope.Add(scope); - } - } - } - - // Handling the auth redemption by MSAL.NET so that a token is available in the token cache - // where it will be usable from Controllers later (through the TokenAcquisition service) - var codeReceivedHandler = options.Events.OnAuthorizationCodeReceived; - options.Events.OnAuthorizationCodeReceived = async context => - { - var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService(); - await tokenAcquisition.AddAccountToCacheFromAuthorizationCodeAsync(context, options.Scope).ConfigureAwait(false); - await codeReceivedHandler(context).ConfigureAwait(false); - }; - - // Handling the token validated to get the client_info for cases where tenantId is not present (example: B2C) - var onTokenValidatedHandler = options.Events.OnTokenValidated; - options.Events.OnTokenValidated = async context => - { - string? clientInfo = context.ProtocolMessage?.GetParameter(ClaimConstants.ClientInfo); - - if (!string.IsNullOrEmpty(clientInfo)) - { - ClientInfo? clientInfoFromServer = ClientInfo.CreateFromJson(clientInfo); - - if (clientInfoFromServer != null) - { - context.Principal.Identities.FirstOrDefault()?.AddClaim(new Claim(ClaimConstants.UniqueTenantIdentifier, clientInfoFromServer.UniqueTenantIdentifier)); - context.Principal.Identities.FirstOrDefault()?.AddClaim(new Claim(ClaimConstants.UniqueObjectIdentifier, clientInfoFromServer.UniqueObjectIdentifier)); - } - } - - await onTokenValidatedHandler(context).ConfigureAwait(false); - }; - - // Handling the sign-out: removing the account from MSAL.NET cache - var signOutHandler = options.Events.OnRedirectToIdentityProviderForSignOut; - options.Events.OnRedirectToIdentityProviderForSignOut = async context => - { - // Remove the account from MSAL.NET token cache - var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService(); - await tokenAcquisition.RemoveAccountAsync(context).ConfigureAwait(false); - await signOutHandler(context).ConfigureAwait(false); - }; - }); + if (AppServicesAuthenticationInformation.IsAppServicesAadAuthenticationEnabled) + { + services.AddScoped(); + } + else + { + services.AddTokenAcquisition(); + + services.AddOptions(openIdConnectScheme) + .Configure((options, serviceProvider) => + { + options.ResponseType = OpenIdConnectResponseType.Code; + options.UsePkce = false; + + // This scope is needed to get a refresh token when users sign-in with their Microsoft personal accounts + // It's required by MSAL.NET and automatically provided when users sign-in with work or school accounts + options.Scope.Add(OidcConstants.ScopeOfflineAccess); + if (initialScopes != null) + { + foreach (string scope in initialScopes) + { + if (!options.Scope.Contains(scope)) + { + options.Scope.Add(scope); + } + } + } + + // Handling the auth redemption by MSAL.NET so that a token is available in the token cache + // where it will be usable from Controllers later (through the TokenAcquisition service) + var codeReceivedHandler = options.Events.OnAuthorizationCodeReceived; + options.Events.OnAuthorizationCodeReceived = async context => + { + var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService(); + await tokenAcquisition.AddAccountToCacheFromAuthorizationCodeAsync(context, options.Scope).ConfigureAwait(false); + await codeReceivedHandler(context).ConfigureAwait(false); + }; + + // Handling the token validated to get the client_info for cases where tenantId is not present (example: B2C) + var onTokenValidatedHandler = options.Events.OnTokenValidated; + options.Events.OnTokenValidated = async context => + { + string? clientInfo = context.ProtocolMessage?.GetParameter(ClaimConstants.ClientInfo); + + if (!string.IsNullOrEmpty(clientInfo)) + { + ClientInfo? clientInfoFromServer = ClientInfo.CreateFromJson(clientInfo); + + if (clientInfoFromServer != null) + { + context.Principal.Identities.FirstOrDefault()?.AddClaim(new Claim(ClaimConstants.UniqueTenantIdentifier, clientInfoFromServer.UniqueTenantIdentifier)); + context.Principal.Identities.FirstOrDefault()?.AddClaim(new Claim(ClaimConstants.UniqueObjectIdentifier, clientInfoFromServer.UniqueObjectIdentifier)); + } + } + + await onTokenValidatedHandler(context).ConfigureAwait(false); + }; + + // Handling the sign-out: removing the account from MSAL.NET cache + var signOutHandler = options.Events.OnRedirectToIdentityProviderForSignOut; + options.Events.OnRedirectToIdentityProviderForSignOut = async context => + { + // Remove the account from MSAL.NET token cache + var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService(); + await tokenAcquisition.RemoveAccountAsync(context).ConfigureAwait(false); + await signOutHandler(context).ConfigureAwait(false); + }; + }); + } } } } diff --git a/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilderExtensions.cs b/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilderExtensions.cs index 3e11efdf2..54a9ad50e 100644 --- a/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilderExtensions.cs +++ b/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilderExtensions.cs @@ -189,13 +189,21 @@ private static MicrosoftIdentityWebAppAuthenticationBuilder AddMicrosoftWebAppWi string cookieScheme, bool subscribeToOpenIdConnectMiddlewareDiagnosticsEvents) { - AddMicrosoftIdentityWebAppInternal( + if (!AppServicesAuthenticationInformation.IsAppServicesAadAuthenticationEnabled) + { + AddMicrosoftIdentityWebAppInternal( builder, configureMicrosoftIdentityOptions, configureCookieAuthenticationOptions, openIdConnectScheme, cookieScheme, subscribeToOpenIdConnectMiddlewareDiagnosticsEvents); + } + else + { + builder.Services.AddAuthentication(AppServicesAuthenticationDefaults.AuthenticationScheme) + .AddAppServicesAuthentication(); + } return new MicrosoftIdentityWebAppAuthenticationBuilder( builder.Services, @@ -236,6 +244,13 @@ private static void AddMicrosoftIdentityWebAppInternal( builder.Services.AddSingleton(); } + if (AppServicesAuthenticationInformation.IsAppServicesAadAuthenticationEnabled) + { + builder.Services.AddAuthentication(AppServicesAuthenticationDefaults.AuthenticationScheme) + .AddAppServicesAuthentication(); + return; + } + builder.AddOpenIdConnect(openIdConnectScheme, options => { }); builder.Services.AddOptions(openIdConnectScheme) .Configure>((options, serviceProvider, microsoftIdentityOptions) => diff --git a/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration.cs b/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration.cs index 0ea59eacf..a5c773b08 100644 --- a/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration.cs +++ b/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration.cs @@ -44,7 +44,18 @@ public MicrosoftIdentityAppCallsWebApiAuthenticationBuilder EnableTokenAcquisiti IEnumerable? initialScopes = null) { return EnableTokenAcquisitionToCallDownstreamApi( - options => ConfigurationSection.Bind(options), + options => + { + ConfigurationSection.Bind(options); + if (AppServicesAuthenticationInformation.IsAppServicesAadAuthenticationEnabled) + { + options.ClientId = AppServicesAuthenticationInformation.ClientId; + options.ClientSecret = AppServicesAuthenticationInformation.ClientSecret; + options.Instance = AppServicesAuthenticationInformation.Issuer; + } + + Services.AddHttpClient(); + }, initialScopes); } } diff --git a/tests/WebAppCallsMicrosoftGraph/Pages/Debugging.cshtml b/tests/WebAppCallsMicrosoftGraph/Pages/Debugging.cshtml new file mode 100644 index 000000000..4131e4efe --- /dev/null +++ b/tests/WebAppCallsMicrosoftGraph/Pages/Debugging.cshtml @@ -0,0 +1,51 @@ +@page +@model WebAppCallsMicrosoftGraph.Pages.DebuggingModel +@{ + string[] importantEnvironmentVariables = new string[] { + "WEBSITE_AUTH_ENABLED", "WEBSITE_AUTH_OPENID_ISSUER", "WEBSITE_AUTH_CLIENT_ID", + "WEBSITE_AUTH_CLIENT_SECRET", "WEBSITE_AUTH_LOGOUT_PATH", "WEBSITE_AUTH_DEFAULT_PROVIDER"}; + + string[] importantHeaders = new string[] { "X-MS-TOKEN-AAD-ID-TOKEN", "X-MS-CLIENT-PRINCIPAL-IDP", "X-MS-TOKEN-AAD-ACCESS-TOKEN" }; +} + + +

Debug

+ +To debug locally the application as if it was deployed in App services: +
    +
  1. + Run the application with Easy auth as deployed, but at the baseUrl/Debugging address (for instance: https://mdidwebtestforeasyauthintegration.azurewebsites.net/Debugging) +
  2. +
  3. + Copy the following JSon blob into the lauchsettings.json for the profile of your choice, in the environmentVariables section, paste the following: +
  4. + +
  5. + Run Visual Studio with debugging with the profile for which you have added the environement variables. +
  6. +
+ +
+ "environmentVariables": { +
+
+ "ASPNETCORE_ENVIRONMENT": "Development" +
+@{ + foreach (string environmentVariable in importantEnvironmentVariables) + { +
"@environmentVariable": "@Environment.GetEnvironmentVariable(environmentVariable)"
+ } + + List headers = new List(); + foreach (string header in importantHeaders) + { + headers.Add($"{header}={Model.HttpContext.Request.Headers[header]}"); + } + +
"APP_SERVICES_AUTH_LOCAL_DEBUG": "@string.Join(";", headers)"
+} + } + + + diff --git a/tests/WebAppCallsMicrosoftGraph/Pages/Debugging.cshtml.cs b/tests/WebAppCallsMicrosoftGraph/Pages/Debugging.cshtml.cs new file mode 100644 index 000000000..1647d1fe3 --- /dev/null +++ b/tests/WebAppCallsMicrosoftGraph/Pages/Debugging.cshtml.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace WebAppCallsMicrosoftGraph.Pages +{ + public class DebuggingModel : PageModel + { + public void OnGet() + { + } + } +} diff --git a/tests/WebAppCallsMicrosoftGraph/Properties/ServiceDependencies/MdIdWebTestForEasyAuthIntegration - Web Deploy/profile.arm.json b/tests/WebAppCallsMicrosoftGraph/Properties/ServiceDependencies/MdIdWebTestForEasyAuthIntegration - Web Deploy/profile.arm.json new file mode 100644 index 000000000..99def8ead --- /dev/null +++ b/tests/WebAppCallsMicrosoftGraph/Properties/ServiceDependencies/MdIdWebTestForEasyAuthIntegration - Web Deploy/profile.arm.json @@ -0,0 +1,113 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_dependencyType": "appService.windows" + }, + "parameters": { + "resourceGroupName": { + "type": "string", + "defaultValue": "MsIdWeb", + "metadata": { + "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking." + } + }, + "resourceGroupLocation": { + "type": "string", + "defaultValue": "centralus", + "metadata": { + "description": "Location of the resource group. Resource groups could have different location than resources, however by default we use API versions from latest hybrid profile which support all locations for resource types we support." + } + }, + "resourceName": { + "type": "string", + "defaultValue": "MdIdWebTestForEasyAuthIntegration", + "metadata": { + "description": "Name of the main resource to be created by this template." + } + }, + "resourceLocation": { + "type": "string", + "defaultValue": "[parameters('resourceGroupLocation')]", + "metadata": { + "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there." + } + } + }, + "variables": { + "appServicePlan_name": "[concat('Plan', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", + "appServicePlan_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/serverFarms/', variables('appServicePlan_name'))]" + }, + "resources": [ + { + "type": "Microsoft.Resources/resourceGroups", + "name": "[parameters('resourceGroupName')]", + "location": "[parameters('resourceGroupLocation')]", + "apiVersion": "2019-10-01" + }, + { + "type": "Microsoft.Resources/deployments", + "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", + "resourceGroup": "[parameters('resourceGroupName')]", + "apiVersion": "2019-10-01", + "dependsOn": [ + "[parameters('resourceGroupName')]" + ], + "properties": { + "mode": "Incremental", + "template": { + "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [ + { + "location": "[parameters('resourceLocation')]", + "name": "[parameters('resourceName')]", + "type": "Microsoft.Web/sites", + "apiVersion": "2015-08-01", + "tags": { + "[concat('hidden-related:', variables('appServicePlan_ResourceId'))]": "empty" + }, + "dependsOn": [ + "[variables('appServicePlan_ResourceId')]" + ], + "kind": "app", + "properties": { + "name": "[parameters('resourceName')]", + "kind": "app", + "httpsOnly": true, + "reserved": false, + "serverFarmId": "[variables('appServicePlan_ResourceId')]", + "siteConfig": { + "metadata": [ + { + "name": "CURRENT_STACK", + "value": "dotnetcore" + } + ] + } + }, + "identity": { + "type": "SystemAssigned" + } + }, + { + "location": "[parameters('resourceLocation')]", + "name": "[variables('appServicePlan_name')]", + "type": "Microsoft.Web/serverFarms", + "apiVersion": "2015-08-01", + "sku": { + "name": "S1", + "tier": "Standard", + "family": "S", + "size": "S1" + }, + "properties": { + "name": "[variables('appServicePlan_name')]" + } + } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/tests/WebAppCallsMicrosoftGraph/Properties/ServiceDependencies/MsIdWebLinuxAppServiceForTesting - Web Deploy/profile.arm.json b/tests/WebAppCallsMicrosoftGraph/Properties/ServiceDependencies/MsIdWebLinuxAppServiceForTesting - Web Deploy/profile.arm.json new file mode 100644 index 000000000..c739f3cf1 --- /dev/null +++ b/tests/WebAppCallsMicrosoftGraph/Properties/ServiceDependencies/MsIdWebLinuxAppServiceForTesting - Web Deploy/profile.arm.json @@ -0,0 +1,119 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_dependencyType": "appService.linux" + }, + "parameters": { + "resourceGroupName": { + "type": "string", + "defaultValue": "b2cauth", + "metadata": { + "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking." + } + }, + "resourceGroupLocation": { + "type": "string", + "defaultValue": "westus", + "metadata": { + "description": "Location of the resource group. Resource groups could have different location than resources, however by default we use API versions from latest hybrid profile which support all locations for resource types we support." + } + }, + "resourceName": { + "type": "string", + "defaultValue": "MsIdWebLinuxAppServiceForTesting", + "metadata": { + "description": "Name of the main resource to be created by this template." + } + }, + "resourceLocation": { + "type": "string", + "defaultValue": "[parameters('resourceGroupLocation')]", + "metadata": { + "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there." + } + } + }, + "variables": { + "appServicePlan_name": "[concat('Plan', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", + "appServicePlan_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/serverFarms/', variables('appServicePlan_name'))]" + }, + "resources": [ + { + "type": "Microsoft.Resources/resourceGroups", + "name": "[parameters('resourceGroupName')]", + "location": "[parameters('resourceGroupLocation')]", + "apiVersion": "2019-10-01" + }, + { + "type": "Microsoft.Resources/deployments", + "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", + "resourceGroup": "[parameters('resourceGroupName')]", + "apiVersion": "2019-10-01", + "dependsOn": [ + "[parameters('resourceGroupName')]" + ], + "properties": { + "mode": "Incremental", + "template": { + "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [ + { + "location": "[parameters('resourceLocation')]", + "name": "[parameters('resourceName')]", + "type": "Microsoft.Web/sites", + "apiVersion": "2015-08-01", + "tags": { + "[concat('hidden-related:', variables('appServicePlan_ResourceId'))]": "empty" + }, + "dependsOn": [ + "[variables('appServicePlan_ResourceId')]" + ], + "kind": "app", + "properties": { + "name": "[parameters('resourceName')]", + "kind": "app", + "httpsOnly": true, + "reserved": false, + "serverFarmId": "[variables('appServicePlan_ResourceId')]", + "siteConfig": { + "linuxFxVersion": "DOTNETCORE|2.1" + } + }, + "identity": { + "type": "SystemAssigned" + }, + "resources": [ + { + "name": "appsettings", + "type": "config", + "apiVersion": "2015-08-01", + "dependsOn": [ + "[concat('Microsoft.Web/Sites/', parameters('resourceName'))]" + ], + "properties": { + "WEBSITE_WEBDEPLOY_USE_SCM": "false" + } + } + ] + }, + { + "location": "[parameters('resourceLocation')]", + "name": "[variables('appServicePlan_name')]", + "type": "Microsoft.Web/serverFarms", + "apiVersion": "2015-02-01", + "kind": "linux", + "properties": { + "name": "[variables('appServicePlan_name')]", + "sku": "Standard", + "workerSizeId": "0", + "reserved": true + } + } + ] + } + } + } + ] +} \ No newline at end of file