ADFS: Restricting Client Access for Office 365

In general the documentation I find on TechNet is fairly spot on.  Usually you will need to adapt it to your environment, but the information is correct.  Then there are times when the technology changes in some way, and the documentation falls behind.  Recently while trying to configure ADFS claim rules for a client I ran into the latter.  Here's what is wrong with the article and how you can work around it.

The article in question is this one regarding the third scenario, where you are trying to restrict external client access to browser-based applications only.  Before we jump into the errors in the document, first we have to talk a little bit about the seconario.

Let's say that you are preparing to move to Office 365, but you would like to control what your users can access when they are outside of the company network.  Assuming that you have ADFS and SSO as part of your configuration, Microsoft provides this ability through the claim rules on the ADFS server.

When a user wants to access an application in Office 365, they are redirected to the ADFS server to get a token.  Some applications, specifically browser-based ones, force you to go and retrieve the token yourself using a redirect.  In this case, Microsoft terms the application request Passive, meaning that the application is forcing the client to perform the authentication.  If the application takes the users credentials and does the authentication for them, then it is called an Active request.  Confusing I know, and this article explains it pretty well.  The practical upshot is that Passive requests will have "/adfs/ls" in the value of the "x-ms-endpoint-absolute-path" property and Active requests will have "/adfs/services/trust/2005/usernamemixed" instead.  Outlook and other thick clients use Active requests, and most browser-based and mobile applications use Passive requests.

So when you filter claims based on the x-ms-endpoint-absolute-path property, you're filtering on whether an application is using Active or Passive requests, and not necessarily blocking all client apps.  The document on TechNet is a little misleading in this regard, so it's good to point it out.  If you're really looking to lock down access for mobile applications, I would recommend looking into Intune and the EMS suite.

The second part of filtering requests is to deny claims that are coming from an external network.  There are two properties in question here.  The first is insidecorporatenetwork, which is set to true if the request did not go through the web application proxy (WAP) to get to the ADFS server.  The second is the x-ms-forwarded-client-ip, which will be the IP address of any proxy that the request traveled through.  Typically this includes your internal proxy server and internet gateway.  When an active application, like Exchange Online requests a token for you it includes the IP addresses of the client request.  The request has to go through your WAP since it is originating from the Exchange Online servers, so the insidecorporatenetwork property will be false.  Therefore, you need to check the x-ms-forwarded-client-ip value to make sure the request originated from a client inside the corporate network.  And here is where Microsoft's documentation is just plain wrong.  Microsoft explicitly states that:
IP addresses related to Exchange Online infrastructure will not be present in the list.
And that is blatantly not true.  Here are the caller identity fields as seen in the Security logs for ADFS audit.

http://schemas.microsoft.com/2012/01/requestcontext/claims/x-ms-forwarded-client-ip
10.xxx.xxx.32,132.245.34.180
http://schemas.microsoft.com/2012/01/requestcontext/claims/x-ms-forwarded-client-ip
10.xxx.xxx.32
http://schemas.microsoft.com/2012/01/requestcontext/claims/x-ms-forwarded-client-ip
132.245.34.180 

I've scrubbed the external IP addresses that belong to me.  But that 132.245.34.180 address belongs to Microsoft.  So yes, it IS in the x-ms-forwarded-client-ip values.  Additionally, ADFS splits out those values into separate claims.  Why does this matter?  Because Microsoft wants you to use this claim rule to filter out requests that aren't coming from your internal network:
c1:[Type == "http://schemas.microsoft.com/ws/2012/01/insidecorporatenetwork",
Value == "false"] &&
c2:[Type == "http://schemas.microsoft.com/2012/01/requestcontext/claims/x-ms-forwarded-client-ip",
Value =~ "^(?!192\.168\.1\.77|10\.83\.118\.23)"]
=> issue(Type = "http://custom/ipoutsiderange",
Value = "true");
In the c2 part of the claim, the regular expression looks for x-ms-forwarded-client-ip values that do not contain your custom range.  And this claim will evaluate as true every time, because your custom range will not include the Microsoft datacenter IP addresses.  Even if you are on your internal network, you Outlook client will not be able to authenticate because the ADFS claim will be denied.

What should you use instead of the above claim rule?  Glad you asked.  Try this one instead:
c1:[Type == "http://schemas.microsoft.com/ws/2012/01/insidecorporatenetwork",
Value == "false"] &&
c2:[Type == "http://schemas.microsoft.com/2012/01/requestcontext/claims/x-ms-forwarded-client-ip",
Value =~ "^(\b192\.168\.1\.77|10\.83\.118\.23\b)"]
=> issue(Type = "http://custom/ipinrange", Value = "true");
The ?! is gone, and I've added \b word delimiters.  Now the claim rule is checking to make sure that one of your custom IP ranges exists in at least one of the x-ms-forwarded-client-ip values.  Then the custom property ipinrange is set to true.  In the next claim rule, we filter out passive requests and if the ipinrange isn't set to true then the claim is denied.
c1:[Type == "http://custom/ipinrange", Value != "true"] &&
c2:[Type == "http://schemas.microsoft.com/2012/01/requestcontext/claims/x-ms-endpoint-absolute-path", Value != "/adfs/ls/"]
=> issue(Type = "http://schemas.microsoft.com/authorization/claims/deny", Value = "DenyUsersWithClaim");
I don't know when the claims process changed on the Microsoft side to get out of line with their documentation, but now you've got a solid way of doing it.  You still need to create a custom IP address range regular expression, but I believe in you and your Google-Fu.

Labels: , ,