ASP.NET Core and Azure AD have been kind of my passion for the last year. Naturally with ASP.NET Core 2.0 coming out I wanted to see what had changed in the area of authentication. I made an article on enabling Azure AD authentication in ASP.NET Core 1.0 almost a year ago.

ASP.NET Core 1.0 Azure AD Authentication

If you want to skip reading and get straight to the code, you can find a small example app I made here: https://github.com/juunas11/aspnetcore2aadauth

Otherwise, let's get started with ASP.NET Core 2.0!

What do you need to work with ASP.NET Core 2.0 anyway?

  1. .NET Core 2.0 SDK and runtime: https://www.microsoft.com/net/download/core
  2. (Optional) Visual Studio 2017 Update 3: https://www.visualstudio.com/downloads/

The only real requirement is the 2.0 SDK and runtime. With those installed, you can already compile and run ASP.NET Core 2.0 apps from the command-line.

If you want to develop them in Visual Studio, you will need the (currently) latest version of Visual Studio 2017 (update 3).

ASP.NET Core 2.0 is available on NuGet and we can use it from there: https://www.nuget.org/packages/Microsoft.AspNetCore.All/2.0.0.

If you haven't heard of it yet, the All package is a meta-package that contains most packages that comprise ASP.NET Core. It makes it quite easy to start using ASP.NET Core. If you are worried that will make your app huge, you need not be. When compiling for .NET Core, the dependencies you don't use get cut out. So only the parts you use become part of the app.

ASP.NET Core 2.0 authentication changes

Many things have changed in 2.0. Authentication is one of them. I will go through changes to other parts in future articles.

In ASP.NET Core 1.X, authentication middleware were registered in the Configure method in the Startup class. That has had a major change. There is now only a single middleware that you add:

app.UseAuthentication();

Other middleware, like cookie authentication middleware need not be added.

Instead, authentication is now done through services. You will add them in ConfigureServices. Here for example we add cookie authentication.

services.AddCookieAuthentication();

Another thing that has changed is that we can now define the default authentication/challenge/sign-in handler in one place. You can see an example of the new approach in the next section.

Setting up Azure AD authentication in Startup

In an MVC application that wants to use Azure AD authentication, we need two authentication handlers:

  1. Cookies
  2. Open Id Connect

We can add them to the service collection like this:

services.AddCookieAuthentication();
services.AddOpenIdConnectAuthentication();

Cookies is responsible for two things:

  1. Signing the user in (creating the authentication cookie and returning it to the browser)
  2. Authenticating cookies in requests and creating user principals from them

Open Id Connect is responsible for only one thing really: Responding to challenges from [Authorize] or ChallengeResult returned from controllers.

When it receives a challenge, it sends the user to authenticate against the identity provider (in this case Azure AD). When the user gets redirected back to the app, it does a multitude of things to authenticate the returned info, and then requests the default sign-in handler to sign the user in.

So let's configure the default handlers:

services.AddAuthentication(auth =>
{
    auth.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    auth.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    auth.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
});

Note there that by default, cookie authentication handles authentication and sign-in. Open Id Connect only handles challenges. In 1.X, these would have been defined in the middleware options (except for the default sign-in middleware).

Then we need to add the authentication middleware to Configure as mentioned before:

app.UseAuthentication();

For basic scenarios, we are actually almost done. Just one thing missing, the configuration!

Configuring Open Id Connect automatically/slightly manually

Previously we added Open Id Connect authentication like this:

services.AddOpenIdConnectAuthentication();

If you check it's documentation, it says:

Adds OpenIdConnect authentication with options bound against the "OpenIdConnect" section from the IConfiguration in the service container.

So, it expects you have a section like this e.g. in appsettings.json:

{
  "OpenIdConnect": {
    "ClientId": "bcd3f4c3-aaaa-aaaa-aaaa-e349f2b4bdac",
    "Authority": "https://login.microsoftonline.com/<tenant-id>/",
    "PostLogoutRedirectUri": "http://localhost:5000",
    "CallbackPath": "/signin-oidc",
    "ResponseType": "code id_token",
    "Resource": "https://graph.microsoft.com/"
  }
}

And in user secrets:

{
  "OpenIdConnect": {
    "ClientSecret": "abcdefghi..."
  }
}

These will automatically map to properties in OpenIdConnectOptions.

So, configuration? Done.

Now, in some cases you want to e.g. define handlers that do something when you receive the authorization code.

There's just one small thing. When you specify handlers like this:

services.AddOpenIdConnectAuthentication(opts =>
{
    opts.Events = new OpenIdConnectEvents
	{
	    OnAuthorizationCodeReceived = ctx =>
	    {
	        return Task.CompletedTask;
	    }
	};
});

Configuration is not bound in this case. You can do that however just by adding one line of code:

services.AddOpenIdConnectAuthentication(opts =>
{
	Configuration.GetSection("OpenIdConnect").Bind(opts);
    opts.Events = new OpenIdConnectEvents
	{
	    OnAuthorizationCodeReceived = ctx =>
	    {
	        return Task.CompletedTask;
	    }
	};
});

If you just need the authentication and no access tokens to APIs etc., the first option with no arguments that auto-binds to the configuration is best.

Otherwise, at least bind options from configuration so you don't have to type configuration keys manually.

Here is a more complete example of configuring Open Id Connect:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(opts =>
    {
        opts.Filters.Add(typeof(AdalTokenAcquisitionExceptionFilter));
    });

    services.AddCookieAuthentication();
    services.AddOpenIdConnectAuthentication(opts =>
    {
        Configuration.GetSection("Authentication").Bind(opts);

        opts.Events = new OpenIdConnectEvents
        {
            OnAuthorizationCodeReceived = async ctx =>
            {
                var request = ctx.HttpContext.Request;
                var currentUri = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path);
                var credential = new ClientCredential(ctx.Options.ClientId, ctx.Options.ClientSecret);

                var distributedCache = ctx.HttpContext.RequestServices.GetRequiredService<IDistributedCache>();
                string userId = ctx.Ticket.Principal.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;

                var cache = new AdalDistributedTokenCache(distributedCache, userId);

                var authContext = new AuthenticationContext(ctx.Options.Authority, cache);

                var result = await authContext.AcquireTokenByAuthorizationCodeAsync(
                    ctx.ProtocolMessage.Code, new Uri(currentUri), credential, ctx.Options.Resource);

                ctx.HandleCodeRedemption(result.AccessToken, result.IdToken);
            }
        };
    });

    services.AddAuthentication(auth =>
    {
        auth.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        auth.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
        auth.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    });
}

We first bind the configuration section "Authentication" to the Open Id Connect options. Then we setup an event handler for when we get an authorization code from Azure AD. We then exchange it for an access token for Microsoft Graph API.

The token cache class that I made here uses the distributed cache to store tokens. In development that would be a memory-backed cache, but in production it could be backed by a Redis cache or an SQL database.

Now while the handler can acquire an access token, I prefer using ADAL/MSAL as tokens then get cached, and it handles token refresh automatically.

We also setup an exception filter for MVC so that if ADAL token acquisition fails (because the token was not found in cache), we redirect the user to Azure AD to get new tokens.

Visual Studio Azure AD template

If you select Work and school accounts for authentication when creating a new 2.0 MVC Core app in Visual Studio, you will get all of the above setup for you with a neat little extension method generated in the project. Take a look at that for some alternative ways on how to implement Azure AD authentication.

Ultimately it does the same things though.

Related links and articles

Thanks for reading! Here are some links that I hope are useful for you. Don't hesitate to leave a comment if you have questions.