Using a local OAuth2/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 OAuth1, OAuth2, OpenID Connect and even the good old OpenID2).

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 OAuth2, 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 OAuth2-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 OAuth2 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 OAuth2 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.

Read more

Creating your own OpenID Connect server with ASOS: conclusion

While this quite long blog posts series about ASOS ends here, there are still many aspects to cover. As promised in my introduction post, I'll dedicate a future post to the client-side part. An in-depth post about token revocation and the differences between JWT and opaque tokens is also planned.

If you have questions about ASOS or OAuth2/OpenID Connect, don't hesitate to join us on Gitter.im.

If you need personal assistance, are looking for a contractor or have remarks about this blog posts series, please ping me at contact [at] kevinchalet.com.

drop-the-mic.gif

Creating your own OpenID Connect server with ASOS: testing your authorization server with Postman

The sample used in this post can be found in the AspNet.Security.OpenIdConnect.Samples repository, that also hosts the Cordova, MVC and SignalR samples for ASOS.

For clarity, it implements both the authorization code flow and the password flow, but doesn't use any membership stack (the user credentials are hardcoded in the authorization provider class and a fake identity is always used to create tokens).

To test REST services, one of the easiest options is indisputably to use Postman. If you're not already familiar with Postman, I encourage you to read the documentation.


Retrieving an access token using the resource owner password credentials grant

Using the password flow with Postman is quite straightforward:

  • Select POST as the HTTP method.
  • Fill the Request URL input with the absolute address of the token endpoint.
  • Click on the Body tab and choose the x-www-form-urlencoded encoding.
  • Add the OAuth2 parameters required by the specification, as shown on this screenshot:
password-grant-token-request.png

Read more

Creating your own OpenID Connect server with ASOS: adding custom claims and granting scopes


Attaching a destination to custom claims using AddClaim or SetDestinations

Unlike OAuthAuthorizationServerMiddleware, ASOS doesn't assume that access tokens are always consumed by your own resource servers and refuses to serialize claims that don't explicitly specify a destination to avoid leaking confidential data to unauthorized parties.

Two destinations are currently supported by ASOS: access_token and id_token. There's no equivalent for authorization codes or refresh tokens as they are always encrypted and only readable by the authorization server itself.

Concretely, this means that all your claims won't be returned to the client application, unless you explicitly call the AddClaim overload taking one or more destinations or use SetDestinations to attach the appropriate destination(s) to your claims.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
// When access_token and id_token are specified,
// the claim will be serialized in both tokens.
identity.AddClaim("username", "Pinpoint",
OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
// If only access_token is specified, the language
// claim won't be added in the identity token.
var claim = new Claim("language", "fr-FR");
claim.SetDestinations(OpenIdConnectConstants.Destinations.AccessToken);
identity.AddClaim(claim);

Read more

Creating your own OpenID Connect server with ASOS: implementing the authorization code and implicit flows

To support interactive flows like the authorization code or the implicit flows, the ValidateAuthorizationRequest event must be implemented to validate the authorization request sent by the client application.


Implementing ValidateAuthorizationRequest to validate response_type, client_id and redirect_uri

To support interactive flows, you must implement ValidateAuthorizationRequest to validate the client_id and the redirect_uri parameters provided by the client application to ensure they correspond to a registered client.

Ideally, the response_type parameter should also be validated to ensure that a client_id corresponding to a confidential application cannot be used with the implicit/hybrid flow to prevent downgrade attacks.

In pure OAuth2, redirect_uri was not mandatory but is now required by the OpenID Connect specification. To support legacy clients, ASOS doesn't reject authorization requests missing the redirect_uri parameter if the openid scope is not present, but in this case, it's up to you to call context.Validate(...) with the redirect_uri the user agent should be redirected to. If you don't need to support such clients, consider rejecting the authorization requests that don't specify a redirect_uri.

While the OpenID Connect specification explicitly states that the redirect_uri MUST exactly match one of the callback URLs associated with the client application, you're actually free to implement a relaxed comparison policy to support advanced scenarios (e.g domain-only/subdomain comparison or wildcard support): use this ability with extreme caution to avoid introducing an open redirect vulnerability.

Nothing surprising: the exact implementation of ValidateAuthorizationRequest will depend on the flows you want to support (e.g authorization code/implicit/hybrid) and on how you store your application details (e.g hardcoded or in a database).

Read more