Devil is in the (RedirectUri) detail

When using Azure Active Directory (AAD) as Identity Provider for your Azure App Services, you will set up App Registrations to tell AAD how to handle your app authentication.

One important bit of this is the ReplyURL (RedirectUri) that you need to specify for AAD to redirect the user back to your app after valid authentication.

The usual flow is:
  1. User requests your app URL (ie: https://myappservice.azurewebsites.net)
  2. User is redirected to the AAD Login page (https://login.microsoftonline.com/.../oauth2/authorize)
  3. User inserts valid credentials
  4. User is redirected back to your defined RedirectUri as a logged on User (https://myappservice.azurewebsites.net)



For this to happen, you will need to specify in your application as AppSettings in the web.config file:
 <add key="ida:PostLogoutRedirectUri“ value="https://myappservice.azurewebsites.net" />
<add key="ida:RedirectUri“ value="https://myappservice.azurewebsites.net" />

And in the Startup.Auth.cs file (here I am using a .NET MVC Web App and OpenID Connect Authentication):
app.UseOpenIdConnectAuthentication(

new OpenIdConnectAuthenticationOptions

{

RedirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"],

PostLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"],
Now, let’s assume that as a security requirement in your organization, your App Service must reside behind an F5 LoadBalancer, and all traffic must go through it, and that also Mutual Client Authentication must be in place between the F5 and your App Service.

For this scenario to occur, you need to set up a few parts in the Azure App Service and AAD (the F5 setup and DNS entries are out of scope for this post):

  •      Enable Client Certificates in the Resource Explorer of the App Service: "clientCertEnabled": true (this will make sure that the App Service expects a SSL Certificate for each request).
  •      Define a Custom Domain on the App Service as specified in the SSL Client Certificate, ie: https://myappdomain.corporateurl.com
  •    Upload the valid SSL Client Certificate to the App Service, and create a SSL binding to the Custom Domain
  •      Define a Custom Domain on the App Service as the F5 public endpoint for this web app, ie: https://myappsf5domain.corporateurl.com
  •       Add the new App Service Custom Domains URLs to the AAD App Registration as ReplyURLs: https://myappdomain.corporateurl.com and https://myappsf5domain.corporateurl.com
  •         Implement custom Certificate Validation code inheriting from System.Web.Mvc.IAuthorizeFilter and FilterAttribute (ie: public class ClientCertificateValidatorFilter : FilterAttribute, IAuthorizationFilter)
  •         Add the corresponding [CustomAuthorizeAttribute] to the desired Controller classes (or Actions) (ie: [ClientCertificateValidatorFilter])
  •         Since the security requirement is that all traffic must go through the F5, specify the F5 URL as RedirectUri and PostLogoutRedirectUri in the web.config file:

<add key="ida:PostLogoutRedirectUri“ value="https://myappsf5domain.corporateurl.com" />
<add key="ida:RedirectUri“ value="https://myappsf5domain.corporateurl.com" />

So now the “Happy Path” flow is:
  1. User requests your app URL to the F5 (https://myappsf5domain.corporateurl.com) 
  2. Since the F5 provides the SSL Certificate in the HTTP Header, no popup shows in the browser
  3. User is redirected to the AAD Login page (https://login.microsoftonline.com/.../oauth2/authorize)
  4. User inserts valid credentials
  5. User is redirected back to your app as a logged on User, going again through the F5 (https://myappsf5domain.corporateurl.com)
  6. The Certificate validation code kicks in and validates the right SSL Certificate provided by the F5 (keep in mind that the Authorize filters will execute only AFTER authentication, hence User login)


So far so good, right?

Now, let’s test some less happy flow.

We know that all traffic must go through the F5, so let’s try to call the Azure URL (https://myappservice.azurewebsites.net) directly.
  1. User requests the Azure URL (https://myappservice.azurewebsites.net)
  2. User is prompted for a SSL Certificate
  3. User cannot provide a SSL Certificate, so hits Cancel on the certificate popup
  4. User receives a 403 – Forbidden response (correctly)

Again, so far so good.

But what happens if the User provides the wrong SSL Certificate, instead of canceling the popup?
  1. User requests the Azure URL (https://myappservice.azurewebsites.net)
  2. User is prompted for a SSL Certificate
  3. User provides any (wrong) SSL Certificate
  4. User is redirected to the AAD Login page (https://login.microsoftonline.com/.../oauth2/authorize)
  5. User inserts valid credentials
  6. User is redirected back to your app as a logged on User (incorrectly)
  7. The Certificate validation code kicks in and validates the right SSL Certificate provided by the F5

A few things will go wrong in this scenario: 
  • Since the defined RedirectUri is the F5 URL, now the User is redirected to this endpoint (https://myappsf5domain.corporateurl.com), however the request was coming from the Azure URL (https://myappservice.azurewebsites.net), so it will result in a AuthenticationFailed error, and the User will land on the Error page of your app, showing the message: “IDX10311: RequireNonce is 'true' (default) but validationContext.Nonce is null. A nonce cannot be validated. If you don't need to check the nonce, set OpenIdConnectProtocolValidator.RequireNonce to 'false'.” 
  • Even though the User provided a wrong SSL Certificate, since now he’s going through the F5 after authenticating, the right SSL Certificate is provided and the validation succeed.
Let leave aside the fact that Client Certificates can only be validated AFTER User authentication (login), which IMHO is a big security design flaw.. but anyways.

So, after several hours spent on the phone with Microsoft Support (thankfully to the Premier level of support, this was possible in the first place), the solution to this scenario has been identified in a small code snippet to be added to the OpenIdAuthenticationOptions in Startup.Auth.cs.

Within the Notifications = new OpenIdConnectAuthenticationNotifications section, let’s add the following code:
RedirectToIdentityProvider = async n => {
n.ProtocolMessage.RedirectUri = "https://" + n.OwinContext.Request.Uri.Host + "/";
       n.ProtocolMessage.PostLogoutRedirectUri = "https://" + n.OwinContext.Request.Uri.Host + "/";
},
},


What this code does, it is simply to force the RedirectUri to whatever URL the request was coming from originally, no matter what has been defined in the AppSettings (either in the web.config file, or in some Application Setting in one of the many Deployment Slots that your app might have… and good luck here).


So now the flow in this scenario becomes: 
  1. User requests the Azure URL (https://myappservice.azurewebsites.net)
  2. User is prompted for a SSL Certificate
  3. User provides any (wrong) SSL Certificate
  4. User is redirected to the AAD Login page (https://login.microsoftonline.com/.../oauth2/authorize)
  5. User inserts valid credentials
  6. User is redirected back to your app on the specific requested URL (https://myappservice.azurewebsites.net)
  7. The Certificate validation code kicks in and validates the wrong SSL Certificate provided by the User, and returns a 403 – Forbidden response
  8. User receives a 403 – Forbidden response (correctly)


Phew, that was easy, wasn’t it?
(:

Now there’s only one last bit remaining: remember we said that all traffic must go through the F5 load balancer?

So what happens now if the User can somehow provide a valid SSL Certificate, and also figures out the Azure URL and calls it directly?

Surprise surprise… He will log onto the web app bypassing altogether the F5!

So for this you will need to implement IP Whitelisting, and so allow ONLY the F5 incoming traffic to the App Service.

Here is a sort of cheat-sheet of the parts needed in this post:

Comments

Popular posts from this blog

Cloud Computing using Microsoft Azure for Dummies

RabbitMQ on Kubernetes Container Cluster in Azure

PowerHell