Skip to content

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
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",
    "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
ServiceScopesstringSpace-separated scopes for client credentials flow
ServiceResourcesstringSpace-separated resource identifiers
UserScopesstringScopes for user authentication flows
UserResourcesstringResources for user authentication flows

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