Skip to content
Version v3.0.0

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

FeatureDescription
Token ManagementAutomatic token acquisition, caching, and refresh
Client CredentialsService-to-service authentication
Token ExchangeRFC 8693 token exchange support
Token IntrospectionValidate tokens via introspection endpoint
Token RevocationRevoke access and refresh tokens
DPoPCreate RFC 9449 proofs for sender-constrained tokens
JWT-Secured Authorization RequestsBuild signed request objects for authorization redirects and PAR
JWT Introspection ResponsesValidate signed or encrypted JWT introspection responses
DiscoveryAutomatic OIDC discovery document handling
Distributed StoragePluggable token stores (In-Memory, Redis, Dapr, ReaFx)
TLS ConfigurationCustom CA and certificate trust configuration

Getting Started

Package Installation

bash
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     # Production

Basic Setup

csharp
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

json
{
  "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

SettingTypeDescription
AuthoritystringProAuth server URL
ClientIdstringOAuth 2.0 client identifier
ClientSecretstringOAuth 2.0 client secret
ClientAuthenticationMethodstringClient authentication method for token, introspection, revocation, device authorization, and PAR requests. Supported values are Auto, None, ClientSecretPost, ClientSecretBasic, ClientSecretJwt, PrivateKeyJwt, TlsClientAuth, and SelfSignedTlsClientAuth.
ClientAssertionobjectSettings for ClientSecretJwt and PrivateKeyJwt, including SigningAlgorithm, Certificate, CertificatePath, CertificatePassword, KeyId, and LifetimeInSeconds.
MutualTlsobjectOutbound client certificate settings for TlsClientAuth and SelfSignedTlsClientAuth, including Certificate, CertificatePath, and CertificatePassword.
SecurityProfilestringSet to Fapi2SecurityProfile to enable client-side FAPI 2.0 baseline checks.
SenderConstrainedTokenModestringNone, Dpop, or MutualTls.
DpopobjectRFC 9449 DPoP settings, including Enabled, JsonWebKey, GenerateEphemeralKeyWhenMissing, and nonce retry behavior.
PushedAuthorizationBehaviorstringPAR behavior: UseIfAvailable, Disable, or Require.
RequestObjectobjectRFC 9101 JAR settings for signed authorization request objects.
AuthorizationRequestClaimsstringRaw OIDC claims request parameter JSON to add to authorization requests.
JwtIntrospectionobjectEnables RFC 9701 JWT introspection response validation.
ServiceScopesstringSpace-separated scopes for client credentials flow
ServiceResourcesstringSpace-separated resource identifiers
UserScopesstringScopes for user authentication flows
UserResourcesstringResources 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:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "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.

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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

csharp
services.AddSingleton<ITokenStore, InMemoryTokenStore>();
services.AddSingleton<ILockProvider, InProcessLockProvider>();

Features:

  • Automatic cleanup of expired tokens
  • Configurable sliding expiration
  • Thread-safe concurrent access

Configuration:

csharp
services.Configure<InMemoryTokenStoreOptions>(options =>
{
    options.CleanupIntervalMinutes = 5;
    options.TokenSlidingExpirationMinutes = 60;
});

Redis Store

Package: ProAuth.Oidc.Client.Redis

csharp
services.AddSingleton<ITokenStore, RedisTokenStore>();
services.AddSingleton<ILockProvider, RedisLockProvider>();

Configuration:

json
{
  "Redis": {
    "ConnectionString": "localhost:6379,password=secret,ssl=true"
  }
}

Dapr Store

Package: ProAuth.Oidc.Client.Dapr

csharp
services.AddSingleton<ITokenStore, DaprTokenStore>();
services.AddSingleton<ILockProvider, DaprLockProvider>();

Requires Dapr state store component.

ReaFx Store

Package: ProAuth.Oidc.Client.ReaFx

csharp
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:

csharp
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:

csharp
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.AccessToken

Thread 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:

csharp
// 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:

csharp
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:

csharp
// Use environment variables or secret management
var clientSecret = Environment.GetEnvironmentVariable("OIDC_CLIENT_SECRET")
    ?? throw new InvalidOperationException("Client secret not configured");

See Also