Desktop and mobile clients
A desktop app (WPF, Windows Forms, console) or a .NET MAUI app signs its user in with OpenID Connect
in a browser, keeps the tokens, and sends the access token to the APIs it calls.
Arc4u.OAuth2.Client.Authentication does this with
Duende.IdentityModel.OidcClient: it
registers the OidcClient, a token provider that signs in, refreshes and
caches the tokens, and an HttpClient handler.
MSAL status
Arc4u 9 does not ship an MSAL (Microsoft Authentication Library) package: the project that
wrapped MSAL (package ID Arc4u.OAuth2.Msal) is removed. The 9.0 roadmap (#129) drops MSAL support and adds
IdentityModel for WPF and MAUI apps instead: that is Arc4u.OAuth2.Client.Authentication.
See MSAL status in the migration guide and the
package support page.
BlazorMsalTokenProvider in Arc4u.OAuth2.Blazor is not affected: it relies on the Microsoft
WebAssembly authentication, not on this package (see Blazor).
Install
dotnet add package Arc4u.OAuth2.Client.Authentication --prerelease
The package references Arc4u.OAuth2 and Duende.IdentityModel.OidcClient. You also need an
IBrowser implementation that shows the sign-in page: Duende provides samples for desktop apps
(a system browser with a loopback redirect), and MAUI has WebAuthenticator.
Configuration
The configuration overload of AddOidcClientAuthentication reads the Authentication section
(pass authenticationSectionName to use another one) and registers the settings named
OidcClient (pass settingsKey to change it). The client ID and scopes are read from the section
whose full path is in the OidcClientIdSettingsSectionPath key of that section, by default
Authentication:OidcClient.Settings: this path does not follow authenticationSectionName.
{
"Authentication": {
"DefaultAuthority": {
"Url": "https://login.microsoftonline.com/<tenant-id>/v2.0"
},
"OidcClient.Settings": {
"ClientId": "<client-id>",
"Scopes": [ "openid", "profile", "offline_access", "api://inventory/access" ]
},
"CallbackPath": "http://127.0.0.1:7890/",
"PostLogoutRedirectUri": "http://127.0.0.1:7890/"
},
"Application.Configuration": {
"ApplicationName": "Inventory.Desktop",
"Environment": {
"Name": "Development",
"LoggingName": "Inventory.Desktop",
"TimeZone": "UTC"
}
}
}
| Key | Type | Default | Description |
|---|---|---|---|
Authentication:DefaultAuthority:Url |
Uri |
none (required) | The authority. The metadata is read from <Url>/.well-known/openid-configuration, or from MetaDataAddress. |
Authentication:OidcClient.Settings:ClientId |
string |
none (required) | Client ID of the app (a public client: no secret). |
Authentication:OidcClient.Settings:Scopes |
string[] |
none (required) | Scopes requested. Add offline_access to get a refresh token. |
Authentication:OidcClient.Settings:ProviderId |
string |
OidcClientIdentityModel |
Key of the token provider. |
Authentication:CallbackPath |
string |
/ |
Redirect URI registered for the app. |
Authentication:PostLogoutRedirectUri |
string |
/ |
Redirect URI after sign-out. |
Authentication:LoadProfile |
bool |
false |
Loads the claims of the user info endpoint. |
Authentication:ForceRefreshTimeoutTimeSpan |
TimeSpan |
00:05:00 |
The access token is renewed with the refresh token once it expires in less than this value. |
Authentication:RefreshTokenLifetime |
TimeSpan |
90.00:00:00 |
Assumed lifetime of a refresh token. Older than that, the user logs in again. Set it to the lifetime configured in the identity provider. |
Authentication:ClockSkew |
TimeSpan |
00:05:00 |
Tolerated clock difference when the tokens are validated. |
Authentication:LoginTimeout |
TimeSpan |
00:05:00 |
Longest time an interactive login (or the logout of a sign-out) can take in the browser. The token calls wait for the login in progress; a call that waits longer fails, and a login or logout that does not complete in time is cancelled. |
Authentication:SecureCacheTimeout |
TimeSpan |
00:00:30 |
Longest time a read, write or removal of the tokens in the secure cache can take during a sign-out or a token write. |
Application.Configuration is read by AddApplicationConfig in the code below (see
Configuration). HTTPS is required for the metadata, except when the
authority is localhost. The Authentication section also
takes the claims settings of the server authentication (ClaimsIdentifier, DomainsMapping,
ClaimsMiddleWare:ClaimsFiller); see Server authentication.
The extra parameters of Authentication:OidcClient.Settings:AuthorizationEndpoint are added to the
authorize request (in the browser), those of Authentication:OidcClient.Settings:TokenEndpoint to
the token requests (code exchange and refresh).
Note
Since 9.0, the client authentication has no NameClaimType, RoleClaimType or
ValidateAuthority: they were never used. The principal of the client is built by the
AppPrincipalFactory. Remove them from your configuration and code.
Register the services
// Program.cs
using Arc4u.Caching;
using Arc4u.Configuration;
using Arc4u.Dependency;
using Arc4u.OAuth2.Client.Authentication.Cache;
using Arc4u.OAuth2.Client.Authentication.Token;
using Arc4u.OAuth2.Client.Authentication.TokenProvider;
using Arc4u.OAuth2.Extensions;
using Arc4u.OAuth2.Token;
using Arc4u.Security.Principal;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddILogger();
builder.Services.AddApplicationConfig(builder.Configuration);
builder.Services.AddSingleton<IApplicationContext, ApplicationInstanceContext>();
builder.Services.AddSingleton<ISecureCache, CacheTokens>();
builder.Services.AddOidcClientAuthentication(new SystemBrowser(), NullLoggerFactory.Instance, builder.Configuration);
builder.Services.AddKeyedSingleton<ITokenProvider, OidcClientIdentityModelTokenProvider>(OidcClientIdentityModelTokenProvider.TokenProviderName);
builder.Services.AddHttpClient<InventoryClient>(client => client.BaseAddress = new Uri("https://inventory.example.com/"))
.AddHttpMessageHandler(sp => new JwtHttpHandler<InventoryClient>(
sp,
sp.GetRequiredService<ILogger<InventoryClient>>(),
"OidcClient"));
using var host = builder.Build();
SystemBrowser is your IBrowser implementation. CacheTokens names its files after the
environment name and logging name of Application.Configuration.
The first call through the handler signs the user in: OidcClientIdentityModelTokenProvider
returns the cached access token while it is valid, redeems the refresh token when the access token
expires in less than ForceRefreshTimeoutTimeSpan, and opens the browser when there is no valid
refresh token or the authority rejects it. The tokens are kept in
the ISecureCache.
Caution
CacheTokens writes the tokens, refresh token
included, as plain JSON files in the OAuth2 folder of the user's local application data
(%LOCALAPPDATA% on Windows, ~/.local/share on Linux, ~/Library/Application Support on
macOS). The files get the default permissions of the process: on Linux and macOS they are
typically readable by the other local users (-rw-r--r--) unless the home folder blocks access;
on Windows the folder is protected by the user's access control list. Register your own
ISecureCache that encrypts them (for example with the Windows Data Protection API, or
SecureStorage in MAUI) when this is not acceptable.
The handler (JwtHttpHandler<T>) needs the
IApplicationContext when it is created, and throws ConfigurationException without it. It uses
the token type as the scheme and, when there is a principal, adds a culture header. The trace
context travels in the standard W3C traceparent header that HttpClient adds. Its constructor that takes a settings
name (for example OidcClient) resolves the named SimpleKeyValueSettings options registered by
AddOidcClientAuthentication (or any services.Configure<SimpleKeyValueSettings>(name, ...)).
Build the principal
AppPrincipalFactory builds the AppPrincipal of the
user from the claims of the access token and of the claims filler, caches the claims in the
ISecureCache (to work offline), and sets the principal in the IApplicationContext. Call
CreatePrincipalAsync(settings) with the OidcClient settings (or CreatePrincipalAsync("OidcClient"))
at startup, and SignOutUserAsync(settings, cancellationToken) to sign out. The overloads without
settings use the settings named OAuth2, resolved like the handler does. It needs an INetworkInformation
(AlwaysConnected when the app has no offline mode) and an ICacheKeyGenerator.
Sign in with a user name and password
UsernamePasswordTokenProvider (key usernamePassword, package
Arc4u.OAuth2.Client) asks the user for credentials through your IUserNamePasswordProvider,
keeps them in the ISecureCache, and gets a token with the password grant through the
CredentialDirect provider. Prefer the browser sign-in above: the password grant does not support
multi-factor authentication.
Caution
The user name and the password are stored in the same ISecureCache as the token. With
CacheTokens, the password is written to disk in clear text. Use an ISecureCache that
encrypts its values with this provider.
Troubleshooting
The API returns 401 after the sign-in window was closed
When the authority rejects the refresh token, OidcClientIdentityModelTokenProvider drops the
tokens and signs the user in again through the browser. When the sign-in fails or is cancelled, it
returns a failed result: the desktop JwtHttpHandler<T> logs the error in the Technical category
and sends the request without a token, so the API answers 401. Sign the user in again and retry.
ConfigurationException: No settings found for OidcClient
No named SimpleKeyValueSettings options are registered with that name (a keyed
IKeyValueSettings is not used). Check the settingsKey given to AddOidcClientAuthentication (the
default is OidcClient). For the same reason, AppPrincipalFactory.CreatePrincipalAsync(name)
returns a failed result.
See also
- Token providers
- HttpClient and gRPC clients
- Migrate from Arc4u 8.x to 9, for the ADAL removal
- Arc4u.OAuth2.Client.Authentication in the API reference