Brian Vander Plaats

Developer

Blog

19 Jan 2017

ADFS Authentication Adding Custom Claims

While user authentication is a key component of AD FS, the returned user claims are powerful tools for client applications.

  • Common user information (Name, login, email) can be retrieved without code duplication in multiple applications
  • Security roles can be added to the claims token, reducing roundtrips or querying security information from a database during application lifetimes
  • Unique system fields / tokens / identifiers that would normally be stored in some global variable or hidden fields

There are two ways to add to the available claims:

  1. Configure AD FS to send additional claims - this will be shown in a future post
  2. Dynamically add claims during user sign-on

Before going into details here, a few high level points to keep in mind:

  1. Claim tokens are shared between all sites in a subdomain e.g https://apps.example.com
  2. Claim tokens can expire (based on AD FS settings), or be removed by the user logging out.
  3. Claims from the AD FS server can be removed at any time.
  4. Changes made to the claims will not affect users that have a current claims token.

Thus, your application should never assume that a claim exists.

Adding Roles to claims

An excellent usage of claims information is populating the application security roles the user has access to. This is information that will be checked frequently during the applications lifetime, and querying a security database on each access is inefficient.

To begin, you will need a security database with users/roles/groups etc. At my company we use the venerable ASPNETDB database that has been part of the asp.net membership system for a long time.

Adding the claims is done in the ConfigureAuth() method. In a vanilla configuration, this looks as follows:

app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

app.UseCookieAuthentication(new CookieAuthenticationOptions());

app.UseWsFederationAuthentication(
    new WsFederationAuthenticationOptions
    {
        Wtrealm = realm,
        MetadataAddress = adfsMetadata
    });

Adding claims is done by specifying additional items in WsFederationAuthenticationOptions

app.UseWsFederationAuthentication(
new WsFederationAuthenticationOptions
{
    Wtrealm = realm,
    MetadataAddress = adfsMetadata,
    Notifications = new WsFederationAuthenticationNotifications
    {
        SecurityTokenValidated = context =>
        {
            string accountName = "";
            foreach (var claim in context.AuthenticationTicket.Identity.Claims)
            {
                if (claim.Type == ClaimTypes.Upn)
                {
                    accountName = claim.Value;
                }
            }

            //Query roles for user from security database
            string[] stringRoles = { "TestRole1", "TestRole2", "TestRole3", "TestRole4" };
            var roles = stringRoles.Select(s => new { RoleName = s });

            //loop through returned roles, and add
            foreach (var role in roles)
            {
                context.AuthenticationTicket.Identity.AddClaim(
                                                    new Claim(ClaimTypes.Role, role.RoleName));
            }
            
            return Task.FromResult(0);
        }

    }
});
}

Basically we specify an event handler to fire as soon as the claims token exists. Note while this is configured in startup.auth.cs, this code only runs when the user signs in. This means that if you change which roles the user has in the database, or in Active Directory, the user will not see these roles until they logout, and signs in again.

The first step is getting the user identity we will use to retreive user roles - the user name is a good option here, but you could use a number of claims (again, based on what the AD FS adminstrator configured)

  • nameidentfier - brianvp
  • upn - brianvp@example.com
  • emailaddress - brian.vanderplaats@example.com

Next, you will take your chosen identifier and query your security database.

Then, iterate through these roles, and add to the claims collection using the AddClaim() method. Make sure to specify ClaimTypes.Role. The actual role name is simply a string. Note that if you specify a role twice, it will be added twice. This should not be a problem with multiple applications, as again, this handler does not fire on app startup, only during the sign-in process. Once the user is signed in to an application on a domain, they will not need to sign in to other applications on the domain.

Using Roles

Now that the roles are added, you need to configure your application to use them. The simplest way is to combine roles into [Authorize] tags on your controllers. For example, you could lock down your editor pages by placing the role over create/edit controller actions, but not over the view actions:

public ActionResult Details(int? id)
{
    ...
}

[HttpPost]
[ValidateAntiForgeryToken]
[Authorize(Roles = "ModelEditorRole")]
public ActionResult Create( Model model)
{
    ...
}

[Authorize(Roles = "ModelEditorRole")]
public ActionResult Edit(int? id)
{
    ...
}

Another way is to look at the user’s current roles while performing an action inside a method. This can be done by iterating through the user’s claims:

bool inTestRole4 = false;
var claims = ((ClaimsIdentity)Thread.CurrentPrincipal.Identity).Claims;
foreach (var claim in claims)
{
    if (claim.Type == ClaimTypes.Role && claim.Value == "TestRole4")
    {
        inTestRole4 = true;
        break;
    }
}

Or, even easier, you can use the simple check:

if(User.IsInRole("TestRole4"))
{

}

Adding custom claim types

Adding a custom claim is basically the same as adding a role claim. Instead of specifying ClaimTypes.Role, you give it your own name / value:

SecurityTokenValidated = context =>
{
    var accountName = context.AuthenticationTicket.Identity.Claims.Where(x => x.Type == ClaimTypes.Upn).FirstOrDefault().Value;

    //do something to retrieve appropriate value for current user
    int CompanyUserID = 4000333;

    context.AuthenticationTicket.Identity.AddClaim(new Claim("CompanyUserID", CompanyUserID.ToString()));

    return Task.FromResult(0);
}

Again, a good candidate for these claims are broad, static bits of information that will not change, or at least the frequency of change should be much longer than the typical claims token expiration period (as the new value would not be received until next user login).

18 Jan 2017

ADFS Authentication Using Visual Studio Wizard

This post demonstrates how to set up a new ASP.NET MVC project using AD FS. Before you can do this, you need to have an AD FS Server up and running. In my testing, I used an on-network AD FS Server, but a cloud / azure AD FS option exists as well (but I haven’t worked with at this point).

You will need a few pieces of information from your AD FS administrator before proceeding:

  • Metadata document URL e.g. https://example.com/federationmetadata/2007-06/federationmetadata.xml
  • URI to identify your application. For local development you can use https://localhost:44300/, but this must be configured on the AD FS side as well. If you specify something else it will not work!

Setting up the Project

  1. Create a new project using ASP.NET Web Application template
  2. Check the MVC Template, and optionally click on the Web API checkbox visual studio new project templates adfs
  3. Click the “Change Authentication” box.
  4. Change the options to “Work and School Accounts”
  5. Select On-Premises and enter the metadata from your adminstrator and click OK visual studio new project adfs metadata
  6. Click OK to create the rest of your Project

Open up your project settings, and change the SSL URL to match the one from your AD FS administrator visual studio project url

By default, your entire project requires authentication, so on your first build & run, you will immediately see the login screen. To see the home page before logging in, remove the [Authorize] filter from HomeController

Trying out the AD FS components

Build and run your project. You should see the following:

new adfs project sign in

click sign in - you will be redirected to your AD FS Server’s login page. Note that this is not your website

new adfs project adfs login

Once you sign in, you will automatically redirect back to your sample website. Note that if any metadata is misconfigured (on the AD FS Server or in your project), you will start to see problems here.

new adfs project signed in

You are now signed in until you explicitly sign out, or your claims token expires (claim expiration is controlled by your server administrator). The claim token itself is stored in a cookie, and sent with each request to the server

new adfs project cookie

Click Sign Out - you will be directed to the sign out page on your AD FS Server, and then immediately redirect back to your website. This does a few things:

  1. invalidates the claims token on the AD FS Server by calling: https://adfs.example.com/adfs/ls/?wtrealm=https%3a%2f%2flocalhost%3a44300%2f&wa=wsignout1.0&wreply=https%3a%2f%2flocalhost%3a44300%2fAccount%2fSignOutCallback
  2. Removes the ASP.NET Cookie from your browser

new adfs project signed out

Accessing the Claims Token

You can access your claims token by casting Thread.CurrentPrincipal.Identity to a ClaimsIdentity and then accessing the Claims collection.

HomeController.cs

 public ActionResult Index()
{
    ViewBag.ClaimsIdentity = Thread.CurrentPrincipal.Identity;

    return View();
}

index.cshtml

<h3>Logged in User Claims:</h3>
<table class="table table-condensed">
    <tr>
        <th>Claim Type</th>
        <th>Claim Value</th>
        <th>Value Type</th>
        <th>Subject Name</th>
        <th>Issuer Name</th>
    </tr>
    @foreach (System.Security.Claims.Claim claim in ViewBag.ClaimsIdentity.Claims)
    {
        <tr>
            <td><small>@claim.Type</small></td>
            <td><small>@claim.Value</small></td>
            <td><small>@claim.ValueType</small></td>
            <td><small>@claim.Subject.Name</small></td>
            <td><small>@claim.Issuer</small></td>
        </tr>
    }
</table>

new adfs project user claims

Conclusion

That’s it! You now have a site that you can lock down to authenticated users only. Simply add [Authorize] tags to the necessary feature controllers, and don’t forget to secure all your api paths! To add role-based security for additional granularity, you can add roles to the claims token itself, which I will go over in a future post.

17 Jan 2017

Website Authentication using ADFS Overview

This is the first in a series of posts going over the use of Active Directory Federation Services (AD FS). For roughly the past year, I’ve been exploring authentication options for an external website project at work. Previously we focused on using Windows Authentication, but that only really works for Intranet applications, and not so well or at all on mobile / tablet environments. Since this application is only for employees, I wanted to use existing Active Directory resources. I briefly explored oAuth as an option, but since we would not be using google/facebook etc for credentials, setting up our own oAuth provider seemed to be as much or more work than utilizing AD FS. Specifically my design goals were:

  • Implement Single Sign-on. Users should not have to log into multiple applications / minimize number of logins
  • Use existing domain accounts - Users do not need another login to manage
  • Do not allow the application to access or store user credentials / secrets - If you are going to use a common credential between websites, no individual website should have access to these credentials directly. At best, each site would need to re-implement security features, and at worst, one site failing to implement security properly exposes all the sites.
  • Work well for Internet and Intranet applications. While Windows Authentication worked for us in the past, it doesn’t work well in a post-windows pc world.

Early last year, I created two demo projects, one using oAuth, and the other using AD FS. Both were successful, but the oAuth solution is using google, so after reviewing with my other developers we decided to use AD FS. At present, we have two live applications using AD FS, and so far has worked out really well. Below are the demo projects for reference:

The rest of this post describes key points of the AD FS architecture

AD FS Overview

Active Directory Federation Services is a service that allows sharing identity information between “trusted” partners, called a “federation”. At a high level, it allows a website to delegate authentication to a trusted service, and accept a “claim” from this service on the user’s behalf to make authorization decisions. For example:

  • John Doe wants to access the corporate payroll site
  • The payroll site requires users to login in (obviously)
  • When John hits the payroll site, he is not authenticated, so the payroll site redirects John to the AD FS login page
  • John enters his credentials (user name and password) to the AD FS login site.
  • The AD FS site verifies the credentials - if valid, it generates a “claims token”, containing certain information about John. This typically includes his username (johnd), full name, “John Doe”, email, “john.doe@company.com”, and any other property the AD FS service is configured to send.
  • AD FS redirects the user back to the payroll site, sending along the claims token
  • The payroll site now assumes that the original user is John Doe, and presents resources John Doe is authorized to use.

Additionally, the claims token is typically stored in a cookie, so if John closes his browser and reloads the payroll site, it can use the existing claims token without requring John to re-log in to AD FS.

Terminology

Some key terminology:

  • Federation - A group of two or more independant sites / services, that are in a special trust relationship. With the proper configuration, theoretically any service could be added to the exising AD Federation. Much of the work required to set up AD FS is managaging this federation, both on the AD FS service, and the website.
  • Account Federation Server - Issues the claims tokens to users based on authenticating that user, basically the AD FS server
  • Relying Party - the website using the claim for authenticating & authorizing the user
  • Single Sign-on / SSO - a user authentication mechanism that allows a user to use one login to access multiple applications
  • Claim - A statement about the user. There are three types of claims - Identity Claims, Group Claims, and Custom Claims
    • An identity claim is basic information about the user e.g. username
    • A group claim is information about which domain groups the user belongs to e.g. PayrollUsers
    • a custom claim is a key/value pair that can be used by applications. For example, we use a corporate user identity code that is standard between all web applications.

Components

Below is a simplified view of what my current AD FS system looks like

ADFS Architecture

  • The AD FS Federation server is a Windows 2012 R2 Instance responsible for:
    • Registering each Relying Party / Web Server / Website
    • defining what claims metdata is in in the claims token
    • Provides a login endpoint for users to enter credentials into
    • Provides a signout endpoint to clear the claims token
    • Initially we used a 2008 R2 Instance, but upgraded to a 2012 R2 instance. The configurations are largely the same, but one key improvement is the ability customize the user login page (the 2008 R2 page looks terrible on mobile devices)
  • The Web Servers are Windows Server (2008 R2 - 2012 R2) instances which host one or more websites configured to used AD FS
    • The websites are built with ASP.NET / MVC / Web API
    • use Owin middleware for communicating with AD FS
      • redirecting the user to the AD FS Server to login
      • redirecting the user to the AD FS Server to sign out
      • Processing the Claims Token / making the claim available to the web application
    • contain AD FS endpoint / configuration metadata
  • The User Instances (PCs / tablets / mobile) access the websites
    • Upon hitting the Web Server instance, user is redirected to the AD FS Server
    • Upon successful login, the claims token is stored in a cookie in the user’s browser.
    • The cookie is read by the website after the AD FS Server redirects the user back to the website. One of the most important configurations in AD FS is specifing the proper redirect endpoints.
    • The website will continue to use the browser cookie until the user signs out, or the cookie expires.
    • Currently we have a cookie expiration of 24 hours, which will force the user to log in at most once per day.

Resources

16 Jan 2017

Understanding Relative Urls

Something you use all the time, but don’t always realize, is how a web browser will request resources in html files with relative url’s.

for example:

    <script src="libs/jquery.min.js" type="text/javascript"></script>

will be served from example.com as http://example.com/libs/jquery.min.js - provided the website is sitting in the web server root e.g wwwroot or /var/www/html.

If, instead, the app is sitting in a sub-folder / virtual directory, the above code will still work, even thought it may now come from http://example.com/salesapp/libs/jquery.min.js/. This is great, as it lets us decouple our application structure from where it is deployed.

What if we changed the URL a bit? say to:

    <script src="/libs/jquery.min.js" type="text/javascript"></script>

With this change, the leading / indicates the root of the website. It is still a relative path, but it is “relative” to the root of the website. http://example.com/libs/jquery.min.js will still work, but http://example.com/salesapp/libs/jquery.min.js will not. This can be a subtle error, as most applications run locally are sitting at http://localhost:12345 or whatever, but many are not deployed to the web root.

Using the leading slash removes a lot of future flexibility. So why would you want it? Well, consider this scenario. You have a script file that has a jquery dependency, which lives at http://example.com/salesapp/scripts/accounts.js. This reference will no longer work:

    <script src="libs/jquery.min.js" type="text/javascript"></script>

It will return as http://example.com/salesapp/scripts/libs/jquery.min.js oops.

The reason this doesn’t work is that the relative path is build based on your current window location, which will change with different url’s:

http://example.com
http://example.com/salesapp
http://example.com/salesapp/accounts

It will have to be one of these:

    <script src="/libs/jquery.min.js" type="text/javascript"></script>
    <script src="../libs/jquery.min.js" type="text/javascript"></script>
    <script src="http://example.com/libs/jquery.min.js" type="text/javascript"></script>

Out of the three, the 2nd is probably the most flexible, but is cumbersome to read and write (especially if your source trees are deep).

What you really want to do is set your url relative to the path of the application. This can be using the <base> tag. According to mdn

“The HTML element specifies the base URL to use for all relative URLs contained within a document.”

For example, if you set up the <base> tag as follows:

<head>
    <base href="/">
</head>

This effecively sets all relative paths relative to the root of the website libs/jquery.min.js will be read as /libs/jquery.min.js, and thus http://example.com/libs/jquery.min.js/

To set an application specific path, you could use:

<head>
    <base href="/salesapp/">
</head>

libs/jquery.min.js will be read as /salesapp/libs/jquery.min.js, and thus http://example.com/salesapp/libs/jquery.min.js/

this works well when deploying to different endpoints as only the <base> reference needs to change, which can be easily handled via a build system.

Be careful though, specifing relative paths with the leading slash will ignore the specified <base>.

Lastly, you can also specify an empty/current directory base path

<head>
    <base href="">
</head>

or

<head>
    <base href="./">
</head>

Which is effectively the same as specifying no <base>. You might use this for local development.

Additionally, you can dynamically write the <base> with a script, but doesn’t appear to be a recommended approach. It seems better to leave this up to your build / depolyment system.

The good news is that broken paths are generally easy to detect when your browser is in development mode - just look for the red requests in the network tab, and check the path. You’ll know something is off when the path is partially correct, but contains additional levels or missing levels altogether.

07 Dec 2016

Default Startup Project in Visual Studio

Had an interesting error the other day. While reviewing an interns test project, I noticed that on a fresh clone of his repo, his website errored out on startup. For our projects we typically have a project for the website, and another for the business classes. For some reason, Visual Studio was trying to run the business project as the website.

This is easily fixed right-clicking the web project and selecting set as startup project, but this setting is stored in the .suo file which is not checked in. So why does this work in our other projects?

Doing some googling, came across this stackoverflow answer. It turns out that Visual Studio looks at the order of the projects in the solution file. Somehow, my intern added the business project first - I don’t think anyone has done that before.

All you need to do is manually edit the .sln file in a text editor, and move the “startup” project to the top of t he Project / EndProject blocks

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.23107.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MySiteApp", "MySite.App\MySiteApp.csproj", "{F5C8B11C-FF25-45B6-8F25-B1C779D4A053}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MySiteBusiness", "MySite.Business\MySiteBusiness.csproj", "{E9561B89-36A0-43C2-B9BA-970843316D23}"
EndProject

On a related note, it seems like Microsoft can’t pick a file format and stay with it. the .suo file is binary, .config files are xml, and the .sln file is basically just simple keys and values.