Skip to main content

3 posts tagged with "authentication"

View All Tags

Azure Easy Auth and Roles with .NET (and .NET Core)

If this post is interesting to you, you may also want to look at this one where we try to use Microsoft.Identity.Web for the same purpose.

Azure has a feature which is intended to allow Authentication and Authorization to be applied outside of your application code. It's called "Easy Auth". Unfortunately, in the context of App Services it doesn't work with .NET Core and .NET. Perhaps it would be better to say: of the various .NETs, it supports .NET Framework. To quote the docs:

At this time, ASP.NET Core does not currently support populating the current user with the Authentication/Authorization feature. However, some 3rd party, open source middleware components do exist to help fill this gap.

Thanks to Maxime Rouiller there's a way forward here. However, as I was taking this for a spin today, I discovered another issue.

Where are our roles?#

Consider the following .NET controller:

[Authorize(Roles = "Administrator,Reader")]
[HttpGet("api/admin-reader")]
public string GetWithAdminOrReader() =>
"this is a secure endpoint that users with the Administrator or Reader role can access";
[Authorize(Roles = "Administrator")]
[HttpGet("api/admin")]
public string GetWithAdmin() =>
"this is a secure endpoint that users with the Administrator role can access";
[Authorize(Roles = "Reader")]
[HttpGet("api/reader")]
public string GetWithReader() =>
"this is a secure endpoint that users with the Reader role can access";

The three endpoints above restrict access based upon roles. However, even with Maxime's marvellous shim in the mix, authorization doesn't work when deployed to an Azure App Service. Why? Well, it comes down to how roles are mapped to claims.

Let's back up a bit. First of all we've added a dependency to our project:

dotnet add package MaximeRouiller.Azure.AppService.EasyAuth

Next we've updated our Startup.cs``ConfigureServices such that it looks like this:

if (Env.IsDevelopment()) {
services.AddMicrosoftIdentityWebAppAuthentication(Configuration);
else
services.AddAuthentication("EasyAuth").AddEasyAuthAuthentication((o) => { });

With the above in place, either the Microsoft Identity platform will directly be used for authentication, or Maxime's package will be used as the default authentication scheme. The driver for this is Env which is an IHostEnvironment that was injected to the Startup.cs. Running locally, both authentication and authorization will work. However, deployed to an Azure App Service, only authentication will work.

It turns out that directly using the Microsoft Identity platform, we see roles claims coming through like so:

[
// ...
{
"type": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
"value": "Administrator"
},
{
"type": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
"value": "Reader"
},
// ...
]

But in Azure we see roles claims showing up with a different type:

[
// ...
{
"type": "roles",
"value": "Administrator"
},
{
"type": "roles",
"value": "Reader"
},
// ...
]

This is the crux of the problem; .NET and .NET Core are looking in a different place for roles.

Role up, role up!#

There wasn't an obvious way to make this work with Maxime's package. So we ended up lifting the source code of Maxime's package and tweaking it. Take a look:

using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
/// <summary>
/// Based on https://github.com/MaximRouiller/MaximeRouiller.Azure.AppService.EasyAuth
/// Essentially EasyAuth only supports .NET Framework: https://docs.microsoft.com/en-us/azure/app-service/app-service-authentication-how-to#access-user-claims
/// This allows us to get support for Authentication and Authorization (using roles) with .NET
/// </summary>
namespace EasyAuth {
public static class EasyAuthAuthenticationBuilderExtensions {
public static AuthenticationBuilder AddEasyAuthAuthentication(
this IServiceCollection services) =>
services.AddAuthentication("EasyAuth").AddEasyAuthAuthenticationScheme(o => { });
public static AuthenticationBuilder AddEasyAuthAuthenticationScheme(
this AuthenticationBuilder builder,
Action<EasyAuthAuthenticationOptions> configure) =>
builder.AddScheme<EasyAuthAuthenticationOptions, EasyAuthAuthenticationHandler>(
"EasyAuth",
"EasyAuth",
configure);
}
public class EasyAuthAuthenticationOptions : AuthenticationSchemeOptions {
public EasyAuthAuthenticationOptions() {
Events = new object();
}
}
public class EasyAuthAuthenticationHandler : AuthenticationHandler<EasyAuthAuthenticationOptions> {
public EasyAuthAuthenticationHandler(
IOptionsMonitor<EasyAuthAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock) {
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync() {
try {
var easyAuthEnabled = string.Equals(Environment.GetEnvironmentVariable("WEBSITE_AUTH_ENABLED", EnvironmentVariableTarget.Process), "True", StringComparison.InvariantCultureIgnoreCase);
if (!easyAuthEnabled) return Task.FromResult(AuthenticateResult.NoResult());
var easyAuthProvider = Context.Request.Headers["X-MS-CLIENT-PRINCIPAL-IDP"].FirstOrDefault();
var msClientPrincipalEncoded = Context.Request.Headers["X-MS-CLIENT-PRINCIPAL"].FirstOrDefault();
if (string.IsNullOrWhiteSpace(easyAuthProvider) ||
string.IsNullOrWhiteSpace(msClientPrincipalEncoded))
return Task.FromResult(AuthenticateResult.NoResult());
var decodedBytes = Convert.FromBase64String(msClientPrincipalEncoded);
var msClientPrincipalDecoded = System.Text.Encoding.Default.GetString(decodedBytes);
var clientPrincipal = JsonSerializer.Deserialize<MsClientPrincipal>(msClientPrincipalDecoded);
if (clientPrincipal == null) return Task.FromResult(AuthenticateResult.NoResult());
var mappedRolesClaims = clientPrincipal.Claims
.Where(claim => claim.Type == "roles")
.Select(claim => new Claim(ClaimTypes.Role, claim.Value))
.ToList();
var claims = clientPrincipal.Claims.Select(claim => new Claim(claim.Type, claim.Value)).ToList();
claims.AddRange(mappedRolesClaims);
var principal = new ClaimsPrincipal();
principal.AddIdentity(new ClaimsIdentity(claims, clientPrincipal.AuthenticationType, clientPrincipal.NameType, clientPrincipal.RoleType));
var ticket = new AuthenticationTicket(principal, easyAuthProvider);
var success = AuthenticateResult.Success(ticket);
Context.User = principal;
return Task.FromResult(success);
} catch (Exception ex) {
return Task.FromResult(AuthenticateResult.Fail(ex));
}
}
}
public class MsClientPrincipal {
[JsonPropertyName("auth_typ")]
public string? AuthenticationType { get; set; }
[JsonPropertyName("claims")]
public IEnumerable<UserClaim> Claims { get; set; } = Array.Empty<UserClaim>();
[JsonPropertyName("name_typ")]
public string? NameType { get; set; }
[JsonPropertyName("role_typ")]
public string? RoleType { get; set; }
}
public class UserClaim {
[JsonPropertyName("typ")]
public string Type { get; set; } = string.Empty;
[JsonPropertyName("val")]
public string Value { get; set; } = string.Empty;
}
}

There's a number of changes in the above code to Maxime's package. Three changes that are not significant and one that is. First the insignificant changes:

  1. It uses System.Text.Json in place of JSON.NET
  2. It uses C#s nullable reference types
  3. It changes the extension method signature such that instead of entering services.AddAuthentication().AddEasyAuthAuthentication((o) =&gt; { }) we now need only enter services.AddEasyAuthAuthentication()

Now the significant change:

Where the middleware encounters claims in the X-MS-CLIENT-PRINCIPAL header with the Type of "roles" it creates brand new claims for each, with the same Value but with the official Type supplied by ClaimsTypes.Role of "http://schemas.microsoft.com/ws/2008/06/identity/claims/role". The upshot of this, is that when the processed claims are inspected in Azure they now look more like this:

[
// ...
{
"type": "roles",
"value": "Administrator"
},
{
"type": "roles",
"value": "Reader"
},
// ...
{
"type": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
"value": "Administrator"
},
{
"type": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
"value": "Reader"
}
]

As you can see, we now have both the originally supplied roles as well as roles of the type that .NET and .NET Core expect. Consequently, roles based behaviour starts to work. Thanks to Maxime for his fine work on the initial solution. It would be tremendous if neither the code in this blog post nor Maxime's shim were required. Still, until that glorious day!

Update: Potential ways forward#

When I was tweeting this post, Maxime was good enough to respond and suggest that this may be resolved within Azure itself in future:

Oh, so that's why they removed the name? ๐Ÿ˜ฒ๐Ÿ˜œ Jokes aside, we hope that this package won't be necessary for the future. I know that @mattchenderson is part of a working group to update Easy Auth. Might want to make sure you follow him as well. ๐Ÿ˜

โ€” Maxime Rouiller (@MaximRouiller) January 14, 2021

There's a prospective PR that would add an event to Maxime's API. If something along these lines was merged, then my workaround would no longer be necessary. Follow the PR here.

Hard-coding a Claim in Development Mode in ASP.Net Core

I was recently part of a hackathon team that put together an API in just 30 hours. We came second. (Not bitter, not bitter...)

We were moving pretty quickly during the hackathon and, when we came to the end of it, we had a working API which we were able to demo. The good news is that the API is going to graduate to be a product! We're going to ship this. Before we can do that though, there's a little tidy up to do.

The first thing I remembered / realised when I picked up the codebase again, was the shortcuts we'd made on the developer experience. We'd put the API together using ASP.Net Core. We're handling authentication using JWTs which is nicely supported. When we're deployed, an external facing proxy calls our application with the appropriate JWT and everything works as you'd hope.

The question is, what's it like to develop against this on your laptop? Getting a JWT for when I'm debugging locally is too much friction. I want to be able to work on the problem at hand, going away to get a JWT each time is a timesuck. So what to do? Well, during the hackathon, we just commented out [Authorize] attributes and hardcoded user ids in our controllers. This works, but it's a messy developer experience; it's easy to forget to uncomment things you've commented and break things. There must be a better way.

The solution I landed on was this: in development mode (which we only use whilst debugging) we hardcode an authenticated user. The way our authentication works is that we have a claim on our principal called something like "our-user-id", the value of which is our authenticated user id. So in the ConfigureServices method of our Startup.cs we have a conditional authentication registration like this:

// Whilst developing, we don't want to authenticate; we hardcode to a particular users id
if (Env.IsDevelopment()) {
services.AddAuthentication(nameof(DevelopmentModeAuthenticationHandler))
.AddScheme<DevelopmentModeAuthenticationOptions, DevelopmentModeAuthenticationHandler>(
nameof(DevelopmentModeAuthenticationHandler),
options => {
options.UserIdToSetInClaims = "this-is-a-user-id";
}
);
}
else {
// The application typically uses this
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
// ...
});
}

As you can see, we're using a special DevelopmentModeAuthenticationHandler authentication scheme in development mode, instead of JWT. As we register that, we declare the user id that we want to use. Whenever the app runs using the DevelopmentModeAuthenticationHandler auth, all requests will arrive using a principal with an "our-user-id" claim with a value of "this-is-a-user-id" (or whatever you've set it to.)

The DevelopmentModeAuthenticationHandler looks like this:

using System.Collections.Generic;
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;
namespace OurApp
{
public class DevelopmentModeAuthenticationOptions : AuthenticationSchemeOptions
{
public string UserIdToSetInClaims { get; set; }
}
public class DevelopmentModeAuthenticationHandler : AuthenticationHandler<DevelopmentModeAuthenticationOptions> {
private readonly ILoggingService _loggingService;
public DevelopmentModeAuthenticationHandler(
IOptionsMonitor<DevelopmentModeAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock
) : base(options, logger, encoder, clock) {
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync() {
var claims = new List<Claim> { new Claim("our-user-id", Options.UserIdToSetInClaims) };
var identity = new ClaimsIdentity(claims, nameof(DevelopmentModeAuthenticationHandler));
var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}
}

Now, developing locally is frictionless! We don't comment out [Authorize] attributes, we don't hard code user ids in controllers.

WCF Transport Windows authentication using NetTcpBinding in an Intranet environment

Update#

Since I wrote this initial post I've taken thinks on a bit further. Take a look at this post to see what I mean: http://icanmakethiswork.blogspot.com/2012/03/wcf-moving-from-config-to-code-simple.html I know I said I'd write about JSON this time. I will get to that but not this time. This time WCF authentication quirks. I've been working on a project that uses .NET Remoting to have a single central point to which web applications and Windows services can call into. This is used in an intranet environment and all the websites and Windows services were hosted on the same single server along with our .NET Remoting Windows service. (They could quite easily have been on different servers but there was no need in this case.) It was decided to "embrace the new" by migrating this .NET Remoting project over to WCF. The plan wasn't to do anything revolutionary, just to move from one approach to the other as easily as possible. I found the following useful article on MSDN: http://msdn.microsoft.com/en-us/library/aa730857%28v=vs.80%29.aspx This particular article was helpful and following the steps enclosed I was quickly up and running with a basic WCF service hosted in a Windows service. It was at this point I started thinking about security. The existing .NET Remoting approach had no security in place. This wasn't ideal but also probably wasn't the worry you might think. It was hosted in an intranet environment and hence not so exposed to the rigours of the Wild Wild Web. However, since I was looking at WCF I thought it would be a good opportunity to get some basic security in place. This generally pleases auditors. I opted to use Windows Transport authentication as this seemed pretty appropriate for an intranet environment. The idea being that we'd authenticate with Windows for an account in our domain. After headbutting Windows for some time I managed to get a successful client call going from the website running on my development machine to the (separate) development server that was hosting our WCF Window service using Transport Windows authentication. However, when deploying the website to the development server I discovered we would experience the following error when the website attempted to call the WCF service (on the same server). ``` Event Type: Failure Audit Event Source: Security Event Category: Logon/Logoff Event ID: 537 Date: 15/02/2012 Time: 16:32:04 User: NT AUTHORITY\SYSTEM Computer: MINE999 Description: Logon Failure: Reason: An error occurred during logon Logon Type: 3 Logon Process: ^ Authentication Package: NTLM Status code: 0xC000006D

Not terribly helpful. At the end of the day it seemed we were suffering from a security "feature" introduced by Microsoft to prevent services calling services on the same box with a fully qualified name. An explanation of this can be found here: [http://developers.de/blogs/damir\_dobric/archive/2009/08/28/authentication-problems-by-using-of-ntlm.aspx](<http://developers.de/blogs/damir_dobric/archive/2009/08/28/authentication-problems-by-using-of-ntlm.aspx>) Using method 1 in the enclosed link I initially worked round this by amending the registry and rebooting the server: [http://support.microsoft.com/kb/887993](<http://support.microsoft.com/kb/887993>) This was not a fantastic solution. Fortunately I subsequently found a better one but since the resources on the web are \***ATROCIOUS**\* on this point I thought I should take the time to note down the full explanation since otherwise it'll be lost in the mists of time. Here we go: The equivalent security to the previous .NET Remoting solution in WCF was to use this config setting on client and service: ```xml
<security mode="None" />

As I've said, this is an intranet environment and so having this "none" security setting in place is made less worrying by the fact that the network itself is secured. But obviously this is not ideal and unlikely to be audit compliant. To use Windows security you need this netTcpBinding config setting on client and service: ```xml

```

To call the service with this setting in place you will need to be an authenticated Windows user. (Or at the very least impersonating one - but you knew that.) NOW FOR THE MOST IMPORTANT BIT..... The endpoint addresses *must* be "localhost" for both

client and service when both are deployed to the same server. If this is not the case then you will suffer from the aforementioned security "feature" which will provide you with unhelpful "the server has rejected the client credentials" messages and *nothing* else. OK FINISHED - MOVE ALONG NOW... NOTHING MORE TO SEE HERE With WCF Windows Transport authentication in place you can interrogate the calling user id within the service methods by simply evaluating ServiceSecurityContext.Current.PrimaryIdentity.Name (which will be something like "myDomain\myUserName"). So we you wanted to, we could have a simple step which evaluated if the calling user is on the "approved" / "authorised" list. I'm sure this could be made more sophisticated by using groups etc I guess - though I haven't investigated it further as yet. In fact, I suspect Microsoft may have something even more sophisticated still available for use which I'm unaware of - if anyone knows a simple explanation of this then please do let me know! In closing, I do think Microsoft could work on providing more helpful error messages than "the server has rejected the client credentials". Going by what I read as I researched this error many people seem to have struggled much as I did before eventually bailing out and ended up chancing it by turning security off in their applications. Clearly it is not desirable to have people so confused by errors that they give up and settle for a less secure solution.