Using Azure AD with ASP.NET Core

Azure Active Directory is cloud-based directory service that allows users to use their personal or corporate accounts to log-in to different applications. Local Active Directory can sync data to its cloud counterpart. Also external users are supported. This blog post shows how to make ASP.NET Core application use Azure AD and how to read data that Azure AD provides about user account.

NB! To use Azure AD valid Microsoft Azure subscription is needed. It also goes for Azure AD services used by Office 365.

Using wizard for Azure AD authentication

Simplest way is adding Azure AD support to application using Visual Studio. Visual Studio 2017 allows to add Azure AD authentication for new applications. Hopefully there will be soon also support for adding Azure AD to existing applications. As this is how-to style post I will go here with new default application.

Steps are simple:

  1. Create new ASP.NET Core application
  2. Choose template
  3. Click on “Change Authentication” button
  4. Select “Work or School Accounts”
  5. Choose Azure AD you want to use
  6. Click “Ok”

ASP.NET Core: Connect to Azure AD

Visual Studio adds automatically new configuration file with all required configuration parametes, updates Startup class of web application and adds Account controller that coordinates authentication processes. If everything went well then we have application that supports Azure AD authentication and we can stop here.

Connecting application to Azure Active Directory manually

If we can’t use nice wizard for some reason then we can enable Azure AD support manually. This section is a short guide to how to do it. Even when Visual Studio wizards work well I suggest you to go through following sections of this blog post too as it gives you better idea how Azure AD support is actually implented.

Add the following NuGet packages to web application:

  • Microsoft.AspNetCore.Authentication.Cookies
  • Microsoft.AspNetCore.Authentication.OpenIdConnect

Add these settings to appsettings.json (this data can be found from Azure Portal).


"Authentication": {
 
"AzureAd"
: {
   
"AADInstance": "https://login.microsoftonline.com/"
,
   
"CallbackPath": "/signin-oidc"
,
   
"ClientId": "your client id"
,
   
"Domain": "your-domain.com"
,
   
"TenantId": "your tenant id"
  }
}

We need couple of changes to Startup class too. To ConfigureServices() method we add call to AddAuthentication() and to Configure() method the call to UseOpenIdConnectAuthentication.


public void ConfigureServices(IServiceCollection services)
{
   
// Add framework services.
    services.AddMvc();

    services.AddAuthentication(
        SharedOptions => SharedOptions.SignInScheme =
                           
CookieAuthenticationDefaults
.AuthenticationScheme
    );
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory
loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection(
"Logging"
));
    loggerFactory.AddDebug();

   
// ...

    app.UseOpenIdConnectAuthentication(
new OpenIdConnectOptions
    {
        ClientId = Configuration[
"Authentication:AzureAd:ClientId"
],
        Authority = Configuration[
"Authentication:AzureAd:AADInstance"] + Configuration["Authentication:AzureAd:TenantId"
],
        CallbackPath = Configuration[
"Authentication:AzureAd:CallbackPath"
]
    });

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name:
"default"
,
            template:
"{controller=Home}/{action=Index}/{id?}");
    });
}

We need additional controller to coordinate authentication operations. The tooling – when it works – adds automatically new Account controller. Here is the code.


public class AccountController : Controller
{
    [
HttpGet
]
   
public IActionResult
SignIn()
    {
       
return
Challenge(
           
new AuthenticationProperties { RedirectUri = "/" }, OpenIdConnectDefaults
.AuthenticationScheme);
    }

    [
HttpGet
]
   
public IActionResult
SignOut()
    {
       
var callbackUrl = Url.Action(nameof(SignedOut), "Account", values: null
, protocol: Request.Scheme);
       
return SignOut(new AuthenticationProperties
{ RedirectUri = callbackUrl },
           
CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults
.AuthenticationScheme);
    }

    [
HttpGet
]
   
public IActionResult
SignedOut()
    {
       
if
(HttpContext.User.Identity.IsAuthenticated)
        {
           
return RedirectToAction(nameof(HomeController.Index), "Home"
);
        }

       
return
View();
    }

    [
HttpGet
]
   
public IActionResult
AccessDenied()
    {
       
return View();
    }
}

It’s easy to see from code that there are two views we need to add. Here is the view for SignedOut action.


@{
    ViewData[
"Title"] = "Sign Out"
;
}

<h2>@ViewData["Title"].</h2
>
<
p class="text-success">You have successfully signed out.</p
>

This is the view for AccessDenied action.


@{
    ViewData[
"Title"] = "Access Denied"
;
}

<header>
    <h1 class="text-danger">Access Denied.</h1>
    <p class="text-danger">You do not have access to this resource.</p
>
</
header
>

Now we are done with coding and it’s time to try out Azure AD authentication.

Trying out Azure AD authentication

When we run our application we are redirected to identity provider login page. In my case it is Microsoft Account login page. Take a look at page title and notice my holy lazyness.

Microsoft Account log-in page

After successful authentication we are returned back to our application. Notice that user name is not e-mail address or GUID or some long sequence of letters and numbers. It has a special format: <authentication provider>#<e-mail address>.

User name with provider

This user name format is good for one thing – it is unique and we can also use it when multiple active directories are available for application users.

What data we get from Azure AD?

User name is not in very user-friendly format and the question is: what we can do to get something better there? Well, it’s a claim based authentication identity and where we should look is claims collection of user. As claims contain also sensitive information I don’t show here screenshot but I show the code that displays claims out.

To display claims that current claim identity has we have to send claims from controller to view. Here is the Index action of Home controller.


public IActionResult Index()
{
   
var claims = ((ClaimsIdentity
)User.Identity).Claims;
   
return View(claims);
}

NB! This code works only if we have identities of type ClaimsIdentity. With other authentication mechanisms we may have other identity types. To support different identitites in same code base we need more common way to detect user attributes.

To show claims on front page we use the following table.


@model IEnumerable<System.Security.Claims.Claim>
@{
    ViewData[
"Title"] = "Home Page"
;
}

<div class="row">
    <div class="col-md-12">
        <table>
            <thead>
                <tr>
                    <th>Claim</th>
                    <th>Value</th>
                </tr>
            </thead>
            <tbody>
                @foreach(var claim in
Model)
                {
                   
<tr>
                        <td>@claim.Type</td>
                        <td>@claim.Value</td>
                    </tr>
                }
           
</tbody>
        </table>
    </div
>
</
div
>

Run application, log in and take a look at table of claims. There is all basic information about user like first name, last name and e-mail address.

Displaying nice user name

User identificator with provider wasn’t the best choice of names we can show to user when he or she is logged in to site. There is _LoginPartial view under shared views folder and this is how this view looks like.


@using System.Security.Principal

@
if
(User.Identity.IsAuthenticated)
{
   
<ul class="nav navbar-nav navbar-right">
        <li class="navbar-text">Hello @User.Identity.Name!</li>
        <li><a asp-area="" asp-controller="Account" asp-action="SignOut">Sign Out</a></li>
    </ul>
}
else
{
   
<ul class="nav navbar-nav navbar-right">
        <li><a asp-area="" asp-controller="Account" asp-action="Signin">Sign in</a></li>
    </ul>
}

We add some additional code here to make this view display full name of user. As we saw from claims table then there is claim called name. This is the claim we will use.


@using System.Security.Principal
@
using
System.Security.Claims
@{ 
   
var claims = ((ClaimsIdentity
)User.Identity).Claims;
   
var name = claims.FirstOrDefault(c => c.Type == "name"
)?.Value;
}

@
if
(User.Identity.IsAuthenticated)
{
   
<ul class="nav navbar-nav navbar-right">
        <li class="navbar-text">Hello @name</li>
        <li><a asp-area="" asp-controller="Account" asp-action="SignOut">Sign Out</a></li>
    </ul>
}
else
{
   
<ul class="nav navbar-nav navbar-right">
        <li><a asp-area="" asp-controller="Account" asp-action="Signin">Sign in</a></li>
    </ul>
}

NB! Instead of writing code to partial view we should use view component and move this mark-up to some view of view component. Here I just wanted to show how to get the full name of current user.

Wrapping up

Adding Azure AD support to ASP.NET Core applications is easy. It can be done using Visual Studio but it also can be done manually. We needed few additional configuration parameters, some lines of code and small change to login view. Although default user name used by ASP.NET Core internally doesn’t look nice we were able to get user e-mail and full name from claims collection we got back from Azure AD.



See also

12 thoughts on “Using Azure AD with ASP.NET Core

  • Andres says:

    Hi! How do you configure the access denied path so when you have a 403 status code you hit a custom action in a controller. Thanks! I can´t do it using Azure Auth.

  • Gunnar says:

    Hi!
    Please describe your scenario more.

  • Andres says:

    Sorry for taking so long. I’m using a custom Authorization Policy Provider.
    In the ConfigureServices methos I’m adding the my custom provider and handler:

    services.AddTransient();
    services.AddSingleton();

    In the Configure Method I set the Cookie and Open IdConnect Options:

    app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
    AutomaticAuthenticate = true,
    SlidingExpiration = false,
    CookieName = “idfr_sga”,
    ExpireTimeSpan = TimeSpan.FromHours(12),
    AccessDeniedPath = new PathString(“/Site/NoAutorizado”),
    Events = new CookieAuthenticationEvents
    {
    OnSigningIn = ctx =>
    {
    ctx.Principal = TransformClaims(ctx, config);
    return Task.FromResult(null);
    }
    }
    });

    app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
    {
    AutomaticChallenge = true,
    ClientId = config[“Authentication:AzureAd:ClientId”],
    Authority = config[“Authentication:AzureAd:AADInstance”] + config[“Authentication:AzureAd:TenantId”],
    PostLogoutRedirectUri = config[“Authentication:AzureAd:PostLogoutRedirectUri”],
    SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme,
    UseTokenLifetime = false,
    });

    The thing is, when i get a 403 the app doesn’t redirect the user to the url “/Site/NoAutorizado” as i have configured in the Cookie Authentication options. Instead it sends the user to the login form, but the user is already logged in so the login form sends the user to the previous page, but that page gives a 403 so it goes to the login form, and then to the page with 403 and again and again and i get an infinite loop. I really can´t figure out what i´m doing wrong. Thanks!!

  • spence says:

    Do you know of an example of testing Controllers behind Azure AD?

  • Gunnar says:

    To unit test controllers you have to get all real authentication out of your way. Otherwise your tests will be polluted with other aspects and integrations that doesn’t relate directly to controller logic.

    For ASP.NET Core you should use different start-up and mock users. Perhaps the code here gives you some hint how to get started: http://aspnet.codeplex.com/SourceControl/latest#Samples/Identity/UnitTestAccountController/UnitTestAccountController.Tests/Controllers/AccountControllerTests.cs.

  • brian says:

    A few details are not clear for me:
    1) There doesn’t appear to be an endpoint in any of your controllers corresponding to signin-oidc, should there be? if not why not?
    2) I don’t see any usage of client key/secret that surprises me. If no key is needed for the app? What circumstances do require a key and how/where would it be specified in the code?
    3) Is the configuration value for AADInstance the same for everybody or is it specific to your Azure account?
    4) What is the “App ID URI” value in Azure Portal, the one that looks like https://example.onmicrosoft.com/… – it has nothing to do with this?

    Any response is appreciated.
    Thanks in advance!

  • Gunnar says:

    In Configure() method of start-up class OpenIdConnect is configured and there Azure AD config values are used. siginin-oidc end-point is created by OpenID component when application starts. AADInstance – in my case it is same for all applications as actually tenant ID and client ID help service to detect the correct Azure AD instance. App ID URI (fix me) is not needed if we don’t have API or Bearer token authentication.

  • Mark Cole says:

    Gunnar:

    I’m receiving a “Untrusted Certificate” error when I attempt to authenticate with Azure AD. The project does work properly in that user is authenticated, but local website does display certificate errors. Do I need to alter the authentication methods to resolve this issue? Any insights or information you could provide would be appreciated.

  • Gunnar says:

    What is the certificate you are using in your local machine? If you are not tricking with real certificate and hosts file then you are probably using development web server certificate that is automatically generated by IIS. If it’s about development server certificate that is issued to localhost then you can ignore the message or add it to trusted certificates store.

  • Mark Cole says:

    Gunnar:

    Thanks, I was able to resolve the issue. Just added the cert to the trusted cert store. I recently starting reading about the MS Graph product, it appears that .net Core does not support Graph, but I could call the APIs via client code. Do you have any resources/documentation that you could recommend for calling the APIs via client code?

  • Chris says:

    How can you use the signed in user to authenticate against a web api that also uses Azure AD with Oauth2 ?
    My goal is that you sign into the web app and then the web app can use your authenticated context to make authenticated calls targeting the api.

  • Gunnar says:

    Hi, Chris!

    For this you should use Bearer Token authentication. Both web applications – the one that users use through browser and web api – must be registered in same Azure AD. To get things done on web application side please check my blog post about bearer token authentication here: http://gunnarpeipman.com/2017/08/aspnet-core-bearer-token/

Leave a Reply

Your email address will not be published. Required fields are marked *