Introducing native applications, per-client token lifetimes and client assertions support in OpenIddict 5.0 preview1

Earlier today, the first OpenIddict 5.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?

This major release introduces breaking changes that require applying migrations to update the database schema. As such it is not recommended to use it in production or in tandem with an existing deployment using OpenIddict 4.x.

Relaxed redirect_uri comparisons for native applications

While the OAuth 2.0 and OpenID Connect specification explicitly mandated the use of the "simple string comparison" logic – put simply, a byte-by-byte comparison – newer specifications like RFC8252 (aka OAuth 2.0 for Native Apps) relaxed this policy for mobile and desktop applications to allow using dynamic ports chosen at runtime.

While OpenIddict already allowed customizing the comparison policy by overriding OpenIddictApplicationManager.ValidateRedirectUriAsync(), built-in support for native applications was not a thing.

OpenIddict 5.0 changes that by introducing a new ApplicationType property for the application entity. This property is always set to web by default, but can be explicitly set to native to opt in the relaxed comparison policy that ignores ports for local redirect_uris:

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
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
// Note: the application must be registered as a native application to force OpenIddict
// to apply a relaxed redirect_uri validation policy that allows specifying a random port.
ApplicationType = ApplicationTypes.Native,
ClientId = "console",
ClientType = ClientTypes.Public,
ConsentType = ConsentTypes.Systematic,
DisplayName = "Console client application",
RedirectUris =
{
// Note: the port must not be explicitly specified as it is selected
// dynamically at runtime by the OpenIddict client system integration.
new Uri("http://localhost/callback/login/local")
},
Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Device,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.GrantTypes.DeviceCode,
Permissions.GrantTypes.RefreshToken,
Permissions.ResponseTypes.Code,
Permissions.Scopes.Email,
Permissions.Scopes.Profile,
Permissions.Scopes.Roles,
Permissions.Prefixes.Scope + "demo_api"
},
Requirements =
{
Requirements.Features.ProofKeyForCodeExchange
}
});

To avoid ambiguities, the existing OpenIddictApplicationDescriptor.Type property (and the properties of the same name in OpenIddictEntityFrameworkApplication, OpenIddictEntityFrameworkCoreApplication and OpenIddictMongoDbApplication) have been replaced by ClientType.

Users migrating to 5.0 will be invited to update their database schema to include the new ApplicationType property and the renamed ClientType member.

Per-client token lifetimes

OpenIddict already supported overriding the default token lifetimes dynamically using the SetAccessTokenLifetime(...) extension but being able to use a static value per client was a popular request.

To support that, OpenIddict 5.0 introduces a new Settings property in OpenIddictApplicationDescriptor (and in the EF Core/EF 6/MongoDB entities) and exposes 6 built-in settings that allow controlling the lifetime of access tokens, authorization codes, device codes, identity tokens, refresh tokens and user codes.

Here's an example that sets a static access token lifetime for a specific client:

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
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ApplicationType = ApplicationTypes.Native,
ClientId = "postman",
ClientType = ClientTypes.Public,
ConsentType = ConsentTypes.Systematic,
DisplayName = "Postman",
RedirectUris =
{
new Uri("https://oauth.pstmn.io/v1/callback")
},
Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Device,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.GrantTypes.DeviceCode,
Permissions.GrantTypes.Password,
Permissions.GrantTypes.RefreshToken,
Permissions.ResponseTypes.Code,
Permissions.Scopes.Email,
Permissions.Scopes.Profile,
Permissions.Scopes.Roles
},
Settings =
{
// Use a shorter access token lifetime for tokens issued to the Postman application.
[Settings.TokenLifetimes.AccessToken] = TimeSpan.FromMinutes(10).ToString("c", CultureInfo.InvariantCulture)
}
});

Even if a static token lifetime was attached to the client registration, the token lifetime can always be dynamically overridden using the SetAccessTokenLifetime(...) extension.

Client assertions

OpenIddict 4.x already supported client assertions, but only in the client stack (this feature was introduced in 4.0 as it was required for the Sign in with Apple integration).

OpenIddict 5.0 finalizes the implementation of client assertions by updating the server and validation stacks to support private_key_jwt for all the authenticated API endpoints (device authorization, token, introspection and revocation endpoints).

To use client assertions instead of client secrets, two things must be done:

Attach a JSON Web Key Set containing at least one signing key to the client registration

For that, a public key/private key pair must be generated first: the public key will be attached to the application entry and used by the server to validate the client assertions while the private key will be only known by the client and used to dynamically generate new client assertions.

To generate an ECDSA private/public key pair, use the following snippet:

1
2
3
4
5
6
7
8
9
using var algorithm = ECDsa.Create(ECCurve.NamedCurves.nistP256);

Console.WriteLine("Public key:");
Console.WriteLine(algorithm.ExportSubjectPublicKeyInfoPem());

Console.WriteLine();

Console.WriteLine("Private key:");
Console.WriteLine(algorithm.ExportECPrivateKeyPem());

Then, attach a JsonWebKeySet instance to OpenIddictApplicationDescriptor.JsonWebKeySet:

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
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ApplicationType = ApplicationTypes.Web,
ClientId = "mvc",
ClientType = ClientTypes.Confidential,
ConsentType = ConsentTypes.Explicit,
DisplayName = "MVC client application",
JsonWebKeySet = new JsonWebKeySet
{
Keys =
{
// Instead of sending a client secret, this application authenticates by
// generating client assertions that are signed using an ECDSA signing key.
//
// Note: while the client needs access to the private key, the server only needs
// to know the public key to be able to validate the client assertions it receives.
JsonWebKeyConverter.ConvertFromECDsaSecurityKey(GetECDsaSigningKey($"""
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEI23kaVsRRAWIez/pqEZOByJFmlXd
a6iSQ4QqcH23Ir8aYPPX5lsVnBsExNsl7SOYOiIhgTaX6+PTS7yxTnmvSw==
-----END PUBLIC KEY-----
"""))
}
},
RedirectUris =
{
new Uri("https://localhost:44381/callback/login/local")
},
PostLogoutRedirectUris =
{
new Uri("https://localhost:44381/callback/logout/local")
},
Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Logout,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.GrantTypes.RefreshToken,
Permissions.ResponseTypes.Code,
Permissions.Scopes.Email,
Permissions.Scopes.Profile,
Permissions.Scopes.Roles,
Permissions.Prefixes.Scope + "demo_api"
},
Requirements =
{
Requirements.Features.ProofKeyForCodeExchange
}
});

static ECDsaSecurityKey GetECDsaSigningKey(ReadOnlySpan<char> key)
{
var algorithm = ECDsa.Create();
algorithm.ImportFromPem(key);

return new ECDsaSecurityKey(algorithm);
}

Attach the signing key to the client registration or the validation options

To use client assertions with the OpenIddict client, the private signing key must be attached to the client registration:

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
services.AddOpenIddict()
.AddClient(options =>
{
// ...

// Add a client registration matching the client application definition in the server project.
options.AddRegistration(new OpenIddictClientRegistration
{
Issuer = new Uri("https://localhost:44395/", UriKind.Absolute),
ProviderName = "Local",

ClientId = "mvc",
Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" },

RedirectUri = new Uri("callback/login/local", UriKind.Relative),
PostLogoutRedirectUri = new Uri("callback/logout/local", UriKind.Relative),

// Instead of sending a client secret, this application authenticates by
// generating client assertions that are signed using a private signing key.
//
// As such, no client secret is set, but an ECDSA key is registered and used by
// the OpenIddict client to automatically generate client assertions when needed.
//
// Note: while the server only needs access to the public key, the client needs
// to know the private key to be able to generate and sign the client assertions.
SigningCredentials =
{
new SigningCredentials(GetECDsaSigningKey($"""
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIMGxf/eMzKuW2F8KKWPJo3bwlrO68rK5+xCeO1atwja2oAoGCCqGSM49
AwEHoUQDQgAEI23kaVsRRAWIez/pqEZOByJFmlXda6iSQ4QqcH23Ir8aYPPX5lsV
nBsExNsl7SOYOiIhgTaX6+PTS7yxTnmvSw==
-----END EC PRIVATE KEY-----
"""), SecurityAlgorithms.EcdsaSha256, SecurityAlgorithms.Sha256)
}
});

static ECDsaSecurityKey GetECDsaSigningKey(ReadOnlySpan<char> key)
{
var algorithm = ECDsa.Create();
algorithm.ImportFromPem(key);

return new ECDsaSecurityKey(algorithm);
}
});

To use client assertions in introspection requests sent by the OpenIddict validation handler, the private signing key must be attached to the options:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
services.AddOpenIddict()
.AddValidation(options =>
{
// ...

options.AddSigningKey(GetECDsaSigningKey($"""
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIMGxf/eMzKuW2F8KKWPJo3bwlrO68rK5+xCeO1atwja2oAoGCCqGSM49
AwEHoUQDQgAEI23kaVsRRAWIez/pqEZOByJFmlXda6iSQ4QqcH23Ir8aYPPX5lsV
nBsExNsl7SOYOiIhgTaX6+PTS7yxTnmvSw==
-----END EC PRIVATE KEY-----
"""));

static ECDsaSecurityKey GetECDsaSigningKey(ReadOnlySpan<char> key)
{
var algorithm = ECDsa.Create();
algorithm.ImportFromPem(key);

return new ECDsaSecurityKey(algorithm);
}
});

Both a client secret and one or more signing keys can be assigned to a client application: in this case, the client will be able to use both authentication methods. To force a client application to use client assertions, you can remove its client secret.

The less secure client_secret_jwt is deliberately not supported, as OpenIddict always "hashes" the client secret using PBKDF before storing it in the database: once a client is created, OpenIddict no longer has access to the original client secret and thus cannot use it to determine whether a JWT client assertion was signed using the correct client secret or not.

What's next?

OpenIddict 5.0 RTM is expected to ship mid-December. Until then, it's likely additional previews will be released to gather precious feedback from the community 😀

Can you use the ASP.NET Core Identity API endpoints with OpenIddict?

TL;DR: yes, but please, don't do it! 🤣

If you're an avid reader of Andrew Lock's blog (certainly one of the best .NET blogs out there!), you probably figured out that the title of this blog post is very similar to his latest post, Can you use the .NET 8 Identity API endpoints with IdentityServer?, in which he describes how you could (but really shouldn't 😂) use ASP.NET Core 8's Identity API endpoints with an OAuth 2.0/OpenID Connect server stack like IdentityServer or OpenIddict.

The similarity is of course completely deliberate, as his post motivated me to write this one.

The approach described in Andrew's post – that mainly consists in using the Identity API endpoints introduced by .NET 8 to handle the user authentication part – also works with OpenIddict, so there's no point covering the same aspects twice: if you haven't read it, please read Andrew's post before reading mine.

Instead, we're going to do something even crazier ('cause why not? 😎): using OpenIddict to process token requests handled by the Identity API login endpoint... and generate token responses containing either JWT or ASP.NET Core Data Protection access tokens!

ASP.NET Core Identity's API endpoints are faux-OAuth 2.0 endpoints...

As a preamble, it's important to note that while the ASP.NET team mentioned multiple times that the Identity API endpoints are not an OAuth 2.0 implementation, it's actually a non-standard equivalent of the OAuth 2.0 resource owner password credentials grant, as I demonstrated here.

(if you're not convinced yet these endpoints are "heavily inspired" by OAuth 2.0, this message posted by Stephen Halter – who wrote the ASP.NET Core Identity API endpoints feature – should speak for itself 😁)

... that can be used with OpenIddict nevertheless

Why is this "OAuth 2.0/not OAuth 2.0" distinction important you may ask? Well, since the ASP.NET Core Identity API endpoints implement a clone of the OAuth 2.0 resource owner password credentials grant with only a few differences, it's going to be very easy to use OpenIddict's advanced events model to make it compatible with the non-standard protocol created by the ASP.NET team.

Create a minimal ASP.NET Core API and enable the ASP.NET Core Identity API endpoints

For that, create a new .csproj referencing the ASP.NET Core Identity UI, the OpenIddict ASP.NET Core metapackage and the EF Core packages:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.0-rc.1.23421.29" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.0-rc.1.23421.29" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.0-rc.1.23421.29" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0-rc.1.23419.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0-rc.1.23419.6" />
<PackageReference Include="OpenIddict.AspNetCore" Version="4.8.0" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="4.8.0" />
</ItemGroup>

</Project>

Read more

Connecting Windows Media Center to Tvheadend with HDHRProxyIPTV: an alternative to DVBLink

This blog post was updated to use a different method for injecting the channels into the WMC database. The attached tool has been updated accordingly.

After more than 13 years of continuous use, I'm saying goodbye to my traditional Windows Media Center TV setup 😊

Like many (if not most) users, I've always been using Windows Media Center with terrestrial tuners physically attached to my HTPCs:

  • A USB dual-tuner Sony PlayTV when I started playing with WMC back in 2008 (an excellent device that's still working just fine).
  • A PCI dual-tuner Hauppauge WinTV-NOVA-TD-500.
  • A PCIe dual-tuner AVerMedia AVerTV Duo Hybrid (the hardware is good but the driver is terrible and has been responsible for a lot of blue screens...)
  • A PCIe quad-tuner Hauppauge WinTV-quadHD – great hardware and great driver! – when I eventually got tired of the crashes caused by the AVerTV Duo Hybrid.

While this setup certainly lacked the flexibility of more elaborate options based on network tuners (like the popular HDHomeRun) or virtualized tuners such as the now defunct DVBLink, it was actually trivial to set up and exceptionally stable, specially since replacing the old TV tuners by newer Hauppauge WinTV-quadHD cards (these things are not only ultra-stable but they also offer low-latency channel switching and have a fairly good reception).

So, why changing something that has been working so well? It all started with a message posted by a user of the My Digital Life forum – acer-5100 – about using Windows Media Center with H.265/HEVC channels.

As you probably already know, due to internal Windows Media Center/DirectShow limitations, only MPEG1, MPEG2 and H.264 channels are supported and it's not possible to watch or record H.265/HEVC channels, even if you install the appropriate DirectShow codecs. Since WMC was abandoned by Microsoft many years ago, it's extremely unlikely we'll ever see an update to make it compatible with HEVC channels.

To work around this limitation, acer-5100 opted for a simple but clever setup that consists in combining DVBLink (and its IPTV source plugin) with Tvheadend: by simply configuring DVBLink to use transcoded streams provided by Tvheadend rather than the original HEVC sources, Windows Media Center always gets a good old MPEG2 or H.264 video stream it can decode without any issue. Simple and clever.

Read more

Introducing system integration support for the OpenIddict client

Note: this blog post was updated to use the new record-based APIs introduced in OpenIddict 4.5.

When I unveiled the new OpenIddict client stack a year ago, I mentioned that one of the core design goals was to avoid coupling it to ASP.NET Core to eventually allow using it basically everywhere. With the release of OpenIddict 4.1, I'm making one additional step towards this goal by adding experimental support for Windows and Linux applications.

Why is a dedicated system integration package necessary?

While the OpenIddict client can already be used as-is to implement non-interactive flows like password or client credentials (thanks to its dedicated APIs in OpenIddictClientService), interactive flows like the code, hybrid or implicit flows are more complicated to implement, as they typically require launching the system browser (or using some sort of web view) to redirect the user to the authorization server and handling the authorization callback, which is generally implemented using an embedded web server or by registering a custom protocol URI scheme.

Leaving these critical parts as an exercise wouldn't offer a great experience. To avoid that, the new OpenIddict.Client.SystemIntegration package takes care of launching the user's preferred browser and handles the authorization responses returned by the identity provider to the protocol URI scheme associated with the application (or posted to the embedded web server), in a completely transparent way.

Once configured, doing a complete code flow dance should be as easy as:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
try
{
// Ask OpenIddict to initiate the authentication
// flow (typically, by starting the system browser).
var result = await _service.ChallengeInteractivelyAsync(new()
{
ProviderName = provider
});

// Wait for the user to complete the authorization process.
var response = await _service.AuthenticateInteractivelyAsync(new()
{
Nonce = result.Nonce
});

MessageBox.Show($"Welcome, {response.Principal.FindFirst(Claims.Name)!.Value}.",
"Authentication successful", MessageBoxButton.OK, MessageBoxImage.Information);
}

catch (ProtocolException exception) when (exception.Error is Errors.AccessDenied)
{
MessageBox.Show("The authorization was denied by the end user.",
"Authorization denied", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}

Read more

OpenIddict 4.0 general availability

Two years after OpenIddict's last release, I'm very pleased to announce that 4.0 is now generally available! 🎉

What's new?

While both the server and validation stacks include many internal improvements (some of them were described in OpenIddict 4.0 preview1 is out), the most visible change of this release is the addition of the new client stack and its 17 web providers that aim at offering an alternative to the aspnet-contrib providers (additional services will be added in future versions of OpenIddict):

Provider name
ApplePayPal
Amazon CognitoPro Santé Connect
DeezerReddit
GitHubStackExchange
GoogleTrakt
KeycloakTwitter
LinkedInWordPress
Microsoft Accounts/Azure ADYahoo
Mixcloud

If you're interested in learning more about the new OpenIddict client, don't miss these blog posts:

To see the OpenIddict client in action, you can give the OpenIddict samples a try, as most of them have been updated to use it.

Migration

While OpenIddict 4.0 comes with some breaking changes, the migration process should be fairly easy. To help users with this process, an OpenIddict 4.0 migration guide was added to the documentation.

OpenIddict 4.0 is fully compatible with ASP.NET Core 2.1 (on .NET Framework), ASP.NET Core 3.1, ASP.NET Core 6.0 and ASP.NET Core 7.0, so the migration can be done without having to upgrade to the latest .NET runtime/ASP.NET Core version.

Support

With OpenIddict 4.0 being now generally available, the previous version, OpenIddict 3.0, stops being supported and won't receive bug fixes or security updates. It is recommended to migrate to OpenIddict 4.0 to continue receiving bug and security fixes.

Acknowledgements

As always, a new major release is a great opportunity to thank all the sponsors who have helped keep OpenIddict free for everyone. Every contribution counts – of course! – but I'd like to dedicate this release to 3 sponsors in particular, who have a special place in my heart:

  • Volosoft: it might sound surprising, but Volosoft is the first and only company that has spontaneously offered to sponsor the project while evaluating OpenIddict as a replacement for IdentityServer4 in their (very!) popular ABP Framework project. Without Volosoft's incredible support, it's very likely the project would no longer be as active as it today (or even free for everyone).

  • Dovydas Navickas: Dovydas is one of the very first OpenIddict sponsors. Even though he stopped using OpenIddict after a big career change some time ago, Dovydas decided to keep sponsoring the project! This level of support is truly amazing so thank you very much Dovydas!

  • Jasmin Savard: Jasmin doesn't use OpenIddict but decided to give away a significant part of the sponsorship he receives as one of the most important Orchard Core contributors to keep OpenIddict free for everyone else. No need to say this dedication leaves me absolutely speechless!

I hope you'll love OpenIddict 4.0 as much as I enjoyed working on it.

Merry Christmas everyone! 🎄 🎁