Skip to content

[FEATURE] Authentication provider for the HAI3 framework #259

@gs-layer

Description

@gs-layer

Description

Headless authentication provider for the HAI3 framework — a pluggable auth() plugin that wires any AuthProvider implementation into the existing @hai3/api REST transport layer, automatically managing credentials, token refresh, and session lifecycle without coupling to any specific auth backend or UI framework.

Problem Statement

HAI3 applications currently have no standardized way to handle authentication. Each project implements its own ad-hoc solution for:

  • Attaching credentials (Bearer tokens, cookies) to API requests
  • Handling 401 responses and token refresh
  • Managing session lifecycle (login, logout, refresh, destroy)
  • Authorization checks (canAccess, permissions)

This leads to duplicated boilerplate, inconsistent error handling, and tight coupling between auth logic and UI components. There is no framework-level contract that separates session acquisition (how you log in) from session transport (how credentials reach the server).

Proposed Solution

A three-layer architecture:

1. @hai3/auth (new L1 SDK package) — headless contract, types only, zero dependencies:

  • AuthProvider interface with 3 required methods (getSession, checkAuth, logout) and 8 optional (login, refresh, handleCallback, getIdentity, getPermissions, canAccess, subscribe, destroy)
  • AuthSession discriminated by kind: bearer, cookie, custom — supporting different auth mechanisms through a single contract
  • AuthContext carries AbortSignal into every provider call for cancellation support

2. auth() framework plugin (in @hai3/framework) — transport binding:

  • Registers AuthRestPlugin as a global REST plugin via apiRegistry.plugins.add()
  • Bearer: attaches Authorization: Bearer <token> header on every request
  • Cookie-session: sets withCredentials: true + optional CSRF header for relative URLs and allowlisted origins
  • 401 refresh+retry: on first 401, calls provider.refresh(), deduplicates concurrent refresh calls into a single in-flight promise, retries original request with new credentials
  • Custom transport: pass a transport option to override the default binding entirely
  • Exposes app.auth runtime surface (getSession, login, logout, etc.) via a new generic provides.app + HAI3AppRuntimeExtensions mechanism

3. REST protocol enhancements (in @hai3/api):

  • RestRequestOptions (params, signal, withCredentials) with backward-compatible overloads
  • AbortSignal propagation through plugin context and axios; axios.isCancel bypasses onError chain
  • Retry preserves plugin-modified request context correctly

Use Case / Example

  • Bearer token (JWT/OAuth): SPA logs in via password or OAuth redirect, stores access+refresh tokens in memory, auth() plugin attaches Bearer header and auto-refreshes on 401
  • Cookie-session (SSR/CSRF): Server sets HttpOnly cookie, plugin enables withCredentials and attaches CSRF token header for mutating requests
  • Custom SSO: Enterprise SSO provider implements AuthProvider.login() with SAML/OIDC redirect flow, handleCallback() processes the redirect response — no framework changes needed
  • Authorization guards: app.auth.canAccess({ action: 'edit', resource: 'invoice', record }) returns 'allow' or 'deny' — UI components use this for conditional rendering
  • Preset integration: createHAI3App({ auth: { provider } }) — single line to wire auth into the full preset

Affected Package(s)

  • @hai3/framework — auth() plugin, HAI3AppRuntimeExtensions, preset integration
  • @hai3/react — re-exports auth types and plugin
  • @hai3/api — RestRequestOptions, AbortSignal support, retry mechanics, MockEventSource fix
  • @hai3/screensets
  • @hai3/cli
  • @hai3/i18n
  • @hai3/state
  • @hai3/studio

New package:

  • @hai3/auth — headless auth contract (L1 SDK, types only, zero deps)

Alternative Solutions

react-admin AuthProvider: considered as reference. Useful lifecycle methods, but tightly coupled to react-admin router semantics (redirectTo in every return value, useCheckAuth hook). HAI3 contract is router-agnostic — AuthTransition returns plain URL, no router semantics leak into the contract.

Transport-level only (no framework plugin): attaching tokens directly in service constructors. Rejected because it duplicates logic across services, doesn't handle 401 refresh globally, and forces each service to know about auth.

Middleware/interceptor approach: global axios interceptor. Rejected because it bypasses the HAI3 plugin architecture, doesn't support per-protocol plugin ordering, and makes testing harder.

Additional Context

  • HAI3AppRuntimeExtensions is a generic mechanism: any plugin can now expose typed APIs on app via provides.app + module augmentation — not auth-specific
  • All provider methods accept optional AuthContext with AbortSignal for request cancellation
  • Concurrent 401 refresh is deduplicated: multiple parallel requests hitting 401 share a single in-flight refresh() promise
  • provider.onTransportError() is called on every transport error (informational callback for logging/telemetry)
  • Cookie-session withCredentials is set per-request (not constructor-time), supporting mixed origin scenarios
  • Test coverage: 27 auth plugin tests + 30 REST plugin integration tests + existing suite (123 total, all green)

Priority

  • High - Blocking or critical for use case

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions