Author : HASSAN MD TAREQ | Updated : 2020/09/07

What is App Service Easy Auth

Create App Service

azure_app_service_easy_auth_create_app_service_to_get_domain

azure_app_service_easy_auth_create_app_service_created

Azure AD Tenant and User

  • Create dedicated tenant and switch to that tenant
    • Do not use default tenant (use dedicated tenant)
    • Delete tenant after PoC/Demo
  • Create dedicated user and login once to change temporary password (Azure AD might ask you to setup authentication app)

Used in demo

tenant name: AppServiceEasyAuthTenant
domain (while creating tenant): EasyAuthDemoApp (=> EasyAuthDemoApp.onmicrosoft.com)


User in newly created tenant
--------------------------------
User: hassan@easyauthdemoapp.onmicrosoft.com
Temporary pass: Wowo8300
Upadeted password: xxx

Create Azure AD Tenant and User Step 1

Create Azure AD Tenant and User Step 2

Create Azure AD Tenant and User Step 3

Create Azure AD Tenant and User Step 4

Create Azure AD Tenant and User Step 5

Create Azure AD Tenant and User Step 6

Create Azure AD Tenant and User Step 7

Create Azure AD Tenant and User Step 8

Create Azure AD Tenant and User Step 9

Create Azure AD Tenant and User Step 10

Create Azure AD Tenant and User Step 11

Create Azure AD Tenant and User Step 12

Create Azure AD Tenant and User Step 13

App Registration in Azure AD

  • For PoC/Demo, create new app registration
  • In case you want to use existing App registration, check and update following accordingly:
    • Branding
    • Authentication
    • Certificates & secrets
    • Expose an API
  • Go to Manifest & verify oauth2Permissions (+ other settings)

Register app in azure AD

Redirect URI: <app-url>/.auth/login/aad/callback

Example: https://easyauthdemoapp.azurewebsites.net/.auth/login/aad/callback

Gather required information for “App Service Easy Auth”

Client ID: 858430e8-169a-4d47-bbe9-ec4f2fda96d9
Tenant ID: d68fc4a4-063a-4ea9-be32-8178a5cc29c7
Client secret (optional): jXr.Ku7M.c9Koj6A.A7_W-fMvs4d4T.5dm
Application ID URI: https://easyauthdemoapp.azurewebsites.net

Scope

Scope name: user_impersonation
Admin consent display name: Access my app
Admin consent description: Access my app... bla bla bla

Register App in Azure AD Step 1

Register App in Azure AD Step 2

Register App in Azure AD Step 3

Register App in Azure AD Step 4

Register App in Azure AD Step 5

Register App in Azure AD Step 6

Register App in Azure AD Step 7

Register App in Azure AD Step 8

Register App in Azure AD Step 9

Register App in Azure AD Step 10

Register App in Azure AD Step 11

Register App in Azure AD Step 12

Register App in Azure AD Step 13

Configure App Service

App Service > Authentication/Authorization > Authentication Provider > Azure Active Directory > Advanced

Client ID: noted before (see above)
Client secret (optional): noted before (see above)
Application ID URI: noted before (see above)

Issuer Url: <authentication-endpoint>/<tenant-id>/v2.0
Example: https://login.microsoft.com/d68fc4a4-063a-4ea9-be32-8178a5cc29c7/v2.0

Don’t forget to save “App Service > Authentication/Authorization”

Configure App Service for Easy Auth Step 1

Configure App Service for Easy Auth Step 2

Configure App Service for Easy Auth Step 3

Configure App Service for Easy Auth Step 4

Configure App Service for Easy Auth Step 5

Configure App Service for Easy Auth Step 6

Creating Custom Middleware

  • Create a project or use existing project
  • Create folders if needed (i.e. Middlewares, Extensions)
  • populating identity data from app service using custom Middleware
    • After successfull login, app service passes identity information to app
    • identity information usrl: https://xxx.azurewebsites.net/.auth/me
  • Dependency: Install-Package NewtonSoft.Json (easy to parse json, in future use: System.Text.Json)
  • Notes:
    • You need to check ‘user_claims’ by going to: ‘https://xxx.azurewebsites.net/.auth/me’ If you are not using Azure AD Global
      • Issuer Url for global == https://login.microsoft.com/tenant-id/v2.0)
      • Example ssuer Url for other Azure AD: ‘https://sts.windows.net/tenant-id/
    • Extract the claim you want: authState.User?.Claims?.FirstOrDefault(c => c.Type == "claim-type-you-want")?.Value;

Middlewares/AzureAppServiceEasyAuthMiddleware.cs

using Microsoft.AspNetCore.Http;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading.Tasks;

namespace AzureAppServicesAuthDemo.Middlewares

{
    public class AzureAppServiceEasyAuthMiddleware
    {
        private const string PrincipalId = "X-MS-CLIENT-PRINCIPAL-ID";
        private const string AuthMeEndPoint = @".auth/me";
        private const string UserClaimsKey = "user_claims";
        private const string ClaimTypeKey = "typ";
        private const string ClaimValueKey = "val";

        private readonly RequestDelegate _next;


        public AzureAppServiceEasyAuthMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {

            if (context.User == null || context.User.Claims == null || !context.User.Claims.Any())
            {
                if (context.Request.Headers.ContainsKey(PrincipalId))
                {
                    var azureAppServicePrincipalIdHeader = context.Request.Headers[PrincipalId][0];

                    var uriString = $"{context.Request.Scheme}://{context.Request.Host}";
                    var cookieContainer = new CookieContainer();
                    var handler = new HttpClientHandler()
                    {
                        CookieContainer = cookieContainer
                    };

                    foreach (var c in context.Request.Cookies)
                    {
                        cookieContainer.Add(new Uri(uriString), new Cookie(c.Key, c.Value));
                    }

                    var jsonResult = string.Empty;
                    using (var client = new HttpClient(handler))
                    {
                        var res = await client.GetAsync($"{uriString}/{AuthMeEndPoint}");
                        jsonResult = await res.Content.ReadAsStringAsync();
                    }

                    if (jsonResult != string.Empty)
                    {
                        try
                        {
                            var obj = JArray.Parse(jsonResult);

                            var claims = new List<Claim>();
                            foreach (var claim in obj[0][UserClaimsKey])
                            {
                                claims.Add(new Claim(claim[ClaimTypeKey].ToString(), claim[ClaimValueKey].ToString()));
                            }

                            var identity = new GenericIdentity(azureAppServicePrincipalIdHeader);
                            identity.AddClaims(claims);

                            context.User = new GenericPrincipal(identity, null);
                        }
                        catch (Exception ex)
                        {
                            Log.Fatal(ex, ex.Message);
                        }
                    }
                }
            }

            await _next(context);
        }
    }
}

Extensions/AzureAppServiceEasyAuthMiddlewareExtension.cs

using AzureAppServicesAuthDemo.Middlewares;
using Microsoft.AspNetCore.Builder;

namespace AzureAppServicesAuthDemo.Extensions
{
    public static class AzureAppServiceEasyAuthMiddlewareExtension
    {
        public static IApplicationBuilder UseAppServiceEasyAuth(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<AzureAppServiceEasyAuthMiddleware>();
        }
    }
}

Configuration in Startup Class

Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using AzureAppServicesAuthDemo.Data;
using AzureAppServicesAuthDemo.Extensions;
using AzureAppServicesAuthDemo.Services;

namespace AzureAppServicesAuthDemo
{
    public class Startup
    {
	
        // ... ... ...
		
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            // ... ... ...

            app.UseAppServiceEasyAuth();
			
			/*
            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapBlazorHub();
                endpoints.MapFallbackToPage("/_Host");
            });
			*/
        }
    }
}

Get Claims using AuthenticationStateTask

  • Add CascadingAuthenticationState
  • Use AuthorizeRouteView instead of RouteView

App.razor

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

Pages/Index.razor.cs

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using System.Threading.Tasks;
using System.Security.Claims;
using System.Collections.Generic;
using System.Linq;

namespace AzureAppServicesAuthDemo.Pages
{
    public class IndexBase: ComponentBase
    {
		public const string PREFERRED_USERNAME = "preferred_username";
		public const string DefaultLoggedInUser = "unknown (default)";

		[CascadingParameter]
		private Task<AuthenticationState> AuthenticationStateTask { get; set; }

		public string CurrentLoggedInUserName { get; set; } = string.Empty;

		public IEnumerable<Claim> LoggedInUserClaims { get; set; } = Enumerable.Empty<Claim>();

        protected override async Task OnInitializedAsync()
        {
			var authState = await AuthenticationStateTask;

			LoggedInUserClaims = authState.User?.Claims;

			var loggedInUserName = authState.User?.Claims?.FirstOrDefault(c => c.Type == PREFERRED_USERNAME)?.Value;

            if (string.IsNullOrWhiteSpace(loggedInUserName))
            {
				loggedInUserName = DefaultLoggedInUser;
			}

			CurrentLoggedInUserName = loggedInUserName;
		}
	}
}

Pages/Index.razor

@page "/"
@inherits IndexBase


<h3>Azure App Service Easy Auth PoC</h3>
<hr class="mb-sm-5" />


<h3 class="mb-sm-5">LoggedIn User Name: @CurrentLoggedInUserName </h3>


<h3>List of Claims: </h3>

@if (LoggedInUserClaims != null && LoggedInUserClaims.Any())
{
    @foreach (var claim in LoggedInUserClaims)
    {
        <ul>
            <li>@claim</li>
        </ul>
    }
}
else
{
    <h3>No claims for loggedin user</h3>
}

Expose Logged In user Information from MainLayout

Based on above approach, we can make it better. A better approach would be:

  • Populate logged in user information in MainLayout using [CascadingParameter] protected Task<AuthenticationState> AuthenticationStateTask { get; set; }
  • MainLayout would expose logged in user information using <CascadingValue Value="@CurrentLoggedInUser">@Body</CascadingValue>
  • Since all components are rendered inside MainLayout, components would have access to logged in user info (exposed by MainLayout)
  • Create BaseComponent class and get logged in user information (exposed by MainLayout) using [CascadingParameter]protected LoggedInUser LoggedInUser { get; set; }

See demo application: https://github.com/hovermind/blazor-demos/tree/master/AzureAppServiceEasyAuth

Deploy to App Service

  • Right click on project > Publish
  • Select App Service
  • Visual Studio will deploy the app and launch it
  • Use Azure AD credential to login and you can see email address and claim list (if not showing, there is error)