Recently, I wrote a blog series about how to configure ADFS as the Identity Provider for Adxstudio Portal. The links to the series can be found at the end of this post. In this article, I will describe how we can configure ADFS to authenticate users in an external LDAP v3-Compliant directory. Let me explain this in another way.
Scenario
Let’s say you are an employee of a large organisation. Your organisation has multiple divisions with different active directory domains, possibly because of acquisitions/mergers or for security reasons. In your division (let’s call it the Division X), has number of applications that is used by internal staff in Division X. Some of the applications are Dynamics 365 (CRM) and Adxstudio Portal. Division Y is interested in you applications and would like to use Adxstudio Portal. You don’t want to add all employees of Division Y in to your Division X Active Directory. You want them to use their own AD credentials from Division Y active directory to authenticate.
Solution
In order for AD FS to authenticate users from an LDAP directory, you must connect this LDAP directory to your AD FS farm by creating a local claims provider trust. A local claims provider trust is a trust object that represents an LDAP directory in your AD FS farm.
Using ADFS 4.0, we can quickly create local claims provider trust (after reading this article of course :)).
Note: Before you configure ADFS, make sure you have a username and password of a service account which has access to the external LDAP directory. Also make sure you have physical connection between the two ADFS farms.
Configure AD FS to authenticate users stored in LDAP directories
You can configure this by running the below PowerShell script. For detailed description, please refer to this article.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
$Credential = Get-credential $ldap_dir=New-AdfsLdapServerConnection -hostname SERVER001.dyn365apps.internal -port 636 -sslmode Ssl -AuthenticationMethod basic -Credential $Credential $email=New-AdfsLdapAttributeToClaimMapping -LdapAttribute email -ClaimType "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email" $givenName = New-AdfsLdapAttributeToClaimMapping -LdapAttribute givenName -ClaimType "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname" $surname = New-AdfsLdapAttributeToClaimMapping -LdapAttribute sn -ClaimType "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" $WindowsAccount = New-AdfsLdapAttributeToClaimMapping -LdapAttribute sAMAccountName "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname" $upn= New-AdfsLdapAttributeToClaimMapping -LdapAttribute upn -ClaimType "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn" Add-AdfsLocalClaimsProviderTrust -Name "Div Y AD" -Identifier "urn:dyn365apps" -type ldap ` -ldapserverconnection $ldap_dir ` -UserObjectClass organizationalPerson -UserContainer "DC=dyn365apps,DC=internal" ` -LdapAuthenticationMethod basic ` -AnchorClaimLdapAttribute sAMAccountName ` -AnchorClaimType "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname" ` -AcceptanceTransformRules "c:[] => issue(claim=c);" -Enabled $true ` -LdapAttributeToClaimMapping @($email, $givenName, $surname, $upn, $WindowsAccount) ` |
Username Mapping
One of the key requirements was the ability to use the username field as username without the domain. For example, if the DOMAIN\USERNAME was DIVY\nadeeja, we wanted to simply use nadeeja as the username without DIVY. To achieve this we changed two attributes.
- -AnchorClaimLdapAttribute
- -AnchorClaimType
1 2 |
-AnchorClaimLdapAttribute sAMAccountName ` -AnchorClaimType "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname" ` |
-AnchorClaimLdapAttribute was set to sAMAccountName and
-AnchorClaimType was set to “http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname“
Once you run this command, you’ll get a second option in ADFS login screen.
Gotcha 1
Once of the things I was stuck on was I was getting username not found errors. The active directory path of the external directory was “OU=Staff,DC=dyn365apps,DC=internal”. All staff accounts were in Staff OU. When we configured this path as the -UserContainer, I was getting errors. Then I went to a level higher and included “DC=dyn365apps,DC=internal” as the active directory path and it worked!
Gotcha 2
ADFS Login page included some client side validation. For example, it checks if the username in DOMAIN\USERNAME or USERNAME@FQDN format. Our requirement was to login using only Username. This created a problem because the client side validation code was rejecting our Username. Luckily, ADFS provides a method to update the scripts on the ADFS login page using PowerShell scripts. More details can be found here.
Steps:
- Export the current ADFS Web Theme
- Create a new ADFS Web Theme based on the current Web Theme
- Make the required changes to the onload.js file
- Add the onload.js file to the Web Theme
- Set the new Web Theme as the active Web Theme
1 2 3 4 5 6 7 8 |
Export-AdfsWebTheme -Name CURRENTTHEME -DirectoryPath C:\ADFS\DYN365APPSTHEME New-AdfsWebTheme -Name "DYN365APPSTHEME" -StyleSheet @{Path="C:\ADFS\Theme\css\style.css"} -RTLStyleSheetPath C:\ADFS\Theme\css\style.rtl.css Set-AdfsWebTheme -TargetName DYN365APPSTHEME -Illustration @{Path="C:\ADFS\Theme\illustration\illustration.jpg"} Set-AdfsWebTheme -TargetName DYN365APPSTHEME -Logo @{Path="C:\ADFS\Theme\logo\logo.png"} Set-AdfsWebTheme -TargetName DYN365APPSTHEME -AdditionalFileResource @{Uri="/adfs/portal/script/onload.js";Path="C:\ADFS\Theme\script\onload.js"} Set-AdfsWebConfig -ActiveThemeName DYN365APPSTHEME Get-AdfsWebTheme -Name DYN365APPSTHEME |
Changes in onload.js
Rewire form validation by changing the Login.submitLoginRequest function which doesn’t enforce domain name in username field.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// rewire form validation function override_form_validation() { Login.submitLoginRequest = function () { var u = new InputUtil(); var e = new LoginErrors(); var userName = document.getElementById(Login.userNameInput); var password = document.getElementById(Login.passwordInput); if (!userName.value) { u.setError(userName, "Username must be supplied"); return false; } if (!password.value) { u.setError(password, e.passwordEmpty); return false; } document.forms['loginForm'].submit(); return false; }; } |
Add following event listeners to make sure the above code runs on page load.
1 2 3 4 5 6 7 8 9 10 |
if (window.addEventListener) { window.addEventListener('resize', computeLoadIllustration, false); window.addEventListener('load', computeLoadIllustration, false); window.addEventListener('load', override_form_validation, false); } else if (window.attachEvent) { window.attachEvent('onresize', computeLoadIllustration); window.attachEvent('onload', computeLoadIllustration); window.attachEvent('onload', override_form_validation); } |
Complete Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
// Copyright (c) Microsoft Corporation. All rights reserved. // This file contains several workarounds on inconsistent browser behaviors that administrators may customize. "use strict"; // iPhone email friendly keyboard does not include "\" key, use regular keyboard instead. // Note change input type does not work on all versions of all browsers. if (navigator.userAgent.match(/iPhone/i) != null) { var emails = document.querySelectorAll("input[type='email']"); if (emails) { for (var i = 0; i < emails.length; i++) { emails[i].type = 'text'; } } } // In the CSS file we set the ms-viewport to be consistent with the device dimensions, // which is necessary for correct functionality of immersive IE. // However, for Windows 8 phone we need to reset the ms-viewport's dimension to its original // values (auto), otherwise the viewport dimensions will be wrong for Windows 8 phone. // Windows 8 phone has agent string 'IEMobile 10.0' if (navigator.userAgent.match(/IEMobile\/10\.0/)) { var msViewportStyle = document.createElement("style"); msViewportStyle.appendChild( document.createTextNode( "@-ms-viewport{width:auto!important}" ) ); msViewportStyle.appendChild( document.createTextNode( "@-ms-viewport{height:auto!important}" ) ); document.getElementsByTagName("head")[0].appendChild(msViewportStyle); } // If the innerWidth is defined, use it as the viewport width. if (window.innerWidth && window.outerWidth && window.innerWidth !== window.outerWidth) { var viewport = document.querySelector("meta[name=viewport]"); viewport.setAttribute('content', 'width=' + window.innerWidth + ', initial-scale=1.0, user-scalable=1'); } // Gets the current style of a specific property for a specific element. function getStyle(element, styleProp) { var propStyle = null; if (element && element.currentStyle) { propStyle = element.currentStyle[styleProp]; } else if (element && window.getComputedStyle) { propStyle = document.defaultView.getComputedStyle(element, null).getPropertyValue(styleProp); } return propStyle; } // The script below is used for downloading the illustration image // only when the branding is displaying. This script work together // with the code in PageBase.cs that sets the html inline style // containing the class 'illustrationClass' with the background image. var computeLoadIllustration = function () { var branding = document.getElementById("branding"); var brandingDisplay = getStyle(branding, "display"); var brandingWrapperDisplay = getStyle(document.getElementById("brandingWrapper"), "display"); if (brandingDisplay && brandingDisplay !== "none" && brandingWrapperDisplay && brandingWrapperDisplay !== "none") { var newClass = "illustrationClass"; if (branding.classList && branding.classList.add) { branding.classList.add(newClass); } else if (branding.className !== undefined) { branding.className += " " + newClass; } if (window.removeEventListener) { window.removeEventListener('load', computeLoadIllustration, false); window.removeEventListener('resize', computeLoadIllustration, false); } else if (window.detachEvent) { window.detachEvent('onload', computeLoadIllustration); window.detachEvent('onresize', computeLoadIllustration); } } }; if (window.addEventListener) { window.addEventListener('resize', computeLoadIllustration, false); window.addEventListener('load', computeLoadIllustration, false); window.addEventListener('load', override_form_validation, false); } else if (window.attachEvent) { window.attachEvent('onresize', computeLoadIllustration); window.attachEvent('onload', computeLoadIllustration); window.attachEvent('onload', override_form_validation); } // Function to change illustration image. Usage example below. function SetIllustrationImage(imageUri) { var illustrationImageClass = '.illustrationClass {background-image:url(' + imageUri + ');}'; var css = document.createElement('style'); css.type = 'text/css'; if (css.styleSheet) css.styleSheet.cssText = illustrationImageClass; else css.appendChild(document.createTextNode(illustrationImageClass)); document.getElementsByTagName("head")[0].appendChild(css); } // rewire form validation function override_form_validation() { Login.submitLoginRequest = function () { var u = new InputUtil(); var e = new LoginErrors(); var userName = document.getElementById(Login.userNameInput); var password = document.getElementById(Login.passwordInput); if (!userName.value) { u.setError(userName, "Username must be supplied"); return false; } if (!password.value) { u.setError(password, e.passwordEmpty); return false; } document.forms['loginForm'].submit(); return false; }; } |
Conclusion
You can easily configure ADFS to allow external LDAP v3-complaint directory to authenticate users by running a PowerShell Script. Pay attention to active directory path of the AD users and use one level up. ADFS Login page can be customised including the onload.js JavaScript file to add/remove validation rules. Do not directly change the existing ADFS Web Theme. Always create a new theme based on an existing theme and activate the new theme.
Thank you for visiting Dyn365Apps.com.
Follow me on Twitter to get the latest news, tips and tricks and more …
Until next time…
About the Author
Nadeeja Bomiriya is a Microsoft MVP, Technical Architect, and Microsoft Solutions Delivery Lead who lives in Melbourne, Australia.
Related Articles
[Integration] ADFS as the Identity Provider for Adxstudio – Part 1 – Overview
[Integration] ADFS as the Identity Provider for Adxstudio – Part 2 – Configure ADFS Server
[Integration] ADFS as the Identity Provider for Adxstudio – Part 3 – Configure Relying Party Trust
[Integration] ADFS as the Identity Provider for Adxstudio – Part 4 – Configure Web Application Proxy
[Integration] ADFS as the Identity Provider for Adxstudio – Part 6 – Configure Adxstudio
References
https://technet.microsoft.com/en-us/itpro/powershell/windows/adfs/new-adfswebtheme