ADFS and ADAL Lab

02 February 2015 - Azure, Visual Studio

This week I will do a workshop about authentication/authorization in web and WPF applications. For that workshop I created a lab environment. This blog post summarizes important links and tips for that. I use it for myself as a reference for preparing the demo. However, maybe someone else has to prepare such a workshop too. Therefore I share all the links and tips with you.

Technologies

Here are the links to the technologies and tools I cover in my auth workshop:

The Lab's Server Environment

For the workshop I created the following server environment (everything is running in Azure):

  • VNet with Point-to-site-VPN (assign all the machines mentioned below to this VNet)
  • Azure VM for the Active Directory Domain Controller (see detailed instructions)
    Note: Install Certificate Services so that we can generate a certificate for ADFS.
  • Second Azure VM for ADFS (see detailed instructions; in my scenario the URL is https://adfs.corp.adfssample.com)
    Tip: You might need to add the ADFS website to the Local Intranet Zone (see this MSDN article).

    Do not use a CNAME record for the ADFS Server, you must use an A record.

  • Windows 8 machine with Visual Studio for code demos (you can use Azure's existing VS VM template to make things easier)

In the workshop I am currently preparing, Azure AD is not relevant (unfortunately). Therefore, I don't setup an ADFS proxy. This will be a topic for future posts.

Samples

OWIN and WS-Federation

You can use this sample to demonstrate how simple it is to connect an ASP.NET MVC application with ADFS using WS-Federation.

GitHub Link to the sample: https://github.com/AzureADSamples/WebApp-WSFederation-DotNet

Vittorio has a description of what to change so that the sample works with ADFS. After the changes described there, your Startup.Auth.cs file will look something like this:

using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.WsFederation;
using Owin;
using System.Configuration;

namespace WebApp_WSFederation_DotNet
{
    public partial class Startup
    {
        private static string realm = ConfigurationManager.AppSettings["ida:Wtrealm"];
        private static string metadata = "https://adfs.corp.adfssample.com/federationmetadata/2007-06/federationmetadata.xml";

        public void ConfigureAuth(IAppBuilder app)
        {
            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

            app.UseCookieAuthentication(new CookieAuthenticationOptions());

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

To make it easier for the audience to follow, consider switching ADFS Authentication Method from Windows Authentication to Forms Authentication. After that change, you have more time talking about URLs and the entire redirect flow.

OWIN, OAuth2, ADFS, and ADAL

The second sample demonstrate the out-of-the-box OAuth2 implementation of ADFS. Currently, ADFS' OAuth2 does only support authorization code grant. So your possibilities are limited. The third sample (see below) will show us how to get around this limitation.

In this sample we start by setting up an OWIN-based web API. We protected it using ADFS Bearer Auth middleware (Microsoft.Owin.Security.ActiveDirectory package):

using Microsoft.Owin.Security.ActiveDirectory;
using Owin;
using System.Configuration;
using System.IdentityModel.Tokens;

namespace OWIN_ADAL_ADFS_WebMVC
{
    public partial class Startup
    {
        public void ConfigureAuth(IAppBuilder app)
        {
            app.UseActiveDirectoryFederationServicesBearerAuthentication(
                new ActiveDirectoryFederationServicesBearerAuthenticationOptions
                {
                    MetadataEndpoint = ConfigurationManager.AppSettings["ida:MetadataEndpoint"],
                    TokenValidationParameters = new TokenValidationParameters()
                    {
                        ValidAudience = ConfigurationManager.AppSettings["ida:Audience"]
                    }
                });
        }
    }
}

Next, we build a WPF (Windows Presentation Foundation) full client using Microsoft's ADAL (Active Directory Authentication Libraryfor .NET:

private async void Button_Click(object sender, RoutedEventArgs e)
{
    string authority = "https://adfs.corp.adfssample.com/adfs";
    string resourceURI = "https://adfssample.com/OWIN-ADAL-ADFS-WebMVC";
    string clientID = "82A2A9DE-131B-4837-8472-EDE0561A0EF6";
    string clientReturnURI = "http://anarbitraryreturnuri/";

    var ac = new AuthenticationContext(authority, false);
    var ar = await ac.AcquireTokenAsync(resourceURI, clientID, new Uri(clientReturnURI), new AuthorizationParameters(PromptBehavior.Auto, new WindowInteropHelper(this).Handle));

    string authHeader = ar.CreateAuthorizationHeader();
    var client = new HttpClient();
    var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:44302/api/Values");
    request.Headers.TryAddWithoutValidation("Authorization", authHeader);
    var response = await client.SendAsync(request);
    string responseString = await response.Content.ReadAsStringAsync();

    MessageBox.Show(responseString);
}

In the code above you find a clientID. Don't forget that you have to create this client in ADFS using Add-AdfsClient.

I recommend looking at the flow of requests using a web debugger like Fiddler. Note that this will only work if you switch to Forms Authentication (see above).

Last but not least we look at a demo showing how this works in an ASP.NET MVC app:

public ActionResult About()
{
    string authorizationUrl = string.Format(
        "https://adfs.corp.adfssample.com/adfs/oauth2/authorize?api-version=1.0&response_type=code&client_id={0}&resource={1}&redirect_uri={2}",
        clientID,
        "https://adfssample.com/OWIN-ADAL-ADFS-WebMVC",
        "https://localhost:44303/Home/CatchCode");

    return new RedirectResult(authorizationUrl);
}

public async Task<ActionResult> CatchCode(string code)
{
    var ac = new AuthenticationContext("https://adfs.corp.adfssample.com/adfs", false);
    var clcred = new ClientCredential(clientID, "asdf");
    var ar = await ac.AcquireTokenByAuthorizationCodeAsync(code, new Uri("https://localhost:44303/Home/CatchCode"), clcred);
            
    string authHeader = ar.CreateAuthorizationHeader();
    var client = new HttpClient();
    var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:44302/api/Values");
    request.Headers.TryAddWithoutValidation("Authorization", authHeader);
    var response = await client.SendAsync(request);
    string responseString = await response.Content.ReadAsStringAsync();

    this.ViewBag.Message = responseString;

    return View("About");
}

Note that in this example the web server does the REST call to the service protected by ADFS.

Enter: Identity Server v3

As you can see, you can do some interesting things with what Microsoft delivers out-of-the-box. However, a lot is still missing. ADFS does not even implement all OAuth2 flows (e.g. implicit grant is missing which would be important for Single Page Apps). ADFS does not implement the new shining star for auth Open ID Connect. Note that you can get all of that by using Microsoft Azure Active Directory. However, that's only an option if you are willing to use the public cloud. We have a few large customers who do not want to use offerings like Azure (yet). They need a different solution.

Identity Server v3 Identity Provider
<h3>The Server</h3><p>Configuring ADFS as an identity provider in IDServer3 isn’t complicated:</p>

using Microsoft.Owin.Security.WsFederation;
using Owin;
using SelfHost.Config;
using Thinktecture.IdentityServer.Core.Configuration;
using Thinktecture.IdentityServer.Host.Config;

namespace SelfHost
{
    internal class Startup
    {
        public void Configuration(IAppBuilder appBuilder)
        {
            var factory = InMemoryFactory.Create(
                users:   Users.Get(), 
                clients: Clients.Get(), 
                scopes:  Scopes.Get());

            var options = new IdentityServerOptions
            {
                IssuerUri = "https://idsrv3.com",
                SiteName = "Thinktecture IdentityServer3 (self host)",

                SigningCertificate = Certificate.Get(),
                Factory = factory,

                AuthenticationOptions = new AuthenticationOptions()
                {
                    EnableLocalLogin = false,
                    IdentityProviders = ConfigureIdentityProviders
                }
            };

            appBuilder.UseIdentityServer(options);
        }

        private void ConfigureIdentityProviders(IAppBuilder app, string signInAsType)
        {
            var adfs = new WsFederationAuthenticationOptions
            {
                AuthenticationType = "adfs",
                Caption = "ADFS",
                SignInAsAuthenticationType = signInAsType,

                MetadataAddress = "https://adfs.corp.adfssample.com/federationmetadata/2007-06/federationmetadata.xml",
                Wtrealm = "urn:idsrv3"
            };
            app.UseWsFederationAuthentication(adfs);
        }
    }
}

Note that this code sample disables local login entirely. If you turn off the consent screen for the client, the end user will typically not see anything from IDServer3.

IDServer3 supports a variety of hosting options including self-hosting in an executable and IIS. You can find samples for many of the options in the IDServer3 sample GitHub repository.

The Client

As IDServer3 implements Open ID Connect completely, you are no longer limited to a specific OAuth2 flow. You can use the flow that fits best to your scenario. Samples for different client implementations can be found in the Clients folder of IDServer3's sample GitHub repository.

An interesting sample is for instance the JavaScript client that uses implicit grant (samplecode on GitHub). If you run it, I recommend looking at the JWT token. You should find AD's claims in there.

Further Readings

Do you want to dig deeper? Of course you should look at the usual sources (OAuth2 and OIC specs, MSDN, etc.). Additionally, the web is full of valuable blogs. Here are some of my security-related favorites: