Share via


Working with identity in .NET Core 2.0

Last year I did a quick code sample on how to use identity in .NET Core (1.x):
https://contos.io/protecting-a-net-core-api-with-azure-active-directory-59bbcd5b3429

Time flies, and just weeks ago I showed how fun/frustrating it can get when we want to secure a SPA with a .NET Core 2.0 back-end:
https://blogs.msdn.microsoft.com/azuredev/2017/09/22/protecting-a-net-core-2-0-spa-with-adfs/

As I stated along with the most recent code snippet there has been changes in .NET Core 2.0 in the identity area:
/en-us/aspnet/core/migration/1x-to-2x/identity-2x

The guide is understandable enough, and you should certainly skim through it if you happen to have a login button in your app. Now, I don´t have strong opinions on whether middleware or services are the best solution for enabling auth. Yes, I like the new approach, but for small scale solutions it doesn´t make that much difference I suppose. Anyways, this isn´t going to be a post discussing the general syntax of .NET Core :)

Authentication and authorization is a pretty vital part of apps these days so I thought it might behoove us to look further into things (specific to .NET Core 2.0).

While the migration guide gives good examples on how to enable different types of auth, it unfortunately still leaves a couple of things open to interpretation and/or further questions.

What is the difference between JwtBearerAuth and OpenIdConnect? Should you implement Microsoft account support through the sample given, or should you use the converged auth experience offered by https://apps.dev.microsoft.com? Does it change anything regarding Azure AD "classic"? What about multi-tenancy? And where does Azure AD B2C play into all of this?

Phew... And that´s just a few questions off the top of my head. Imagine what a more clever mind could come up with.

So, am I able to address all of these and give a definite answer or not? Let´s see. (Well, to pre-empt criticism that I´m leaning more against the authentication than the authorization part - I am aware of this. I try not to mix in too much in one go since that will just be confusing. I might return with more on authZ later.)

[caption id="attachment_7135" align="alignnone" width="169"] Must keep my identity secret...[/caption]

JwtBearerAuth vs OpenIdConnect
In my .NET Core 1.x sample I used JwtBearerAuth for AAD integration. In my .NET Core 2.0 sample, (the one with the SPA), I used OpenIdConnect. But why are there two - isn´t everything a JWT these days?

The naming gives you no clues, but what you will notice if you use the Visual Studio 2017 templates is that creating a .NET Core 2.0 Web API will generate code based on JwtBearerAuth, whereas creating a .NET Core 2.0 Web App will generate code using OpenIdConnect. (Note: I am assuming the usage of these two generated apps for the rest of this article.)

Ok, that´s understandable, right? Use one for APIs and use another for web pages. But hold on a minute - doesn´t the OpenID Connect protocol also use JSON-style tokens, so are we not still talking about two ways of achieving the same goal? Ah, yes, the low level intricacies of protocols. If you load the web app, and capture the traffic in Fiddler, you will notice that you acquire an id token, and this is in turn used for authorization. But when actually calling the protected parts of the web page afterwards a cookie is used. Bearer tokens are nice and flexible, and if you have one you can copy it and use it wherever you like. A cookie however can´t be replayed as easily, so it´s better to have one of those living in the browser. So, there is a slight nuance as to what the different approaches do behind the covers.

This reminds me of a trick I employed previously where you could configure an API to not allow browser usage by adding [HostAuthentication("OAuth2Bearer")] to the controller. But this doesn´t seem to compile with .NET Core 2 - is there a new trick?

Yes, several options really. Change the attribute to something like this:

 
[Route("api/[controller]")]
[Authorize(ActiveAuthenticationSchemes = "Bearer")]
public class ValuesController : Controller

Or change the token validation procedure (in AzureAdAuthenticationBuilderExtensions.cs):

 
public void Configure(string name, JwtBearerOptions options)
{
    options.Audience = _azureOptions.ClientId;
    options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}";
    options.TokenValidationParameters = new IdentityModel.Tokens.TokenValidationParameters
    {
        AuthenticationType = "Bearer",
    };
}

Or go all in and create your custom policy which states that members of the "Developer" group in Azure AD are allowed to auth however they like, while everybody else has to adhere to a specific standard. (On second thought - should we even allow commoners access at all?)

Custom policies are powerful, and deserves to be treated as a separate topic so we´ll skip that part for now too.

Multi-tenancy
If you went ahead and let Visual Studio guide you in creating apps you have something that should work nicely for signing into your Azure AD home tenant. But did you notice that multi-tenant support was not an option in the wizard? Does VS not care about supporting different tenants? To be honest I don´t know why the wizard doesn´t support this. (Full .NET Framework wizards allow it.) Azure AD certainly supports multi-tenancy in the app model so that is not the limitation either.

So, what happens when signing in with a user from a different tenant in the code as it is? An error message will appear:

AADSTS50020: User account 'cloudy@northwind.onmicrosoft.com' from identity provider 'https://sts.windows.net/northwind-tenant-id/' does not exist in tenant 'Contoso' and cannot access the application 'app-guid' in that tenant. The account needs to be added as an external user in the tenant first. Sign out and sign in again with a different Azure Active Directory user account.

It´s re-assuring that you cannot just randomly sign in at least :) This indicates that the first step we need to perform is to enable multi-tenancy for the app. (The message is actually a bit ambigious as you might be tempted to think along the lines of Azure AD B2B where you invite users in a semi-multi-tenant way, but we´re looking for "true" multitenancy.) While in the portal find your app in the App registrations list. You will find it listed in Enterprise applications too, but the option to make it multi-tenant isn´t available there.

Let´s see what happens now...hmmm... we get the same error message as before. Is there something more to this multi-tenancy thing?

Yes, the default setup is that your app always assumes that the tokens are issued by your own authorization endpoint. But your endpoint can only issue tokens for users it owns. Other tenants will issue tokens for the users they manage. To get around this we need to make a change - we don´t hit other organization´s endpoints specifically; rather we hit the common endpoint. You need to edit applicationsettings.json like this:

 
"AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "contoso.com",
    //"TenantId": "tenant-guid-placeholder",
    "TenantId":  "common",
    "ClientId": "client-guid-placeholder",
    "CallbackPath": "/signin-oidc"
  },

What does this change then? Well, when attempting to sign in this time, we first need to grant the app access to our user object, which should work provided you have the necessary rights. But then we´re thrown into a full-blown exception instead:

SecurityTokenInvalidIssuerException: IDX10205: Issuer validation failed. Issuer: 'https://sts.windows.net/northwind-tenant-id/'. Did not match: validationParameters.ValidIssuer: 'null' or validationParameters.ValidIssuers: 'https://sts.windows.net/{tenantid}/'.

This is interesting though. It seems we have a token, and the app is able to read it to since it refers to a different tenant id. This indicates that Azure AD is happy to do its part, but the token validation in the app is failing instead (and we don´t properly catch exceptions either it seems). We´re no longer failing due to the setup in Azure AD, so nothing to reconfigure in the portal. How to get around this?

There is always the blunt approach - modify the token validation as follows (in AzureAdAuthenticationBuilderExtensions.cs):

 
public void Configure(string name, OpenIdConnectOptions options)
{
    options.ClientId = _azureOptions.ClientId;
    options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}";
    options.UseTokenLifetime = true;
    options.CallbackPath = _azureOptions.CallbackPath;
    options.RequireHttpsMetadata = false;
    options.TokenValidationParameters = new IdentityModel.Tokens.TokenValidationParameters
    {
        //Available for everyone!
        ValidateIssuer = false
    };
}

There are scenarios where this might be what you want, but most likely you will want to put some restrictions in place. Fortunately you can compile a list of approved issuers by using the ValidIssuers enumeration. (I will it leave it to you to experiment with this.)

Azure AD B2C
That should cover some of the "old school Azure AD" questions. How about Azure AD B2C? Yes, it is supported as well. B2C also relies on OpenId Connect, so it should be a matter of mere config. Alright, what kind of config then I hear.

You can treat the B2C tenant like any other tenant if you like actually. Modify applicationsettings.json to something like this:

 
"Instance": "https://login.microsoftonline.com/",
"Domain": "contosob2c.onmicrosoft.com",
"ClientId": "b2c-app-guid",
"TenantId": "contosob2c.onmicrosoft.com",

(You should already have a B2C app configured to retrieve the guid.)

Notice any difference from the regular AAD signin experience? I didn´t.

Isn´t the whole point of B2C that you get a choice of different IdPs to sign in with? Yes, it is. Now, here´s the funny thing - you can invoke B2C in different ways. The different policies you create are the triggers for which "journeys" are available during signin.

I have policies setup from previous labs that handle ADFS and non-B2C tenants. (See previous posts for details: https://blogs.msdn.microsoft.com/azuredev/2017/06/23/using-adfs-as-an-identity-provider-for-azure-ad-b2c/)

This means I can change up the settings further:

 
"Instance": "https://login.microsoftonline.com/tfp/",
"Domain": "contosob2c.onmicrosoft.com",
"ClientId": "b2c-app-guid",
"TenantId": "contosob2c.onmicrosoft.com/B2C_1A_SignUpOrSignInWithAAD/v2.0/",

Notice how I make the policy name part of the TenantId setting? This modifies the URL for metadata retrieval, (which means that "TenantId" as the setting name is slightly misleading), and in turn triggers different redirects. I also added tfp to the root URL - it´s part of the B2C setup and stands for TrustFrameworkPolicy.

In my case this brings up the screen where I get to choose between AAD and On-prem AD.

Neat right? And we´re just changing config here - no code.

Making the config slightly cleaner we could have something like this:

 
"AzureAd": {
    //AAD Settings
  },
"AADB2C-NoPolicies": {
    //AAD B2C Settings
  },
"AADB2C-Policy"  : {
    //AAD B2C Settings
  },

And your app could support all the identity providers required with a minimum of fuss :)

Wrapping up
Yes, I know I have skipped the questions on converged auth and "native handling" of social identities. I think it will fit nicely into this section :)

I have only covered OAuth and OpenId Connect in various forms here. That is on purpose on my part. My opinion is still that you should of course support other protocols in legacy systems, and other components that needs to have things in another form. But in general avoid it if you can in new solutions as the newer protocols are more manageable.

You might have the impression that there was a lot of back and forth here between different setups, and you would be right. However, if you re-read it you will notice that most of the changes was to configuration settings only. While having a custom token validation procedure will require you to write code most of the code actually stayed the same between use cases. I would say that this is a good thing in .NET Core 2 so far. I would like to see better official documentation on how to string along the configurations (oh, the wordplay), but I like the fact that you don´t need to have ten different paths in the code itself. I remember back in the day where you more or less had to compile different builds for supporting Azure AD and ADFS; this shouldn´t be a concern any longer.

As increasingly often is the case, we´re still spoilt for choice in a sense. Do I like how easy it is to integrate Facebook auth in your code without extra fuss? Yes, I do. Do I like how it´s fairly achievable for me to support converged auth on the AAD v2 endpoint, (for addressing Microsoft and AAD accounts), if I want to? Yes, I do. Do I like having to consider carefully if I want to go through B2C or "classic" Azure AD in case I want to expand my options in the future? Well, everyone enjoys having a choice, but it does require you to do more thinking up-front :)

I would say that from an identity perspective .NET Core is maturing, and I am feeling less worried about using in it new projects. (Apps requiring a few other components to have something workable means you might have to evaluate other parts of .NET Core as well before going all in of course.)

Comments

  • Anonymous
    October 17, 2017
    Man I remember getting into Azure when it was first taking off, and trying to wire in Azure Access Control (I think that is what it was?) into a Silverlight application. Now THAT was a challenge. ;) Incredible work on this post. Glad to see someone taking authentication to the depth it deserves.
    • Anonymous
      October 17, 2017
      Azure Access Control Service (ACS) was a great concept. Sure, it wasn´t exactly intuitive on the portal side, and Windows Identity Framework on the client side had a certain level of complexity too. (As for Silverlight...well, what to say...) Unfortunately there were some challenges on the back-end of ACS too which prevented it from being just a matter of a nicer UI experience to make things work.We´re starting to see B2C sort of take over the role ACS had though, so things are improving :)
  • Anonymous
    January 16, 2019
    Yes, Mr. Lego Man. Please keep your identity a secret! This has become a very popular blog post. Thanks, Andreas!