Last year, Mike Rousos posted a great post about token authentication on the .NET blog and demonstrated how you could leverage ASP.NET Core Identity and OpenIddict to create your own tokens in a completely standard way.
Since then, many people emailed me to know if using ASP.NET Core Identity was really mandatory. Good news! While the first OpenIddict alpha bits were tied to Identity, the two have been completely decoupled as part of OpenIddict beta1 and beta2. Concretely, this means you can now use OpenIddict with your own authentication method or your own membership stack.
Get started
This post was updated to include code snippets demonstrating how to register OpenIddict in an ASP.NET Core 2.x application. When using OpenIddict in an ASP.NET Core 2.x application, make sure you're referencing the OpenIddict 2.x packages.
Update your .csproj file to reference the OpenIddict packages
For this demo, you'll need to reference 2 packages:
OpenIddict, that references the core services, the token server and the validation services.
OpenIddict.EntityFrameworkCore, that contains the Entity Framework Core stores.
Earlier today, I pushed new packages for all the aspnet-contrib projects. This is the first release since July (and probably one of the most exciting so far).
New primitives for the OpenID Connect server middleware
Starting with beta7, the OpenID Connect server middleware (ASOS) no longer relies on IdentityModel's OpenIdConnectMessage, that proved to be way too limited to represent complex JSON payloads and wasn't able to preserve non-string parameters types.
Instead, ASOS now comes with its own primitives: OpenIdConnectMessage, OpenIdConnectRequest and OpenIdConnectResponse. Unlike their IdentityModel equivalent, these types are backed by JSON.NET's primitives, which means that code like this will now work flawlessly:
1 2 3 4 5 6 7 8 9
var response = new OpenIdConnectResponse(); response["array_parameter"] = new JArray(new[] { 1, 2, 3 }); response["object_parameter"] = JObject.FromObject(new { name = "value" });
The other good news is that these primitives are part of a whole new .NET Standard 1.0 package (AspNet.Security.OpenIdConnect.Primitives) that is shared between the OWIN/Katana and the ASP.NET Core flavors of ASOS, which helps reduce code duplication between the two projects.
Proof Key for Code Exchange (PKCE) is now supported
OAuth 2.0 [RFC6749] public clients are susceptible to the authorization code interception attack.
In this attack, the attacker intercepts the authorization code returned from the authorization endpoint within a communication path not protected by Transport Layer Security (TLS), such as inter-application communication within the client's operating system.
Once the attacker has gained access to the authorization code, it can use it to obtain the access token.
This change makes ASOS fully compatible with client libraries supporting PKCE, like AppAuth for iOS.
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):
// 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('=') selectnew { 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)) { thrownew 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)) { thrownew 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.
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 OAuth 2.0/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.
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 OAuth 2.0 parameters required by the specification, as shown on this screenshot: