← All @molecule/* packages · App templates

@molecule/app-auth

Core interface · auth · App (browser) · v1.1.0 · Apache-2.0

Client-side authentication interface for molecule.dev

npm install @molecule/app-auth

npm · Source on GitHub

How it works

@molecule/app-auth is the auth core interface on the app (browser) side: the API your app calls, with no vendor inside.

Bond a provider to choose the implementation.

import { createJWTAuthClient, setClient } from '@molecule/app-auth'

const client = createJWTAuthClient({ baseURL: '/api' })
setClient(client)
await client.initialize() // restore the session from the httpOnly cookie

// anywhere in the app
import { getUser, isAuthenticated, login, logout } from '@molecule/app-auth'
await login({ email, password })
if (isAuthenticated()) console.log(getUser()?.email)

Works with: @molecule/app-bond, @molecule/app-i18n, @molecule/app-logger

Reference

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Client-side authentication interface for molecule.dev.

Provides a unified auth API that works across different auth strategies (JWT, session, OAuth, etc.): createJWTAuthClient builds a fetch-based client; setClient/getClient bond it; login/logout/register/ isAuthenticated/getUser delegate to the bonded client. Framework bindings (e.g. AuthProvider/useAuth in @molecule/app-react) wrap the same client.

Quick Start

import { createJWTAuthClient, setClient } from '@molecule/app-auth'

const client = createJWTAuthClient({ baseURL: '/api' })
setClient(client)
await client.initialize() // restore the session from the httpOnly cookie

// anywhere in the app
import { getUser, isAuthenticated, login, logout } from '@molecule/app-auth'
await login({ email, password })
if (isAuthenticated()) console.log(getUser()?.email)

Type

core

Installation

npm install @molecule/app-auth @molecule/app-bond @molecule/app-i18n @molecule/app-logger

API

Interfaces

AuthClient

Auth client interface that all auth bond packages must implement. Provides login/logout/register flows, token management, profile updates, and auth state subscription.

interface AuthClient<T = UserProfile> {
  /**
   * Returns the current authentication state snapshot.
   */
  getState(): AuthState<T>

  /**
   * Returns whether the user is currently authenticated.
   */
  isAuthenticated(): boolean

  /**
   * Gets the current user.
   */
  getUser(): T | null

  /**
   * Updates the cached user object (state + persistent storage) without
   * hitting the network. Intended for local refreshes after a per-app
   * mutation (e.g., the user just PATCHed their own profile and the
   * server returned the canonical row). Does NOT change tokens.
   */
  setUser(user: T | null): void

  /**
   * Gets the current access token.
   */
  getAccessToken(): string | null

  /**
   * Stores the access token in the configured token storage adapter (in-memory
   * by default). Use this to seed the token after an out-of-band exchange (e.g.
   * the OAuth code→token redirect) instead of writing to `localStorage` directly,
   * which would violate the in-memory-default storage contract and make the bearer
   * token JS-readable (XSS-exfiltratable). Pass `null` to clear it.
   */
  setAccessToken(token: string | null): void

  /**
   * Gets the refresh token.
   */
  getRefreshToken(): string | null

  /**
   * Logs in with credentials.
   */
  login(credentials: LoginCredentials): Promise<AuthResult<T>>

  /**
   * Logs out the current user.
   */
  logout(): Promise<void>

  /**
   * Registers a new user.
   */
  register(data: RegisterData): Promise<AuthResult<T>>

  /**
   * Refreshes the access token.
   */
  refresh(): Promise<AuthResult<T>>

  /**
   * Requests a password reset.
   */
  requestPasswordReset(data: PasswordResetRequest): Promise<void>

  /**
   * Confirms a password reset.
   */
  confirmPasswordReset(data: PasswordResetConfirm): Promise<void>

  /**
   * Updates the current user's profile.
   */
  updateProfile(data: Partial<T>): Promise<T>

  /**
   * Changes the current user's password.
   */
  changePassword(oldPassword: string, newPassword: string): Promise<void>

  /**
   * Initializes auth state (e.g., from stored tokens).
   */
  initialize(): Promise<void>

  /**
   * Subscribes to auth state changes.
   */
  subscribe(callback: (state: AuthState<T>) => void): () => void

  /**
   * Subscribes to auth state changes (alias for subscribe).
   */
  onAuthChange(callback: (state: AuthState<T>) => void): () => void

  /**
   * Gets the current access token (alias for getAccessToken).
   */
  getToken?(): string | null

  /**
   * Adds an auth event listener.
   */
  addEventListener(listener: AuthEventListener): () => void

  /**
   * Destroys the auth client.
   */
  destroy(): void
}

AuthClientConfig

Configuration options for creating an auth client, including API endpoints, storage strategy, token refresh, and OAuth providers.

interface AuthClientConfig {
  /**
   * API base URL.
   */
  baseURL?: string

  /**
   * Login endpoint.
   */
  loginEndpoint?: string

  /**
   * Logout endpoint.
   */
  logoutEndpoint?: string

  /**
   * Register endpoint.
   */
  registerEndpoint?: string

  /**
   * Refresh endpoint.
   */
  refreshEndpoint?: string

  /**
   * User profile endpoint.
   */
  profileEndpoint?: string

  /**
   * Current-user endpoint for session restore (default `/users/me`). Called by
   * `initialize()` with credentials to re-establish the session from the
   * httpOnly cookie after a full page load — the only way to stay logged in when
   * the bearer token is held in memory (the secure default; a localStorage copy
   * is XSS-exfiltratable). Must return the user (as `user` or `props`).
   */
  currentUserEndpoint?: string

  /**
   * Storage key prefix.
   */
  storagePrefix?: string

  /**
   * Token storage type or custom storage adapter.
   * - 'memory': In-memory storage (lost on page refresh)
   * - StorageAdapter: Custom storage implementation (e.g., from `@molecule/app-storage`)
   *
   * @example
   * ```typescript
   * // Use in-memory storage
   * storage: 'memory'
   *
   * // Use custom storage from `@molecule/app-storage`
   * import { getProvider } from '@molecule/app-storage'
   * const storageProvider = getProvider()
   * storage: {
   *   getItem: (key) => storageProvider.get(key),
   *   setItem: (key, value) => storageProvider.set(key, value),
   *   removeItem: (key) => storageProvider.remove(key),
   * }
   * ```
   */
  storage?: 'memory' | StorageAdapter

  /**
   * Decide whether a user returned by the cookie-restore probe
   * (`currentUserEndpoint`) constitutes a signed-in CLIENT session. Return
   * `false` to reject the restore: the client stays unauthenticated and the
   * `mol_auth` presence hint is cleared so later loads skip the probe (the
   * httpOnly session cookie is untouched and keeps authenticating API calls).
   * Use this when the server also issues sessions that must never appear
   * signed-in client-side — e.g. an anonymous/guest session whose user row
   * (`isAnonymous`) would otherwise hydrate as a logged-in user. Defaults to
   * accepting every restored user.
   * @param user - The user returned by the restore probe.
   * @returns Whether to establish the client session from this user.
   */
  shouldRestoreUser?: (user: UserProfile) => boolean

  /**
   * Auto refresh tokens before expiry.
   */
  autoRefresh?: boolean

  /**
   * Refresh tokens this many seconds before expiry.
   */
  refreshBuffer?: number

  /**
   * Forgot password / password reset request endpoint.
   */
  forgotPasswordEndpoint?: string

  /**
   * Password reset confirmation endpoint.
   */
  resetPasswordEndpoint?: string

  /**
   * Change password endpoint.
   */
  changePasswordEndpoint?: string

  /**
   * Available OAuth providers (e.g., ['github', 'google', 'gitlab']).
   */
  oauthProviders?: string[]

  /**
   * OAuth endpoint path (default: '/oauth').
   */
  oauthEndpoint?: string
}

AuthResult

Successful authentication result containing the user, access/refresh tokens, and expiration.

interface AuthResult<T = UserProfile> {
  /**
   * Authenticated user.
   */
  user: T

  /**
   * Access token (for token-based auth).
   */
  accessToken?: string

  /**
   * Refresh token (for token-based auth).
   */
  refreshToken?: string

  /**
   * Token expiration time (Unix timestamp).
   */
  expiresAt?: number

  /**
   * Set (with HTTP 206) when the account has two-factor auth enabled and the
   * login did not include a valid `twoFactorToken`. No tokens or user are
   * issued until login is retried with the TOTP — callers must check this
   * flag BEFORE treating the result as a successful authentication.
   */
  twoFactorRequired?: boolean
}

AuthState

Reactive authentication state snapshot (initialized, authenticated, user, loading, and error).

interface AuthState<T = UserProfile> {
  /**
   * Whether auth state has been initialized.
   */
  initialized: boolean

  /**
   * Whether the user is authenticated.
   */
  authenticated: boolean

  /**
   * Current user (if authenticated).
   */
  user: T | null

  /**
   * Whether an auth operation is in progress.
   */
  loading: boolean

  /**
   * Last auth error (if any).
   */
  error: string | null
}

LoginCredentials

Email/password login credentials with optional "remember me" flag.

interface LoginCredentials {
  /**
   * Email or username.
   */
  email: string

  /**
   * Password.
   */
  password: string

  /**
   * Whether to remember the session.
   */
  remember?: boolean

  /**
   * Time-based one-time password (TOTP) from the user's authenticator app.
   * Required when the account has two-factor auth enabled — the server
   * answers `{ twoFactorRequired: true }` (HTTP 206) until a valid token
   * is supplied.
   */
  twoFactorToken?: string
}

PasswordResetConfirm

Password reset confirmation data.

interface PasswordResetConfirm {
  /**
   * Reset token.
   */
  token: string

  /**
   * New password.
   */
  password: string
}

PasswordResetRequest

Password reset request data.

interface PasswordResetRequest {
  /**
   * Email address.
   */
  email: string
}

RegisterData

New user registration payload (email, password, optional name and metadata).

interface RegisterData {
  /**
   * Email address.
   */
  email: string

  /**
   * Password.
   */
  password: string

  /**
   * Display name.
   */
  name?: string

  /**
   * Additional registration fields.
   */
  metadata?: Record<string, unknown>
}

StorageAdapter

Minimal key-value storage adapter used by the auth client for persisting tokens and user data.

interface StorageAdapter {
  getItem(key: string): string | null
  setItem(key: string, value: string): void
  removeItem(key: string): void
}

TokenStorage

Internal token storage interface for reading and writing access/refresh tokens and user profile data.

interface TokenStorage {
  getAccessToken(): string | null
  setAccessToken(token: string | null): void
  getRefreshToken(): string | null
  setRefreshToken(token: string | null): void
  getUser<T = UserProfile>(): T | null
  setUser<T = UserProfile>(user: T | null): void
  clear(): void
}

UserProfile

User profile information.

interface UserProfile {
  /**
   * Unique user identifier.
   */
  id: string

  /**
   * User's email address.
   */
  email?: string

  /**
   * User's display name.
   */
  name?: string

  /**
   * User's avatar/profile image URL.
   */
  avatar?: string

  /**
   * User's roles or permissions.
   */
  roles?: string[]

  /**
   * Additional metadata.
   */
  metadata?: Record<string, unknown>
}

Types

AuthEvent

Auth event types.

type AuthEvent =
  | { type: 'login'; user: UserProfile }
  | { type: 'logout' }
  | { type: 'register'; user: UserProfile }
  | { type: 'refresh' }
  | { type: 'error'; error: string }

AuthEventListener

Callback invoked when an authentication event occurs (login, logout, etc.).

type AuthEventListener = (event: AuthEvent) => void

Functions

createJWTAuthClient(config)

Creates a simple JWT-based auth client.

This is a basic implementation that can be extended or replaced with more sophisticated auth providers.

function createJWTAuthClient(config?: AuthClientConfig): AuthClient<T>
  • config — Auth client configuration including endpoints, storage, and refresh settings.

Returns: A fully configured AuthClient instance.

createTokenStorage(storage, prefix)

Creates a token storage implementation backed by either in-memory storage or a custom StorageAdapter.

function createTokenStorage(storage?: 'memory' | StorageAdapter, prefix?: string): TokenStorage
  • storage'memory' for in-memory storage (lost on refresh), or a StorageAdapter for persistent storage.
  • prefix — Key prefix for storage items (default: 'molecule:auth:').

Returns: A TokenStorage instance.

getClient()

Retrieves the bonded auth client, throwing if none is configured.

function getClient(): AuthClient<T>

Returns: The bonded auth client.

getTokenExpiration(token)

Returns the expiration timestamp of a JWT token in milliseconds.

function getTokenExpiration(token: string): number | null
  • token — The raw JWT string.

Returns: The expiration time in ms since epoch, or null if the token has no exp claim.

getUser()

Returns the current user profile, or null if not authenticated.

function getUser(): T | null

Returns: The user profile, or null if not authenticated.

hasClient()

Checks whether an auth client is currently bonded.

function hasClient(): boolean

Returns: true if an auth client is bonded.

isAuthenticated()

Checks if the current user is authenticated.

function isAuthenticated(): boolean

Returns: true if the user has an active session.

isTokenExpired(token, bufferSeconds)

Checks if a JWT token is expired (or will expire within the buffer window).

function isTokenExpired(token: string, bufferSeconds?: number): boolean
  • token — The raw JWT string.
  • bufferSeconds — Seconds before actual expiry to consider the token expired (default: 0).

Returns: true if the token is expired or lacks an exp claim.

login(credentials)

Logs in with the given credentials (email/username + password).

function login(credentials: LoginCredentials): Promise<AuthResult<UserProfile>>
  • credentials — Email/username and password.

Returns: The auth result containing tokens and user profile.

logout()

Logs out the current user, clearing tokens and session state.

function logout(): Promise<void>

Returns: A promise that resolves when logout completes.

parseJWT(token)

Parses a JWT token payload without verification (base64-decodes the payload segment).

function parseJWT(token: string): T | null
  • token — The raw JWT string (header.payload.signature).

Returns: The decoded payload object, or null if parsing fails.

register(data)

Registers a new user account.

function register(data: RegisterData): Promise<AuthResult<UserProfile>>
  • data — Registration data (email, password, name, etc.).

Returns: The auth result containing tokens and user profile.

setClient(client)

Registers an auth client as the active singleton. Called by bond packages during application startup.

function setClient(client: AuthClient<UserProfile>): void
  • client — The auth client implementation to bond.

Injection Notes

Requirements

Peer dependencies:

  • @molecule/app-bond ^1.0.1
  • @molecule/app-i18n ^1.0.1
  • @molecule/app-logger ^1.0.1

Runtime Dependencies

  • @molecule/app-bond

  • @molecule/app-i18n

  • @molecule/app-logger

  • A page reload "logging the user out" is the config default, not a bug. Token storage defaults to 'memory' (the secure default — a localStorage bearer token is XSS-exfiltratable). Staying signed in across reloads works by calling client.initialize() once at startup: it re-fetches currentUserEndpoint (default /users/me) with the httpOnly cookie the API set at login. Wire initialize(); do NOT "fix" reload-logout by copying tokens into localStorage.

  • Endpoint defaults must match your API's real routes (/auth/login, /auth/register, /auth/refresh, /users/logout, /users/me, …). Align them via createJWTAuthClient({ baseURL, ...endpoints }) — never by hand-editing fetch calls in components.

  • Client-side auth state is UX only — the server enforces authorization on every request; hiding a screen is not protection.

E2E Tests

Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:

  • Logging in with VALID credentials on the real login screen resolves login() and flips AuthState to authenticated: true with getUser() returning the actual UserProfile — the UI shows that user's own email / name / avatar, never a placeholder or a different account.
  • A WRONG password shows a visible error (AuthState error is set and the promise rejects) and establishes NO session: isAuthenticated() stays false and no protected screen becomes reachable.
  • Registering through the sign-up form calls register(), creates the account, and lands authenticated (authenticated: true, user populated) on the app's post-signup screen — not back on the login form. If the flow shows an email-verification / "check your inbox" step, it must be genuinely backed (this client's register() returns an authenticated session directly and exposes no verify method) — never a dead screen the auth contract cannot advance past.
  • Logout calls logout() and clears the session (authenticated: false, user: null, tokens cleared); afterward every protected screen redirects back to login.
  • Visiting a protected route while logged OUT redirects to login (the guard reads isAuthenticated()), and after authenticating you land back on the originally-requested screen rather than a generic home.
  • The session survives a FULL page reload: after login, hard-reload → initialize() restores it from the httpOnly cookie (via currentUserEndpoint, default /users/me) and you stay signed in — you are NOT bounced to login. (The in-memory bearer token is dropped by design; the cookie restore is what keeps you in — do not "fix" this by writing the token to localStorage.)
  • Password reset round-trips: requestPasswordReset({ email }) then confirmPasswordReset({ token, password }) with the emailed token, after which login() with the NEW password succeeds and the OLD password no longer works.
  • Authorization holds: a signed-in user only ever reads/edits their OWN record (getUser() is the caller's profile, never another user's); the bearer token lives in memory only — it is never written to localStorage / sessionStorage where another script could read it; and an expired/invalid token fails closed — the app re-authenticates (refresh() or logout()), never silently serving another user's data.

Translations

Translation strings are provided by @molecule/app-locales-auth.