Architecture
capacitor-oidc adapts oidc-client-ts to Capacitor. It does not implement OAuth or OpenID Connect protocol operations itself.
This document is the implementation boundary. Changes that move behavior across that boundary require an ADR.
Goals
- Expose one manager and configuration contract across web, iOS, and Android.
- Use Authorization Code Flow with PKCE for public clients on every platform.
- Present authorization and provider logout with platform authentication UI.
- Reuse
oidc-client-tsfor discovery, requests, response processing, user management, refresh, UserInfo, revocation, logout request construction, and events. - Use configurable browser storage on web and platform-secure storage on native.
- Let a future iOS or Android widget read the stored session.
- Keep the custom code and dependency surface small.
Non-goals
- Client secrets in an installed application.
- Resource Owner Password Credentials.
- Embedded WebView authentication.
- Iframe login, iframe logout, or browser session monitoring on native platforms.
- A custom
fetch, a globalfetchpatch, or native OIDC networking. - Guaranteed refresh while the operating system has suspended or terminated the application.
- DPoP in v1. DPoP requires a separate design for non-exportable native signing keys.
- Autonomous widget refresh in v1. Widgets may read the current session.
Runtime model
text
Application
|
v
CapacitorUserManager extends oidc-client-ts UserManager
|
+-- internal web manager ------> oidc-client-ts navigators/stores
|
+-- internal native manager ---> CapacitorNavigator + CapacitorSecureStateStore
|
v
native auth UI + TokenVault
All protocol networking uses normal global fetch.oidc-client-ts owns
- OIDC discovery and metadata.
- Authorization request construction.
- State, nonce, and PKCE values.
- Authorization response and token response processing.
- Code exchange and refresh-token grants.
- UserInfo and revocation requests.
- End-session request construction.
- The
Usermodel, events, and foreground expiry timer.
This package owns
- Adapting
INavigatorto native authentication UI. - Adapting
StateStoreto native secure storage. - A native-readable, versioned session record for widgets.
- Native refresh serialization and application-resume renewal.
- Runtime configuration resolution and a small portable API around interactive signin and signout.
The exported manager is the common API and factory. It normalizes layered or legacy input once, then creates an internal web or native implementation. The web implementation owns browser session-monitor disposal. The native implementation owns secure storage, native navigation, application lifecycle, and session snapshots. Platform behavior does not branch throughout the common manager.
This package must not own
- Protocol request or response parsing.
- Discovery, token, UserInfo, or revocation HTTP clients.
- JWT or JOSE algorithms.
- Provider-specific branches in native code.
Public API
The main type extends UserManager so callers retain its user model, events, and non-browser-specific methods.
ts
const manager = await CapacitorUserManager.create({
common: { authority, client_id, scope },
web: { settings: { redirect_uri: webRedirectUri } },
native: { settings: { redirect_uri: nativeRedirectUri } },
ios: { settings: iosOverrides },
android: { settings: androidOverrides },
});
await manager.signin();
const validUser = await manager.getValidUser();
await manager.revokeTokens();
await manager.signout();
await manager.cancel();
await manager.dispose();signin() and signout() are the recommended interactive methods and return Promise<void> on every platform. Web redirect navigation leaves the page; native and popup interactions complete within the promise. User state comes from events or getUser() / getValidUser().
Configuration resolves once at creation:
text
web: common -> web
iOS: common -> native -> ios
Android: common -> native -> androidAll merges are shallow. Every platform excludes client secrets, client authentication, disabling PKCE, and response-type changes. Web-specific settings such as custom stores, DPoP, iframe callbacks, and session monitoring are accepted only in the web section. Native forces secure stores and disables iframe session behavior.
Web runtime
The web branch uses the normal oidc-client-ts redirect, popup, iframe, and storage implementations. signinCallback() and signoutCallback() remain the application's browser-route boundary. This package adds no browser protocol or navigation implementation; it selects and configures the upstream behavior.
Web redirect is the default interactive mode. Popup mode is an explicit configuration choice. Configured default arguments and per-call arguments are merged shallowly, with the call taking precedence.
Interactive navigation
CapacitorNavigator implements INavigator. prepare() creates an IWindow; navigate() sends the URL to the native bridge and resolves with the complete callback URL; close() cancels the native session.
Only one native session may be active. Concurrent attempts fail with AUTH_SESSION_IN_PROGRESS.
iOS
- Use
ASWebAuthenticationSession. - Set a valid
presentationContextProviderfrom the Capacitor view hierarchy. - Default
prefersEphemeralWebBrowserSessiontofalsefor shared SSO. - Allow the caller to request an ephemeral session.
- Let iOS own the consent dialog, sheet, and browser appearance.
- Map system cancellation to
USER_CANCELLED. - Use the same implementation for authorization and end-session navigation.
Android
- Use AndroidX Auth Tab with its Custom Tab fallback, never WebView.
- Return redirects through a
singleTaskhost activity's intent handling. - Support custom-scheme redirects and verified App Links.
- Use the same implementation for authorization and end-session navigation.
Networking
oidc-client-ts uses the unmodified global fetch implementation.
- Do not patch
fetch. - Do not enable CapacitorHttp on behalf of the application.
- Do not provide a native fallback.
- Document that discovery, token, UserInfo, and revocation endpoints must allow the application's Capacitor origin through CORS.
- Require HTTPS authorization-server endpoints in production.
Static provider metadata can replace discovery, but it does not remove CORS requirements from the other endpoints.
Session renewal
oidc-client-ts performs scheduled foreground renewal. On native platforms, the adapter adds:
- A single-flight
signinSilent()so rotating refresh tokens cannot be used by concurrent refresh requests. - A foreground/resume check that refreshes an expired or nearly expired user.
getValidUser(minimumValiditySeconds)for authenticated call sites.- No iframe fallback when a refresh token is absent.
An invalid_grant refresh result clears the local session. A transient network failure does not destroy the refresh token.
This design does not promise exact refresh timing while the app is suspended. The web implementation retains the upstream signinSilent() behavior, including independent concurrent calls and their per-call arguments.
Secure storage
Both userStore and stateStore use CapacitorSecureStateStore, implementing:
ts
set(key, value);
get(key);
remove(key);
getAllKeys();The store has separate transaction and session namespaces. Writes are atomic and logs never contain stored values.
iOS TokenVault
- Store secrets as Keychain generic-password items.
- Disable synchronization.
- Use a configurable Keychain/App Group access group.
- Use a
ThisDeviceOnlyaccessibility class. - Do not require biometrics by default, because unattended renewal must remain possible.
Android TokenVault
- Generate a non-exportable AES-GCM key in Android Keystore.
- Store only ciphertext, IV, and format metadata in app-private no-backup storage.
- Do not use deprecated
EncryptedSharedPreferences. - Do not require user authentication for each key operation by default.
Widget compatibility
The storage implementation is a public native library component, not private code inside the Capacitor plugin class.
The vault exposes a versioned native session record:
ts
interface StoredSessionV1 {
version: 1;
issuer: string;
clientId: string;
accessToken: string;
refreshToken?: string;
idToken?: string;
tokenType: string;
scope?: string;
expiresAt?: number;
}An iOS widget uses the same Keychain/App Group entitlement. An Android widget in the same application package uses the same vault. V1 allows reading a valid token; native widget refresh is a future feature.
Errors
Keep adapter errors small and stable:
AUTH_SESSION_IN_PROGRESSUSER_CANCELLEDBROWSER_UNAVAILABLEINVALID_CALLBACKSECURE_STORAGE_ERRORUNSUPPORTED_RUNTIME
OIDC and OAuth server errors remain oidc-client-ts errors.
Security assumptions
The JavaScript runtime must provide crypto.subtle and crypto.getRandomValues. The selected OIDC engine must validate the required ID-token issuer, audience, and expiration claims. Current limitations and the stable-release decision are recorded in SECURITY.md.
Do not add package-local crypto or JWT validation to compensate for a protocol engine limitation. Reconsider the engine instead.
