Esc

Type to search across all documentation pages...

/ Developer Docs
Feedback Docs Home Landing Page Log in

Authentication & Session Overview

How WildwoodComponents handles authentication, JWT token lifecycle, session management, and multi-tenant identity across Blazor Server, WebAssembly, and MAUI applications.

Key Concept WildwoodComponents provides a complete authentication pipeline: a Blazor UI component handles user interaction, a service layer communicates with the WildwoodAPI, and a session manager maintains JWT lifecycle with automatic refresh. All operations are scoped by AppId for multi-tenant isolation.

Architecture Overview

The authentication system consists of three primary layers that work together to provide a seamless login experience:

Layer Class Responsibility
UI AuthenticationComponent Blazor component rendering login/registration forms, OAuth buttons, passkey prompts, and 2FA challenge screens
Service AuthenticationService HTTP client that calls WildwoodAPI endpoints, stores tokens via ILocalStorageService, and fires auth change events
Session WildwoodSessionManager Manages JWT lifecycle: proactive refresh timers, reactive 401 handling, session expiry detection, and app resume recovery

End-to-End Authentication Flow

The following describes the complete path from user action to established session:

User enters credentials
    |
    v
AuthenticationComponent (Blazor UI)
    |  Calls AuthenticationService.LoginAsync()
    v
AuthenticationService (HTTP Client)
    |  POST /api/auth/login  { Username, Password, AppId, Platform, DeviceInfo }
    v
WildwoodAPI (Server)
    |  Validates credentials, checks 2FA, generates JWT + RefreshToken
    v
AuthenticationResponse returned
    |  Contains: JwtToken, RefreshToken, UserId, Email, Roles, Permissions
    |  May contain: RequiresTwoFactor, RequiresPasswordReset
    v
AuthenticationService stores tokens
    |  localStorage: accessToken, refreshToken, user
    |  Sets Authorization header on HttpClient
    |  Fires OnAuthenticationChanged event
    v
WildwoodSessionManager.LoginAsync()
    |  Stores auth data + session expiry in localStorage
    |  Sets token on IAIService (cross-service sync)
    |  Schedules proactive refresh timer at 80% of JWT lifetime
    v
Session established - app continues with authenticated user

Core Components

AuthenticationComponent

The primary UI component that handles all authentication modes. It inherits from BaseWildwoodComponent for automatic error handling and loading state management.

<AuthenticationComponent
    AppId="@AppId"
    OnAuthenticationSuccess="@HandleLogin"
    OnAuthenticationError="@HandleError"
    ShowPasswordField="true"
    ShowDetailedErrors="false" />
Parameter Type Description
AppId string? The application ID for multi-tenant scoping. Determines which auth providers, captcha config, and password policies apply.
OnAuthenticationSuccess EventCallback<AuthenticationResponse> Callback fired after successful authentication (including post-2FA). Receives the full AuthenticationResponse.
OnAuthenticationError EventCallback<string> Callback fired when authentication fails. Receives the error message string.
ShowPasswordField bool Whether to show the password input. Defaults to true. Set to false for OAuth-only flows.
ShowDetailedErrors bool Whether to display detailed error messages. Defaults to true. Can be overridden by server-side AuthenticationConfiguration.ShowDetailedErrors.
Title string? Custom title for the auth card. Defaults to "Sign In" or "Create Account" depending on mode.
AvailableProviders List<AuthProvider>? Pre-loaded auth providers. If null, the component fetches them from the API using the AppId.
CaptchaConfig CaptchaConfiguration? Pre-loaded captcha configuration. If null, fetched automatically.
AuthConfig AuthenticationConfiguration? Pre-loaded authentication configuration. If null, fetched automatically.

AuthenticationService

The service layer that communicates with the WildwoodAPI. It is registered as a scoped service and uses HttpClient with the configured base URL.

public interface IAuthenticationService
{
    Task<AuthenticationResponse> LoginAsync(LoginRequest request);
    Task<AuthenticationResponse> RegisterAsync(RegistrationRequest request);
    Task<AuthenticationResponse> RegisterWithTokenAsync(RegistrationRequest request);
    Task<AuthenticationResponse> AuthenticateWithProviderAsync(string providerName, string appId);
    Task<List<AuthProvider>> GetAvailableProvidersAsync(string appId);
    Task<CaptchaConfiguration?> GetCaptchaConfigurationAsync(string appId);
    Task<AuthenticationConfiguration?> GetAuthenticationConfigurationAsync(string appId);
    Task<bool> RefreshTokenAsync();
    Task LogoutAsync();

    // Passkey/WebAuthn
    Task<object> GetPasskeyAuthenticationOptionsAsync(string appId);
    Task<AuthenticationResponse> VerifyPasskeyAuthenticationAsync(string appId, JsonElement credential);

    // Two-Factor
    Task<TwoFactorSendCodeResponse> SendTwoFactorCodeAsync(string sessionId);
    Task<TwoFactorVerifyResponse> VerifyTwoFactorCodeAsync(TwoFactorVerifyRequest request);
    Task<TwoFactorVerifyResponse> VerifyTwoFactorRecoveryCodeAsync(string sessionId, string recoveryCode, string ipAddress);

    event Action<AuthenticationResponse>? OnAuthenticationChanged;
    event Action? OnLogout;
}

WildwoodSessionManager

Orchestrates session lifecycle. See the Session Management page for a deep dive.

public interface IWildwoodSessionManager : IDisposable
{
    bool IsAuthenticated { get; }
    bool IsInitialized { get; }
    string? AccessToken { get; }
    string? UserId { get; }
    string? UserEmail { get; }
    AuthenticationResponse? CurrentUser { get; }

    Task InitializeAsync();
    Task<bool> LoginAsync(AuthenticationResponse authResponse);
    Task LogoutAsync();
    Task<bool> RefreshTokenAsync();
    Task OnAppResumedAsync();
    Task TouchSessionAsync();

    event EventHandler? SessionExpired;
    event EventHandler<AuthenticationResponse>? TokenRefreshed;
    event EventHandler? SessionInitialized;
}

JWT-Based Authentication

WildwoodComponents uses JWT (JSON Web Tokens) for authentication. The token flow works as follows:

  1. The API issues an access token (short-lived JWT) and a refresh token (long-lived) upon successful login.
  2. The access token is stored in local storage via ILocalStorageService under the key accessToken.
  3. The refresh token is stored separately under the key refreshToken.
  4. The AuthenticationService sets the access token as the Authorization: Bearer header on its HttpClient for all subsequent API calls.
  5. The WildwoodSessionManager parses the JWT exp claim and schedules a proactive refresh at 80% of the token's remaining lifetime.
Storage Keys The AuthenticationService uses the keys accessToken, refreshToken, and user in local storage. The WildwoodSessionManager uses ww_auth_data and ww_session_expiry. Both layers must remain in sync.

Multi-Tenant Scoping

Every authentication operation is scoped by AppId. This ID determines:

  • Which auth providers are available (Google, Facebook, Microsoft, Apple, GitHub, passkeys)
  • Which password policy applies (minimum length, uppercase, digit, special character requirements)
  • Whether captcha is required (Google reCAPTCHA configuration per app)
  • Whether registration is allowed (open registration, token-based registration, or disabled)
  • Whether 2FA is enforced and which methods are available
  • The JWT claims issued by the API (including app_id, company_id, roles, and permissions)
// The AppId scopes all configuration loading
<AuthenticationComponent AppId="d6e61c7a-eec5-4164-a004-9b99eb5eb6de" />

// The component automatically loads app-specific configuration:
// GET /api/AppComponentConfigurations/{appId}/auth-configuration
// GET /api/AppComponentConfigurations/{appId}/auth-providers
// GET /api/AppComponentConfigurations/{appId}/captcha

Supported Authentication Methods

Method Description API Endpoint
Local (Email/Password) Username + password authentication with configurable password policy POST /api/auth/login
OAuth (Google, Facebook, Microsoft, Apple, GitHub) Social login via configured OAuth providers. Providers are per-app configurable. Provider-specific redirect flow
Passkeys / WebAuthn Biometric/hardware key authentication using the Web Authentication API POST /api/webauthn/authenticate/options, POST /api/webauthn/authenticate
Two-Factor Authentication TOTP authenticator apps, email codes, SMS codes, and recovery codes POST /api/twofactor/verify, POST /api/twofactor/send-code
Registration Tokens Invite-based registration using pre-generated tokens with associated pricing models POST /api/userregistration/register-with-token

Scoped API Keys (Service Keys)

Alongside user authentication, WildwoodAPI authenticates machine callers with API keys sent in the X-API-Key header. Every app has rows in one ApiKeys table, but they come in two kinds that must never be confused:

Client API key (ambient) Service key (scoped)
Who holds it Every SDK client / app runtime — it is distributed The seeder / server-side automation only
Scopes Never — the server refuses to add any At least one, set at mint (e.g. tiers:manage)
Mirrored to the app record Yes — that is how the App Details page displays it Never — mirroring would hand the capability to every client
Expiry Optional Forced — default 1 year
Value visible Any time, on the app record Exactly once, at mint

A scope opts a key into one specific authorization policy — it never grants a role. Each policy accepts an Admin/CompanyAdmin JWT or an api-key principal carrying its scope, so a headless caller (the Wildwood Seeder) can manage the app's tier catalog with tiers:manage, provision its AI assistant with ai:manage, read its own encrypted settings in plaintext with settings:read, and define its per-app roles and user assignments with roles:manage. Entitlement granting (subscriptions, tier changes, overrides, usage resets), secret material, and key minting itself deliberately stay JWT+role only.

roles:manage is additionally bound to one app: the server compares the key's own app_id claim to the route's {appId}, so a key minted for app A is refused on app B even when the same company owns both. Only the five writes carry the policy; the two role reads admit whatever the controller's per-app filter admits — an api-key principal for its own app (deliberately including the plain browser-shipped client key, so an app can list its own roles without a service key), or any authenticated JWT caller whose company owns the app. That second path is wider than one app: a signed-in user of one app can read the roles, and a given user's assignments, of a sibling app in the same company. Accepted by design — role names and assignments are not secret — but do not encode anything sensitive in a role name or description.

Because a service key is a normal ApiKeys row for its app, it authenticates everywhere the plain key does plus the scoped surfaces — so automation like the seeder needs only one credential: the service key. Mint one under App Details → Service Keys in this admin (Admin/CompanyAdmin only). The key value is shown once at mint and can never be retrieved again; the forced expiry (1 year unless a custom date is set) means service keys need periodic rotation (mint new → swap environment → revoke old). A key's scopes, name and expiry stay editable after minting — only the value is fixed — so widening a seeder's capability does not require a rotation.

A service key has three states, and the two inactive ones are not the same thing. Disabled (IsActive = false) is a pause you can undo; Revoked additionally stamps RevokedAt and is permanent — enable, update and regenerate all refuse a revoked key. Both fail closed on the key's very next request, since nothing caches key validation.

That makes disable the right tool for intermittent automation. Rather than leaving a tiers:manage key live for the months between provisioning jobs, keep it disabled on the Service Keys card and enable it only for the minutes a seed run takes, then disable it again. The credential exists, is auditable, and is inert the rest of the time. Reserve revoke for a key that is being retired or is suspected leaked.

The client key can never be scoped. The API resolves scopes purely by key value, and the client key is mirrored to the app record and distributed with every client — scoping it would hand the elevated capability to every client holding it. The server rejects any attempt; mint a separate service key instead.

See the API Keys reference for the endpoints, the scope discovery route, the disable/enable/revoke lifecycle, and the role gates on mint/update/revoke.

Service Registration

All authentication services are registered via the AddWildwoodComponents extension method:

// Program.cs
builder.Services.AddWildwoodComponents(options =>
{
    options.BaseUrl = "https://your-api.example.com";
    options.SessionExpirationMinutes = 60;         // Session duration (default: 60)
    options.EnableAutoTokenRefresh = true;          // Enable proactive + reactive refresh
    options.SlidingExpiration = true;               // Extend session on activity
    options.PersistentSession = false;              // Survive browser close?
    options.EnableAntiforgeryValidation = true;     // CSRF protection
});

This registers IAuthenticationService, IWildwoodSessionManager, ILocalStorageService, ICaptchaService, and all required dependencies as scoped services.

Configuration from appsettings.json You can also read options from configuration using the IConfiguration overload:
builder.Services.AddWildwoodComponents(builder.Configuration, "WildwoodAPI");
This reads from the WildwoodAPI section in appsettings.json, including BaseUrl, SessionExpirationMinutes, EnableAutoTokenRefresh, SlidingExpiration, and PersistentSession.

Key Models

AuthenticationResponse

Returned by all successful authentication operations. Contains the JWT token pair and user identity.

public class AuthenticationResponse
{
    public string UserId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public string JwtToken { get; set; }           // Short-lived access token
    public string RefreshToken { get; set; }        // Long-lived refresh token
    public bool RequiresTwoFactor { get; set; }     // True if 2FA challenge needed
    public bool RequiresPasswordReset { get; set; } // True if temp password used
    public List<string> Roles { get; set; }
    public string? CompanyId { get; set; }
    public List<string> Permissions { get; set; }

    // 2FA fields (only when RequiresTwoFactor == true)
    public string? TwoFactorSessionId { get; set; }
    public List<TwoFactorMethodInfo>? AvailableTwoFactorMethods { get; set; }
    public string? DefaultTwoFactorMethod { get; set; }
    public int? TwoFactorSessionExpiresIn { get; set; }
}

LoginRequest

public class LoginRequest
{
    public string Username { get; set; }            // Primary login identifier
    public string Email { get; set; }               // Optional, backward compat
    public string? Password { get; set; }
    public string? AppId { get; set; }              // Multi-tenant scoping
    public bool RememberMe { get; set; }
    public string? CaptchaResponse { get; set; }
    public string? Platform { get; set; }           // Auto-detected: Web, iOS, Windows, etc.
    public string? DeviceInfo { get; set; }         // User agent + platform info
    public string? TrustedDeviceToken { get; set; } // For 2FA bypass on remembered devices
    public string? ProviderName { get; set; }       // For OAuth flows
    public string? ProviderToken { get; set; }      // OAuth provider token
}

Next Steps

Last updated: February 2026
Something went wrong on this page. Reload 🗙