OIDC Client Library
Introduction
The ProAuth OIDC Client library (ProAuth.Oidc.Client) provides a comprehensive set of tools for integrating .NET applications with ProAuth as an OpenID Connect identity provider. It handles OAuth 2.0 / OIDC protocol operations, token management, and secure API communication.
While the BFF package uses this library internally for browser-based applications, you can also use the OIDC Client directly for:
- Backend services requiring service-to-service authentication
- API gateways that need to manage tokens
- Custom authentication flows
- Token exchange scenarios
- Applications requiring direct OIDC protocol access
Features
| Feature | Description |
|---|---|
| Token Management | Automatic token acquisition, caching, and refresh |
| Client Credentials | Service-to-service authentication |
| Token Exchange | RFC 8693 token exchange support |
| Token Introspection | Validate tokens via introspection endpoint |
| Token Revocation | Revoke access and refresh tokens |
| DPoP | Create RFC 9449 proofs for sender-constrained tokens |
| JWT-Secured Authorization Requests | Build signed request objects for authorization redirects and PAR |
| JWT Introspection Responses | Validate signed or encrypted JWT introspection responses |
| Discovery | Automatic OIDC discovery document handling |
| Distributed Storage | Pluggable token stores (In-Memory, Redis, Dapr, ReaFx) |
| TLS Configuration | Custom CA and certificate trust configuration |
Getting Started
Package Installation
dotnet add package ProAuth.Oidc.Client
# Add a token store provider
dotnet add package ProAuth.Oidc.Client.InMemory # Development
dotnet add package ProAuth.Oidc.Client.Redis # ProductionBasic Setup
using ProAuth.Oidc.Client;
using ProAuth.Oidc.Client.InMemory;
var builder = WebApplication.CreateBuilder(args);
// Configure authentication settings
builder.Services.Configure<AuthenticationSettings>(
builder.Configuration.GetSection("Authentication"));
// Add OIDC client services
builder.Services.AddOidcClient();
// Add token store and locking
builder.Services.AddSingleton<ITokenStore, InMemoryTokenStore>();
builder.Services.AddSingleton<ILockProvider, InProcessLockProvider>();
// Add HTTP client factory
builder.Services.AddHttpClient();
var app = builder.Build();Configuration
{
"Authentication": {
"Authority": "https://auth.example.com",
"ClientId": "my-service",
"ClientSecret": "service-secret",
"ClientAuthenticationMethod": "Auto",
"ServiceScopes": "openid api://my-api/.default",
"ServiceResources": "api://my-api"
}
}Configuration Options
| Setting | Type | Description |
|---|---|---|
Authority | string | ProAuth server URL |
ClientId | string | OAuth 2.0 client identifier |
ClientSecret | string | OAuth 2.0 client secret |
ClientAuthenticationMethod | string | Client authentication method for token, introspection, revocation, device authorization, and PAR requests. Supported values are Auto, None, ClientSecretPost, ClientSecretBasic, ClientSecretJwt, PrivateKeyJwt, TlsClientAuth, and SelfSignedTlsClientAuth. |
ClientAssertion | object | Settings for ClientSecretJwt and PrivateKeyJwt, including SigningAlgorithm, Certificate, CertificatePath, CertificatePassword, KeyId, and LifetimeInSeconds. |
MutualTls | object | Outbound client certificate settings for TlsClientAuth and SelfSignedTlsClientAuth, including Certificate, CertificatePath, and CertificatePassword. |
SecurityProfile | string | Set to Fapi2SecurityProfile to enable client-side FAPI 2.0 baseline checks. |
SenderConstrainedTokenMode | string | None, Dpop, or MutualTls. |
Dpop | object | RFC 9449 DPoP settings, including Enabled, JsonWebKey, GenerateEphemeralKeyWhenMissing, and nonce retry behavior. |
PushedAuthorizationBehavior | string | PAR behavior: UseIfAvailable, Disable, or Require. |
RequestObject | object | RFC 9101 JAR settings for signed authorization request objects. |
AuthorizationRequestClaims | string | Raw OIDC claims request parameter JSON to add to authorization requests. |
JwtIntrospection | object | Enables RFC 9701 JWT introspection response validation. |
ServiceScopes | string | Space-separated scopes for client credentials flow |
ServiceResources | string | Space-separated resource identifiers |
UserScopes | string | Scopes for user authentication flows |
UserResources | string | Resources for user authentication flows |
Advanced Client Authentication
ProAuth.Oidc.Client applies the configured client authentication method to all client-authenticated OAuth endpoints it calls: token, introspection, revocation, device authorization, and PAR.
Explicit secret-based methods fail closed. ClientSecretPost, ClientSecretBasic, and ClientSecretJwt require ClientSecret; startup or request construction fails instead of silently downgrading to public-client authentication. Auto uses ClientSecretBasic when a secret is configured and None when no secret is configured.
Use ClientSecretJwt to sign assertions with the configured client secret:
{
"Authentication": {
"Authority": "https://auth.example.com",
"ClientId": "my-service",
"ClientSecret": "service-secret",
"ClientAuthenticationMethod": "ClientSecretJwt"
}
}Use PrivateKeyJwt when the matching public key is registered as a ClientAppKeySet on the ProAuth client app with usage ClientAssertionSigning:
{
"Authentication": {
"Authority": "https://auth.example.com",
"ClientId": "my-service",
"ClientAuthenticationMethod": "PrivateKeyJwt",
"ClientAssertion": {
"CertificatePath": "/var/run/secrets/proauth/client-assertion.pfx",
"CertificatePassword": "<pfx-password>",
"KeyId": "client-key-1"
}
}
}Use TlsClientAuth or SelfSignedTlsClientAuth when ProAuth expects an outbound client certificate:
{
"Authentication": {
"Authority": "https://auth.example.com",
"ClientId": "my-service",
"ClientAuthenticationMethod": "TlsClientAuth",
"MutualTls": {
"CertificatePath": "/var/run/secrets/proauth/mtls-client.pfx",
"CertificatePassword": "<pfx-password>"
}
}
}Advanced Authorization Security
For FAPI 2.0-style clients, configure PAR plus sender-constrained tokens:
{
"Authentication": {
"Authority": "https://auth.example.com",
"ClientId": "my-service",
"SecurityProfile": "Fapi2SecurityProfile",
"PushedAuthorizationBehavior": "Require",
"SenderConstrainedTokenMode": "Dpop",
"Dpop": {
"Enabled": true,
"JsonWebKey": null,
"GenerateEphemeralKeyWhenMissing": true,
"RetryWithNonce": true
}
}
}The client library can add dpop_jkt to authorization requests, attach DPoP proofs to token endpoint calls, and retry once when ProAuth responds with a DPoP nonce challenge. For browser authorization requests, call PrepareAuthorizationRequest before building the redirect URL or before calling PAR. This applies configured OIDC claims, DPoP authorization binding, and JAR request objects.
var authRequest = oidcClient.PrepareAuthorizationRequest(new AuthRequest
{
ClientId = "my-service",
ResponseType = "code",
RedirectUri = "https://app.example.com/signin-oidc",
Scopes = new[] { "openid", "profile" },
State = state,
Nonce = nonce
});Enable RequestObject.Enabled to sign authorization parameters as a JWT request object. The request-object key can be supplied as base64 PFX certificate material or as a PFX file path through RequestObject.CertificateInput.
Use IntrospectTokenAsJwtAsync when ProAuth is configured to return RFC 9701 JWT introspection responses. The handler validates issuer, audience, signing keys, and configured token decryption keys before converting the JWT back to an introspection result.
Core Interfaces
IOidcClient
The IOidcClient interface provides direct access to OIDC protocol operations:
public interface IOidcClient
{
// Client credentials grant
Task<TokenResponse> ClientCredentialsAsync(
IEnumerable<string> scopes = null,
IEnumerable<string> resources = null,
CancellationToken cancellationToken = default);
// Refresh an access token
Task<TokenResponse> RefreshAccessTokenAsync(
string refreshToken,
IEnumerable<string> scopes = null,
IEnumerable<string> resources = null,
CancellationToken cancellationToken = default);
// Token exchange (RFC 8693)
Task<TokenResponse> ExchangeTokenAsync(
string subjectToken,
string subjectTokenType,
string requestedTokenType = null,
IEnumerable<string> scopes = null,
IEnumerable<string> resources = null,
string audience = null,
CancellationToken cancellationToken = default);
// Token introspection
Task<IntrospectionResponse> IntrospectTokenAsync(
string token,
string tokenTypeHint = null,
CancellationToken cancellationToken = default);
// Token revocation
Task<RevokeResponse> RevokeTokenAsync(
string token,
string tokenTypeHint = null,
CancellationToken cancellationToken = default);
}ITokenHandler
The ITokenHandler interface provides high-level token management with automatic caching and refresh:
public interface ITokenHandler
{
// Get access token for a user (from token store)
Task<JwtSecurityToken> GetAccessTokenForUser(
string subjectIdentifier,
CancellationToken cancellationToken = default);
// Get access token for service-to-service calls
Task<JwtSecurityToken> GetAccessTokenForService(
CancellationToken cancellationToken = default);
}ITokenStore
The ITokenStore interface defines token persistence:
public interface ITokenStore
{
Task<UserTokens?> GetUserTokens(string subjectIdentifier, CancellationToken ct = default);
Task<ServiceTokens?> GetServiceTokens(string subjectIdentifier, CancellationToken ct = default);
Task StoreUserTokens(string subjectIdentifier, UserTokens userTokens, CancellationToken ct = default);
Task StoreServiceTokens(string subjectIdentifier, ServiceTokens serviceTokens, CancellationToken ct = default);
}Usage Examples
Client Credentials Flow
For service-to-service authentication:
public class MyBackendService
{
private readonly ITokenHandler _tokenHandler;
private readonly HttpClient _httpClient;
public MyBackendService(ITokenHandler tokenHandler, IHttpClientFactory httpClientFactory)
{
_tokenHandler = tokenHandler;
_httpClient = httpClientFactory.CreateClient();
}
public async Task<string> CallProtectedApiAsync()
{
// Get service access token (automatically cached and refreshed)
var token = await _tokenHandler.GetAccessTokenForService();
// Call protected API
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token.RawData);
var response = await _httpClient.GetAsync("https://api.example.com/data");
return await response.Content.ReadAsStringAsync();
}
}Token Exchange
Exchange a user's token for a token with different scope/audience:
public class TokenExchangeService
{
private readonly IOidcClient _oidcClient;
public TokenExchangeService(IOidcClient oidcClient)
{
_oidcClient = oidcClient;
}
public async Task<string> ExchangeForDownstreamApiAsync(string userAccessToken)
{
var response = await _oidcClient.ExchangeTokenAsync(
subjectToken: userAccessToken,
subjectTokenType: "urn:ietf:params:oauth:token-type:access_token",
scopes: new[] { "api://downstream-api/.default" },
audience: "api://downstream-api");
if (response.IsError)
{
throw new InvalidOperationException($"Token exchange failed: {response.Error}");
}
return response.AccessToken;
}
}Token Introspection
Validate a token and get its claims:
public class TokenValidationService
{
private readonly IOidcClient _oidcClient;
public TokenValidationService(IOidcClient oidcClient)
{
_oidcClient = oidcClient;
}
public async Task<bool> ValidateTokenAsync(string token)
{
var response = await _oidcClient.IntrospectTokenAsync(
token: token,
tokenTypeHint: "access_token");
if (response.IsError)
{
return false;
}
return response.IsActive;
}
}Token Revocation
Revoke tokens when a user logs out:
public class LogoutService
{
private readonly IOidcClient _oidcClient;
public LogoutService(IOidcClient oidcClient)
{
_oidcClient = oidcClient;
}
public async Task RevokeUserTokensAsync(string accessToken, string refreshToken)
{
// Revoke refresh token first
if (!string.IsNullOrEmpty(refreshToken))
{
await _oidcClient.RevokeTokenAsync(refreshToken, "refresh_token");
}
// Revoke access token
if (!string.IsNullOrEmpty(accessToken))
{
await _oidcClient.RevokeTokenAsync(accessToken, "access_token");
}
}
}Token Store Providers
In-Memory Store
Package: ProAuth.Oidc.Client.InMemory
services.AddSingleton<ITokenStore, InMemoryTokenStore>();
services.AddSingleton<ILockProvider, InProcessLockProvider>();Features:
- Automatic cleanup of expired tokens
- Configurable sliding expiration
- Thread-safe concurrent access
Configuration:
services.Configure<InMemoryTokenStoreOptions>(options =>
{
options.CleanupIntervalMinutes = 5;
options.TokenSlidingExpirationMinutes = 60;
});Redis Store
Package: ProAuth.Oidc.Client.Redis
services.AddSingleton<ITokenStore, RedisTokenStore>();
services.AddSingleton<ILockProvider, RedisLockProvider>();Configuration:
{
"Redis": {
"ConnectionString": "localhost:6379,password=secret,ssl=true"
}
}Dapr Store
Package: ProAuth.Oidc.Client.Dapr
services.AddSingleton<ITokenStore, DaprTokenStore>();
services.AddSingleton<ILockProvider, DaprLockProvider>();Requires Dapr state store component.
ReaFx Store
Package: ProAuth.Oidc.Client.ReaFx
services.AddSingleton<ITokenStore, ReaFxTokenStore>();
services.AddSingleton<ILockProvider, ReaFxLockProvider>();INFO
ReaFx integration requires a ReaFx license.
TLS Configuration
For environments with custom CAs or self-signed certificates:
services.Configure<TlsCertificateValidationConfiguration>(options =>
{
// Trust specific CA certificates
options.CustomTrustedRootCaFilePaths = new[]
{
"/etc/ssl/certs/custom-ca.crt"
};
// Or trust specific server certificates
options.CustomTrustedTlsCertificatePaths = new[]
{
"/etc/ssl/certs/auth-server.crt"
};
// Enable hot-reload of certificates
options.EnableFileSystemWatcher = true;
});DANGER
Never use AcceptAnyServerCertificates = true in production. This disables all certificate validation and exposes your application to man-in-the-middle attacks.
Error Handling
All protocol operations return response objects with error information:
var response = await _oidcClient.ClientCredentialsAsync();
if (response.IsError)
{
_logger.LogError(
"Token acquisition failed: {Error} - {Description}",
response.Error,
response.ErrorDescription);
throw new AuthenticationException(response.Error);
}
// Use response.AccessTokenThread Safety and Concurrency
The OIDC Client library is designed for concurrent use:
- Token caching: Tokens are cached and only refreshed when necessary
- Distributed locking: Token refresh operations use distributed locks to prevent race conditions
- Thread-safe stores: All token store implementations are thread-safe
Best Practices
Token Refresh Strategy
The ITokenHandler automatically refreshes tokens 30 seconds before expiration. For long-running operations, consider:
// Check if token will expire soon
var token = await _tokenHandler.GetAccessTokenForService();
if (token.ValidTo < DateTime.UtcNow.AddMinutes(5))
{
// Token is close to expiration, get a fresh one
token = await _tokenHandler.GetAccessTokenForService();
}Connection Resilience
Configure HTTP clients with retry policies:
services.AddHttpClient("OidcClient")
.AddPolicyHandler(GetRetryPolicy());
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}Secure Secret Storage
Never store client secrets in source code:
// Use environment variables or secret management
var clientSecret = Environment.GetEnvironmentVariable("OIDC_CLIENT_SECRET")
?? throw new InvalidOperationException("Client secret not configured");See Also
- BFF (Backend-For-Frontend) - For browser-based applications
- Client Applications - Configure clients in ProAuth
- Token Exchange Configuration - Enable token exchange