Introducing OpenIddict 3.0 beta1

For over a year now, I've been working on what is for me the most exciting OpenIddict release: OpenIddict 3.0 beta1, whose server and validation features have been almost completely rewritten. You can find all the 3.0 beta1 packages on NuGet.org.

What has changed?

Some of the major changes introduced in this release were described in the OpenIddict 3.0 roadmap (the detailed list can be found on GitHub), but here's a recap of the most important ones:

The aspnet-contrib OAuth 2.0 server/validation/introspection handlers and OpenIddict were merged into a single codebase

This is the most important change of this release: ASOS – the low-level OpenID Connect server framework used in OpenIddict 1.0/2.0 – and the aspnet-contrib validation and introspection middleware were all merged into OpenIddict.

To ensure OpenIddict can be used as a replacement for ASOS (for which people usually write their own persistence layer, at least to validate things like client_id and redirect_uri), a new degraded mode was introduced to allow using the OpenIddict server components without all the additional logic that relies on the OpenIddict managers/stores (e.g client authentication, client validation, token storage).

For more information about this feature, read my previous post: Creating an OpenID Connect server proxy with OpenIddict 3.0's degraded mode.

OpenIddict has been decoupled from ASP.NET Core and now natively supports OWIN/Katana and ASP.NET 4.x

In OpenIddict 1.0/2.0, the core and the EF 6/EF Core/MongoDB stores were already decoupled from ASP.NET Core. In 3.0, the server and validation features have been revamped to avoid depending on ASP.NET Core. Instead, ASP.NET Core integration is now provided by separate packages named OpenIddict.Server.AspNetCore and OpenIddict.Validation.AspNetCore. For convenience, a metapackage named OpenIddict.AspNetCore can be referenced to import the OpenIddict core, server and validation packages and their ASP.NET Core integration with a single PackageReference.

Unlike the previous versions, OpenIddict 3.0 will also support multiple ASP.NET Core versions: 2.1, 3.1 and 5.0.

Decoupling OpenIddict from ASP.NET Core was also a great opportunity to make it natively compatible with OWIN/Katana, which will allow using its server or validation features in any ASP.NET (non-core) 4.6.1 application. OWIN/Katana integration is provided by the OpenIddict.Server.Owin and OpenIddict.Validation.Owin packages, which follows the same pattern as the ASP.NET Core hosts.

Here's the framework/runtime combinations that will be officially supported in 3.0:

Web framework version.NET runtime version
ASP.NET Core 2.1.NET Framework 4.6.1
ASP.NET Core 2.1.NET Framework 4.7.2
ASP.NET Core 2.1.NET Framework 4.8
ASP.NET Core 2.1.NET Core 2.1
ASP.NET Core 3.1.NET Core 3.1
ASP.NET Core 5.0.NET 5.0
OWIN/Katana 4.1.NET Framework 4.6.1
OWIN/Katana 4.1.NET Framework 4.7.2
OWIN/Katana 4.1.NET Framework 4.8

For more information about OWIN/Katana and ASP.NET 4.x support, read this dedicated post: Adding OpenIddict 3.0 to an OWIN application.

OpenIddict now uses JSON Web Token (JWT) as the default token format

In OpenIddict 1.0/2.0, the ASP.NET Core Data Protection stack is always used to encrypt the authorization codes, refresh tokens and access tokens, unless you explicitly opt for JWT access tokens with options.UseJsonWebTokens().

In 3.0, OpenIddict will now use encrypted JWT as the default token format for all the token types. Developers who need to disable access token encryption will be able to do so using options.DisableAccessTokenEncryption():

1
2
3
4
5
services.AddOpenIddict()
.AddServer(options =>
{
options.DisableAccessTokenEncryption();
});

Unlike previous versions, the OpenIddict 3.0 validation handler now supports JWT and introspection. Developers who use JWT access tokens in 2.0 and the JWT bearer middleware developed by Microsoft are strongly encouraged to move to the OpenIddict validation handler, that provides a simpler configuration story and includes dedicated logic to ensure tokens produced by OpenIddict 1.0/2.0 can still be used when migrating to OpenIddict 3.0.

ASP.NET Core Data Protection tokens are still supported in 3.0 and even recommended if you migrate from 1.0/2.0 (to ensure tokens produced by older versions can still be read after migrating to 3.0) or if you enable device flow support, as they provide additional protection against token leakage. To enable Data Protection support, simply call options.UseDataProtection() in both the server and validation options:

1
2
3
4
5
6
7
8
9
10
services.AddOpenIddict()
.AddServer(options =>
{
options.UseDataProtection();
})
.AddValidation(options =>
{
options.UseDataProtection();
});

ASP.NET Core Data Protection support is provided by the OpenIddict.Server.DataProtection and OpenIddict.Validation.DataProtection packages. These packages are referenced by the OpenIddict.AspNetCore metapackage but not by OpenIddict.Owin for naming and layering reasons.

Unlike the rest of ASP.NET Core, Data Protection is still compatible with the .NET Framework and thus can be used in ASP.NET 4.x applications. To enable ASP.NET Core Data Protection with the OWIN host, reference OpenIddict.Server.DataProtection and OpenIddict.Validation.DataProtection.

JSON.NET was replaced by System.Text.Json

All the OpenIddict libraries have been updated to use System.Text.Json (developed by Microsoft) instead of JSON.NET.

While it's a relatively new library (that still lacks features like a writable DOM), the performance boost it offers is impressive and makes System.Text.Json a fantastic candidate for replacing JSON.NET in OpenIddict.

Here's a tiny benchmark showing the difference between deserializing a OpenIdConnectMessage – the type used in OpenIddict 2.0 – with JSON.NET and deserializing its equivalent in 3.0 (where OpenIddictMessage was optimized and is now internally powered by System.Text.Json).

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
[MarkdownExporterAttribute.GitHub, MemoryDiagnoser]
public class Benchmark
{
[Benchmark(Baseline = true)]
public OpenIdConnectMessage OpenIddict20()
{
return JsonConvert.DeserializeObject<OpenIdConnectMessage>(@"{
""redirect_uris"": [
""https://client.example.org/callback"",
""https://client.example.org/callback2""
],
""client_name"": ""My Example Client"",
""token_endpoint_auth_method"": ""client_secret_basic"",
""logo_uri"": ""https://client.example.org/logo.png"",
""jwks_uri"": ""https://client.example.org/my_public_keys.jwks"",
""example_extension_parameter"": ""example_value""
}");
}
[Benchmark]
public OpenIddictMessage OpenIddict30()
{
return JsonSerializer.Deserialize<OpenIddictMessage>(@"{
""redirect_uris"": [
""https://client.example.org/callback"",
""https://client.example.org/callback2""
],
""client_name"": ""My Example Client"",
""token_endpoint_auth_method"": ""client_secret_basic"",
""logo_uri"": ""https://client.example.org/logo.png"",
""jwks_uri"": ""https://client.example.org/my_public_keys.jwks"",
""example_extension_parameter"": ""example_value""
}");
}
}

As you can see, the performance gain is excellent: not only it is 35% faster, but it also allocates much less memory!

1
2
3
4
5
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.264 (2004/?/20H1)
Intel Core i9-9900K CPU 3.60GHz (Coffee Lake), 1 CPU, 16 logical and 8 physical cores
.NET Core SDK=5.0.100-preview.4.20258.7
[Host] : .NET Core 3.1.4 (CoreCLR 4.700.20.20201, CoreFX 4.700.20.22101), X64 RyuJIT
DefaultJob : .NET Core 3.1.4 (CoreCLR 4.700.20.20201, CoreFX 4.700.20.22101), X64 RyuJIT
MethodMeanErrorStdDevRatioGen 0Gen 1Gen 2Allocated
OpenIddict204.510 μs0.0725 μs0.0642 μs1.000.84690.0229-6.92 KB
OpenIddict302.948 μs0.0160 μs0.0149 μs0.650.2136--1.77 KB

... and serialization is even more impressive!

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
[MarkdownExporterAttribute.GitHub, MemoryDiagnoser]
public class Benchmark
{
[Benchmark(Baseline = true)]
public string OpenIddict20()
{
return JsonConvert.SerializeObject(new OpenIdConnectMessage
{
["redirect_uri"] = new[]
{
"https://client.example.org/callback",
"https://client.example.org/callback2"
},
["client_name"] = "My Example Client",
["token_endpoint_auth_method"] = "client_secret_basic",
["logo_uri"] = "https://client.example.org/logo.png",
["jwks_uri"] = "https://client.example.org/my_public_keys.jwks",
["example_extension_parameter"] = "example_value"
});
}
[Benchmark]
public string OpenIddict30()
{
return JsonSerializer.Serialize(new OpenIddictMessage
{
["redirect_uri"] = new[]
{
"https://client.example.org/callback",
"https://client.example.org/callback2"
},
["client_name"] = "My Example Client",
["token_endpoint_auth_method"] = "client_secret_basic",
["logo_uri"] = "https://client.example.org/logo.png",
["jwks_uri"] = "https://client.example.org/my_public_keys.jwks",
["example_extension_parameter"] = "example_value"
});
}
}
1
2
3
4
5
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.264 (2004/?/20H1)
Intel Core i9-9900K CPU 3.60GHz (Coffee Lake), 1 CPU, 16 logical and 8 physical cores
.NET Core SDK=5.0.100-preview.4.20258.7
[Host] : .NET Core 3.1.4 (CoreCLR 4.700.20.20201, CoreFX 4.700.20.22101), X64 RyuJIT
DefaultJob : .NET Core 3.1.4 (CoreCLR 4.700.20.20201, CoreFX 4.700.20.22101), X64 RyuJIT
MethodMeanErrorStdDevRatioGen 0Gen 1Gen 2Allocated
OpenIddict204.209 μs0.0667 μs0.0714 μs1.000.99180.0076-8.11 KB
OpenIddict301.361 μs0.0257 μs0.0275 μs0.320.1621--1.34 KB

In most cases, this change should be completely transparent, but if you manually use JSON.NET to serialize or deserialize OpenIdConnectMessage, OpenIdConnectRequest or OpenIdConnectResponse instances, consider moving to System.Text.Json when migrating to OpenIddict 3.0, as 3.0 no longer includes a built-in JSON.NET JsonConverter for these types.

OpenIddict now supports the device authorization grant

Standardized in 2019, the device authorization grant (aka device flow) is now natively supported by OpenIddict 3.0.

Adding device flow support required making changes to the OpenIddict entities. This should be transparent for MongoDB users but Entity Framework 6 and Entity Framework Core users will have to create and apply a migration for the new schema to be reflected in the database.

Enabling reference tokens is no longer required to use immediate access token revocation

In 1.0/2.0, developers who needed immediate access token revocation support had to use reference tokens. As of 3.0, this is no longer required: even self-contained JWT or Data Protection access tokens can be revoked, as OpenIddict 3.0 now always creates a database entry for all token types.

Like in previous versions, the API projects needing immediate access token revocation need to either have a direct access to OpenIddict's database or use introspection:

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
services.AddOpenIddict()
.AddCore(options =>
{
// ...
})
.AddServer(options =>
{
// ...
})
.AddValidation(options =>
{
// Import the configuration from the local OpenIddict server instance.
options.UseLocalServer();
// For applications that need immediate access token or authorization
// revocation, the database entry of the received tokens and their
// associated authorizations can be validated for each API call.
// Enabling these options may have a negative impact on performance.
options.EnableAuthorizationEntryValidation();
options.EnableTokenEntryValidation();
// Register the ASP.NET Core host.
options.UseAspNetCore();
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
services.AddOpenIddict()
.AddValidation(options =>
{
// Note: the validation handler uses OpenID Connect discovery
// to retrieve the address of the introspection endpoint.
options.SetIssuer("http://localhost:12345/");
// Configure the validation handler to use introspection and register the client
// credentials used when communicating with the remote introspection endpoint.
options.UseIntrospection()
.SetClientId("resource_server_1")
.SetClientSecret("846B62D0-DEF9-4215-A99D-86E6B8DAB342");
// Register the System.Net.Http integration.
options.UseSystemNetHttp();
// Register the ASP.NET Core host.
options.UseAspNetCore();
});

Reference tokens can still be used in OpenIddict 3.0 and are now stored either as JWT or Data Protection tokens in the database, but enabling them only affects how tokens are returned to the client: when reference tokens are enabled, the original token payload is added to the database entry and a reference identifier (a 256-bit random value) is returned instead of the token, which greatly reduces the size of the tokens returned to the clients in exchange for more space used in the database.

Independently of whether options.UseReferenceTokens() is called or not, user codes used in the device flow are always user-readable reference tokens. As such, the device flow cannot be enabled when disabling token storage with options.DisableTokenStorage() and will require custom code when using the degraded mode.

OpenIddict's managers and stores now use IAsyncEnumerable<T>

The application, authorization, scope and token managers (located in OpenIddict.Core) and all the Entity Framework 6, Entity Framework 6 and MongoDB stores now natively support IAsyncEnumerable<T>. While this is a massive binary/source breaking change, migrating to IAsyncEnumerable<T> allows stores to implement streamed enumerations, which was not possible in OpenIddict 2.0 (that uses Task<ImmutableArray<T>> instead of IAsyncEnumerable<T>).

IAsyncEnumerable<T> is fully supported even on .NET Framework >= 4.6.1, thanks to the Microsoft.Bcl.AsyncInterfaces compatibility package that OpenIddict.Abstractions references.

The Entity Framework 6, Entity Framework Core and MongoDB entities have been renamed

To make more obvious the fact the OpenIddict entities are designed for a specific ORM/database, the application, authorization, scope and tokens entities have all been renamed to include the name of their corresponding store (e.g OpenIddictApplication -> OpenIddictMongoDbApplication).

The OpenIddict endpoints are all non-pass-through by default

In previous versions of OpenIddict, the authorization, logout, token and userinfo endpoints were always pass-through: OpenIddict validated the requests for you, but you needed to add an MVC controller – typically named AuthorizationController – or a custom middleware to handle them later in the ASP.NET Core pipeline.

Starting in 3.0, none of the OpenIddict endpoints is pass-through by default, which helps with debugging as OpenIddict will now throw an exception if the request is not handled using the events model and if the pass-through mode was not enabled, instead of letting ASP.NET Core return a 404 response.

To enable pass-through for a specific endpoint, use the methods provided by OpenIddictServerAspNetCoreBuilder or OpenIddictServerOwinBuilder:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
services.AddOpenIddict()
.AddServer(options =>
{
// When using ASP.NET Core:
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableLogoutEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.EnableUserinfoEndpointPassthrough()
.EnableVerificationEndpointPassthrough();
// When using OWIN/Katana:
options.UseOwin()
.EnableAuthorizationEndpointPassthrough()
.EnableLogoutEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.EnableUserinfoEndpointPassthrough()
.EnableVerificationEndpointPassthrough();
});

Status code pages middleware integration is no longer enabled by default

In 3.0, integration with the ASP.NET Core status code pages middleware is now opt-in:

1
2
3
4
5
6
services.AddOpenIddict()
.AddServer(options =>
{
options.UseAspNetCore()
.EnableStatusCodePagesIntegration();
});

Returning custom parameters via AuthenticationProperties.Parameters is now supported

Returning custom parameters was already supported in 1.0/2.0, but they had to be stored as strings in AuthenticationProperties.Items with a special suffix. This mechanism is no longer supported, but returning custom bool, long, string, string[] or JsonElement parameters is now much simpler:

1
2
3
4
5
6
7
8
9
10
11
12
var properties = new AuthenticationProperties(
items: new Dictionary<string, string>(),
parameters: new Dictionary<string, object>
{
["boolean_parameter"] = true,
["integer_parameter"] = 42,
["string_parameter"] = "Bob l'Eponge",
["array_parameter"] = JsonSerializer.Deserialize<JsonElement>(@"[""Contoso"",""Fabrikam""]"),
["object_parameter"] = JsonSerializer.Deserialize<JsonElement>(@"{""parameter"":""value""}")
});
return SignIn(principal, properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);

AuthenticationProperties.Parameters was introduced in ASP.NET Core 2.1 and thus is not supported on OWIN. To return custom properties on OWIN, you must store them in AuthenticationProperties.Items as strings and create a custom event handler to return them as part of the response. See Add additional values into Authorize endpoint response for an example.

What's next?

There'll likely be a few other beta releases before RTM to ensure everything is working as intended and to add features like localization support. All the samples contained in the openiddict-samples have already been updated to target OpenIddict 3.0 beta1 and new samples will be progressively added to cover the newly supported scenarios (e.g device flow).

Considerable effort has been dedicated to making sure that all users of ASOS, the aspnet-contrib extensions or OpenIddict have a migration path to 3.0 – whether they are using the latest ASP.NET Core version and the latest .NET Core runtime, are stuck with 2.1 on .NET Framework or still use ASP.NET 4.6.1 with OWIN/Katana.

As such, I'll no longer offer free support for OpenIddict 1.0/2.0 or the aspnet-contrib packages that were merged into OpenIddict as part of this release once the 3.0 RTM packages ship (in a few months). The 2 aspnet-contrib repositories will also be archived.

The following NuGet packages will be eventually flagged as obsolete to inform users that they are no longer actively developed or supported:

Package namePackage version
AspNet.Security.OpenIdConnect.ExtensionsAll
AspNet.Security.OpenIdConnect.PrimitivesAll
AspNet.Security.OpenIdConnect.ServerAll
Owin.Security.OpenIdConnect.ExtensionsAll
Owin.Security.OpenIdConnect.ServerAll
AspNet.Security.OAuth.IntrospectionAll
AspNet.Security.OAuth.ValidationAll
Owin.Security.OAuth.IntrospectionAll
Owin.Security.OAuth.ValidationAll
OpenIddict.*< 3.0

Users who substantially contributed to OpenIddict, sponsored the project or benefit from a support contract will still get bug fixes for these packages via a private feed to give them additional time to migrate to OpenIddict 3.0.

With that in mind, I encourage everyone to start migrating to OpenIddict 3.0 beta1 and testing it in a non-production environment.

What can you do to help?

While super exciting, this release has been way more time-consuming than I had initially anticipated, and there are still many important things to do, like adding documentation and a migration guide to help developers update their applications to OpenIddict 3.0.

For that, I need your help: please consider contributing to the effort or sponsoring me, so I can spend more time working on OpenIddict. Without external help, I likely will not have enough spare time to work on things that would benefit the community, like documentation.

Contribution area
Core/server/validation components
Documentation
Samples

If you're interested in getting dedicated support or want to discuss sponsoring options, don't hesitate to reach me at contact@kevinchalet.com.