Developing locally using Azure's Authentication Gateway

Securing a web site in Azure got pretty easy recently with the release of App Service Authentication/Authorization. Mobile App development has had this luxury for a while but now it’s being supported for web apps.

If you’re familiar with the service and just looking for a way to do your authenticated testing locally, skip below for the details.

Express Setup

Microsoft has made it incredibly easy to set this up via the portal/dashboard. Once you have a web app up and running, you’ll find an Authentication/Authorization blade under the Features group in settings. Turning it on is pretty easy - you just need to flip that switch to On

After that you can follow the instructions for setting up any providers you want your app to support.

How it works

Once you have the service on and providers hooked up, Azure will have a gateway setup for you under your website root. Let’s say you wanted to login with Google, you could create a link on your site in the following format:

https://<mysite>.azurewebsites.net/.auth/login/google

Want to redirect to a specific url, such as “/Dashboard”?

https://<mysite>.azurewebsites.net/.auth/login/google?post_login_redirect_url=/Dashboard

Want to see what’s in your auth token?

https://<mysite>.azurewebsites.net/.auth/me

Want to logout?

https://<mysite>.azurewebsites.net/.auth/logout

What about redirecting after logout?

https://<mysite>.azurewebsites.net/.auth/logout?post_logout_redirect_uri=/Dashboard

Local Development

At this point you were probably as happy as I was about how easy this was to setup. Everything works great up in Azure - but then it hits you, how the hell am I supposed to test this out locally? Sprawling through the comments on the blog articles I noticed I wasn’t the only one.

The first thought that ocurred to me was “Man, this cool, but pretty useless.” You have to be able to test authenticated and unauthenticated interactions with your application. Frankly, there just isn’t a way to use the Authentication/Authorization service locally.

Which puts you in a spot with a couple options:

  1. Don’t use the service and roll your own integration - MVC apps ship with the code to help you with this
  2. The route I took - simulate a user on-demand

First, where do we set the user?

The biggest issue we have to work with is HttpContext.Current.User. The trouble is that this value gets set for us under normal conditions. The good news is we can set it - but it’s just a matter of knowing where.

Let’s start here: Global.asax

public class MyApplication : System.Web.HttpApplication 
{
	...
}

What order do the events get called?

  1. Validate the request, which examines the information sent by the browser and determines whether it contains potentially malicious markup. For more information, see ValidateRequest and Script Exploits Overview.
  2. Perform URL mapping, if any URLs have been configured in the UrlMappingsSection section of the Web.config file.
  3. Raise the BeginRequest event.
  4. Raise the AuthenticateRequest event.
  5. Raise the PostAuthenticateRequest event.
  6. Raise the AuthorizeRequest event.
  7. Raise the PostAuthorizeRequest event. ….

What is expected of the AuthenticateRequest event?

The AuthenticateRequest event signals that the configured authentication mechanism has authenticated the current request. Subscribing to the AuthenticateRequest event ensures that the request will be authenticated before processing the attached module or event handler.

Simulating a User

The next question is, what exactly do we need to set to HttpContext.Current.User? Well, since the Authentication/Authorization service confirms to OAuth 2.0, as expected, we’ll be getting a Bearer Token.

What this translates to is that we’ll be getting a ClaimsPrincipal when we hit up HttpContext.Current.User.

Global.asax

public class MyApplication : System.Web.HttpApplication 
{
	protected void Application_AuthenticateRequest() {
        var ident = new ClaimsIdentity(new List<Claim>() {
	    	new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
	    				, "johndoe@mailinator.com"),

	        new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"
	        			, DateTime.Now.Ticks.ToString()),

	        new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
	        			, "johndoe@mailinator.com"),

	        new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"
	        			, "John"),

	        new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"
	        			, "Doe")
        });

        var user = new ClaimsPrincipal(ident);

        HttpContext.Current.User = user;
        Thread.CurrentPrincipal = user;
    }
}

Not Quite There Yet

If you run the code above, you’ll notice we still don’t show as having an authenticated user session. Let’s see what ClaimsIdentity does to determine if the user is authenticated.

[OptionalField(VersionAdded = 2)]
private string m_authenticationType;
...
public virtual bool IsAuthenticated
{
  get
  {
    return !string.IsNullOrEmpty(this.m_authenticationType);
  }
}
...

Well, that’s going to be tricky so, why don’t we just take advantage of that virtual modifier.

public class TestIdentity : ClaimsIdentity
{
    public override bool IsAuthenticated => true;

    public TestIdentity() : base(TestClaims) { }

    public static IEnumerable<Claim> TestClaims => new List<Claim>()
    {
        new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
    				, "johndoe@mailinator.com"),

        new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"
        			, DateTime.Now.Ticks.ToString()),

        new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
        			, "johndoe@mailinator.com"),

        new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"
        			, "John"),

        new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"
        			, "Doe")
    };
}

Couple Steps Further

At this point we should have core of the issue resolved. However, to make it function better locally, what I like to do is take it a little further.

Global.asax

internal static IPrincipal DebugUser { get; set; }

protected void Application_AuthenticateRequest()
{
    if (!ConfigurationManager.AppSettings["EnableDebugUser"] == "true")
        return;

    HttpContext.Current.User = DebugUser;
    Thread.CurrentPrincipal = DebugUser;
}

AccountController

public class AccountController : Controller
{
    public ActionResult SignIn()
    {
        if (ConfigurationManager.AppSettings["EnableDebugUser"] == "true")
            App.DebugUser = new TestPrincipal();
        return Redirect("/");
    }

    public ActionResult SignOut()
    {
        if (ConfigurationManager.AppSettings["EnableDebugUser"] == "true")
            App.DebugUser = null;
        return Redirect("/");
    }
}

With this, I can setup a login/logout button in my app and easily switch between a user that is logged in and one that isn’t.