Поделиться через


Build your own external authentication provider for AD FS in Windows Server 2012 R2: Walk-through Part 1

Introduction

Welcome to the Build your own external authentication provider walk-through for AD FS in Windows Server 2012 R2!

This article provides a step by step walk through to get you started building your provider.  The example we will build is for educational purposes only.  It's the most minimal, bare bones implementation possible to expose the required elements of the model.  There is no actual authentication back end, error processing, or configuration data.  But it is enough to get you started, so let's begin!

Setting up the development box

This walk-through uses Visual Studio 2012.  The project can be built using any development environment that can create a .NET class for Windows.

The project must target .NET 4.5 because the BeginAuthentication and TryEndAuthentication methods use the type System.Security.Claims.Claim, part of .NET Framework version 4.5.

There is one reference required for the project:

Reference dll

Where to find it

Required for

Microsoft.IdentityServer.Web.dll

The dll is located in %windir%\ADFS on a Windows Server 2012 R2 machine on which AD FS has been installed.

 

This dll must be copied to the development machine and an explicit reference created in the project

Interface types including IAuthenticationContext, IProofData

Create the provider

In Visual Studio 2012: Choose File->New->Project...

Select Class Library and be sure you are targeting .NET 4.5:

 

Make a copy Microsoft.IdentityServer.Web.dll from %windir%\ADFS on a Windows Server 2012 R2 machine on which AD FS has been installed and paste it in your Project folder on your development machine.

In Solution Explorer, right click References and Add Reference...

Browse to your local copy of Microsoft.IdentityServer.Web.dll and Add...

Click OK to confirm the new reference:

 

You should now be setup to resolve all of the types required for the provider. 

Add a new class to your project (Right click your project, Add...Class...) and give it a name like MyAdapter, shown below:

 

In the new file MyAdapter.cs, replace the existing code with the following:

  using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using System.Globalization;
 using System.IO;
 using System.Net;
 using System.Xml.Serialization;
 using Microsoft.IdentityServer.Web.Authentication.External;
 using Claim = System.Security.Claims.Claim;
 
 namespace MFAadapter
 {
 class MyAdapter : IAuthenticationAdapter
 {
 
 }
 }

Now you should be able to F12 (right click – Go To Definition) on IAuthenticationAdapter to see the set of required interface members.  Now we will do a simple implementation of these.

Replace the entire contents of your class with the below.

 namespace MFAadapter
 {
 class MyAdapter : IAuthenticationAdapter
 {
 public IAuthenticationAdapterMetadata Metadata
 {
 //get { return new <instance of IAuthenticationAdapterMetadata derived class>; }
 }
 
 public IAdapterPresentation BeginAuthentication(Claim identityClaim, HttpListenerRequest request, IAuthenticationContext authContext)
 {
 //return new instance of IAdapterPresentationForm derived class
 
 }
 
 public bool IsAvailableForUser(Claim identityClaim, IAuthenticationContext authContext)
 {
 return true; //its all available for now
 
 }
 
 public void OnAuthenticationPipelineLoad(IAuthenticationMethodConfigData configData)
 {
 //this is where AD FS passes us the config data, if such data was supplied at registration of the adapter
 
 }
 
 public void OnAuthenticationPipelineUnload()
 {
 
 }
 
 public IAdapterPresentation OnError(HttpListenerRequest request, ExternalAuthenticationException ex)
 {
 //return new instance of IAdapterPresentationForm derived class
 
 }
 
 public IAdapterPresentation TryEndAuthentication(IAuthenticationContext authContext, IProofData proofData, HttpListenerRequest request, out Claim[] outgoingClaims)
 {
 //return new instance of IAdapterPresentationForm derived class
 
 }
 
 }
 }
  

We’re not ready to build yet...just two more interfaces to go :)
Add two more classes to your project: one will be for the metadata, and the other for the presentation form.  You can add these within the same file as the class above.

  class MyMetadata : IAuthenticationAdapterMetadata
 {
 
 }
 
 class MyPresentationForm : IAdapterPresentationForm
 {
 
 }
 

Now we will add the required members for each.

First, the metadata (with some hopefully helpful inline comments)

 class MyMetadata : IAuthenticationAdapterMetadata
 {
 //Returns the name of the provider that will be shown in the AD FS management UI (not visible to end users)
 public string AdminName
 {
 get { return "My Example MFA Adapter"; }
 }
 
 //Returns an array of strings containing URIs indicating the set of authentication methods implemented by the adapter 
 /// AD FS requires that, if authentication is successful, the method actually employed will be returned by the
 /// final call to TryEndAuthentication(). If no authentication method is returnd, or the method returned is not
 /// one of the methods listed in this property, the authentication attempt will fail.
 public virtual string[] AuthenticationMethods 
 {
 get { return new[] { "https://example.com/myauthenticationmethod1", "https://example.com/myauthenticationmethod2" }; }
 }
 
 /// Returns an array indicating which languages are supported by the provider. AD FS uses this information
 /// to determine the best language\locale to display to the user.
 public int[] AvailableLcids
 {
 get
 {
 return new[] { new CultureInfo("en-us").LCID, new CultureInfo("fr").LCID};
 }
 }
 
 /// Returns a Dictionary containg the set of localized friendy names of the provider, indexed by lcid. 
 /// These Friendly Names are displayed in the "choice page" offered to the user when there is more than 
 /// one secondary authentication provider available.
 public Dictionary<int, string> FriendlyNames
 {
 get
 {
 Dictionary<int, string> _friendlyNames = new Dictionary<int, string>();
 _friendlyNames.Add(new CultureInfo("en-us").LCID, "Friendly name of My Example MFA Adapter for end users (en)");
 _friendlyNames.Add(new CultureInfo("fr").LCID, "Friendly name translated to fr locale");
 return _friendlyNames;
 }
 }
 
 /// Returns a Dictionary containing the set of localized descriptions (hover over help) of the provider, indexed by lcid. 
 /// These descriptions are displayed in the "choice page" offered to the user when there is more than one 
 /// secondary authentication provider available.
 public Dictionary<int, string> Descriptions
 {
 get 
 {
 Dictionary<int, string> _descriptions = new Dictionary<int, string>();
 _descriptions.Add(new CultureInfo("en-us").LCID, "Description of My Example MFA Adapter for end users (en)");
 _descriptions.Add(new CultureInfo("fr").LCID, "Description translated to fr locale");
 return _descriptions; 
 }
 }
 
 /// Returns an array indicating the type of claim that that the adapter uses to identify the user being authenticated.
 /// Note that although the property is an array, only the first element is currently used.
 /// MUST BE ONE OF THE FOLLOWING
 /// "https://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"
 /// "https://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"
 /// "https://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
 /// "https://schemas.microsoft.com/ws/2008/06/identity/claims/primarysid"
 public string[] IdentityClaims
 {
 get { return new[] { "https://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn" }; }
 }
 
 //All external providers must return a value of "true" for this property.
 public bool RequiresIdentity
 {
 get { return true; }
 }
}

Next, the presentation form:

  class MyPresentationForm : IAdapterPresentationForm
 {
 /// Returns the HTML Form fragment that contains the adapter user interface. This data will be included in the web page that is presented
 /// to the cient.
 public string GetFormHtml(int lcid)
 {
 string htmlTemplate = Resources.FormPageHtml; //todo we will implement this
 return htmlTemplate;
 }
 
 /// Return any external resources, ie references to libraries etc., that should be included in 
 /// the HEAD section of the presentation form html. 
 public string GetFormPreRenderHtml(int lcid)
 {
 return null;
 }
 
 //returns the title string for the web page which presents the HTML form content to the end user
 public string GetPageTitle(int lcid)
 {
 return "MFA Adapter";
 }
 
 
 }
 

Note the ‘todo’ for the Resources.FormPageHtml element above.  We will get to that, but first let's add the final required return statements, based on our newly implemented types, to our initial MyAdapter class.  To do this, add the yellow highlighted items below to your existing IAuthenticationAdapter implementation:

 class MyAdapter : IAuthenticationAdapter
 {
 public IAuthenticationAdapterMetadata Metadata
 {
 //get { return new <instance of IAuthenticationAdapterMetadata derived class>; }
 get { return new MyMetadata(); }
 }
 
 public IAdapterPresentation BeginAuthentication(Claim identityClaim, HttpListenerRequest request, IAuthenticationContext authContext)
 {
 //return new instance of IAdapterPresentationForm derived class
 return new MyPresentationForm();
 }
 
 public bool IsAvailableForUser(Claim identityClaim, IAuthenticationContext authContext)
 {
 return true; //its all available for now
 }
 
 public void OnAuthenticationPipelineLoad(IAuthenticationMethodConfigData configData)
 {
 //this is where AD FS passes us the config data, if such data was supplied at registration of the adapter
 
 }
 
 public void OnAuthenticationPipelineUnload()
 {
 
 }
 
 public IAdapterPresentation OnError(HttpListenerRequest request, ExternalAuthenticationException ex)
 {
 //return new instance of IAdapterPresentationForm derived class
 return new MyPresentationForm();
 }
 
 public IAdapterPresentation TryEndAuthentication(IAuthenticationContext authContext, IProofData proofData, HttpListenerRequest request, out Claim[] outgoingClaims)
 {
 //return new instance of IAdapterPresentationForm derived class
 outgoingClaims = new Claim[0];
 return new MyPresentationForm();
 }
 
 }

Now for the resource file containing the html fragment. 

Create a new text file in your project folder with the following contents:

 <div id="loginArea">
 <form method="post" id="loginForm" >
 <!-- These inputs are required by the presentation framework. Do not modify or remove -->
 <input id="authMethod" type="hidden" name="AuthMethod" value="%AuthMethod%"/>
 <input id="context" type="hidden" name="Context" value="%Context%"/>
 <!-- End inputs are required by the presentation framework. -->
 <p id="pageIntroductionText">This content is provided by the MFA sample adapter. Challenge inputs should be presented below.</p>
 <label for="challengeQuestionInput" class="block">Question text</label>
 <input id="challengeQuestionInput" name="ChallengeQuestionAnswer" type="text" value="" class="text" placeholder="Answer placeholder" />
 <div id="submissionArea" class="submitMargin">
 <input id="submitButton" type="submit" name="Submit" value="Submit" onclick="return AuthPage.submitAnswer()"/>
 </div>
 </form>
 <div id="intro" class="groupMargin">
 <p id="supportEmail">Support information</p>
 </div>
 <script type="text/javascript" language="JavaScript">
 //<![CDATA[
 function AuthPage() { }
 AuthPage.submitAnswer = function () { return true; };
 //]]>
 </script></div>

Then, select Project->Add Component... Resources file and name the file Resources and click Add:

Then, within the Resources.resx file, choose Add Resource...Add existing file.  Navigate to the text file (containing the html fragment) that you saved above.   

 

Ensure your GetFormHtml code resolves the name of the new resource correctly by the resources file (.resx file) name prefix followed by the name of the resource itself:

 public string GetFormHtml(int lcid)
{
 string htmlTemplate = Resources.MfaFormHtml; //Resxfilename.resourcename
 return htmlTemplate;
}
 

You should now be able to build. 

Build the adapter

The adapter should be built into a strongly named .NET assembly that can be installed into the GAC in Windows.  To achieve this in a Visual Studio project:

  1. Right click your project name in Solution Explorer and click Properties
  2. On the Signing tab, check Sign the assembly and choose <New...> under Choose a strong name key file:   Enter a key file name and password and click OK.  Then ensure Sign the assembly is checked and Delay sign only is unchecked.  The properties Signing page should look like the below:

Then build the solution.

Deploy the adapter to your AD FS test machine

Before an external provider will be invoked by AD FS, it must be registered in the system.  Adapter providers MUST provide an installer which performs the necessary installation actions including installation in the GAC, and the installer SHOULD support registration in AD FS.  If that is not done, the administrator will need to execute the PowerShell steps below.  These steps can be used in the lab to enable testing and debugging.

Prepare the test AD FS machine

You’ll need to perform the following steps to get your test box ready

Copy files and add to GAC

1)      Ensure you have a Windows Server 2012 R2 machine or virtual machine

2)      Install the AD FS role service and configure a farm with at least one node.

For detailed steps to setup a federation server in a lab environment, see the guidance on Technet.

3)      Copy the Gacutil.exe tools to the server

Gacutil.exe can be found in %homedrive%\Program Files (x86)\Microsoft SDKs\Windows\v8.0A\bin\NETFX 4.0 Tools\ on a Windows 8 machine.  You will need the gacutil.exe file itself as well as the 1033 and en-US other localized resource folder below the NETFX 4.0 Tools location

4)      Copy your provider file(s) (one or more strong name signed dll files) to the same folder location as gacutil (the location is just for convenience)

5)      Add your dll file(s) to the GAC on each AD FS federation server in the farm:

Example: using command line tool GACutil.exe to add a dll to the GAC: 

C:\>.\gacutil.exe /if .\<yourdllname>.dll

To view the resulting entry in the GAC:

C:\>.\gacutil.exe /l <yourassemblyname>

Register your provider in AD FS

Once the above pre-requisites are met, open a PowerShell command window on your federation server and enter the following commands (note that if you are using federation server farm that uses Windows Internal Database, you must execute these commands on the primary federation server of the farm):

1)      Register-AdfsAuthenticationProvider –TypeName YourTypeName –Name “AnyNameYouWish” [–ConfigurationFilePath (optional)]

Where YourTypeName is your .NET strong type name: "YourDefaultNamespace.YourIAuthenticationAdapterImplementationClassName, YourAssemblyName, Version=YourAssemblyVersion, Culture=neutral, PublicKeyToken=YourPublicKeyTokenValue, processorArchitecture=MSIL"

This will register your external provider in AD FS, with the Name you provided as AnyNameYouWish above

2)      Restart the AD FS service (using the Windows Services snapin for example)

3)      Get-AdfsAuthenticationProvider

This shows your provider as one of the providers in the system

Example powershell command line:

PS C:\>$typeName = "MFAadapter.MyAdapter, MFAadapter, Version=1.0.0.0, Culture=neutral, PublicKeyToken=e675eb33c62805a0, processorArchitecture=MSIL”

PS C:\>Register-AdfsAuthenticationProvider -TypeName $typeName -Name “MyMFAAdapter”

PS C:\>net stop adfssrv

PS C:\>net start adfssrv

If you have the device registration service enabled in your AD FS environment, also execute the following: 

PS C:\>net start drs

To verify the registered provider, use the command below:

PS C:\>Get-AdfsAuthenticationProvider

This shows your provider as one of the providers in the system

Create AD FS policy that invokes your adapter

Create policy using the AD FS Management snapin

1)      Open the AD FS Management Snapin (from Server Manager Tools menu)

2)      Click Authentication Policies at left

3)      In the center pane, under Multi-Factor Authentication, click the blue Edit link to the right of Global Settings.

4)      Under Select additional authentication methods at the bottom of the page, check the box for your provider’s AdminName. Click Apply.

5)      To provide a “trigger” to invoke MFA using your adapter, under Locations check both “Extranet” and “Intranet”, for example. Click OK. (To configure triggers per relying party, see Create policy using PowerShell below.)

6)      Check the results using PSH

First use Get-AdfsGlobalAuthenticationPolicy. You should see your provider Name as one of the AdditionalAuthenticationProvider values

Then use Get-AdfsAdditionalAuthenticationRule. You should see the rules for Extranet and Intranet configured as a result of your policy selection in the admin UI

Create policy using PowerShell

To configure MFA policy using PowerShell, use the following steps:

First, enable the provider in global policy:

PS C:\>Set-AdfsGlobalAuthenticationPolicy -AdditionalAuthenticationProvider “YourAuthProviderName”

Note that the value provided for the AdditionalAuthenticationProvider parameter corresponds to the value you provided for the “Name” parameter in the Register-AdfsAuthenticationProvider cmdlet above and to the “Name” property from Get-AdfsAuthenticationProvider cmdlet output. 

Example powershell command to enable provider:

PS C:\>Set-AdfsGlobalAuthenticationPolicy –AdditionalAuthenticationProvider “MyMFAAdapter”

 

Next, configure global or relying party specific rules to trigger MFA:

Example 1: Powershell command to create global rule to require MFA for External requests:

PS C:\>Set-AdfsAdditionalAuthenticationRule –AdditionalAuthenticationRules 'c:[type == "https://schemas.microsoft.com/ws/2012/01/insidecorporatenetwork", value == "false"] => issue(type = "https://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod", value = "https://schemas.microsoft.com/claims/multipleauthn" );'

Example 2: Powershell command to create MFA rules to require MFA for external requests to a specific relying party.  (Note that individual providers cannot be connected to individual relying parties in AD FS in Windows Server 2012 R2).

PS C:\>$rp = Get-AdfsRelyingPartyTrust –Name <Relying Party Name>

PS C:\>Set-AdfsRelyingPartyTrust –TargetRelyingParty $rp –AdditionalAuthenticationRules 'c:[type == "https://schemas.microsoft.com/ws/2012/01/insidecorporatenetwork", value == "false"] => issue(type = "https://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod", value = "https://schemas.microsoft.com/claims/multipleauthn" );'

 

Authenticate with MFA using your adapter

Finally, perform the steps below to test your adapter:

  • Ensure the  AD FS global Primary authentication type is configured as Forms Authentication for both Extranet and Intranet (this makes your demo easier to authenticate as a specific user)
    • In the AD FS snapin, under Authentication Policies, in the Primary Authentication area, click Edit next to Global Settings
      • Or just click the Primary tab from the Multi-factor policy UI
  • Ensure Forms Authentication is the only option checked for both the Extranet and the Intranet authentication method.  Click OK.
  • Open the IDP initiated signon html page (https://<fsname>/adfs/ls/idpinitiatedsignon.htm) and sign in as a valid AD user in your test environment
  • Enter credentials for primary authentication
  • You should see the MFA forms page with example challenge questions appear. 
    • if you have more than one adapter configured, you will see the MFA choice page with your friendly name from above

 

 

You now have a working implementation of the interface (though granted it does not do much yet!), but you can at least see how the model works. For example, try setting break points in the BeginAuthentication as well as the TryEndAuthentication.  Notice how BeginAuthentication is executed when the user first enters the MFA form, whereas TryEndAuthentication is triggered at each Submit of the form.

But wait – our example adapter will never successfully authenticate!  This is because nothing in our code returns null for TryEndAuthentication.  So let’s add that in part 2...