次の方法で共有


How to create a Custom Authentication Provider for Active Directory Federation Services on Windows Server 2012 R2 - Part 5

In this series of five blog posts I want to show you how you can create your own Authentication Provider in AD FS on Windows Server 2012 R2. This Authentication Provider can then be used in AD FS for multi-factor authentication (MFA). The solution will use the users mobile device as a second factor for authentication, by sending a One-Time Password (OTP) or PIN to the device.

For those of you interested only in the AD FS related components, part 2 will be most interesting. That's where we will be creating the Authentication Provider itself. But if you are looking for a complete, working, solution, you might want to go over the entire set. So you might be interested by now what's in the other parts... or at least I hope you are. So here's a quick agenda;

  1. Introduction
  2. Creating the Authentication Provider
  3. Setting up Azure Mobile Services
  4. Creating a Windows Phone Authenticator App
  5. Putting it all together

Now, before we get started I have to remind you that this blog post series is only to show you how you can create this Authentication Provider. By no means is the solution we are creating an enterprise ready MFA solution. If you are looking into such an enterprise ready solution, I would recommend you take a look at the Windows Azure Multi-Factor Authentication.

We're almost there. The only thing left now is to put all the components together for our MFA solution!

Part 5 - Putting it all together

We have now got the Authentication Provider that we created in Part 2. This Authentication Provider works (well, AD FS recognizes it and we are able to ask a user for input). Later, in Part 4, we created a Windows Phone App able to receive push notifications from the Windows Azure Mobile Service we created in Part 3. So in this fifth, and last, part of this series we're going to put it all together to end up with the solution we were striving for in Part 1. We won't have to touch the Windows Azure Mobile Service anymore, nor do we have to make any modifications to the Windows Phone App. The only thing we have to revise is our Authentication Provider.

Back to the Authentication Provider

Since all the modification that we need to do are in the Authenication Provider, open the MyAuthenticationProvider project in Visual Studio. These are the modifications that we need to do in order for the Authentication Provider to work exactly how we want;

  • Get the users Mobile Device from Active Directory
  • Generate a Random PIN and send this to the users Mobile Device.
  • Validate the user input.

Only three items to modify in out Authentication Provider. Let's get started on retrieving the user's Mobile Device from Active Directory.

Get the users Mobile Device from Active Directory

When we implemented the Windows Azure Mobile Service in Part 3, we decided to store the unique device identifier which we learning in Part 4 is unique per device and publisher, in a table called Registrations in our Mobile Service. It is stored in this Registrations table, in the column deviceid. Yet, this does not provide us a proper user-to-mobile-device mapping. Hence, we store the device identifiers, as shown by our mobile device, in the user object in Active Directory. We could also create a SQL Attribute store in AD FS to store this mapping, but for now Active Directory is more convenient. Let the user show you the application, and store this identifier somewhere in the users object. In our Authentication Provider, we are going to store this information in the Notes field in Active Directory Users and Computers.

When we check the Attribute Editor (which is available after you have enabled the Advanced Features view in the View menu, we see that this Notes field in fact, within Active Directory is stored in the info field.

Okay, so that's where our Authentication Provider has to search for the device id for a user. We have already configured our Authentication Provider in such a way that it requires an Identity Claim in order to work. We have decided on using a users UPN as the Identity Claim. (Remember? It was in the AuthenticationAdapterMetadata; the RequiresIdentity property defines if we require an Identity Claim, and the IdentityClaims property of the class defines which Identity Claims we need. This Identity Claim is then passed to the AuthenticationAdapter by means of the BeginAuthentication method.

So we need some code that will lookup the user's device id from Active Directory, when we have the user's UPN. Here is the C# code that will do this. We will add this code to the AuthenticationAdapter class:

         private string GetDeviceId(string upn)
        {
            DirectoryEntry entry = new DirectoryEntry();
            DirectorySearcher mySearcher = new DirectorySearcher(entry, "(&(objectClass=user)(objectCategory=person)(userPrincipalName=" + upn + "))");
            SearchResult result = mySearcher.FindOne();
            string deviceId = (string)result.Properties["info"][0];
            return deviceId;
        }

We see that input is a string, upn, and the output is a string as well. It will use classes out of the System.DirectoryService namespace. Therefore, we need to add a reference to these components from our Project. After adding this reference we add this using statement to the existing using statements in the AuthenticationAdapter. But since we will see that we will need more references, I will show them ALL here.

using System.DirectoryServices;
using System.Net;
using System.Runtime.Serialization.Json;
using System.IO;

We can add the reference by right-clicking References under our MyAuthenticationProvicer project in the Solution Explorer, and click Add Reference...

In the Reference Manager dialog, at the top left, select Assemblies and type directory in the search box. Select the checkbox in front of System.DirectoryServices and click OK.

We will also create some code that decides whether or not MFA is available for a user. We can do this by checking if there is any value in the Notes (or info) field. This method can then be used in the IsAvailableForUser property in the AutheticationAdapter. We can implement it directly in the IsAvailableForUser property:

         public bool IsAvailableForUser(System.Security.Claims.Claim identityClaim, IAuthenticationContext context)
        {
            string upn = identityClaim.Value;
            DirectoryEntry entry = new DirectoryEntry();
            DirectorySearcher mySearcher = new DirectorySearcher(entry, "(&(objectClass=user)(objectCategory=person)(userPrincipalName=" + upn + "))");
            SearchResult result = mySearcher.FindOne();
            if (result.Properties["info"].Count == 0) return false;
            string deviceId = (string)result.Properties["info"][0];
            return !String.IsNullOrEmpty(deviceId);
        }

Generate a Random PIN and send this to the users Mobile Device.

Now creating a pseudo-random is not too hard in C#. But we also need to find a way to insert a new record in the Authentications table in our Windows Azure Mobile Service. The Authentications table must be provided with a deviceid, this make sure the toast notification is sent to the proper device, and a text and pin. We have created two columns, because it might be nice to localize the messages that goes along with the pin.

We will create a method that uses basic HTTP classes from the .NET Framework to accomplish this. Besides these classes we will use a JSON serializer. In the same fashion as with our Window Phone App, we need to create a class to hold the Authentications class.

Let's start by creating this new Authentications class.(Press Shift-Alt-C in Visual Studio, type Authentications in the Name box and hit enter.) This class will require a reference to System.Runtime.Serialization. This reference has to be added first. (No screenshots any more... we did this quite often.) In the newly created class, add a using statement to this namespace;

using System.Runtime.Serialization;

Then implement the class like this:

     [DataContract]
    class Authentications
    {
        [DataMember(Name = "id")]
        public string Id { get; set; }

        [DataMember(Name = "text")]
        public string Text { get; set; }

        [DataMember(Name = "pin")]
        public string Pin { get; set; }

        [DataMember(Name = "deviceid")]
        public string DeviceID { get; set; }
    }

Before we get started on actually sending the toast message, I would like you to add two properties to the AuthenticationAdapter class:

        private string url;
        private string key;

Then, from the OnAuthenticationPipelineLoad method, we will set these properties and we can use these in the rest of our code. The url would contain the URL of our Windows Azure Mobile Service and the key would contain the key that is required in order to write things to the database. So the OnAuthenticationPipelineLoad method would look like this:

         public void OnAuthenticationPipelineLoad(IAuthenticationMethodConfigData configData)
        {
            this.key = "kx5IznPLilXrhKlBBO6HNvmhPepIrr75";
            this.url = "myauthenticationprovider.azure-mobile.net";
        }

Now we are ready to create a method that will create a (pseudo-) random PIN and send this to the user. We will do this by storing the PIN in the Authentications table in our Windows Azure Mobile Service. After we have inserted a new record in the Authentications table there, we will fetch the unique key for this row. We do this because we need to validate this PIN later on, when the user has typed the PIN and clicked Continue. Since we have to maintain state using a form field (which will be passed to the TryEndAuthentication method in the proofData parameter) we have to make sure we can find the PIN again. We could simply store the PIN in a hidden form field in the AD FS logon page, but a user could very easily find that PIN by looking at the source code of the logon page. Hence, we don't want to pass the actual PIN. In stead, we pass the unique key of the record in the Windows Azure Mobile Service's Authentications table, and pass that. When we start TryEndAuthentication, we will then go back to this Authentications table and fetch the record with that unique key. We then have the PIN and can compare that with the PIN the user entered. (Also available in the proofData parameter in the TryEndAuthentication method.)

This is the helper method that will allow us to create a random PIN and send this to the user's device. (Or actually, save it as a new record in the Authentications table in our Windows Azure Mobile Service.) The method will return this unique identifier, so that another method can put this in a hidden text file in the user logon page.

This code goes into the AuthenticationAdapter class:

         private string SendToast(string text, string deviceId)
        {
            Random random = new Random();
            int randomNumber = random.Next(0, 100000);
            string pin = randomNumber.ToString("D5");
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create("https://" + this.url + "/tables/Authentications");
            request.Headers.Add("X-ZUMO-APPLICATION", this.key);
            request.Method = "POST";
            request.ContentType = "application/json";
            Authentications authentication = new Authentications()
            {
                Text = text,
                Pin = pin,
                DeviceID = deviceId
            };
            DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(Authentications));
            string postData;
            using (MemoryStream ms = new MemoryStream())
            {
                serializer.WriteObject(ms, authentication);
                postData = Encoding.Default.GetString(ms.ToArray());
            }
            byte[] byteArray = Encoding.UTF8.GetBytes(postData);
            request.ContentLength = byteArray.Length;
            Stream dataStream = request.GetRequestStream();
            dataStream.Write(byteArray, 0, byteArray.Length);
            dataStream.Close();
            using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
            {
                if (response.StatusCode != System.Net.HttpStatusCode.Created)
                    throw new Exception(String.Format("Server error (HTTP {0}: {1}).", response.StatusCode, response.StatusDescription));
                DataContractJsonSerializer jsonSerializer = new DataContractJsonSerializer(typeof(Authentications));
                object objResponse = jsonSerializer.ReadObject(response.GetResponseStream());
                authentication = objResponse as Authentications;
            }
            string authId = authentication.Id;
            return authId;
        }

Since the PIN has to be created when the user starts authentication, it no more than logical that his method must be called somewhere within the BeginAuthentication method of the AuthenticationAdapter. We will change the code for the BeginAuthentication method to a) send the PIN to the users device and b) store the unique key for the record in the logon form of the user in AD FS. Our new BeginAuthentication method should now look like this:

         public IAdapterPresentation BeginAuthentication(System.Security.Claims.Claim identityClaim, System.Net.HttpListenerRequest request, IAuthenticationContext context)
        {
            string upn = identityClaim.Value;
            string text = "Your Authentication PIN is:";
            string deviceId = GetDeviceId(upn);
            string authId = SendToast(text, deviceId);
            return new AdapterPresentation(authId);
        }

Pass required information between posts

In order for this to work, we also have to change the AdapterPresentation class. We have now introduced a new construction (one that takes the new key during creation). We have to add a new property to the AdapterPresentation class:

private string authId;

And we need to change the constructors of the AdapterPresentation class:

         public AdapterPresentation(string authId)
        {
            this.message = string.Empty;
            this.isPermanentFailure = false;
            this.authId = authId;
        }
        public AdapterPresentation(string message, string authId, bool isPermanentFailure)
        {
            this.message = message;
            this.isPermanentFailure = isPermanentFailure;
            this.authId = authId;
        }

And there is more. We need to make sure that the key that helps us find the PIN is present in the proofData in TryEndAuthentication. Hence, we have to make sure that the GetFormHtml method in our AdapterPresentation class will place this key in the form that the user submits. We will add a hidden form field called authid to the existing set. Add this line anywhere in the GetFormHtml method of the AdapterPresentation class that modifies the HTML form:

result += "<input id=\"authid\" type=\"hidden\" name=\"AuthId\" value=\"" + this.authId + "\"/>";

Check the PIN

Now that we can generate a PIN, send it to the user's device and have the user input the PIN in the logon form, there is one last thing to do; check the PIN that the user provides. This magic will happen in the TryEndAuthentication method. The first thing we need to do is to get the pin that the user typed. This is provided through the proofData parameter. The second thing we need is the key of the record in our Authentications table in the WIndows Azure Mobile Services. With this key, we can get the corresponding Authentications record which will provide us with the original PIN the AuthenticationAdapter generated. We then only have to compare the two.

Let's first create a helper method that gets the original PIN based on the Key provided in the proofData. This method goes into the AuthenticationAdapter class:

         private string GetPin(string authId)
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create("https://" + this.url + "/tables/Authentications?$filter=(id%20eq%20'" + authId + "')&$select=pin");
            request.Headers.Add("X-ZUMO-APPLICATION", this.key);
            request.Method = "GET";
            request.ContentType = "application/json";
            string pin;
            using (HttpWebResponse response = request.GetResponse() as System.Net.HttpWebResponse)
            {
                if (response.StatusCode != System.Net.HttpStatusCode.OK)
                    throw new Exception(String.Format(
                    "Server error (HTTP {0}: {1}).",
                    response.StatusCode,
                    response.StatusDescription));
                DataContractJsonSerializer jsonSerializer = new DataContractJsonSerializer(typeof(Authentications[]));
                object objResponse = jsonSerializer.ReadObject(response.GetResponseStream());
                Authentications[] authentications = objResponse as Authentications[];
                if (authentications.Length != 1)
                {
                    throw new Exception("ERROR!");
                }
                pin = authentications[0].Pin;
            }
            return pin;
        }

Now that we have a nice helper method to get the PIN from the Windows Azure Mobile Service using the key in the Authentications table, we can change the TryEndAuthentication method to check the original pin and the provided pin. This is the new code for the method:

         public IAdapterPresentation TryEndAuthentication(IAuthenticationContext context, IProofData proofData, System.Net.HttpListenerRequest request, out System.Security.Claims.Claim[] claims)
        {
            string pin = proofData.Properties["pin"].ToString();
            string authId = proofData.Properties["authid"].ToString();
            string originalPin = GetPin(authId);
            if (pin == originalPin)
            {
                System.Security.Claims.Claim claim = new System.Security.Claims.Claim("https://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod", "https://schemas.microsoft.com/ws/2012/12/authmethod/otp");
                claims = new System.Security.Claims.Claim[] { claim };
                return null;
            }
            else
            {
                claims = null;
                return new AdapterPresentation("Authorization failed.", authId, false);
            }
        }

When we see that the pin's match, we will issue an authenticationmethod claim and not return anything. If we find that there is a mismatch, we do not issue the authenticationmethod claim (by setting the out parameter claims to null) but in stead issue a new HTML form that indicates to the user that something went wrong.

Building the new Authentication Provider

Now we are ready to build our project Authentication Provider!

In Visual Studio, click Build and then Build Solution (or simply hit F6) to build the solution, and check if everything went well. (You can see in Part 2 how we can do this, and where the output DLL is.)
Again, we need to copy the result to the AD FS server and register it into the Global Assembly Cache. We need to unregister the previous DLL first though.

So stop the AD FS service, and unregister the current DLL from the Global Assembly Cache:

 Set-location "C:\MyAuthenticationProvider"
[System.Reflection.Assembly]::Load("System.EnterpriseServices, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")
$publish = New-Object System.EnterpriseServices.Internal.Publish
$publish.GacRemove("C:\MyAuthenticationProvider\MyAuthenticationProvider.dll")

Overwrite the old MyAuthenticationProvider.dll file in C:\MyAuthenticationProvider on the AD FS server and register the DLL again in the GAC. Remember to properly check what you PublicKeyToken is, as explained in Part 2 of this series. (Use the SN tool.) If you used the same Visual Studio solution, and you did not modify the signing key, the PublicKeyToken should be the same.

 Set-location "C:\MyAuthenticationProvider"
[System.Reflection.Assembly]::Load("System.EnterpriseServices, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")
$publish = New-Object System.EnterpriseServices.Internal.Publish
$publish.GacInstall("C:\MyAuthenticationProvider\MyAuthenticationProvider.dll")

Since we did not change anything in our AuthenticationAdapterMetadata, and we were using the same signing key, we do not need to re-register the Authentication Provider in AD FS. If you ever need to un-register the provider and then register it again, make sure the Authentication Provider is not used before you try to unregister it using. You will need to re-register the Authentication Adapter when the Public Key Token changes or the metadata changes. You can unregister you Authentication Provider by using this PowerShell command:

Unregister-AdfsAuthenticationProvider -Name MyAuthenticationProvider

Okay, let's start AD FS again, check the event logs if everything loaded properly.

Preparing a user account for our Authentication Provider

On the Windows Phone where you have deployed the App we created in Part 4, start the MyPhoneAuthentication App and note the Device ID presented by the application. Now, open up the users Active Directory account in Active Directory Users and Computers, and open the Telephones tab. In the Notes field, enter the exact Device ID the application shows. Click OK to save and close the dialog. An alternative way of checking the users Device ID is to Browse the Registrations table in the Windows Azure Mobile Service. Part 3 covers that.

Testing

Now that we have a user that's running the MyPhoneAuthentication App, we have a means to send the phone a PIN through a push notification and we have an Authentication Provider in AD FS that can send out PINs to users, it's time to test! Check the AD FS settings and make sure that the Authentication Provider is enabled for a specific Relying Party. Instruct the user to navigate to the URL of this application and wait for a redirection to AD FS. Once the user is authenticated there though WIA/FBA or Basic Authentication, AD FS should call the Authentication Provider. If our Authentication Provider is the only authentication provider available, it will directly be called upon. If multiple Authentication Providers are enabled, the user can choose which method to use:

After our Authentication Provider is chosen the mobile device should receive a push notification with a PIN.

The AD FS server should show the user a page to enter the PIN:

If multiple Authentication Adapters are available for the user the screen will look like this:

If the user enters the correct PIN, the user will continue in the Claims Pipeline, get a token and is taken to the web application. If the wrong PIN is entered, the user can retry:

If MFA is required, but it's not available (e.g. the IsAvailableForUser method in the AuthenticationAdapter returns false, we see this error message:

We have a working Authentication Provider for AD FS running on Windows Server 2012 R2 doing proper Multi-Factor Authentication!

Some things to take into account before you start using this codebase for you own authentication provider:

Safety

This code has not been created with safety in mind. Your biggest concern should be the way that the Phone Application as well as the Authentication Provider gain access to the Windows Azure Mobile Service. There are ways to improve the security of this. PIN codes are stored in the database in plain text.
Also, a user can retry the PIN forever. You should implement something like a counter that will prevent this from happening.

Stability

No proper error handling has been implement. Not in the Authentication Provider, nor in the Windows Phone Application. This could severely impact the working of the AD FS server.

Support

This is only Proof-of-Concept code. This code is NOT SUPPORTED by me or Microsoft in any way, shape or form.

 # THIS CODE AND ANY ASSOCIATED INFORMATION ARE PROVIDED “AS IS” WITHOUT
# WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
# LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS
# FOR A PARTICULAR PURPOSE. THE ENTIRE RISK OF USE, INABILITY TO USE, OR 
# RESULTS FROM THE USE OF THIS CODE REMAINS WITH THE USER.

Take me back to Part 4 - Creating a Windows Phone Authenticator App

Comments

  • Anonymous
    February 01, 2014
    In this series of five blog posts I want to show you how you can create your own Authentication Provider
  • Anonymous
    February 01, 2014
    In this series of five blog posts I want to show you how you can create your own Authentication Provider
  • Anonymous
    February 01, 2014
    In this series of five blog posts I want to show you how you can create your own Authentication Provider
  • Anonymous
    February 01, 2014
    In this series of five blog posts I want to show you how you can create your own Authentication Provider
  • Anonymous
    August 27, 2014
    We have a requirments to customize primary authentication in ADFS 3. Since the ls website is completely locked down, Will it be possible to customize primary authentication of ADFS 3 using AuthenticationAdapter?
  • Anonymous
    October 19, 2016
    That was awesome !