9

We have a web app built on Asp.Net core. It doesn't contain any authentication middleware configured in it.

We are hosting on Azure App Service and using the Authentication/Authorization option (EasyAuth) to authenticate against Azure AD.

The authentication works well - we get the requisite headers inserted and we can see the authenticated identity at /.auth/me. But the HttpContext.User property doesn't get populated.

Is this a compatibility issue for Asp.Net core? Or am I doing something wrong?

Vaibhav
  • 11,310
  • 11
  • 51
  • 70

5 Answers5

15

I've created a custom middleware that populates the User property until this gets solved by the Azure Team.

It reads the headers from the App Service Authentication and create a a user that will be recognized by the [Authorize] and has a claim on name.

// Azure app service will send the x-ms-client-principal-id when authenticated
app.Use(async (context, next) =>
{

    // Create a user on current thread from provided header
    if (context.Request.Headers.ContainsKey("X-MS-CLIENT-PRINCIPAL-ID"))
    {
        // Read headers from Azure
        var azureAppServicePrincipalIdHeader = context.Request.Headers["X-MS-CLIENT-PRINCIPAL-ID"][0];
        var azureAppServicePrincipalNameHeader = context.Request.Headers["X-MS-CLIENT-PRINCIPAL-NAME"][0];

        // Create claims id
        var claims = new Claim[] {
        new System.Security.Claims.Claim("http://schemas.microsoft.com/identity/claims/objectidentifier", azureAppServicePrincipalIdHeader),
        new System.Security.Claims.Claim("name", azureAppServicePrincipalNameHeader)
        };

        // Set user in current context as claims principal
        var identity = new GenericIdentity(azureAppServicePrincipalIdHeader);
        identity.AddClaims(claims);

        // Set current thread user to identity
        context.User = new GenericPrincipal(identity, null);
    };

    await next.Invoke();
});
Jonas Stensved
  • 14,378
  • 5
  • 51
  • 80
  • @ChrisGillum Many thanks for this posting, its helped me. I had a followon question. I'm using ASP Membership in the backend for authorization. Adding user to Context doesnt seem to sign them in. Is there a way I could call the SignInManager from this same section of code to do that? – John Lee Feb 21 '17 at 19:59
12

Yes, this is a compatibility issue. ASP.NET Core does not support flowing identity info from an IIS module (like Easy Auth) to the app code, unfortunately. This means HttpContext.User and similar code won't work like it does with regular ASP.NET.

The workaround for now is to invoke your web app's /.auth/me endpoint from your server code to get the user claims. You can then cache this data as appropriate using the x-ms-client-principal-id request header value as the cache key. The /.auth/me call will need to be properly authenticated in the same way that calls to your web app need to be authenticated (auth cookie or request header token).

Chris Gillum
  • 14,526
  • 5
  • 48
  • 61
  • 1
    Thank you, Chris. We do also have a requirement to add additional claims from the app database based on the identity we get from AAD. So, I guess I will look at the option of adding a custom middleware which reads the information through the .auth/me and the database together and create the ClaimsPrincipal from there. It will allow us to keep the rest of the Asp.Net Core authentication / authorization framework in place. – Vaibhav Jan 06 '17 at 08:36
  • its now a almost 2 years later and azure/Microsoft has still not updated azure web[/api/mobile] app EasyAuth implementation update that automatically handles mapping EasyAuth authenticated user X-MS-* header data into this.User object similar to where it shows up if you implement oauth or openid connection authentication code yourself in asp.net core web application? I just got through testing azure function app v2 and their EasyAuth story does this for you and allows you to dependency inject ClaimsPrincipal into any function [ / controller method ] that you want it. – myusrn Dec 07 '18 at 22:54
  • 1
    @myusrn The design of ASP.NET Core doesn't allow for automatic injection. This needs to be done in app code, unfortunately. – Chris Gillum Dec 08 '18 at 07:48
  • Fair enough but wouldn't it make sense to include a nuget package that does that work similar to what some of the folks have taken cracks at? – myusrn Dec 08 '18 at 23:33
  • Hi @Chris Gilliam, shouldn't i expect that the easyauth filters will always take the browser client openid connect or native app oauth authorization header bearer token authentication and from that create X-MS-CLIENT-PRINCIPAL-NAME, X-MS-CLIENT-PRINCIPAL-IDP, X-MS-CLIENT-PRINCIPAL, X-MS-TOKEN-AAD-ID-TOKEN, etc. request headers that I can trust and use to create this.Context.User resulting in no network requests being required to azurewebsites.net/.auth/me ? – myusrn Dec 10 '18 at 02:10
  • . . . and i recall reading in some related SO exchange that easyauth will always toss any incoming X-MS-* request headers it sees and and where applicable insert the ones it creates making it so you can trust their contents if present. Is that correct? – myusrn Dec 10 '18 at 02:11
  • Until MS find a way to hydrate the claims principal for us, we need to hydrate it from the headers ourselves I guess. I just created this to help others facing similar issues https://github.com/dasiths/NEasyAuthMiddleware – Dasith Wijes Apr 23 '19 at 09:14
5

I wrote a small basic middleware to do this. It will create an identity based off of the .auth/me endpoint. The identity is created in the authentication pipeline so that [authorize] attributes and policies work with the identity.

You can find it here:

https://github.com/lpunderscore/azureappservice-authentication-middleware

or on nuget:

https://www.nuget.org/packages/AzureAppserviceAuthenticationMiddleware/

Once added, just add this line to your startup:

app.UseAzureAppServiceAuthentication();

user3159405
  • 111
  • 5
  • I looked at the Repo and tried it without success. To me it seems like you are not passing headers such as X-ZUMO-AUTH to the /.auth/me. you are sending cookies though. could that be issue? – n00b Mar 26 '17 at 06:22
  • That GitHub Repo is updated to also work with headers. I used it successfully in my app – n00b Mar 28 '17 at 10:53
  • @n00b thx for the contribution, your changes have been merged, i will update the nuget as soon as I have some time. – user3159405 Mar 30 '17 at 00:07
  • 1
    Hi, I did some work to get this working with asp.net core 2.0 and up. I would appreciate some feedback here: https://github.com/kirkone/KK.AspNetCore.EasyAuthAuthentication – KirKone Oct 02 '18 at 19:11
  • @KirKone thanks for the update as i'm trying to do everything using asp.net core 2.1+ web application projects to have some portability across azure function app, web app and container deployment scenarios. I tried user3159405 nuget and it blows up at runtime. I tried your solution and it doesn't blow up at runtime but I am having issue in that it works for easyauth browser openid connect session cookie requests but not for easyauth desktop/mobile app oauth authorization header bearer token secured requests. Will post question to your GitHub repo to try and collaborate to get that resolved. – myusrn Dec 08 '18 at 00:03
  • . . . and @KirkKone I was super happy to see readme notes on being able to use if (this.Environment.IsDevelopment()) { options.AuthEndpoint = "auth/me.json"; } to enable test User/ClaimsPrincipal data for localhost debugging, nice work – myusrn Dec 08 '18 at 00:25
2

The following code decrypts the AAD token from the Azure App Service HTTP header and populates HttpContext.User with the claims. It's rough as you'd want to cache the configuration rather than look it up on every request:

    OpenIdConnectConfigurationRetriever r = new OpenIdConnectConfigurationRetriever();
    ConfigurationManager<OpenIdConnectConfiguration> configManager = new ConfigurationManager<OpenIdConnectConfiguration>(options.Endpoint, r);
    OpenIdConnectConfiguration config = await configManager.GetConfigurationAsync();

    var tokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuerSigningKey = true,
        IssuerSigningKeys = config.SigningKeys.ToList(),
        ValidateIssuer = true,
        ValidIssuer = config.Issuer,
        ValidateAudience = true,
        ValidAudience = options.Audience,
        ValidateLifetime = true,
        ClockSkew = new TimeSpan(0, 0, 10)
    };

    JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();

    ClaimsPrincipal principal = null;
    SecurityToken validToken = null;

    string token = context.Request.Headers["X-MS-TOKEN-AAD-ID-TOKEN"];

    if (!String.IsNullOrWhiteSpace(token))
    {
        principal = handler.ValidateToken(token, tokenValidationParameters, out validToken);

        var validJwt = validToken as JwtSecurityToken;

        if (validJwt == null) { throw new ArgumentException("Invalid JWT"); }

        if (principal != null)
        {
            context.User.AddIdentities(principal.Identities);
        }
    }

It only works for Azure AD. To support other ID providers (Facebook, Twitter, etc) you'd have to detect the relevant headers and figure out how to parse each provider's token. However, it should just be variations on the above theme.

Rhysk
  • 33
  • 1
  • 5
  • 1
    You only need to parse the JWT. Azure App Service has already validated it. That would simplify your solution quite a bit. – Chris Gillum Feb 21 '17 at 21:53
1

You can give this library a try. I faced a similar problem and created this to simplify the use.

https://github.com/dasiths/NEasyAuthMiddleware

Azure App Service Authentication (EasyAuth) middleware for ASP.NET CORE with fully customizable components with support for local debugging

It hydrates the HttpContext.User by registering a custom authentication handler. To make things easier when running locally, it even has the ability to use a json file to load mocked claims.

Dasith Wijes
  • 1,328
  • 12
  • 22