ADFS & RelayState
Active Directory Federation Services (ADFS) has been around for some time now, and many organizations use it to provide single sign-on capabilities to Office 365 without giving it a second glance, but ADFS is really a generic identity provider that can work with other Security Assertion Markup Language (SAML) 2.0 endpoints. The incredibly over-simplified gist of SAML is that some identity provider (ADFS + Active Directory) authenticates a user, hands them a token, and the user takes that token to log in to other web applications such as Office 365, Salesforce, Workday, Jobvite, or any SAML 2.0 compliant service. The benefit here is organizations have a single point of authentication in ADFS, but can use those credentials to access multiple services that they don’t necessarily host or deploy.
Often times these services will email a user with a hyperlink embedded in the message that links to a specific page or “deep link” on the service. If a user clicks on that link, it should take them directly to the referenced opportunity or page. The issue with the initial release of ADFS was that this feature did not work. The users could click one of these links and get logged in to the service, but they would always end up on the home or main page – not the link they clicked on. The reason for this is that ADFS did not support the RelayState parameter, which actually contains that end state or desired URL after login occurs. Update Rollup 2 for ADFS adds this functionality and can be downloaded from here: http://support.microsoft.com/kb/2681584 .
However, this does not mean that RelayState will begin working automagically. There are some requirements about how the URL is formatted in order for ADFS to properly consume the RelayState and send the user to the link after authentication. Those guidelines are laid out here, and includes the fact that part of it must already be URL encoded: http://technet.microsoft.com/en-us/library/jj127245(v=ws.10).aspx
So the options are to have the service provider deliver deep links in the format ADFS requires (unlikely to happen), or modify the links to the correct format. Fortunately, we can use the IIS URL Rewrite Module to match and manipulate patterns based on regular expressions (your Lync voice administrators should be very familiar with this concept.) Install the URL Rewrite module on your ADFS servers from here: http://www.iis.net/downloads/microsoft/url-rewrite
Let’s work through an example that uses the Jobvite service. I apologize for the color barf throughout the rest of the post, but I hope it highlights how everything lines up. A deep link that a user receives in an email from Jobvite resembles the following format:
https://www.jobvite.com/m/default.aspx?3EGNofwx
When a user clicks that link, Jobvite knows the tenant is enabled for SSO, and provides a redirect URL to the browser where the user should authenticate. It’s the equivalent of saying “I don’t authenticate you, but this URL (ADFS) can. Come back when you have a token that says you’ve authenticated.” In Jobvite’s case, that URL includes an identifier for ADFS to match a relying party trust (found in the loginToRp parameter), and two RelayState parameters – RelayState and Target. The values in RelayState and Target are identical, so I can only assume Jobvite is hoping the SSO provider will consume one of these. Unfortunately, ADFS doesn’t like either! This is the URL a user is redirected to by Jobvite:
https://sso.confusedamused.com/adfs/ls/idpinitiatedsignon.aspx?loginToRp=http://jobvite.com/saml&RelayState=/m/default.aspx?3EGNofwx&target=/m/default.aspx?3EGNofwx
Now the user’s browser flips to the ADFS link and logs in. Afterwards, the user is dumped back in to the main Jobvite page because the RelayState parameter didn’t match ADFS’s expected format. There are no errors, and a user is now logged in to Jobvite, but they didn’t end up on the URL they clicked in the email. It’s more of an annoyance than a major issue, but it can be fixed. The format ADFS wants to see looks like this and the part in italics must be URL encoded.
https://sso.confusedamused.com/adfs/ls/idpinitiatedsignon.aspx?RelayState=RPID=[Relying Party ID]&RelayState=[Relay State]
Covering how regular expressions or the rewrite module works is outside the scope of this article, but to be quick:
1. We match a specific URL and pattern received by the IIS server.
2. We then “rewrite” or modify that URL before it is actually processed and delivered to the ADFS application.
Start by opening the URL Rewrite module and click “Add Rule(s).” Select “Blank rule” and click OK.
First we need to set a match URL. The URL is in this window is really just the FQDN part of the URL, so in this example it would be sso.confusedamused.com. I’ve simply entered .* as the match URL pattern to allow any FQDN that points to this server to be accepted. I could use DNS to reach this server via sso.confusedamused.com, adfs.confusedamused.com, yourmomgoestocollege.confusedamused.com, or anything else and it would still match.
Next, we need a pattern to match. Expand the Conditions section and press “Add”. The condition input we’ll check is the {REQUEST_URI}, which refers to everything after the FQDN we just checked. The pattern we’ll use looks like this.
^(.*)/adfs/ls/idpinitiatedsignon.aspx\?loginToRp=(.*)&RelayState=(.*)&(.*)$
In English, the pattern means “Must start with”, “any string” (that’s the .* part), followed by the exact text “/adfs/ls/idpinitiatedsignon.aspx\?” (we have to escape the “?” with a “\”), followed by loginToRp=”, capture the next “any string” (because this one is in parentheses it means we’re capturing this data to re-use in the rewrite later), followed by the exact string “&RelayState=”, capture the next string, followed by the exact string “&”, capture the next string, and finally, “must end with”. Technically, we don’t need to capture that last string because it just contains the target=/m/default.aspx?3EGNofwx part, which we already know is identical to the RelayState parameter passed right before it. But you get the idea.
For reference, here is the URL again that matches the above pattern. The colors should line up.
https://sso.confusedamused.com/adfs/ls/idpinitiatedsignon.aspx?loginToRp=http://jobvite.com/saml&RelayState=/m/default.aspx?3EGNofwx&target=/m/default.aspx?3EGNofwx
In the Action section the type we’ll select is rewrite. Remember how we “captured” some parts of text in the pattern previously? Those line up to variables that we can now reuse and reference in the rewrite pattern in the format {C:[Variable Number]}. So we have {C:1} that matches the first string we captured, and {C:2} matches the second one, etc. {C:1} actually refers to the very beginning of our pattern (the first (.*)), so the text we’re interested in starts at {C:2} which is the Relying Party ID, and includes {C:3}, our RelayState link.
Just to refresh, the end-state we need to get to is this, and we have to URL encode everything after the first RelayState=:
https://sso.confusedamused.com/adfs/ls/idpinitiatedsignon.aspx?RelayState=RPID=[Relying Party ID]&RelayState=[Relay State]
The URL Rewrite module actually includes a function to URL Encode a section of text so the rewrite to use will look like this:
adfs/ls/idpinitiatedsignon.aspx?RelayState={UrlEncode:RPID={C:2}&RelayState={C:3}}
After that, simply select “Stop processing of subsequent rules” and uncheck the box “Append query string.” The end result is that the original link returned by Jobvite that ADFS doesn’t recognize…
https://sso.confusedamused.com/adfs/ls/idpinitiatedsignon.aspx?loginToRp=http://jobvite.com/saml&RelayState=/m/default.aspx?3EGNofwx&target=/m/default.aspx?3EGNofwx
… will be transformed into this URL before it hits ADFS. The user experience is now that a deep link works as expected. A user that is not logged in to Jobvite can click a URL from an email, be seamlessly authenticated via ADFS, and end up on the URL they clicked on.
https://sso.confusedamused.com/adfs/ls/idpinitiatedsignon.aspx?RelayState=RPID%3Dhttp%3A%2F%2Fjobvite.com%2Fsaml%26RelayState%3D%2Fm%2Fdefault.aspx%3F3EGNofwx
To recap – the process goes:
1. User clicks link in email: https://www.jobvite.com/m/default.aspx?3EGNofwx
2. Jobvite redirects user to this link: https://sso.confusedamused.com/adfs/ls/idpinitiatedsignon.aspx?loginToRp=http://jobvite.com/saml&RelayState=/m/default.aspx?3EGNofwx&target=/m/default.aspx?3EGNofwx
3. URL Rewrite module matches the redirect link pattern and modifies the link to the following before delivering to ADFS: https://sso.confusedamused.com/adfs/ls/idpinitiatedsignon.aspx?RelayState=RPID%3Dhttp%3A%2F%2Fjobvite.com%2Fsaml%26RelayState%3D%2Fm%2Fdefault.aspx%3F3EGNofwx
You can create as many rewrite rules as necessary in case different service providers each send unique formats with the RelayState parameter.