OpenIddict 4.0 preview1 is out

Earlier today, the first OpenIddict 4.0 preview was pushed to NuGet.org 🎉

As always, all the changes can be found on GitHub but here's a recap of the most interesting ones:

What changed?

OpenIddict now includes a client stack

The introduction of a client stack in 4.0 is by far the most exciting change (well, at least for me 😁).

If you missed my previous post, I'd suggest reading it first as it details the motivation for creating it and the differences with existing offers like the ASP.NET Core OpenID Connect or OAuth 2.0 handlers.

The OpenIddict client will come with a companion package to simplify well-known Web services integrations

I had mentioned in my previous blog post that I was interested in developing an alternative to the aspnet-contrib providers that would be based on the new OpenIddict client instead of the ASP.NET Core 2.0 base handler and would have the following advantages:

  • Thanks to its dual-protocol nature, the OpenIddict client could enforce all the security checks introduced by OpenID Connect that a simple OAuth 2.0 client implementation like the ASP.NET Core OAuth 2.0 base handler wouldn't support, which means we could easily support both OAuth 2.0 and OpenID Connect providers without sacrificing security.

  • By being natively compatible with ASP.NET Core (from 2.1 to 7.0) and OWIN/Katana 4.2, the providers built on top of the OpenIddict client could be used in both recent ASP.NET Core apps and legacy ASP.NET >= 4.6.1 apps, making them usable in many more applications than the aspnet-contrib providers, that always target the most recent ASP.NET Core version.

  • Unlike the OAuth 2.0 base handler developed by Microsoft, the OpenIddict client comes with a composable events model that could allow eliminating code that we needed to copy in the aspnet-contrib providers when overriding certain OAuthHandler methods.

The good news is that this idea materialized as a dedicated OpenIddict.Client.WebIntegration package that depends on OpenIddict.Client and includes the logic necessary to deal with all the non-standard aspects of each supported provider. Unlike the aspnet-contrib providers, OpenIddict.Client.WebIntegration relies on Roslyn Source Generators to dynamically create all the plumbing code, which makes it much easier to maintain. If you're interested in learning more about the technical details, don't miss the PR that introduced it.

If you're already familiar with the aspnet-contrib providers, migrating to the OpenIddict-based providers shouldn't be too complicated:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
services.AddOpenIddict()

// Register the OpenIddict core components.
.AddCore(options =>
{
// Configure OpenIddict to use the Entity Framework Core stores and models.
// Note: call ReplaceDefaultEntities() to replace the default OpenIddict entities.
options.UseEntityFrameworkCore()
.UseDbContext<ApplicationDbContext>();
})

// Register the OpenIddict client components.
.AddClient(options =>
{
// Enable the redirection endpoint needed to handle the callback stage.
//
// Note: to mitigate mix-up attacks, it's recommended to use a unique redirection endpoint
// address per provider, unless all the registered providers support returning an "iss"
// parameter containing their URL as part of authorization responses. For more information,
// see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.
options.SetRedirectionEndpointUris(
"/signin-local",
"/signin-github",
"/signin-google",
"/signin-reddit",
"/signin-twitter");

// Register the signing and encryption credentials used to protect
// sensitive data like the state tokens produced by OpenIddict.
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();

// Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
options.UseAspNetCore()
.EnableStatusCodePagesIntegration()
.EnableRedirectionEndpointPassthrough();

// Register the Web providers integrations.
options.UseWebProviders()
.AddGitHub(new()
{
ClientId = "[client identifier]",
ClientSecret = "[client secret]",
RedirectUri = new Uri("https://localhost:44381/signin-github", UriKind.Absolute)
})
.AddGoogle(new()
{
ClientId = "[client identifier]",
ClientSecret = "[client secret]",
RedirectUri = new Uri("https://localhost:44381/signin-google", UriKind.Absolute),
Scopes = { Scopes.Profile }
});
});

The following providers are already part of OpenIddict 4.0 preview1 and it's likely external contributions will introduce additional providers in future versions:

Provider name
Apple
GitHub
Google
Microsoft
Reddit
Twitter

If you're interested in seeing a specific provider supported, please open a ticket on GitHub.

The OpenIddict client and the Web integration companion package will be compatible with the following .NET environments:

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 3.1.NET Core 3.1
ASP.NET Core 6.0.NET 6.0
ASP.NET Core 7.0.NET 7.0
Microsoft.Owin 4.2.NET Framework 4.6.1
Microsoft.Owin 4.2.NET Framework 4.7.2
Microsoft.Owin 4.2.NET Framework 4.8

At this stage, it's still unclear whether support for Blazor will be part of 4.0 RTM as discussions around Blazor's authentication stack are still pending. I hope I'll hear from the ASP.NET team in the near future.

The token generation/validation pipeline got a massive overhaul

While this change should be invisible to most OpenIddict users, the ProcessAuthentication and ProcessSignIn events were revamped in 4.0 to be much more flexible and give advanced users more control over the token generation and validation processes.

As part of this change, two new "sub-events" named GenerateToken(Context) and ValidateToken(Context) were introduced to enable the following scenarios:

  • The token format (JWT or ASP.NET Core Data Protection) can now be controlled dynamically.
  • Whether the token will have a corresponding entry in the database can now be controlled dynamically.
  • Whether the token will be a reference token can now be controlled dynamically.

The OpenIddict claims extensions were reworked

In previous versions, OpenIddict offered various extensions for ClaimsPrincipal but not for ClaimsIdentity, which made working with the latter (common with the OWIN integration, where AuthenticationTicket.Identity is a ClaimsIdentity) quite cumbersome. To fix that, many new extensions and overloads have been introduced for both ClaimsPrincipal and ClaimsIdentity.

As part of this change, the AddClaim overloads taking a list of claim destinations have been removed as overloads taking params string[] parameters were too hard to version. Instead, users are encouraged to use the new SetDestinations() overload on ClaimsIdentity/ClaimsPrincipal to set claim destinations of all claims in a single pass:

1
2
3
4
5
6
7
8
9
10
principal.SetDestinations(static claim => claim.Type switch
{
// Allow the "name" claim to be stored in both the access and identity tokens
// when the "profile" scope was granted (by calling principal.SetScopes(...)).
Claims.Name when claim.Subject.HasScope(Scopes.Profile)
=> new[] { Destinations.AccessToken, Destinations.IdentityToken },

// Otherwise, only store the claim in the access tokens.
_ => new[] { Destinations.AccessToken }
});

netcoreapp2.1 and net5.0 TFMs were removed

.NET Core 2.1 and .NET 5.0 are no longer supported by Microsoft so the OpenIddict 4.0 packages no longer target netcoreapp2.1 and net5.0. While OpenIddict 4.0 can still be used on .NET Core 2.1 and .NET 5.0 thanks to its .NET Standard 2.0 TFM, users are encouraged to migrate to a supported .NET version to ensure they can receive security fixes.

OpenIddict 4.0 – client, server and validation – will still be fully compatible with ASP.NET Core 2.1 on .NET Framework 4.6.1 and higher (that has the same support lifecycle as ASP.NET 4.x applications).

Support for the iss parameter was added to the OpenIddict server

Standardized in March 2022, the iss authorization response parameter was introduced by RFC9207 as a way for OAuth 2.0 (and OpenID Connect) clients that support multiple providers to mitigate mix-up attacks. Since it's an important security measure, native iss support was added to the OpenIddict server and client stacks.

System.Text.Json.Nodes is now fully supported on .NET >= 6.0

The OpenIddictParameter primitive was updated to support JsonObject, JsonArray and JsonValue on .NET >= 6.0 (the lower-level JsonElement struct is already supported by OpenIddict 3.0 on all .NET platforms).

You can also use these types via AuthenticationProperties to return custom parameters from the OpenIddict endpoints. E.g:

1
2
3
4
5
6
7
8
9
10
11
12
13
var properties = new AuthenticationProperties
{
Parameters =
{
["custom_array"] = new JsonArray(1, 2, "3")
["custom_object"] = new JsonObject
{
["property"] = "42"
}
}
};

return SignIn(principal, properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"access_token": "[access_token]",
"token_type": "Bearer",
"expires_in": 3599,
"scope": "openid email profile offline_access demo_api",
"id_token": "[id_token]",
"custom_array": [
1,
2,
"3"
],
"custom_object": {
"property": "42"
}
}

AuthenticationProperties.Items is now preserved by the OpenIddict server and client stacks

In 4.0, the authentication properties are now preserved by the ASP.NET Core and OWIN hosts in a special claim in all tokens (except access and identity tokens), which was required by the new client stack to flow certain properties set by Identity (e.g the selected external identity provider or a XSRF value bound to the user when associating an external provider to an existing ASP.NET Core Identity account). For consistency, the server stack was updated to offer the same feature.

1
2
3
4
5
6
7
8
9
10
11
12
var properties = _signInManager.ConfigureExternalAuthenticationProperties(
provider: issuer,
redirectUrl: Url.Action("ExternalLoginCallback", "Account", new
{
ReturnUrl = Request.PathBase + Request.Path + QueryString.Create(parameters)
}));

// Note: when only one client is registered in the client options,
// setting the issuer property is not required and can be omitted.
properties.SetString(OpenIddictClientAspNetCoreConstants.Properties.Issuer, issuer);

return Challenge(properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);

Challenge handling in the server and validation stacks was improved

Last but not least, the way authentication challenges are handled by the OpenIddict server and validation services has been massively improved to be more consistent, standard-compliant and work around annoying issues caused by some OWIN or ASP.NET 4.x components (like the cookies middleware or the FormsAuthentication module) that are known to rewrite 401 responses to 302 redirects even if the response was already fully handled by OpenIddict.

With 4.0, Katana applications using the cookies middleware with the automatic mode enabled (the default value) or ASP.NET 4.x applications still using FormsAuthenticationModule should no longer require any workaround for their 401 responses to work properly, making the user experience as good as with ASP.NET Core.

What's next?

OpenIddict 4.0 preview1 will be the first release to include the new client stack, so it's likely improvements will be made based on the collected feedback. I also plan to include logout support in a future preview. Oh and hopefully, external contributions should also introduce additional providers in the Web integration companion package (at least, I hope so 😁).