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

App Service Easy Auth

The Authentication/Authorization feature provided by App Service is referred to as “Easy Auth”.

See Azure App Service - Easy Auth and perform tasks accordingly first, and then proceed below.

Creating Custom Middleware

Why Custom Middleware?

Create Middleware:

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

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:

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

Deploy to App Service

Links related to workaround (custom middleware / nuget packages)