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 | try |
How does that work?
If you're already using OpenIddict, you probably already know that it makes heavy use of IServiceCollection
to support dependency injection and simplify the configuration process by exposing dedicated IServiceCollection
extensions and builders.
The new OpenIddict.Client.SystemIntegration
package goes further by also leveraging the .NET Generic Host to implement all the hooks and plumbing needed to handle the authorization callbacks that will be returned to the application.
But concretely, how does that work?
When you call
AuthenticateInteractivelyAsync()
, OpenIddict launches the system browser (using eitherLauncher.LaunchUriAsync()
orShellExecuteEx()
on Windows orxdg-open
on Linux). While not recommended for most scenarios,WebAuthenticationBroker
can also be used on UWP if you prefer a web-view-like-but-better approach that doesn't involve launching the system browser.When the user approves the authorization demand, the response is returned to a callback URI, that can materialize as two things, depending on the type of URI:
A protocol activation that is received and managed by the operating system (e.g
com.contoso.client:/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz
). For that, at least one custom URI scheme must be registered with the OS:For packaged Windows applications (e.g UWP applications or packaged WinForms/WPF/WinUI 3 applications), by declaring the desired URI scheme in the application manifest (e.g
<uap:Protocol Name="com.contoso.client"/>
). For more information, see Handle URI activation.For non-packaged Windows applications (e.g traditional Win32 WinForms/WPF applications), by adding a registry entry for the desired URI scheme pointing to the executable that will be launched to handle the protocol activation:
- Globally, under
HKEY_CLASSES_ROOT
(since it requires administrator rights, this would be typically done at the setup stage by the application installer). - Per user, under
HKEY_CURRENT_USER\SOFTWARE\Classes
(thanks Adam Braden for this great suggestion! 🧡)
- Globally, under
For Linux applications, by adding a
[Desktop Entry]
. For more information, see Create a custom URL Protocol Handler.
A loopback HTTP request (e.g
http://localhost:49158/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz
). For scenarios where registering a protocol handler registration is not possible or practical, it is possible to use the embedded HTTP web server that ships withOpenIddict.Client.SystemIntegration
: when the application starts, OpenIddict automatically chooses a random port in the49152-65535
range and starts listening to callback HTTP requests sent tolocalhost
, pretty much like how that would work withOpenIddict.Client.AspNetCore
orOpenIddict.Client.Owin
.
To extract and handle protocol activations transparently in multi-instance applications, OpenIddict implements a blocking
IHostedService
that will determine whether the current application instance was created to react to a protocol activation (either using the WinRTAppInstance.GetActivatedEventArgs()
API or by extracting the protocol activation URI from the command line arguments). If so, it invokes the OpenIddict client pipeline to handle the authorization response: once the response is validated, it is redirected to the correct instance (whose identifier is stored in the state token) and the current instance is terminated.To handle authorization responses redirected by other instances, it implements a background
IHostedService
that waits for inter-process notifications to be posted to a named pipe. Once the authorization response is transferred, it is validated and the call toAuthenticateInteractivelyAsync()
returns the final response with the authentication details.
While the internals are a bit complicated (as there are multiple application models and many scenarios to cover), it is fortunately completely transparent for the developer.
What types of applications can be supported with this new integration?
The OpenIddict.Client.SystemIntegration
package doesn't depend on a specific application model and has been designed to be usable in most types of Linux and Windows applications (whether they are packaged or not and run full-trust or in an AppContainer).
That said, two technical aspects will limit cases where the OpenIddict client can be used:
.NET Standard 2.0 support: OpenIddict depends on packages that require .NET Standard 2.0 support (for instance, the
Microsoft.Extensions.*
packages), which excludes all the applications that run on a limited .NET flavor, like Windows 8's universal apps or UWP applications prior to Windows 10 1809, as these legacy platforms don't expose any of the APIs introduced in .NET Standard 2.0..NET Generic Host support: while the .NET Generic Host can be theoretically used in any application that can target .NET Standard 2.0, not all application models will offer a perfect experience:
Windows and Linux .NET console applications don't need anything specific as the .NET Generic Host already ships with a built-in
.UseConsoleLifetime()
extension that takes care of managing the lifetime of the host (typically, by listening toCTRL+C
combinations andSIGTERM
events).WinForms and Windows Presentation Foundation applications can reference the excellent Dapplo.Microsoft.Extensions.Hosting.WinForms and Dapplo.Microsoft.Extensions.Hosting.Wpf packages developed by Robin Krom: the result is both very clean and perfectly integrated.
While there's currently no .NET Generic Host companion package for WinUI 3 applications, a pull request proposed by Jöra Malek should address that in the future: Implement WinUI.
To my knowledge, there's no integration for WinUI 2/UWP applications, which makes using the .NET Generic Host more complicated. There are also other annoying limitations, like Entity Framework Core not fully supporting UWP (and since .NET Standard 2.0 is no longer supported by recent versions of EF Core, proper UWP support will very likely never happen). Given that Microsoft pretty much halted the development of the UWP platform, using the OpenIddict client in UWP applications should be reserved to developers who are familiar with UWP and its inherent limitations.
While it features an application builder that is inspired by the .NET Generic Host, MAUI doesn't support any of the .NET Generic Host abstractions, like
IHostedService
orIHostApplicationLifetime
(that are required by the OpenIddict system integration). The MAUI team is already aware of this limitation, but since there are persistent rumors indicating that the MAUI project is under-funded and doesn't have the human resources needed for such a project, it's not clear whether things will change any time soon. In the meantime, it is possible to work around that by using adapters, as shown in this pull request: Add a MAUI (WinUI-only) client sample.Similarly to MAUI, Avalonia UI doesn't natively support the .NET Generic Host, but it should be possible to use it side-by-side with the regular Avalania UI host model.
To make things – hopefully – a bit easier, here's a matrix listing the different Windows/.NET versions and application models:
Windows version | .NET runtime version | Console | WinForms | WPF | WinUI 2 | WinUI 3 | MAUI |
---|---|---|---|---|---|---|---|
Windows 7 SP1 | .NET Framework 4.6.1 | ✔ | ✔ | ✔ | ❌ | ❌ | ❌ |
Windows 7 SP1 | .NET Framework 4.7.2 | ✔ | ✔ | ✔ | ❌ | ❌ | ❌ |
Windows 7 SP1 | .NET Framework 4.8 | ✔ | ✔ | ✔ | ❌ | ❌ | ❌ |
Windows 7 SP1 | .NET 6.0 | ✔ | ✔ | ✔ | ❌ | ❌ | ❌ |
Windows 7 SP1 | .NET 7.0 | ❗ | ❗ | ❗ | ❌ | ❌ | ❌ |
Windows 8.1 | .NET Framework 4.6.1 | ✔ | ✔ | ✔ | ❌ | ❌ | ❌ |
Windows 8.1 | .NET Framework 4.7.2 | ✔ | ✔ | ✔ | ❌ | ❌ | ❌ |
Windows 8.1 | .NET Framework 4.8 | ✔ | ✔ | ✔ | ❌ | ❌ | ❌ |
Windows 8.1 | .NET 6.0 | ✔ | ✔ | ✔ | ❌ | ❌ | ❌ |
Windows 8.1 | .NET 7.0 | ❗ | ❗ | ❗ | ❌ | ❌ | ❌ |
Windows 8.1 | .NET Native/UAP | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
Windows 10 1507 | .NET Framework 4.6.1 | ✔ | ✔ | ✔ | ❌ | ❌ | ❌ |
Windows 10 1507 | .NET Framework 4.7.2 | ✔ | ✔ | ✔ | ❌ | ❌ | ❌ |
Windows 10 1507 | .NET Framework 4.8 | ✔ | ✔ | ✔ | ❌ | ❌ | ❌ |
Windows 10 1507 | .NET 6.0 | ❗ | ❗ | ❗ | ❌ | ❌ | ❌ |
Windows 10 1507 | .NET 7.0 | ❗ | ❗ | ❗ | ❌ | ❌ | ❌ |
Windows 10 1507 | .NET Native/UAP | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
Windows 10 1809 | .NET Framework 4.6.1 | ✔ | ✔ | ✔ | ❌ | ⚠ | ❌ |
Windows 10 1809 | .NET Framework 4.7.2 | ✔ | ✔ | ✔ | ❌ | ⚠ | ❌ |
Windows 10 1809 | .NET Framework 4.8 | ✔ | ✔ | ✔ | ❌ | ⚠ | ❌ |
Windows 10 1809 | .NET 6.0 | ✔ | ✔ | ✔ | ❌ | ⚠ | ⚠ |
Windows 10 1809 | .NET 7.0 | ✔ | ✔ | ✔ | ❌ | ⚠ | ⚠ |
Windows 10 1809 | .NET Native/UAP | ❌ | ❌ | ❌ | ⚠ | ❌ | ❌ |
Windows 11 21H2 | .NET Framework 4.6.1 | ✔ | ✔ | ✔ | ❌ | ⚠ | ❌ |
Windows 11 21H2 | .NET Framework 4.7.2 | ✔ | ✔ | ✔ | ❌ | ⚠ | ❌ |
Windows 11 21H2 | .NET Framework 4.8 | ✔ | ✔ | ✔ | ❌ | ⚠ | ❌ |
Windows 11 21H2 | .NET 6.0 | ✔ | ✔ | ✔ | ❌ | ⚠ | ⚠ |
Windows 11 21H2 | .NET 7.0 | ✔ | ✔ | ✔ | ❌ | ⚠ | ⚠ |
Windows 11 21H2 | .NET Native/UAP | ❌ | ❌ | ❌ | ⚠ | ❌ | ❌ |
Microsoft officially stopped supporting Windows 7 in .NET 7.0. As such, applications that still need to be usable on Windows 7 should probably stay on .NET Framework 4.8 (or .NET 6.0, but it should be noted that it will reach EoL in November 2024).
What do I need to know before using OpenIddict.Client.SystemIntegration
?
As of OpenIddict 4.5, OpenIddict.Client.SystemIntegration
is no longer considered experimental and using <EnablePreviewFeatures>true</EnablePreviewFeatures>
is no longer necessary.
First, it's important to note that OpenIddict.Client.SystemIntegration
may be subject to API or behavior changes depending on the feedback received after the OpenIddict 4.1 release. To reflect that, the OpenIddict.Client.SystemIntegration
package explicitly requires adding <EnablePreviewFeatures>true</EnablePreviewFeatures>
to your .csproj
file to use any of its APIs.
Given that they all share the same base OpenIddict.Client
package, using OpenIddict.Client.SystemIntegration
is not fundamentally different than using OpenIddict.Client.AspNetCore
or OpenIddict.Client.AspNetCore
:
1 | services.AddDbContext<DbContext>(options => |
That said, two specific points deserve a special attention:
Signing and encryption credentials
Just like the ASP.NET Core and OWIN hosts, OpenIddict.Client.SystemIntegration
requires registering a signing and an encryption key (or a X.509 certificate) to protect the state tokens created by OpenIddict. There are, however, important requirements that must be respected for production applications:
- The keys MUST be stored in a place that is only accessible by the user account running the application (i.e each user MUST have his/her own key set).
- The same keys MUST be accessible by all the instances of the application running under the same user account.
On Windows, the recommended option is to use Microsoft's Cryptography API: Next Generation
API (aka CNG) to generate on-the-fly and persist the keys in a safe place. Here's an example:
1 | static RsaSecurityKey GetRsaCngKey(string name, CngKeyUsages usages, CngProvider provider = null) |
1 | services.AddOpenIddict() |
On Linux, a new 4096-bit RSA key can be generated using RSA.Create(4096)
, exported using the RSA.ExportRSAPrivateKey()
API introduced in .NET Core 3.0 and written to a file that is only accessible by the current user.
Database
To keep track of the state tokens it produces and offer native protection against token replays, the OpenIddict client requires having access to a database shared by all the instances of your application. If your application already uses MongoDB, Entity Framework 6.4.4+ or Entity Framework Core, you can leverage the corresponding OpenIddict package to easily use your preferred database, as you'd do with ASP.NET Core or OWIN.
If your application doesn't use any of the supported providers, the recommendation is to use Entity Framework Core + SQLite:
1 | services.AddDbContext<DbContext>(options => |
1 | services.AddOpenIddict() |
As for the encryption and signing keys, the database MUST be stored in a place that is only accessible by the current user (e.g on Windows, the most common option is to store it in %AppData%
alongside your per-user configuration files).
Are samples already available?
Yes! You can find dedicated samples in the openiddict/openiddict-samples
repository:
Application model | Supported OS | |
---|---|---|
Console | Windows, Linux | Mimban.Client |
WinForms | Windows | Sorgan.WinForms.Client |
WPF | Windows | Sorgan.Wpf.Client |
Blazor Hybrid (on WPF) | Windows | Sorgan.BlazorHybrid.Client |
What's next?
Support for additional platforms like iOS, Android, macOS or WASM (for browser-based apps) will depend on community interest, so if you're interested in seeing a specific platform being supported, please add your voice below!