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?
- Before .Net 5, ASP.Net core did not have built-in mechanism to get identity information (passed to WebApp by App service) after authentication
- So, we need to create a custom middleware to fetch identity information
- If you are using .Net 5+, custom middleware is not needed
Create 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;
- You need to check ‘user_claims’ by going to: ‘
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 ofRouteView
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)
Links
- https://docs.microsoft.com/en-us/aspnet/core/blazor/security/
- https://docs.microsoft.com/en-us/azure/app-service/overview-authentication-authorization
- https://docs.microsoft.com/en-us/azure/app-service/configure-authentication-provider-aad#-configure-with-advanced-settings
- https://docs.microsoft.com/en-us/azure/active-directory/develop/id-tokens#v20
- https://docs.microsoft.com/en-us/dotnet/api/system.security.claims.claimtypes
- https://docs.microsoft.com/en-us/azure/active-directory/develop/developer-glossary
- https://docs.microsoft.com/en-us/azure/app-service/app-service-authentication-how-to
Links related to workaround (custom middleware / nuget packages)
- https://cmatskas.com/working-with-easyauth-app-service-authentication-and-net-core-3-1/
- https://github.com/MaximRouiller/MaximeRouiller.Azure.AppService.EasyAuth
- https://docs.microsoft.com/en-us/azure/app-service/overview-authentication-authorization#how-it-works
- https://stackoverflow.com/a/51781114/4802664