AspNet.Security.OpenIdConnect.Server 1.0.0 general availability

Today is a great day for me, as I just uploaded the ASOS 1.0.0 packages to NuGet.org, concluding a 3-year work on this library. A huge thanks to everyone – clients, sponsors, contributors, users – who helped me make this possible: ASOS wouldn't exist without your support!

thanks.gif

Migrating to 1.0.0

Migrating from ASOS RC1 to RTM should be rather smooth as no major change was adopted in this release, but there's an important requirement you must comply with to ensure your application still works correctly after the migration: you must also update the validation/introspection middleware to the 1.0.0 RTM version (the previous beta versions won't deliberately work):

1
2
3
4
5
6
7
8
9
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<PackageReference Include="AspNet.Security.OAuth.Introspection" Version="1.0.0" />
<PackageReference Include="AspNet.Security.OAuth.Validation" Version="1.0.0" />
<PackageReference Include="AspNet.Security.OpenIdConnect.Server" Version="1.0.0" />
</ItemGroup>
</Project>

If you use recent OpenIddict packages (that use ASOS 1.0.0) or if you explicitly downloaded AspNet.Security.OpenIdConnect.Server or AspNet.Security.OAuth.Introspection 1.0.0 from the aspnet-contrib MyGet feed, consider clearing your NuGet packages folder, as the bits published on NuGet.org slightly differ from the packages initially published on MyGet.org (e.g the ASOS package uploaded to NuGet.org offers ECDSA support when running on .NET Framework 4.7).

For that, close your VS instances, go to C:\Users\[username]\.nuget\packages (on Windows) or ~/.nuget/packages (on macOS) and remove the following folders:

aspnet-contrib-packages.png

Then, re-open VS and restore your projects or run dotnet restore to download the latest binaries from NuGet.org.

Support lifecycle

No software can reasonably ship without a solid support lifecycle and ASOS is no exception. To keep things simple, I've decided to adopt the same support policy as the one used by Microsoft for the .NET Core platform, which means the aspnet-contrib 1.0.0 RTM packages will get critical fixes and security patches until at least July of 2018.

What's next?

The next big step is to port ASOS to the new ASP.NET Core 2.0 authentication stack I helped redesign, as the 1.0 ASOS bits won't work at all due to the massive breaking changes adopted in 2.0 (e.g authentication handlers are now registered in the DI container and a unique authentication middleware handles everything at the pipeline level, in Startup.Configure(IApplicationBuilder app).

Migrating to AspNet.Security.OpenIdConnect.Server RC1

Earlier today, I released the RC1 version of the OpenID Connect server middleware, alongside the other aspnet-contrib packages.

This version – the latest before RTM – includes a few design changes that will directly impact your own code:

The built-in claims mapping feature was removed

Starting with RC1, ASOS no longer includes a built-in claims mapping feature, which means claims like ClaimTypes.NameIdentifier, ClaimTypes.Name or ClaimTypes.Role are no longer mapped to their OpenID Connect/JWT equivalents (sub, name, role).

Concretely, if you have code like that in your authorization provider class, you should update it to use the OpenID Connect claims instead of the legacy claims exposed by the static ClaimTypes class:

1
2
3
4
5
6
7
8
9
10
11
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
identity.AddClaim(ClaimTypes.NameIdentifier, "[unique identifier]");
identity.AddClaim(ClaimTypes.Name, "Bob",
OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
identity.AddClaim(ClaimTypes.Role, "Administrator",
OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Configure ClaimsIdentity to use the OpenID Connect claims instead of
// the legacy ClaimTypes claims to populate the ClaimsIdentity.Name property
// and determine how roles are resolved when calling ClaimsPrincipal.IsInRole(...).
var identity = new ClaimsIdentity(
OpenIdConnectServerDefaults.AuthenticationScheme,
OpenIdConnectConstants.Claims.Name,
OpenIdConnectConstants.Claims.Role);
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "[unique identifier]");
identity.AddClaim(OpenIdConnectConstants.Claims.Name, "Bob",
OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
identity.AddClaim(OpenIdConnectConstants.Claims.Role, "Administrator",
OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);

You're actually free to keep using the ClaimTypes claims, but the OpenID Connect server middleware will throw an exception if you don't (at least) add the sub claim:

InvalidOperationException: The authentication ticket was rejected because it doesn't contain the mandatory subject claim.

If you use the JWT bearer middleware, you'll also want to disable its own claims mapping feature and update the token validation parameters to use the dedicated JWT name/role claims:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear();
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
Authority = "http://localhost:58795/",
Audience = "resource_server",
RequireHttpsMetadata = false,
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = OpenIdConnectConstants.Claims.Name,
RoleClaimType = OpenIdConnectConstants.Claims.Role
}
});

The introspection middleware now uses name and role as the default claim types

In the same vein, the introspection middleware was updated to use name and role as the default claim types (instead of ClaimTypes.Name and ClaimTypes.Role):

1
2
3
4
5
6
7
8
9
10
11
app.UseOAuthIntrospection(options =>
{
options.Authority = new Uri("https://openid.yourapp.com/");
options.Audiences.Add("resource_server");
options.ClientId = "resource_server";
options.ClientSecret = "875sqd4s5d748z78z7ds1ff8zz8814ff88ed8ea4z4zzd";
// Override the default claim types used by the introspection middleware:
options.NameClaimType = "custom_name_claim";
options.RoleClaimType = "custom_role_claim";
});

The token format was improved

To support multi-valued authentication properties containing spaces, we had to tweak the token format to store these complex properties as JSON strings.

Unfortunately, this change makes old authorization codes, access and refresh tokens incompatible with the new format (and vice versa). In practice, this means that you can't use tokens issued by ASOS RC1 with old versions of the validation middleware (or with the OpenID Connect server middleware itself): such tokens will be automatically rejected.

To make sure everything runs smoothly, migrate to the latest version of the validation middleware (1.0.0-beta1-final).

Registering a signing key is no longer required when using the default access token format

Starting with RC1, ASOS now includes a "degraded mode" that allows you to use it without registering a signing key or a signing certificate (ephemeral or not) if you don't opt for JWT access tokens and don't use the implicit or hybrid flows.

Concretely, using AddEphemeralKey() or AddCertificate() is no longer mandatory if you use non-interactive flows like password or client credentials AND the default access token format.

And voilà, that's all. For the complete changelist, feel free to take a look at the GitHub issues page.


What's next?

No new release candidate is currently planned, which means the next version will be the RTM package.

The next (and last) step is to rework the XML documentation. Depending on how well this work item goes, the RTM bits should be published at the end of the month or in April. If you're willing to contribute to this stask, don't hesitate to ping me.

Implementing simple token authentication in ASP.NET Core with OpenIddict

Introduction

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

Register the aspnet-contrib feed

Before coding, you'll have to register the aspnet-contrib MyGet feed, where the preliminary OpenIddict beta bits are currently hosted. For that, create a new NuGet.config at the root of your solution:

NuGet.config
1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="aspnet-contrib" value="https://www.myget.org/F/aspnet-contrib/api/v3/index.json" />
</packageSources>
</configuration>

Update your .csproj file to reference the OpenIddict packages

For this demo, you'll need to reference 4 packages:

  • AspNet.Security.OAuth.Validation, that provides the authentication middleware needed to validate the access tokens issued by OpenIddict.
  • OpenIddict, that references the OpenID Connect server middleware and provides the logic required to validate token requests.
  • OpenIddict.EntityFrameworkCore, that contains the default EntityFramework stores.
  • OpenIddict.Mvc, that provides an ASP.NET Core MVC binder allowing to use OpenIdConnectRequest as an action parameter.
1
2
3
4
5
6
7
8
9
10
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<PackageReference Include="AspNet.Security.OAuth.Validation" Version="1.0.0" />
<PackageReference Include="OpenIddict" Version="1.0.0-*" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="1.0.0-*" />
<PackageReference Include="OpenIddict.Mvc" Version="1.0.0-*" />
</ItemGroup>
</Project>

Read more

The new aspnet-contrib packages are out

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).

What's new?

New OAuth2 social providers

Thanks to our amazing contributors, 10 new providers have been added in this release:


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"
});
// Outputs {"array_parameter":[1,2,3],"object_parameter":{"name":"value"}}
Console.WriteLine(JsonConvert.SerializeObject(response));

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

In August, ASOS was updated to support the Proof Key for Code Exchange specification:

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.

Read more

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