MsDeploy to Azure Web App with Application Insights extension enabled when deleting additional destination files

When performing an MsDeploy to an Azure Web App and you have the App Insights extension enabled you may find something interesting happens if you use the option to delete additional files on the destination that don’t appear in the source. If you look at the deployment log you may see something like this:

2017-01-30T07:29:27.5515545Z Info: Deleting file ({sitename}\ApplicationInsights.config).
2017-01-30T07:29:27.5515545Z Info: Deleting file ({sitename}\App_Data\packages\Microsoft.ApplicationInsights.2.2.0\Microsoft.ApplicationInsights.2.2.0.nupkg).
2017-01-30T07:29:27.5515545Z Info: Deleting directory ({sitename}\App_Data\packages\Microsoft.ApplicationInsights.2.2.0).
2017-01-30T07:29:27.5515545Z Info: Deleting file ({sitename}\App_Data\packages\Microsoft.ApplicationInsights.Agent.Intercept.2.0.6\Microsoft.ApplicationInsights.Agent.Intercept.2.0.6.nupkg).
2017-01-30T07:29:27.5515545Z Info: Deleting directory ({sitename}\App_Data\packages\Microsoft.ApplicationInsights.Agent.Intercept.2.0.6).
2017-01-30T07:29:27.5515545Z Info: Deleting file ({sitename}\App_Data\packages\Microsoft.ApplicationInsights.Azure.WebSites.2.2.0\Microsoft.ApplicationInsights.Azure.WebSites.2.2.0.nupkg).
2017-01-30T07:29:27.5515545Z Info: Deleting directory ({sitename}\App_Data\packages\Microsoft.ApplicationInsights.Azure.WebSites.2.2.0).
2017-01-30T07:29:27.5525645Z Info: Deleting file ({sitename}\App_Data\packages\Microsoft.ApplicationInsights.DependencyCollector.2.2.0\Microsoft.ApplicationInsights.DependencyCollector.2.2.0.nupkg).
2017-01-30T07:29:27.5525645Z Info: Deleting directory ({sitename}\App_Data\packages\Microsoft.ApplicationInsights.DependencyCollector.2.2.0).
2017-01-30T07:29:27.5525645Z Info: Deleting file ({sitename}\App_Data\packages\Microsoft.ApplicationInsights.PerfCounterCollector.2.2.0\Microsoft.ApplicationInsights.PerfCounterCollector.2.2.0.nupkg).
2017-01-30T07:29:27.5525645Z Info: Deleting directory ({sitename}\App_Data\packages\Microsoft.ApplicationInsights.PerfCounterCollector.2.2.0).
2017-01-30T07:29:27.5525645Z Info: Deleting file ({sitename}\App_Data\packages\Microsoft.ApplicationInsights.Web.2.2.0\Microsoft.ApplicationInsights.Web.2.2.0.nupkg).
2017-01-30T07:29:27.5525645Z Info: Deleting directory ({sitename}\App_Data\packages\Microsoft.ApplicationInsights.Web.2.2.0).
2017-01-30T07:29:27.5525645Z Info: Deleting file ({sitename}\App_Data\packages\Microsoft.ApplicationInsights.WindowsServer.2.2.0\Microsoft.ApplicationInsights.WindowsServer.2.2.0.nupkg).
2017-01-30T07:29:27.5525645Z Info: Deleting directory ({sitename}\App_Data\packages\Microsoft.ApplicationInsights.WindowsServer.2.2.0).
2017-01-30T07:29:27.5525645Z Info: Deleting file ({sitename}\App_Data\packages\Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel.2.2.0\Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel.2.2.0.nupkg).
2017-01-30T07:29:27.5525645Z Info: Deleting directory ({sitename}\App_Data\packages\Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel.2.2.0).
2017-01-30T07:29:27.5525645Z Info: Deleting file ({sitename}\App_Data\packages\Microsoft.Web.Infrastructure.1.0.0.0\Microsoft.Web.Infrastructure.1.0.0.0.nupkg).
2017-01-30T07:29:27.5525645Z Info: Deleting directory ({sitename}\App_Data\packages\Microsoft.Web.Infrastructure.1.0.0.0).
2017-01-30T07:29:27.5535680Z Info: Deleting directory ({sitename}\App_Data\packages).
2017-01-30T07:29:27.5535680Z Info: Deleting file ({sitename}\bin\Microsoft.AI.Agent.Intercept.dll).
2017-01-30T07:29:27.5535680Z Info: Deleting file ({sitename}\bin\Microsoft.AI.DependencyCollector.dll).
2017-01-30T07:29:27.5535680Z Info: Deleting file ({sitename}\bin\Microsoft.AI.HttpModule.dll).
2017-01-30T07:29:27.5535680Z Info: Deleting file ({sitename}\bin\Microsoft.AI.PerfCounterCollector.dll).
2017-01-30T07:29:27.5535680Z Info: Deleting file ({sitename}\bin\Microsoft.AI.ServerTelemetryChannel.dll).
2017-01-30T07:29:27.5535680Z Info: Deleting file ({sitename}\bin\Microsoft.AI.Web.dll).
2017-01-30T07:29:27.5535680Z Info: Deleting file ({sitename}\bin\Microsoft.AI.WindowsServer.dll).
2017-01-30T07:29:27.5535680Z Info: Deleting file ({sitename}\bin\Microsoft.ApplicationInsights.AzureWebSites.dll).
2017-01-30T07:29:27.5535680Z Info: Deleting file ({sitename}\bin\Microsoft.ApplicationInsights.dll).

The cool thing about this is it gives you an indication of what the extension actually does. The fact there is an App_Data/packages folder with what is clearly unpacked NuGet packages tells us that the extension is installing a NuGet package into your site for you. That makes a lot of sense given you don’t need to install the extension if you installed the NuGet package yourself (I generally don’t bother because I don’t need App Insights locally and see it as a deployment concern so I like App Service adding it for me :)).

Setting the MsDeploy option to delete extraneous files is very useful so it’s not something I want to simply turn off. However, a knowledge of MsDeploy gives us some indication as to a possible solution. In this case we can make use of the skip option to specify that MSDeploy should ignore the above affected files.

Putting it all together, if you specify the following rules in your msdeploy.exe call then you should have success:

-skip:objectname='filePath',absolutepath='ApplicationInsights.config' -skip:objectname='dirPath',absolutepath='App_Data\\packages\\*.*' -skip:objectname='filePath',absolutepath='bin\\Microsoft.AI.*.dll'  -skip:objectname='filePath',absolutepath='bin\\Microsoft.ApplicationInsights.*.dll'

After doing that your deployment log should look something like this:

2017-01-30T08:10:26.7172425Z Info: Object filePath ({sitename}\ApplicationInsights.config) skipped due to skip directive 'CommandLineSkipDirective 1'.
2017-01-30T08:10:26.7182428Z Info: Object dirPath ({sitename}\App_Data\packages) skipped due to skip directive 'CommandLineSkipDirective 2'.
2017-01-30T08:10:26.7192429Z Info: Object filePath ({sitename}\bin\Microsoft.AI.Agent.Intercept.dll) skipped due to skip directive 'CommandLineSkipDirective 3'.
2017-01-30T08:10:26.7192429Z Info: Object filePath ({sitename}\bin\Microsoft.AI.DependencyCollector.dll) skipped due to skip directive 'CommandLineSkipDirective 3'.
2017-01-30T08:10:26.7192429Z Info: Object filePath ({sitename}\bin\Microsoft.AI.HttpModule.dll) skipped due to skip directive 'CommandLineSkipDirective 3'.
2017-01-30T08:10:26.7192429Z Info: Object filePath ({sitename}\bin\Microsoft.AI.PerfCounterCollector.dll) skipped due to skip directive 'CommandLineSkipDirective 3'.
2017-01-30T08:10:26.7192429Z Info: Object filePath ({sitename}\bin\Microsoft.AI.ServerTelemetryChannel.dll) skipped due to skip directive 'CommandLineSkipDirective 3'.
2017-01-30T08:10:26.7202428Z Info: Object filePath ({sitename}\bin\Microsoft.AI.Web.dll) skipped due to skip directive 'CommandLineSkipDirective 3'.
2017-01-30T08:10:26.7202428Z Info: Object filePath ({sitename}\bin\Microsoft.AI.WindowsServer.dll) skipped due to skip directive 'CommandLineSkipDirective 3'.
2017-01-30T08:10:26.7202428Z Info: Object filePath ({sitename}\bin\Microsoft.ApplicationInsights.AzureWebSites.dll) skipped due to skip directive 'CommandLineSkipDirective 4'.
2017-01-30T08:10:26.7202428Z Info: Object filePath ({sitename}\bin\Microsoft.ApplicationInsights.dll) skipped due to skip directive 'CommandLineSkipDirective 4'.

What to do when accidentally deleting App Insights files

If you find yourself in the position that you have accidentally deleted the App Insights files you simply need to delete the App Insights extension and then re-add it and it should work again.

Adding the SDK to your application

If you eventually end up adding the App Insights SDK to your application then take note that you will need to make sure the version of the extension and the version of the SDK DLLs match otherwise you’ll get a version mismatch exception on app startup.

You still need to install the exception because the SDK alone doesn’t collect all information.

Azure Resource Manager intro presentation and workshop

I attended the Azure Saturday event here in Perth last weekend. Matt and I did a basic intro presentation on Azure Resource Manager and ran an associated workshop, which we have published to our GitHub organisation.

Azure Resource Manager is one of the most important things to understand about Azure if you plan on using it since it’s the platform that underpins the provisioning and management of all resources in Azure going forward.

Azure Saturday Perth 2015 presentation

Automating Azure Resource Manager

I’ve recently been (finally) getting to speed with Azure Resource Manager (ARM). It’s the management layer that drives the new Azure Portal and also features like Resource Groups and Role-Based Access Control.

You can interact with ARM in a number of ways:

To authenticate to the ARM API you need to use an Azure AD credential. This is all well and good if you are logged into the Portal, or running a script on your computer (where a web browser login prompt to Azure AD will pop up), but when automating your API calls that’s not available.

Luckily there is a post by David Ebbo that describes how to generate a Service Principal (equivalent of the concept of an Active Directory Service Account) attached to an Azure AD application.

The only problem with this post is that there are a few manual steps and it’s quite fiddly to do (by David’s own admission). I’ve developed a PowerShell module that you can use to idempotently create a Service Principal against either an entire Azure subscription or against a specific Resource Group that you can then use to automate your ARM code.

I’ve published the code to GitHub.

In order to use it you need to:

  1. Ensure you have the Windows Azure PowerShell commandlets installed
  2. Download the Set-ARMServicePrincipalCredential.psm1 file from my GitHub repository
  3. Download the Azure Key Vault PowerShell commandlets and put the AADGraph.ps1 file next to the file from GitHub
  4. Execute the Set-ARMServicePrincipalCredential command as per the examples on GitHub

This will pop up a web browser prompt to authenticate (this will happen twice since I’m using two disjointed libraries – hopefully this will get resolved if Azure AD commandlets end up becoming integrated with the Azure Commandlets) give you the following information:

  • Tenant ID
  • Client ID
  • Password

From there you have all the information you need to authenticate your automated script with ARM.

If using PowerShell then this will look like:

    $securePassword = ConvertTo-SecureString $Password -AsPlainText -Force
    $servicePrincipalCredentials = New-Object System.Management.Automation.PSCredential ($ClientId, $securePassword)
    Add-AzureAccount -ServicePrincipal -Tenant $TenantId -Credential $servicePrincipalCredentials | Out-Null

If using ARMClient then this will look like:

    armclient spn $TenantId $ClientId $Password | Out-Null

One last note: make sure you store the password securely when automating the script, e.g. TeamCity password, Bamboo password or Octopus sensitive variable.

Creating a SharePoint-style user lookup control backed by Azure AD

This post describes how to create a SharePoint-style user lookup control backed by Azure AD.

Practical Microsoft Azure Active Directory Blog Series

This post is part of the Practical Microsoft Azure Active Directory Blog Series.

SharePoint-style user lookup

If you have used SharePoint then you will likely be familiar with a user lookup control that allows you to type in someone’s name, press ctrl+k (or click on the Check Names button) and it will find the people from Active Directory and complete them for you.

This screenshot shows an example of what I’m describing:

SharePoint user lookup control

If you are using Azure AD for authentication of your application then the Graph API that it provides allows you to create a similar control.

This post provides instructions for a way to get this kind of control working. It’s based on a commit to the example repository and you can see it in action on the example website (note: the username and password to log in from the example repository homepage).

Querying the graph API

In a previous post I introduced the AzureADGraphConnection class to wrap up calls to the Azure AD Graph API. For the purposes of adding a user lookup there are two methods that are useful to add:

        public IList<User> SearchUsers(string query)
        {
            var displayNameFilter = ExpressionHelper.CreateStartsWithExpression(typeof(User), GraphProperty.DisplayName, query);
            var surnameFilter = ExpressionHelper.CreateStartsWithExpression(typeof(User), GraphProperty.Surname, query);
            var usersByDisplayName = _graphConnection
                .List<User>(null, new FilterGenerator { QueryFilter = displayNameFilter })
                .Results;
            var usersBySurname = _graphConnection
                .List<User>(null, new FilterGenerator { QueryFilter = surnameFilter })
                .Results;

            return usersByDisplayName.Union(usersBySurname, new UserComparer()).ToArray();
        }

        public User GetUser(Guid id)
        {
            try
            {
                return _graphConnection.Get<User>(id.ToString());
            }
            catch (ObjectNotFoundException)
            {
                return null;
            }
        }

        class UserComparer : IEqualityComparer<User>
        {
            public bool Equals(User x, User y)
            {
                return x.ObjectId == y.ObjectId;
            }

            public int GetHashCode(User obj)
            {
                return obj.ObjectId.GetHashCode();
            }
        }
  • The SearchUsers method searches the DisplayName and Surname properties to see if they start with the given search string
    • The search is case insensitive
    • The API didn’t seem to support an OR expression so I had to issue 2 API calls and union them together using a comparison class to remove duplicates
    • It returns a list of User objects, which leaks the internal implementation detail, you could construct a Read Model class that only contains the properties you are interested if you like, but that’s let as an exercise for the reader
  • The GetUser method allows you to pass in the ObjectId of a specific user to get back the corresponding User object
    • This is useful for post-back when you have used the user lookup control to get the ObjectId of a user

For this to work you need to ensure your Azure AD application has the permissions to read data from the directory as discussed in the last post.

Creating the user control semantics

The way the SharePoint control works is to automatically select the user if there is only one that is returned from the given search term, it highlights the search as incorrect when nobody is found for the search term, it presents a drop down list if multiple people are returned and it removes the complete person if a “resolved” name is backspaced.

In order to try and model these semantics as closely and simply as I could I created a jQuery plugin in the example repository called userlookup.

I wanted to make it useful by default so it contains a bunch of default options to get you up and running quickly, but also allows you to completely customise it’s behaviour.

The configuration options include:

  • Whether or not to show a lookup button on the right of the control and what the HTML for it should be (true by default with HTML for appending an icon in a Bootstrap 3 application)
  • What keypress(es) should prompt the lookup to occur (by default ctrl+k)
  • What classes should be given to the control in the event that a user is successfully found, no match could be found and it’s currently querying the server for users (matchfound, nomatchfound and loading by default)
  • Whether or not the id of the user should be populated into a hidden field on selection of a user and the name of the data property that contains the id of the field if so (true and data-user-lookup by default)
  • The URL of the API to call to query users (the API must accept a GET request with a query string of ?q=searchTerm and return a JSON list of users) (/api/userlookup by default)
  • The name of the property of the user’s name and id properties in the JSON returned by the API (DisplayName and ObjectId by default)
  • The options to pass to the Twitter Typeahead plugin, which is used for the drop-down selection of users (some sensible defaults by default)

One thing to note is that there is nothing specific in the plugin about Azure AD (apart from the default name and id property names, that can be overriden) so this plugin could be easily used for querying other user stores.

As mentioned above, the plugin uses Twitter Typeahead for auto-complete when multiple users are returned from the search.

In order to use the plugin you need to:

  1. Include Twitter Typeahead JavaScript in the page (note: requires you have jQuery referenced on the page)
  2. Include userlookup JavaScript in the page
  3. Include any required styling for Twitter Typeahead (e.g. for Bootstrap 3)
  4. Include any required styling for userlookup (e.g. what I have in the example repository)
  5. Create the HTML for the control on your page, e.g. using Bootstrap 3:
            <div class="form-group">
                <label class="col-md-4 control-label" for="UserName">User name</label>
                <div class="col-md-6">
                    <input type="hidden" name="UserId" id="UserId" value="@Model.UserId"/>
                    <div class="input-group">
                        <input id="UserName" name="UserName" type="text" placeholder="Enter user's name and hit ctrl+k" class="form-control" required autofocus value="@Model.UserName" data-user-lookup="UserId">
                    </div>
                </div>
            </div>
    
  6. Invoke the userlookup plugin, e.g.:
    <script type="text/javascript">
        $("[data-user-lookup]").userlookup();
        // Or you might want to configure some options, e.g. in this case the API url from a Razor MVC view
        $("[data-user-lookup]").userlookup({apiUrl: "@Url.Action("Search", "UserLookup")"});
    </script>
    

This will mean that you can type a query into the UserName field, press ctrl+k or click the lookup button and then it will guide you through “resolving” the user and when found set their id into the UserId field.

Creating the API

The API can be created using anything that can return JSON. The example project contains an ASP.NET MVC action as an example:

        public ActionResult Search(string q)
        {
            var users = _graphConnection.SearchUsers(q);

            return Json(users, JsonRequestBehavior.AllowGet);
        }

Dealing with the input on the server-side

The example repository contains an example of resolving the selected user and then re-displaying it in the same view, a slightly more realistic example is shown below:

    public class UserLookupController : Controller
    {
        ...

        public ActionResult Index()
        {
            return View(new UserLookupViewModel());
        }

        [HttpPost]
        public ActionResult Index(UserLookupViewModel vm)
        {
            vm.User = ModelState.IsValid ? _graphConnection.GetUser(vm.UserId.Value) : null;
            if (!ModelState.IsValid || vm.User == null)
                return View(vm);

            // Do stuff
        }
    }

    public class UserLookupViewModel
    {
        private User _user;

        [Required]
        public Guid? UserId { get; set; }

        [Required]
        public string UserName { get; set; }

        [ReadOnly(true)]
        public User User
        {
            get
            {
                return _user;
            }
            set
            {
                _user = value;
                if (_user == null)
                {
                    UserId = null;
                    return;
                }

                UserId = Guid.Parse(_user.ObjectId);
                UserName = _user.DisplayName;
            }
        }
    }

Summary

This blog post illustrated some example code to create a SharePoint-like user lookup control. Of course, you could also just put in Twitter Typeahead and connect that to the user lookup API as well – that would perform more API calls though since it’s not triggered by an explicit user action to issue the lookup, but arguably it’s more discoverable for users.

Feel free to use the userlookup jQuery plugin in your projects, it’s released as part of the MIT license on the example project.

Add role-based authorisation based on Azure AD group membership

This post describes how to use Azure AD groups for role-based authorisation in your ASP.NET web application.

Practical Microsoft Azure Active Directory Blog Series

This post is part of the Practical Microsoft Azure Active Directory Blog Series.

Add role-based authorisation based on Azure AD group membership

These instructions will help you easily add role-based authorisation based on Azure AD group membership to your existing ASP.NET application with Azure AD authentication.  The links show either a commit from the example project or to relevant documentation.

Note: Ignore the ...‘s and replace the {SPECIFIED_VALUES} with the correct values.

  1. Create groups in your Azure AD tenant
  2. Assign your users to relevant groups
  3. Configure your Azure AD application to have application permissions to read directory data from Azure Active Directory
    • If you get a “Insufficient privileges to complete the operation.” exception then you might need to wait for a few minutes or an hour since it seems to cache the old permissions, or it may be the problem mentioned by Jeff Dunlop in the comments
  4. In the Configure tab of your Azure AD application create a key in the keys section and copy it
  5. Configure the client id of your Azure AD application and the key you created in the last step in your web.config file
      <appSettings>
        ...
        <add key="ida:ClientId" value="{AZURE_AD_APP_CLIENT_ID}" />
        <add key="ida:Password" value="{AZURE_AD_APP_KEY}" />
      </appSettings>
    
  6. Install-Package Microsoft.Azure.ActiveDirectory.GraphClient -Version 1.0.3 (alternatively, you can use the latest version if you follow the steps mentioned by Jeff Dunlop in the comments)
  7. Install-Package Microsoft.IdentityModel.Clients.ActiveDirectory
  8. Create an AzureADGraphConnection class:
    Infrastructure\Auth\AzureADGraphConnection.cs
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Security.Claims;
    using Microsoft.Azure.ActiveDirectory.GraphClient;
    using Microsoft.IdentityModel.Clients.ActiveDirectory;
    
    namespace {YOUR_NAMESPACE}.Infrastructure.Auth
    {
        public interface IAzureADGraphConnection
        {
            IList<string> GetRolesForUser(ClaimsPrincipal userPrincipal);
        }
    
        public class AzureADGraphConnection : IAzureADGraphConnection
        {
            const string Resource = "https://graph.windows.net";
            public readonly Guid ClientRequestId = Guid.NewGuid();
            private readonly GraphConnection _graphConnection;
    
            public AzureADGraphConnection(string tenantName, string clientId, string clientSecret)
            {
                var authenticationContext = new AuthenticationContext("https://login.windows.net/" + tenantName, false);
                var clientCred = new ClientCredential(clientId, clientSecret);
                var token = authenticationContext.AcquireToken(Resource, clientCred).AccessToken;
    
                _graphConnection = new GraphConnection(token, ClientRequestId);
            }
    
            public IList<string> GetRolesForUser(ClaimsPrincipal userPrincipal)
            {
                return _graphConnection.GetMemberGroups(new User(userPrincipal.Identity.Name), true)
                    .Select(groupId => _graphConnection.Get<Group>(groupId))
                    .Where(g => g != null)
                    .Select(g => g.DisplayName)
                    .ToList();
            }
        }
    }
    
  9. Create an AzureADGraphClaimsAuthenticationManager class:
    Infrastructure\Auth\AzureADGraphClaimsAuthenticationManager.cs
    
    using System.Configuration;
    using System.Security.Claims;
    
    namespace AzureAdMvcExample.Infrastructure.Auth
    {
        public class AzureADGraphClaimsAuthenticationManager : ClaimsAuthenticationManager
        {
            public override ClaimsPrincipal Authenticate(string resourceName, ClaimsPrincipal incomingPrincipal)
            {
                if (incomingPrincipal == null || !incomingPrincipal.Identity.IsAuthenticated)
                    return incomingPrincipal;
    
                // Ideally this should be the code below so the connection is resolved from a DI container, but for simplicity of the demo I'll leave it as a new statement
                //var graphConnection = DependencyResolver.Current.GetService<IAzureADGraphConnection>();
                var graphConnection = new AzureADGraphConnection(
                    ConfigurationManager.AppSettings["AzureADTenant"],
                    ConfigurationManager.AppSettings["ida:ClientId"],
                    ConfigurationManager.AppSettings["ida:Password"]);
    
                var roles = graphConnection.GetRolesForUser(incomingPrincipal);
                foreach (var r in roles)
                    ((ClaimsIdentity)incomingPrincipal.Identity).AddClaim(
                        new Claim(ClaimTypes.Role, r, ClaimValueTypes.String, "GRAPH"));
                return incomingPrincipal;
            }
        }
    }
    
  10. Configure your application to use the AzureADGraphClaimsAuthenticationManager class for processing claims-based authentication in your web.config file:
      <system.identityModel>
        <identityConfiguration>
          <claimsAuthenticationManager type="{YOUR_NAMESPACE}.Infrastructure.Auth.AzureADGraphClaimsAuthenticationManager, {YOUR_ASSEMBLY_NAME}" />
          ...
        </identityConfiguration>
      </system.identityModel>
    
  11. Add [Authorize(Roles = "{AZURE_AD_GROUP_NAME}")] to any controller or action you want to restrict by role and call User.IsInRole("{AZURE_AD_GROUP_NAME}") to check if a user is a member of a particular group

Explaining the code

Microsoft.Azure.ActiveDirectory.GraphClient and AzureADGraphConnection

The ActiveDirectory.GraphClient provides a wrapper over the Azure AD Graph API, which allows you to query the users, groups, etc.

The AzureADGraphConnection class constructs a graph client connection and a method to take a user and return a list of the groups that user is a member of.

This is needed because the claims that the Azure AD token comes with by default do not include any roles.

AzureADGraphClaimsAuthenticationManager

This class provides a claims authentication manager that hooks into the point that authentication occurs and augments the Claims Principal that is generated by default by getting the Azure AD Groups that the user is a member of (via AzureADGraphConnection) and turning them into a ClaimTypes.Role claim. ClaimTypes.Role is the claim type that automatically hooks into ASP.NETs roles processing.

The web.config change is how you override the Claims Authentication Manager.

Using an enum for roles

To avoid the use of magic strings in your application and assuming the group names in AD are relatively stable you can encapsulate them in an enum. There is a corresponding commit in the example project that demonstrates how to do it.

This involves three main steps:

  1. Define an enum with your roles and using the [Description] attribute to tag each role with the Display Name of the equivalent Azure AD group
  2. Parse the group name into the corresponding enum value by using Humanizer.Dehumanize in AzureADGraphConnection
  3. Create an AuthorizeRolesAttribute that extends AuthorizeAttribute and an extension on IClaimsPrincipal that provides an IsInRole method that both take the enum you defined rather than magic strings to define the roles

Explaining the code behind authenticating MVC5 app with Azure AD

This post explains the code outlined in the last post on installing Azure AD authentication to an existing (or new) ASP.NET MVC 5 (or 3 or 4) application.

Practical Microsoft Azure Active Directory Blog Series

This post is part of the Practical Microsoft Azure Active Directory Blog Series.

Microsoft.Owin.Security.ActiveDirectory

The Microsoft.Owin.Security.ActiveDirectory package is part of the Katana project, which produces a bunch of libraries that build on top of Owin.

It allows your application to accept a Bearer Authorization header in the HTTP request that contains a JSON Web Token (JWT) token issued from Azure AD and will create a ClaimsPrincipal in the thread from that token. This is mainly useful for creating Web APIs and thus is optional if you just need web authentication.

Note: if you use bearer tokens make sure you request resources with HTTPS.

This package is enabled up by the app.UseWindowsAzureActiveDirectoryBearerAuthentication(...) call in Startup.cs.

There are two configurations in the Startup.cs code to configure the package:

  • TokenValidationParameters – this controls how tokens that are presented by a user are checked for validity
    • In the code example in the previous blog post we set ValidAudience, which ensures that any tokens presented are valid for the given audience (alternatively, you can use ValidAudiences if you want to accept tokens from multiple audiences)
    • There is more information later in this post about audiences
  • Tenant – This sets which Azure AD tenant you are accepting tokens from

WSFederationAuthenticationModule (WS-FAM) and SessionAuthenticationModule (SAM)

These modules are part of WIF via System.IdentityModel.Services and are the mechanism by which the authentication hooks into ASP.NET and works. For this to work you need to enter a bunch of code in web.config, but Microsoft is currently working on OWIN-only components that hide all of that away and provide for a much simpler configuration so in the future you won’t need to do any of this. At the time of writing the samples don’t quite seem to work (for me at least) yet, so for now it makes sense to keep using the WIF modules, but it’s worth keeping out an eye on what happens with the samples Microsoft are working on, in particular the OpenIdConnect one.

So what do these WIF modules do? From the WSFederationAuthenticationModule documentation:

When an unauthenticated user tries to access a protected resource, the [Relying Party (RP)] returns a “401 authorization denied” HTTP response. The WS-FAM intercepts this response instead of allowing the client to receive it, then it redirects the user to the specified [Security Token Service (STS)]. The STS issues a security token, which the WS-FAM again intercepts. The WS-FAM uses the token to create an instance of ClaimsPrincipal for the authenticated user, which enables regular .NET Framework authorization mechanisms to function.

Because HTTP is stateless, we need a way to avoid repeating this whole process every time that the user tries to access another protected resource. This is where the SessionAuthenticationModule comes in. When the STS issues a security token for the user, SessionAuthenticationModule also creates a session security token for the user and puts it in a cookie. On subsequent requests, the SessionAuthenticationModule intercepts this cookie and uses it to reconstruct the user’s ClaimsPrincipal.

From the SessionAuthenticationModule documentation:

The SAM adds its OnAuthenticateRequest event handler to the HttpApplication.AuthenticateRequest event in the ASP.NET pipeline. This handler intercepts sign-in requests, and, if there is a session cookie, deserializes it into a session token, and sets the Thread.CurrentPrincipal and HttpContext.User properties to the claims principal contained in the session token.

These modules are then configured by the system.identityModel and system.identityModel.services sections in web.config.

issuerNameRegistry and signing key refresh

This configures which tenants and issuers of authentication your application trusts as well as the thumbprint of their public signing key certificates.

The certificate thumbprints will change over time for security reasons so hardcoding the keys in web.config is not a good option hence you need to make sure to implement code that changes the keys for you. The simplest, built-in way to do that is using ValidatingIssuerNameRegistry.WriteToConfig, which updates web.config for you automatically when it changes. That’s the instruction that was given in the last blog post.

Another option is to store the keys in a database, which is what the default code that Visual Studio’s Identity and Access Tools add (using EntityFramework). Yet another option is to store them in-memory and there is a class floating about that you can use to do that. Storing it in-memory is probably the nicest option.

Audiences and realm

The audienceUris configuration in web.config allows your application to control a list of identifiers that will be accepted for the scope of a presented authentication token (in Azure AD this maps to the App ID URI of your Azure AD application).

The realm attribute in the wsFederation element of the federationConfiguration in web.config tells the WSFederationAuthenticationModule to specify the WS Federation wtrealm to use for the request, which identifies the security realm that will be used for the request. For Azure AD this provides the App ID URI of the Azure AD application that should be used to service the authentication request. Thus, this value will generally be the same as the audience you configured unless you are doing multi-tenancy.

The difference between realm and audience is explained in this StackOverflow post.

securityTokenHandlers

A security token handler provides a way for interpreting a security token of some sort into a Claims Principal for a given request. The code in the previous post gets you to remove the default handler that takes care of looking at cookies in the request, SessionSecurityTokenHandler, because it uses DPAPI to encrypt and decrypt the cookies and that doesn’t work in an environment with multiple web servers. Instead you are guided to add a MachineKeySessionSecurityTokenHandler, which uses the machine key to encrypt and decrypt the cookie.

The configured securityTokenHandler will be what the SessionAuthenticationModule will make use of to store and retrieve the token from and to the cookie.

certificateValidation

On first thought, it might be confusing to see that certificate validation is turned off, but this is by design. The validating issuer name registry as explained above is a replacement for standard certificate validation. The only information that I’ve been able to find that explains this further is a post on the MSDN forums.

To illustrate what happens when you try using certificate validation, you can change certificateValidationMode to, say, ChainTrust and then you will get the following error:

The X.509 certificate CN=accounts.accesscontrol.windows.net chain building failed. The certificate that was used has a trust chain that cannot be verified. Replace the certificate or change the certificateValidationMode. A certificate chain processed, but terminated in a root certificate which is not trusted by the trust provider.

HTTPS

You can ensure that the security cookie is set to require SSL with the requireSSL attribute of the cookieHandler element in web.config and ensure that the authentication requests require a HTTPS connection with the requireHttps attribute of the wsFederation element in web.config.

In production environments it’s absolutely essential that you set both to true otherwise you are vulnerable to MITM attacks. You can set them to true locally if you use https with IIS Express or via a self-signed cert with IIS.

issuer

Setting this attribute on the wsFederation element in web.config determines where sign-in and sign-out requests are redirected.

passiveRedirectEnabled

Setting this attribute on the wsFederation element in web.config to true allows the WSFederationAuthenticationModule to automatically redirect the user to the authentication server in the event of a 401. Without this set to true you would need to explicitly call SignIn to log the user in.

reply

Setting this attribute on the wsFederation element in web.config allows the application to control where the user is taken after they authenticate. The last post didn’t tell you to set it because by default it’s not required. When using Azure AD, in the instance that it’s not specified, the user will be redirected to the first Reply URL specified in the Azure AD application.

This requires that you can only have a one-to-one relationship between an Azure AD application and a web application requiring authentication. You can actually add multiple reply URLs to your Azure AD application though. This fact in combination with the reply attribute means that you can support multiple web applications (e.g. local, dev, staging, prod or even just different applications altogether) with the same Azure AD application. You just need to config transform your web.config file for each different environment as explained in this post.

If you are in a situation where you want to only change an app setting to control the reply URL (e.g. you are using VSO to deploy different branches to separate Azure Web Sites) then you can change the reply URL in code like so:

    public static class IdentityConfig
    {
        public static void ConfigureIdentity()
        {
            ...
            FederatedAuthentication.FederationConfigurationCreated += FederatedAuthentication_FederationConfigurationCreated;
        }

        ...

        private static void FederatedAuthentication_FederationConfigurationCreated(object sender, FederationConfigurationCreatedEventArgs e)
        {
            var federationConfiguration = new FederationConfiguration();
            federationConfiguration.WsFederationConfiguration.Reply =
                ConfigurationManager.AppSettings["AuthenticationReplyUrl"];
            e.FederationConfiguration = federationConfiguration;
        }
    }

Anti-forgery config

AntiForgeryConfig.UniqueClaimTypeIdentifier allows you to set which claim from the claims-based authentication token can be used to uniquely identify a user for the purposes of creating an appropriate anti-forgery token. Side note: this post about anti-forgery token is great. The claim that was shown in the last post is the correct one to use for Azure AD.

Logout

There are two parts to the logout code in the last post. Firstly, there is a call to the SessionAuthenticationModule, which will cause the cookie you have on the current site to be dropped by your browser. Secondly, there is a redirect to a URL that the WS Federation code generates that will log you out of the source system (in this case your Azure AD session) and then redirect you back to a callback page with [AllowAnonymous] so they don’t get redirected back to login again straight away.

Scripted/Automated installation script to set up Cordova/PhoneGap and Android on Windows

I recently worked on a Cordova project and one of the things we found is that it’s an absolute pain to set up a development environment since there is a whole bunch of tools that need to be downloaded and installed and configured in specific ways.

We ended up creating a page in our project’s OneNote notebook with developer setup instructions, but even though we were using Chocolatey it was still a tedious process with numerous console restarts to refresh environment variables (that had to be manually set).

In the process of writing a post on Cordova I wanted to check something and realised I had repaved my machine since the last time I installed the Android SDK / Cordova etc.

I consulted the OneNote page we had created and looked in despair at the instructions. What a PITA! So what did I do?

I spun up a Windows Azure VM and stumbled through creating a PowerShell script to automate the setup. Then I spun up a second VM to check that the script worked :). Then I deleted both of them – probably cost a few cents and the servers had a really fast download speed so the installations were really quick. God I love the cloud 😀

I’ve uploaded it to a Gist. If you are setting up a PhoneGap/Cordova & Android development environment then I’m sure it will be useful to you.

Enjoy!

My stance on Azure Worker Roles

tl;dr 99% of the time Worker Role is not the right solution. Read on for more info.

Worker Role Deployments

I quite often get asked by people about the best way to deploy Worker Roles because it is a pain – as an Azure Cloud Service the deployment time of a Worker Role is 8-15+ minutes. In the age of continuous delivery and short feedback loops this is unacceptable (as I have said all along).

On the surface though, Worker Roles are the most appropriate and robust way to deploy heavy background processing workloads in Azure. So what do we do?

Web Jobs

The advice I generally give people when deploying websites to Azure is to use Azure Web Sites unless there is something that requires them to use Web Roles (and use Virtual Machines as a last resort). That way you are left with the best possible development, deployment, debugging and support experience possible for your application.

Now that Web Jobs have been released for a while and have a level of maturity and stability I have been giving the same sort of advice when it comes to background processing: if you have a workload that can run on the Azure Web Sites platform (e.g. doesn’t need registry/COM+/GDI+/elevated privileges/custom software installed/mounted drives/Virtual Network/custom certificates etc.) and it doesn’t have intensive CPU or memory resource usage then use Web Jobs.

I should note that when deploying Web Jobs you can deploy them automatically using the WebJobsVs Visual Studio extension.

As a side note: some of my colleagues at Readify have recently started using Web Jobs as a platform for deploying Microservices in asynchronous messaging based systems. It’s actually quite a nice combination because you can put any configuration / management / monitoring information associated with the micro-service in the web site portion of the deployment and it’s intrinsically linked to the Web Job in both the source code and the deployment.

Worker Roles

If you are in a situation where you have an intense workload, you need to scale the workload independently  of your Web Sites instances or your workload isn’t supported by the Azure Web Sites platform (and thus can be run as a Web Job) then you need to start looking at Worker Roles or some other form of background processing.

Treat Worker Roles as infrastructure

One thing that I’ve been trying to push for a number of years now (particularly via my AzureWebFarm and AzureWebFarm.OctopusDeploy projects) is for people to think of Cloud Services deployments as infrastructure rather than applications.

With that mindset shift, Cloud Services becomes amazing rather than a deployment pain:

  • Within 8-15+ minutes a number of customised, RDP-accessible, Virtual Machines are being provisioned for you on a static IP address and those machines can be scaled up or down at any time and they have health monitoring and diagnostics capabilities built-in as well as a powerful load balancer and ability to arbitrarily install software or perform configurations with elevated privileges!
  • To reiterate: waiting 8-15+ minutes for a VM to be provisioned is amazing; waiting 8-15+ minutes for the latest version of your software application to be deployed is unacceptably slow!

By treating Cloud Services as stateless, scalable infrastructure you will rarely perform deployments and the deployment time is then a non-issue – you will only perform deployments when scaling up or rolling out infrastructure updates (which should be a rare event and if it rolls out seamlessly then it doesn’t matter how long it takes).

Advantages of Web/Worker Roles as infrastructure

  • As above, slow deployments don’t matter since they are rare events that should be able to happen seamlessly without taking out the applications hosted on them.
  • As above, you can use all of the capabilities available in Cloud Services.
  • Your applications don’t have to have a separate Azure project in them making the Visual Studio solution simpler / load faster etc.
  • Your applications don’t have any Azure-specific code in them (e.g. CloudConfiguationManager, RoleEnvironment, RoleEntryPoint, etc.) anymore
    • This makes your apps simpler and also means that you aren’t coding anything in them that indicates how/where they should be deployed – this is important and how it should be!
    • It also means you can deploy the same code on-premise and in Azure seamlessly and easily

How do I deploy a background processing workload to Worker Role as infrastructure?

So how does this work you might ask? Well, apart from rolling your own code in the Worker Role to detect, deploy and run your application(s) (say, from blob storage) you have two main options that I know of (both of which are open source projects I own along with Matt Davies):

  • AzureWebFarm and its background worker functionality
    • This would see you deploying the background work as part of MSDeploying a web application and it works quite similar to (but admittedly probably less robust than) Web Jobs – this is suitable for light workloads
  • AzureWebFarm.OctopusDeploy and using OctopusDeploy to deploy a Windows Service
    • In general I recommend using Topshelf to develop Windows Services because it allows a nicer development experience (single console app project that you can F5) and deployment experience (you pass an install argument to install it)
    • You should be able to deploy heavyweight workloads using this approach (just make sure your role size is suitable)

The thing to note about both of these approaches is that you are actually using Web Roles, not Worker Roles! This is fine because there isn’t actually any difference between them apart from the fact that Web Roles have IIS installed and configured. If you don’t want anyone to access the servers over HTTP because they are only used for background processing then simply don’t expose a public endpoint.

So, when should I actually use Worker Roles (aka you said they aren’t applicable 99% of the time – what about the other 1%)?

OK, so there is definitely some situations I can think of and have come across before occasionally that warrant the application actually being coded as a Worker Role – remember to be pragmmatic and use the right tool for the job! Here are some examples (but it’s by no means exhaustive):

  • You need the role to restart if there are any uncaught exceptions
  • You need the ability to control the server as part of the processing – e.g. request the server start / stop
  • You want to connect to internal endpoints in a cloud service deployment or do other complex things that require you to use RoleEnvironment
  • There isn’t really an application-component (or it’s tiny) – e.g. you need to install a custom application when the role starts up and then you invoke that application in some way

What about Virtual Machines?

Sometimes Cloud Services aren’t going to work either – in a scenario where you need persistent storage and can’t code your background processing code to be stateless via RoleEntryPoint then you might need to consider standing up one or more Virtual Machines. If you can avoid this at all then I highly recommend it since you then need to maintain the VMs rather than using a managed service.

Other workloads

This post is targeted at the types of background processing workloads you would likely deploy to a Worker Role. There are other background processing technologies in Azure that I have deliberately not covered in this post such as Hadoop.

Announcing 1.0.0 of ReliableDbProvider library

I’d like to announce that today I’ve released v1.0.0 of the ReliableDbProvider library. It’s been kicking around for a while, has a reasonable number of downloads on NuGet and has just received a number of bug fixes from the community so I feel it’s ready for the 1.0.0 badge :).

ReliableDbProvider is a library that allows you to unobtrusively handle transient errors when connecting to Azure SQL Database when using ADO.NET, Linq 2 Sql, EntityFramework < 6 (EF6 has similar functionality in-built) or any library that uses ADO.NET (e.g. Massive).

Check it out on GitHub.

Authenticating an ASP.NET MVC 5 application with Microsoft Azure Active Directory

This post outlines how to easily add Azure AD authentication to an existing (or new) ASP.NET MVC 5 (or 3 or 4) application.

Practical Microsoft Azure Active Directory Blog Series

This post is part of the Practical Microsoft Azure Active Directory Blog Series.

Add Azure AD Authentication

These instructions will help you easily add authentication to your new or existing ASP.NET application, based on what the Visual Studio Identity and Access tools do. It’s a basic setup for a single tenant. Read the next post in the series to understand what’s going on and ways that it can be extended. The links show either a commit from the example project or to relevant documentation.

Note: Ignore the ...‘s and replace the {SPECIFIED_VALUES} with the correct values.

  1. Create an Azure Active Directory tenant; note: AD tenants are not associated with your Azure Subscription, they are “floating” so add any live ids for people you want to administer it as Global Administrators
  2. Create an Application in your AD tenant with audience URL and realm being your website homepage (minus the slash at the end)
    • Record the name of your AD tenant e.g. {name}.onmicrosoft.com
    • Record the GUID of your AD tenant by looking at the FEDERATION METADATA DOCUMENT URL under View Endpoints
    • The image upload and Sign On URL are used for the Azure AD Applications Portal
  3. Create a user account in your tenant that you can use to log in with
  4. Install-Package Microsoft.Owin.Security.ActiveDirectory
  5. Install-Package System.IdentityModel.Tokens.ValidatingIssuerNameRegistry
  6. Add a reference to System.IdentityModel
  7. Add a reference to System.IdentityModel.Services
  8. Add a Startup.cs file (if it doesn’t already exist) and configure OWIN to use Azure Active Directory (edit for new version)
    using System.Configuration;
    using Microsoft.Owin.Security.ActiveDirectory;
    using Owin;
    
    namespace {YOUR_NAMESPACE}
    {
        public class Startup
        {
            public void Configuration(IAppBuilder app)
            {
                app.UseWindowsAzureActiveDirectoryBearerAuthentication(
                    new WindowsAzureActiveDirectoryBearerAuthenticationOptions
                    {
                        TokenValidationParameters = new TokenValidationParameters
                        {
                            ValidAudience = ConfigurationManager.AppSettings["ida:AudienceUri"]
                        },
                        Tenant = ConfigurationManager.AppSettings["AzureADTenant"]
                    });
            }
        }
    }
    
  9. Add the correct configuration to your web.config file; change requireSsl and requireHttps to true if using a https:// site (absolutely required for production scenarios)
    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
      <configSections>
        <section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
        <section name="system.identityModel.services" type="System.IdentityModel.Services.Configuration.SystemIdentityModelServicesSection, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
      </configSections>
    ...
      <appSettings>
        ...
        <add key="ida:AudienceUri" value="{YOUR_WEBSITE_HOMEPAGE_WITHOUT_TRAILING_SLASH}" />
        <add key="ida:FederationMetadataLocation" value="https://login.windows.net/{YOUR_AD_TENANT_NAME}.onmicrosoft.com/FederationMetadata/2007-06/FederationMetadata.xml" />
        <add key="AzureADTenant" value="{YOUR_AD_TENANT_NAME}.onmicrosoft.com" />
      </appSettings>
    ...
      <system.webServer>
        ...
        <modules>
          <add name="WSFederationAuthenticationModule" type="System.IdentityModel.Services.WSFederationAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="managedHandler" />
          <add name="SessionAuthenticationModule" type="System.IdentityModel.Services.SessionAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="managedHandler" />
        </modules>
      </system.webServer>
    ...
      <system.identityModel>
        <identityConfiguration>
          <issuerNameRegistry type="System.IdentityModel.Tokens.ValidatingIssuerNameRegistry, System.IdentityModel.Tokens.ValidatingIssuerNameRegistry">
            <authority name="https://sts.windows.net/{YOUR_AD_TENANT_GUID}/">
              <keys>
                <add thumbprint="0000000000000000000000000000000000000000" />
              </keys>
              <validIssuers>
                <add name="https://sts.windows.net/{YOUR_AD_TENANT_GUID}/" />
              </validIssuers>
            </authority>
          </issuerNameRegistry>
          <audienceUris>
            <add value="{YOUR_WEBSITE_HOMEPAGE_WITHOUT_TRAILING_SLASH}" />
          </audienceUris>
          <securityTokenHandlers>
            <add type="System.IdentityModel.Services.Tokens.MachineKeySessionSecurityTokenHandler, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
            <remove type="System.IdentityModel.Tokens.SessionSecurityTokenHandler, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
          </securityTokenHandlers>
          <certificateValidation certificateValidationMode="None" />
        </identityConfiguration>
      </system.identityModel>
      <system.identityModel.services>
        <federationConfiguration>
          <cookieHandler requireSsl="false" />
          <wsFederation passiveRedirectEnabled="true" issuer="https://login.windows.net/{YOUR_AD_TENANT_NAME}.onmicrosoft.com/wsfed" realm="{YOUR_WEBSITE_HOMEPAGE_WITHOUT_TRAILING_SLASH}" requireHttps="false" />
        </federationConfiguration>
      </system.identityModel.services>
    </configuration>
    
  10. Configure AntiForgery to use the correct claim type to uniquely identify users
    Global.asax.cs
    
              protected void Application_Start()
              {
                  ...
                  IdentityConfig.ConfigureIdentity();
              }
    
    App_Start\IdentityConfig.cs
    
    using System.IdentityModel.Claims;
    using System.Web.Helpers;
    
    namespace {YOUR_NAMESPACE}
    {
        public static class IdentityConfig
        {
            public static void ConfigureIdentity()
            {
                AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.Name;
            }
        }
    }
    
  11. Configure the application to refresh issuer keys when they change
            public static void ConfigureIdentity()
            {
                ...
                RefreshIssuerKeys();
            }
    
            private static void RefreshIssuerKeys()
            {
                // http://msdn.microsoft.com/en-us/library/azure/dn641920.aspx
                var configPath = AppDomain.CurrentDomain.BaseDirectory + "\\" + "Web.config";
                var metadataAddress = ConfigurationManager.AppSettings["ida:FederationMetadataLocation"];
                ValidatingIssuerNameRegistry.WriteToConfig(metadataAddress, configPath);
            }
    
  12. Add LogoutController
    Controllers\LogoutController.cs
    
    using System;
    using System.IdentityModel.Services;
    using System.Web.Mvc;
    
    namespace {YOUR_NAMESPACE}.Controllers
    {
        public class LogoutController : Controller
        {
            public ActionResult Index()
            {
                var config = FederatedAuthentication.FederationConfiguration.WsFederationConfiguration;
    
                var callbackUrl = Url.Action("Callback", "Logout", null, Request.Url.Scheme);
                var signoutMessage = new SignOutRequestMessage(new Uri(config.Issuer), callbackUrl);
                signoutMessage.SetParameter("wtrealm", config.Realm);
                FederatedAuthentication.SessionAuthenticationModule.SignOut();
    
                return new RedirectResult(signoutMessage.WriteQueryString());
            }
    
            [AllowAnonymous]
            public ActionResult Callback()
            {
                if (Request.IsAuthenticated)
                    return RedirectToAction("Index", "Home");
    
                return View();
            }
        }
    }
    
    Views\Logout\Callback.cshtml
    
    @{
        ViewBag.Title = "Logged out";
    }
    
    <h1>Logged out</h1>
    
    <p>You have successfully logged out of this site. @Html.ActionLink("Log back in", "Index", "Home").</p>
    
  13. Add logout link somewhere@Html.ActionLink("Logout", "Index", "Logout")
  14. Add authentication to the app; do this as you normally would with [Authorize] to specific controller(s) or action(s) or globally by adding to GlobalFilters.Filters.Add(new AuthorizeAttribute());
  15. Load the site and navigate to one of the authenticated pages – it should redirect you to your Azure AD tenant login page whereupon you need to log in as one of the users you created and it should take you back to that page, logged in
  16. The usual User.Identity.Name and User.Identity.IsAuthenticated objects should be populated and if you want access to the claims to get the user’s name etc. then use something like ClaimsPrincipal.Current.FindFirst(ClaimTypes.GivenName).Value