OpenIddict 7.0 is out

Earlier today, the OpenIddict 7.0 packages were pushed to NuGet.org 🎉

For those familiar with OpenIddict's release cadence, this may sound surprising (as major versions typically ship in December), but there are two reasons for that:

  • OpenIddict 7.0 replaces ASP.NET Core 2.1 by ASP.NET Core 2.3 as the new minimal version, which has important implications.
  • OpenIddict 7.0 introduces OAuth 2.0 Token Exchange support in both the client and server stacks: since it was by far the most requested feature, I didn't want to wait December for users to be able to play with it 😊

This blog post will highlight the most important changes, but the complete list can be found in the release notes of the four 7.0 preview releases:

What's new?

ASP.NET Core 2.3 replaces 2.1 as the minimal version on .NET Framework

If you're using ASP.NET Core 2.1 (or 2.2) on .NET Framework, you're probably already aware that the ASP.NET team re-released ASP.NET Core 2.1 as ASP.NET Core 2.3 in January.

That move may sound surprising, but the ASP.NET team realized that a lot of users were still using ASP.NET Core 2.2 (the latest ASP.NET Core version compatible with .NET Framework). Unlike ASP.NET Core 2.1, 2.2 is longer supported, but since it's listed as the latest 2.x version available on NuGet.org, many people use it thinking it is the supported version. By rebranding 2.1 to 2.3, the unsupported 2.2 packages no longer appear as the latest 2.x packages.

The thing is: while it was released as a minor version update, ASP.NET Core 2.3 is not 100% compatible with ASP.NET Core 2.2, as none of the changes or APIs introduced in 2.2 – no longer supported since December 2019 – is present in 2.3. Unfortunately, since security fixes for the ASP.NET Core 2.x branch now exclusively ship as 2.3.x packages, updating the version referenced by OpenIddict was essential to ensure OpenIddict users are always running a supported and secure version.

When migrating to OpenIddict 7.0, you'll need to carefully review your dependencies to ensure your application doesn't accidentally depend on any ASP.NET Core 2.2-specific API or package and still runs fine on 2.3.

OpenIddict 7.0 now references .NET Extensions 8.0 on .NET Framework 4.6.2+ and .NET Standard 2.0/2.1

While ASP.NET Core 2.1 referenced the 2.1 version of the Microsoft.Extensions.* packages, ASP.NET Core 2.3 now references the 8.0 version: as such, all the OpenIddict packages – even those who don't reference ASP.NET Core – have been updated to use the 8.0 version, which allowed introducing some significant improvements

Canary testing has confirmed that OWIN/Katana or "legacy" ASP.NET 4.6.2+ applications are not negatively impacted by this change: in almost all cases, regenerating (or manually updating the binding redirects if necessary) after migrating to OpenIddict 7.0 is enough to ensure the application will still work fine after the migration.

For more information, see Update the .NET Framework/.NET Standard TFMs to reference ASP.NET Core/Entity Framework Core 2.3 and the .NET Extensions version 8.0.

The Entity Framework Core stores now require Entity Framework Core 2.3+

Exactly like ASP.NET Core, Entity Framework Core 2.1 was re-released as 2.3 (but doesn't include any of the changes introduced in 2.2). Since security and critical bug fixes for Entity Framework Core on .NET Framework will be released as 2.3.x packages, I decided to also require 2.3 as the minimal version for projects referencing the .NET Framework 4.6.2+ and .NET Standard 2.0/2.1 assemblies of the OpenIddict Entity Framework Core stores.

When migrating to OpenIddict 7.0, you'll need to test your application to ensure it doesn't depend on any Entity Framework Core 2.2-specific API or logic.

The OpenIddict packages are now trimming/Native AOT-compatible on .NET 9.0 and higher

As part of this release, a huge effort was dedicated to making all the OpenIddict assemblies trimming and Native AOT-compatible. While removing code that couldn't be statically analyzed by the IL trimmer required introducing massive changes in the core stack, these changes should be transparent to most users.

MongoDB and Entity Framework Core are not yet fully compatible with trimming and Native AOT, so rough edges are expected when using the OpenIddict MongoDB or Entity Framework Core stores. It's likely future versions will improve that.

For more information, see Remove all the store resolvers and mark all the assemblies as trimming/Native AOT-compatible.

OAuth 2.0 Token Exchange is now supported by the client and server stacks

OAuth 2.0 Token Exchange was by far the most requested feature and it's finally landing in OpenIddict 7.0!

If you've already implemented the refresh token flow in your application, implementing OAuth 2.0 Token Exchange should feel extremely familiar:

1
2
3
4
5
6
7
8
9
10
11
12
13
services.AddOpenIddict()
.AddClient(options =>
{
// ...

options.AllowTokenExchangeFlow();
})
.AddServer(options =>
{
// ...

options.AllowTokenExchangeFlow();
});
1
2
3
4
5
6
7
8
9
10
11
// Ask OpenIddict to send the specified subject token (and actor token, if available).
var response = await _service.AuthenticateWithTokenExchangeAsync(new()
{
ActorToken = actorToken,
ActorTokenType = TokenTypeIdentifiers.AccessToken,
CancellationToken = cancellationToken,
ProviderName = "Local",
RequestedTokenType = TokenTypeIdentifiers.AccessToken,
SubjectToken = subjectToken,
SubjectTokenType = TokenTypeIdentifiers.AccessToken
});
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
[HttpPost("~/connect/token"), IgnoreAntiforgeryToken, Produces("application/json")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

if (request.IsPasswordGrantType())
{
// ...
}

else if (request.IsAuthorizationCodeGrantType() || request.IsDeviceCodeGrantType() || request.IsRefreshTokenGrantType())
{
// ...
}

else if (request.IsTokenExchangeGrantType())
{
// Retrieve the claims principal stored in the subject token.
//
// Note: the principal may not represent a user (e.g if the token was issued during a client credentials token
// request and represents a client application): developers are strongly encouraged to ensure that the user
// and client identifiers are randomly generated so that a malicious client cannot impersonate a legit user.
//
// See https://datatracker.ietf.org/doc/html/rfc9068#SecurityConsiderations for more information.
var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);

// If available, retrieve the claims principal stored in the actor token.
var actor = result.Properties?.GetParameter<ClaimsPrincipal>(OpenIddictServerAspNetCoreConstants.Properties.ActorTokenPrincipal);

// Retrieve the user profile corresponding to the subject token.
var user = await _userManager.FindByIdAsync(result.Principal!.GetClaim(Claims.Subject)!);
if (user is null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid."
}));
}

// Ensure the user is still allowed to sign in.
if (!await _signInManager.CanSignInAsync(user))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in."
}));
}

// Note: whether the identity represents a delegated or impersonated access (or any other
// model) is entirely up to the implementer: to support all scenarios, OpenIddict doesn't
// enforce any specific constraint on the identity used for the sign-in operation and only
// requires that the standard "act" and "may_act" claims be valid JSON objects if present.

var identity = new ClaimsIdentity(
authenticationType: TokenValidationParameters.DefaultAuthenticationType,
nameType: Claims.Name,
roleType: Claims.Role);

// Add the claims that will be persisted in the issued token.
identity.SetClaim(Claims.Subject, await _userManager.GetUserIdAsync(user))
.SetClaim(Claims.Email, await _userManager.GetEmailAsync(user))
.SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user))
.SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user))
.SetClaims(Claims.Role, [.. await _userManager.GetRolesAsync(user)]);

// Note: IdentityModel doesn't support serializing ClaimsIdentity.Actor to the
// standard "act" claim yet, which requires adding the "act" claim manually.
//
// For more information, see
// https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/pull/3219.
if (!string.IsNullOrEmpty(actor?.GetClaim(Claims.Subject)) &&
!string.Equals(identity.GetClaim(Claims.Subject), actor.GetClaim(Claims.Subject), StringComparison.Ordinal))
{
identity.SetClaim(Claims.Actor, new JsonObject
{
[Claims.Subject] = actor.GetClaim(Claims.Subject)
});
}

// Note: in this sample, the granted scopes match the requested scope
// but you may want to allow the user to uncheck specific scopes.
// For that, simply restrict the list of scopes before calling SetScopes.
identity.SetScopes(request.GetScopes());
identity.SetResources(await _scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync());
identity.SetDestinations(GetDestinations);

// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}

throw new InvalidOperationException("The specified grant type is not supported.");
}

For more information, see Implement built-in delegation/impersonation support (RFC8693) and Implement OAuth 2.0 Token Exchange support.

OpenIddict 7.0 implements the Updates to Audience Values for OAuth 2.0 Authorization Servers draft to address a security issue

OpenIddict 7.0 proactively implements the Updates to Audience Values for OAuth 2.0 Authorization Servers draft: while it hasn't been officially adopted yet, it fixes a vulnerability affecting the standard private_jwt_key client authentication method. Since this draft introduces important breaking changes in multiple OAuth 2.0 and OpenID Connect specifications to mitigate the vulnerability, implementing it in OpenIddict 7.0 was a better option than having to the next major version to address this issue.

Due to this change, the following cases won't be supported by OpenIddict 7.0:

  • The application authenticates using the OpenIddict client with a third-party server that requires the use of the token_endpoint as the audience of client assertions, even for endpoints other than the token endpoint (e.g., the "pushed authorization endpoint" or the "introspection endpoint").
  • The application authenticates using the OpenIddict client with a third-party server that does not support the new client-authentication+jwt JSON Web Token type defined by the specification for client assertions.
  • The application uses the OpenIddict server and allows clients to authenticate with client assertions that use token_endpoint instead of issuer as the audience.
  • The application uses the OpenIddict server and allows clients to authenticate with client assertions that do not use the new client-authentication+jwt JSON Web Token type.

The OpenIddict client, server and validation stacks have all been updated to support the new requirements introduced by this specification and it is expected that other implementations will make similar changes in the future. OpenIddict 7.0 doesn't allow reverting to the unsafe behavior, but I'll monitor the situation to determine whether opt-in compatibility quirks should be introduced in a future version to support unsafe client assertions.

Migration

While OpenIddict 7.0 comes with some breaking changes, the migration process shouldn't be too complicated. To help users with this process, an OpenIddict 7.0 migration guide was added to the documentation.

OpenIddict 7.0 is fully compatible with ASP.NET Core 2.3 (on .NET Framework), ASP.NET Core 8.0 and ASP.NET Core 9.0, so the migration can be done without having to upgrade to the latest .NET runtime/ASP.NET Core version: when possible, it is even recommended to decouple the .NET runtime/OpenIddict updates for a smoother upgrade.

Support

With OpenIddict 7.0 being now generally available, the previous version, OpenIddict 6.0, stops being supported and won't receive bug fixes or security updates. As such, it is recommended to migrate to OpenIddict 7.0 to continue receiving bug and security fixes.

As for the previous major version, there are however two exceptions to this policy:

  • ABP Framework users receive patches for OpenIddict for as long as ABP Framework itself is supported by Volosoft (typically a year following the release of a major ABP version), whether they have a commercial ABP license or just use the free packages:
OpenIddict branchABP Framework branchEnd of support date (estimated)
4.x7.xDecember 19, 2024
5.x8.xNovember 19, 2025
6.x9.xCurrently supported
7.x (current)Not supported yetNot supported yet
  • OpenIddict sponsors are offered extended support depending on the selected sponsorship tier:
    • Tier 6 sponsors get full support for the previous version 1 month following the release of a new major version.
    • Tier 7 sponsors get full support for the previous version 6 months following the release of a new major version.
    • Tier 8 sponsors get full support for the previous version 12 months following the release of a new major version.
    • Tier 9 sponsors get full support for the previous version 24 months following the release of a new major version.
OpenIddict branchSponsorship tierEnd of support date
4.xTier 6January 18, 2024
4.xTier 7June 18, 2024
4.xTier 8December 18, 2024
4.xTier 9December 18, 2025
5.xTier 6January 17, 2025
5.xTier 7June 17, 2025
5.xTier 8December 17, 2025
5.xTier 9December 17, 2026
6.xTier 6August 7, 2025
6.xTier 7January 7, 2026
6.xTier 8July 7, 2026
6.xTier 9July 7, 2027
7.x (current)AnyCurrently supported

Acknowledgements

As always, a new major release is a great opportunity to thank all the sponsors who have helped keep OpenIddict free for everyone:

Volosoft logo
OpenIddict Components logo

Sébastien RosSchmitt ChristianSebastian StehleCommunicatie CockpitJasmin SavardThomasEYERIDE Fleet Management SystemJulien DebacheStian HåveRavindu LiyanapathiranaHieronymusBlazeAkhan ZhakiyanovCorentin BBarry DorransDevQ S.r.l.GrégoireForterroMarcelJens WillmerBlauhaus Technology (Pty) LtdJan TrejbalAviationexam s.r.o.MonoforRatiodata SEDennis van ZettenJeroen BaidenmannLombiq Technologies Ltd.Andrew Babbittsoftaware gmbhSingular SystemsSCP-srlRealisable SoftwareSipke SchoorstraJoshua Nixondzmitry-lahodaJames Hough

Happy summer everyone! 🏖️