Using a local OAuth 2.0/OpenID Connect server with WebAuthenticationBroker

Last week, I received a mail from a client who was desperately trying to use his legacy OAuthAuthorizationServerMiddleware-based server with WebAuthenticationBroker, a WinRT component developed by Microsoft for Windows 8/Windows Phone 8.1 that helps developers deal with authentication servers in a protocol-agnostic manner (it can work with OAuth 1.0, OAuth 2.0, OpenID Connect and even the good old OpenID 2.0).

To be honest, I've never been a huge fan of WebAuthenticationBroker: while I love the fact it executes in a separate AuthHost process managed by the OS (which is great from a security perspective), the fact it relies on a modal dialog that doesn't even mention the current URL to render the authorization page has always been a major issue for me. If your app allows me to log in using my Google account, there's a high chance I'll end up aborting the authorization flow if I have no way to ensure your authorization server doesn't redirect me to a fake Google login page.

That's why my initial suggestion was to use IdentityModel.OidcClient, a portable OpenID Connect client developed by Dominick Baier (one of the two guys behind IdentityServer), that also works with UWP. OidcClient supports the same web view approach as WebAuthenticationBroker but it also allows you to manually control the authorization process (e.g by launching the device browser and pointing it to the authorization endpoint), which is the option recommended by the OAuth 2.0 for Native Apps draft.

Since WebAuthenticationBroker is not tied to a specific protocol, it's up to you to handle the last phase: trivial with OAuth 2.0, it can become really complex with more advanced protocols like OpenID Connect, as you must validate the authorization/token response. That's why using an OIDC-specific library like IdentityModel.OidcClient that handles the protocol details for you is generally a better option if you're not familiar with the protocol.

Unfortunately, this library is not compatible with OAuth 2.0-only servers and there's no plan to change that, so using it was not possible. Migrating the legacy authorization server to an OpenID Connect server like ASOS was also out of the question, so WebAuthenticationBroker was pretty much the only viable option in this case.

To ensure he was not missing something obvious, my client sent me something similar to this snippet (that I've updated to make it more concise and to remove app-specific 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
// Retrieve the app-specific redirect_uri. This value must correspond
// to the redirect_uri registered with your authorization server.
var callback = Uri.EscapeDataString(WebAuthenticationBroker.GetCurrentApplicationCallbackUri().AbsoluteUri);

// Note: the requestUri parameter must be a HTTPS address: an exception
// will be thrown if an HTTP address is used, even for local testing scenarios.
var result = await WebAuthenticationBroker.AuthenticateAsync(
options: WebAuthenticationOptions.None,
requestUri: new Uri("https://localhost:24500/api/Account/ExternalLogin" +
$"?client_id=uwp-app&response_type=token&redirect_uri={callback}"));

if (result.ResponseStatus == WebAuthenticationStatus.Success)
{
// Note: ResponseData contains the redirect URL and the OAuth 2.0 response parameters.
// To make the response easier to parse, the redirect_uri part is removed.
var payload = result.ResponseData.Substring(result.ResponseData.IndexOf('#') + 1);

var parameters = (from parameter in payload.Split('&')
let pair = parameter.Split('=')
select new { Name = pair[0], Value = pair[1] })
.ToDictionary(element => element.Name, element => element.Value);

string error;
// If an "error" parameter has been added by the authorization server, return an exception.
// Note: the optional "error_description" can be used to determine why the process failed.
if (parameters.TryGetValue("error", out error))
{
throw new InvalidOperationException("An error occurred during the authorization process.");
}

string token;
// Ensure an access token has been returned by the authorization server.
if (!parameters.TryGetValue("access_token", out token))
{
throw new InvalidOperationException("The access token was missing from the OAuth 2.0 response.");
}

// Use the access token to query the resource server.
}

Aside the fact it implements the implicit flow (which is not the most appropriate flow for mobile apps), this snippet should have worked as-is.

The cool thing with WebAuthenticationBroker is that the hard parts — embedded web view handling, response interception — are automatically managed for you: simply call AuthenticateAsync and it will open a dedicated web view rendering the login/consent form returned by your authorization server.

When the authorization process is finalized by the user, the asynchronous Task returned by WebAuthenticationBroker.AuthenticateAsync completes, and you can extract the authorization response from WebAuthenticationResult.ResponseData.

Though the snippet was fine, it didn't work as expected and the following error was systematically returned instead of the consent form:

We can't connect to the service you need right now. Check your network connection or try this again later.

So if the code is not the culprit, what could be causing this error?

There are actually 3 common pitfalls when using WebAuthenticationBroker:

  • The Internet (Client) capability must be added to the package manifest.
  • Your authorization server must use a valid and trusted SSL certificate.
  • Lookback isolation must be disabled if your authorization server is hosted locally.

Enable Internet communication in the package manifest

To use WebAuthenticationBroker, your application has to be granted the Internet (Client) capability, even if the authorization server is hosted locally.

This capability is now automatically enabled by default in the most recent "UWP blank app" template.

Adding it to the application manifest is easy and can be done using the built-in UI: simply double-click on the Package.appxmanifest file, go to the Capabilities tab and select Internet (Client):

Use a trusted SSL certificate

When developing a mobile/desktop application that communicates with an API, it is extremely frequent to avoid using SSL, since there's no need for transport security during the development phase, specially when the server is hosted locally.

Unfortunately, using a non-HTTPS address when calling AuthenticateAsync will simply result in an ArgumentException being thrown. Since there's currently no way to disable this requirement (even for pure testing scenarios), your authorization server must use an SSL certificate.

Luckily, using a trusted SSL certificate doesn't necessarily mean that the certificate has to be provided by a well-known authority: a self-issued certificate will work as long as it's added to the Trusted Root Certification Authorities user (or machine) store:

  1. You'll need to generate a self-signed certificate if you don't have one yet (note: IIS Express generates one for you, but you can replace it by your own one). This procedure is well documented and many tools or websites can help you with this task (you can even use Powershell for that!). Just make sure to use localhost as the subject of the certificate when generating it and you should be okay.

  2. When using your own certificate, you'll need to import it in your certificates store. Don't worry, it's rather easy to do and a detailed walkthrough can be found on Technet.

  3. You'll have to configure your web server to use your self-signed certificate. With IIS Express, it should be as simple as checking the Use SSLcheckbox in your ASP.NET project properties.

Remove loopback isolation

For security and reliability reasons, UWP applications are not allowed to send requests to the loopback interface. While Visual Studio automatically creates exemptions for debugged apps, this feature won't be helpful in this case, as the authentication broker always executes in a separate process.

If you see this (cryptic) error message in your Windows event logs, then you're likely facing this issue:

AuthHost encountered a navigation error at URL: [...] with StatusCode: 0x800C0005.

One option to fix it is to use the loopack exemption utility developed by Eric Lawrence. It's natively included in Fiddler 4 but can also be downloaded as a standalone software. To allow the authentication broker to communicate with the loopback interface, exempt the applications starting with microsoft.windows.authhost and save your changes:

If everything was properly configured, you should now see the login/consent page returned by your server: