Authentication & Session Overview
How WildwoodComponents handles authentication, JWT token lifecycle, session management, and multi-tenant identity across Blazor Server, WebAssembly, and MAUI applications.
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:
- The API issues an access token (short-lived JWT) and a refresh token (long-lived) upon successful login.
- The access token is stored in local storage via
ILocalStorageServiceunder the keyaccessToken. - The refresh token is stored separately under the key
refreshToken. - The
AuthenticationServicesets the access token as theAuthorization: Bearerheader on itsHttpClientfor all subsequent API calls. - The
WildwoodSessionManagerparses the JWTexpclaim and schedules a proactive refresh at 80% of the token's remaining lifetime.
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.
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.
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
- Session Management — Deep dive into
WildwoodSessionManagerconfiguration and lifecycle - Token Refresh — How proactive and reactive token refresh works
- Two-Factor Authentication — 2FA methods, trusted devices, and recovery codes
- Login Flow — Step-by-step walkthrough of the complete login process