How to use @owlmeans/client-auth — the browser side of OwlMeans authentication. Root exports the auth service, the shared client entrypoints and the dispatcher HOC; ./manager holds the authentication screen, its control and the plugin registry; ./manager/plugins holds the authentication-plugin contract a plugin package writes against; ./login holds the login-plugin host, the sign-in method registry, the terms confirmation and the useLogin/useLogout hooks. Auto-invoked when importing client-au...
Installs into .claude/skills of the current project.
Are you the author of Client Auth?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/owlmeans-client-auth)
---
name: client-auth
description: How to use @owlmeans/client-auth — the browser side of OwlMeans authentication. Root exports the auth service, the shared client entrypoints and the dispatcher HOC; ./manager holds the authentication screen, its control and the plugin registry; ./manager/plugins holds the authentication-plugin contract a plugin package writes against; ./login holds the login-plugin host, the sign-in method registry, the terms confirmation and the useLogin/useLogout hooks. Auto-invoked when importing client-auth helpers, registering an authentication plugin, or wiring a sign-in control.
user-invocable: false
---
# @owlmeans/client-auth
**Layer:** Client
**Install:** `"@owlmeans/client-auth": "^0.1.18-rc.59"` in `dependencies`
Five subpaths, five jobs:
| Subpath | Job |
|---------|-----|
| `.` | The client `AuthService`, the shared auth entrypoints, and the dispatcher HOC |
| `./manager` | The registry's four names plus the authentication screen, control and error — *how* a user proves identity. Importing it registers the three shipped browser plugins and `pluginMethodSource` by side effect |
| `./manager/entrypoints` | The manager application's own front-end entrypoint bindings |
| `./manager/plugins` | The plugin-authoring surface, and a superset of the registry: the `AuthenticationPlugin` contract, `pluginMethodSource`, the wallet-tunnel helpers and the shipped plugin objects — with no registration side effect |
| `./login` | The **login**-plugin host — *where* the authorization round trip runs, and which method a person picks |
A plugin package imports from both, and the split is fixed: `AuthenticationPlugin`,
`AuthMethodMeta`, `pluginMethodSource`, `PinSchema`, `createWalletFacade`,
`TunnelAuthenticationRenderer` and the shipped plugin objects resolve **only** through
`./manager/plugins`; the registry (`plugins`, `registerAuthPlugin`, …) and the rendering types
(`AuthenticationRenderer`, `AuthenticationControl`, `ClientAuthType`, `AuthenticationHOC`) resolve
through `./manager`. Importing the contract from `./manager` gets an unresolvable name.
`./manager/plugins` and `./login` answer different questions and are separate registries. Do not
conflate them.
## Key Exports — root
| Export | Description |
|--------|-------------|
| `makeAuthService(alias?)` | The client `AuthService` — `authenticate(token)`, `update(token)`, `authenticated()`, `user()`, `store()`. Decodes the bearer envelope and persists the record |
| `appendAuthService(ctx, alias?)` | Register it, register `authMiddleware`, and expose `context.auth()` |
| `setupExternalAuthentication(service)` | Point the `CAUTHEN_FLOW_ENTER` entrypoint at a service, so an external provider can redirect into this app |
| `entrypoints` | The shared auth entrypoint list with `DISPATCHER_AUTHEN` bound |
| `DEFAULT_ALIAS` | `'auth'` — the client-side counterpart of `DEFAULT_GUARD` |
| `AUTH_RESOURCE` | `'auth'` — the resource the token record is stored in |
| `USER_ID` | `'user'` — the id of that single record |
| `DEFAULT_ENTITY` | `'owlmeans'` |
| `DispatcherHOC` | The return-leg HOC. It wraps a `DispatcherRenderer`, hands it `provideToken(token, query)` and `navigate()`, adopts the supplied token through the auth service, and strips `AUTH_QUERY` before navigating on. Reading the return-leg query is the renderer's job — `@owlmeans/web-client` reads `AUTH_QUERY`, `@owlmeans/web-oidc-rp` also reads `OIDC_ERROR_QUERY` and forwards the remaining params |
| `DispatcherProps`, `TDispatcherHOC`, `DispatcherRenderer`, `DispatcherRendererProps` | Dispatcher types |
| `useWs(entrypoint, request?, options?)` | A socket hook that attaches the current token as the `AUTH_QUERY` param and refreshes it on every reconnect attempt (via `WsOptions.beforeConnect`), unless the request already carried its own token — in which case that stays untouched across reconnects too. `options` passes straight through to `@owlmeans/client-socket`'s `useWs`, so `{ reconnect: false }` still opts a stateful one-shot handshake out of retries (see `manager/plugins/tunnel-consumer.tsx`) |
| `useSelfAuth(force?)` | Whether this context is authenticated; navigates to `DISPATCHER` when it is not and `force` |
| `AuthServiceAppend`, `ClientAuthRecord`, `ClientAuthResource` | Types |
## `./manager` — the authentication screen and the registry
| Export | Description |
|--------|-------------|
| `plugins`, `authPluginHelper` → `.registerAuthPlugin`, `.getAuthPlugin`, `.listAuthPlugins` | The module-global registry |
| `AuthenticationHOC`, `AuthenticationProps`, `TAuthenticationHOC` | The screen that hosts a plugin's implementation |
| `makeControl`, `AuthenticationControl`, `AuthenticationControlState` | The control a plugin drives: `requestAllowence`, `authenticate`, and the state it persists across a provider redirect |
| `AuthenticationRenderer`, `AuthenticationRendererProps`, `ClientAuthType`, `ClientAuthenticationMethod`, `AuthenticationCallback` | Rendering contract |
| `TunnelConsumer`, `TunnelAuthenticationProps`, `TunnelAuthCallback` | The wallet-tunnel consumer screen |
| `AuthenCredError` | Thrown when the entered credential cannot be used |
## `./manager/plugins` — the authentication-plugin contract
| Export | Description |
|--------|-------------|
| `AuthenticationPlugin` | `type`, `Implementation`, optional `Renderer`, `requiresRenderer?`, `method?` and the `authenticate` / `beforeAuthenticate` / `afterAuthenticate` hooks |
| `PluginImplemnetation` (sic) | `(Renderer?) => FC<AuthenticationRendererProps>` — the shape of `Implementation` |
| `AuthMethodMeta` | How the plugin presents itself as a sign-in method — see `login-methods` |
| `plugins`, `authPluginHelper` | The same registry as `./manager` |
| `pluginMethodSource` | The `LoginMethodSource` that turns registered plugins into offerable methods |
| `createWalletFacade`, `PinSchema`, `PinForm`, `TunnelAuthenticationRenderer`, `TunnelAuthenticationRendererProps` | Wallet-tunnel helpers a consumer plugin builds on |
| `ed25519BasicUIPlugin`, `reCaptchaPlugin`, `tunnelConsumerUIPlugin` | The shipped plugin objects |
Shipped plugins register themselves by side effect: `basic-ed25519`, `re-captcha` and
`wallet-consumer` when `@owlmeans/client-auth/manager` is imported, OIDC and Google from
`@owlmeans/web-oidc-rp/auth/plugins`, the PK supervisor from `@owlmeans/web-auth`.
## `./login` — the login-plugin host
An `AuthenticationPlugin` answers how a credential is proven; a `LoginPlugin` answers in which
browsing context the round trip can complete at all.
| Export | Description |
|--------|-------------|
| `appendLogin(ctx)` | Register the host and expose it as `context.login()` |
| `makeLoginService(alias?)`, `ensureLoginService(ctx)` | The host itself — a **lazy** service, so `makeContext` can reach it at the Loading stage — and the idempotent getter a plugin package calls before `registerPlugin` |
| `LoginPlugin`, `LoginEnv`, `LoginRequest`, `LogoutRequest`, `LoginOutcome`, `LoginIntent`, `LoginService`, `LoginContext`, `LoginPrecondition` | The contract |
| `loginMethodsHelper` → `.registerMethodSource`, `.listMethodSources`, `.resolveLoginMethods`, `.primaryLoginMethod` | Which sign-in methods are offered — see `login-methods` |
| `LoginMethod`, `LoginMethodSource`, `LoginMethodContext` | Method types |
| `loginLandingOf(ctx)` → `.landAfterLogin(opts?)`, `.continueLogin(opts?)`, `.landingUrl(landing)`; `useContinueLogin()` | The post-login landing decision (`src/login/land.ts`) — see `login-plugins`. `landAfterLogin` runs due landing hooks then delegates to `continueLogin`, which walks pending `LoginStep`s, then `flowLandingOf(ctx).resumeSuspendedFlow`, then `LandOptions.fallback ?? { alias: HOME }`; `landingUrl` builds the absolute URL a plugin's own `window.location.href` needs; `useContinueLogin()` is what a step's own screen calls once satisfied |
| `LoginStep`, `LoginLanding`, `LoginLandingHook`, `LandOptions`, `LoginLandingParams` | Landing-seam types. `registerStep`/`steps`/`onLanded`/`landingHooks` on `LoginService` are the same replace-by-alias, priority-sorted registries as `registerPlugin`. `LoginStep` also carries `confirmsTerms?: boolean` (this step is where the Terms confirmation lives — see `loginTermsHelper.termsDeferred` below) and `required?: boolean` (a throw/timeout in `pending` reads as PENDING, not "not pending" — see `login-plugins`) |
| `LOGIN_STEP_TIMEOUT`, `LOGIN_LANDED_STORAGE` | A step's `pending`/a hook's `landed` is bounded by the former; the latter is where the last-landed token is recorded (the raw string, never a digest) |
| `loginTermsHelper` → `.resolveTerms`, `.termsAccepted`, `.acceptTerms`, `.termsSentence`, `ResolvedTerms`, `ResolvedTermsDocument`, `TermsSentencePart` | The confirmation. `resolveTerms` produces `documents` (what the checkbox agrees to: terms, then billing/product/custom when configured) and `notices` (what is only disclosed: privacy, plus cookies per its own inclusion rule) — see the terms-confirmation section of `login-methods`. The extra `LoginTermsConfig` fields (`billing`, `product`, `documents`, `revisions`, `showRevision`) are added by module augmentation in `src/login/terms-config.ts`, never by editing `@owlmeans/config` — importing anything from `@owlmeans/client-auth/login` pulls it in |
| `loginTermsHelper.termsLabelResolver(translate, locale)`, `loginTermsHelper.termsAcceptanceOf(resolved, locale?)`, `TermsAcceptanceRef` | The ONE document-label resolver and ONE wire-shape builder every terms renderer/recorder shares (`FallbackLoginScreen`, `web-panel`'s `LoginTerms`, `web-marketing-consent`'s Terms box and `termsRecorder`) — no more hand-kept duplicate `DEFAULT_LABEL`/`resolveLabelFor` per package. `termsAcceptanceOf` keeps only `{key, href, revisedAt}` per document — structurally `@owlmeans/marketing-consent`'s `TermsAcceptance`, with no dependency on that package |
| `loginTermsHelper.termsDeferred(ctx)` | True once a registered AND BOUND `LoginStep` declares `confirmsTerms` — see the "Deferring the confirmation" section of `login-methods`. Reads `ctx.hasService`/`.hasEntrypoint` directly, never `ensureLoginService` (which has the side effect of registering an empty host) |
| `resolveCredit`, `ResolvedCredit` | The credit and copyright line |
| `FallbackLoginScreen`, `LoginScreenProps` (now also carries `locale?: string`, for `Intl.ListFormat` and a custom document's locale-keyed label), `LoginScreenComponent` | The plain sign-in screen a relying party renders when no UI family registered one |
| `surrogatePath(ctx, target)`, `SurrogateTarget` | Where a surrogate login window opens; `null` on an older entrypoint list |
| `loginResumeHelper.resumeAction(outcome)`, `ResumeAction`, `loginResumeHelper.loginAttemptError(outcome)` | The one reading of a `resume` outcome, and the one reading of a finished attempt |
| `registerNotifier(notifier)` (on `LoginService`), `LoginNotifier` | Surfaces a `begin`/`logout` outcome that has no inline screen to render it on — e.g. a toast on `LoginOutcome.Blocked` for a header "Log in"/"Log out" control. `web-panel`'s `appendLoginScreen` registers a default; unregistered, it is silence |
| `enterOidcAuthorization(model)` | Move a flow to the step that can authorize — idempotent, call it before every `authenticate` |
| `loginTokenOf(ctx).adoptToken(token)`, `.revokeToken()` | The single adoption and de-adoption paths |
| `useLogin(target?)`, `useLogout(target?)` | Wiring for a sign-in / sign-out control. `useLogin`'s `target` is the screen to land on AFTER sign-in (parked, never navigated to first); `useLogout`'s is where the document goes once the session is gone |
| `loginStartOf(ctx).startLogin({ url, target?, go })`, `LoginStart` | The React-free decision behind `useLogin` (`src/login/start.ts`): a target with a session already held goes straight there; otherwise `begin` with the target and a continuation to `DISPATCHER` |
| `loginEnvHelper` → `.isEmbedded`, `.isSurrogate`, `.markSurrogate`, `.clearSurrogate`, `.defaultLoginEnv` | Environment probes the host builds `LoginEnv` from |
| `LOGIN_SERVICE`, `LOGIN_SURROGATE_NAME`, `LOGIN_TOKEN_MESSAGE`, `LOGIN_LOGOUT_MESSAGE`, `LOGIN_SURROGATE_MARKER`, `LOGIN_SURROGATE_WIDTH`, `LOGIN_SURROGATE_HEIGHT`, `LOGIN_WATCH_INTERVAL`, `LOGIN_INTENT_QUERY`, `LOGIN_NEXT_QUERY`, `LOGIN_METHOD_QUERY`, `LOGIN_TERMS_STORAGE`, `LOGIN_TARGET_TTL`, `DEFAULT_LOGIN_PRIORITY`, `DEFAULT_METHOD_ORDER` | Aliases and the fixed cross-document wire values. `LOGIN_SURROGATE_FEATURES` also still exports (deprecated, never centered) — `@owlmeans/web-client`'s `centeredPopupFeatures(LOGIN_SURROGATE_WIDTH, LOGIN_SURROGATE_HEIGHT)` is what the surrogate plugin actually opens the window with |
```typescript
import { useLogin, useLogout } from '@owlmeans/client-auth/login'
const [loginPath, onLogIn] = useLogin()
const onLogOut = useLogout()
```
Both handlers are deliberately **not** async and await nothing before delegating — a window opened
after the user gesture has finished being handled is eaten by the popup blocker. Each resolves its
entrypoint **inside** the hook, never at module scope — `DISPATCHER` for `useLogin`, the surrogate
path for `useLogout`. A module body runs before `registerEntrypoints`, so `useLogin`'s lookup at
top level throws `Entrypoint dispatcher not found` during import, taking the whole render down.
`useLogin(target)` signs in FIRST and lands on `target` after, in an ordinary tab (redirect plugin)
and in a framed application (surrogate popup) alike, with every pending post-sign-in step run on the
way. Its `navigate` continuation is ALWAYS `DISPATCHER` — never the target, because a guarded screen
reached before sign-in renders signed out. The target travels as `LoginRequest.target`: the facade's
`begin`, past the preconditions, parks it with `flowLandingOf(ctx).suspendLanding`
(`@owlmeans/client-flow`, the `RESUME_FLOW` record its `resumeSuspendedFlow` reads, expiring after
`LOGIN_TARGET_TTL`) — the write is started, never awaited before the plugin, so a popup still opens
inside the gesture — and holds the continuation until the write has landed. The dispatcher's
`landAfterLogin` (landing hooks → pending steps such as a consent screen → the parked target) then
ends on it. An attempt that signs nobody in (`Blocked`, `Failed`, `Gesture`) discards the parked
target; a refused precondition never parks it. A session already held (the auth service's in-memory
`token`) goes straight to the target. Without a target, `useLogin()` is the plain sign-in to the
ordinary landing. A caller of `login().begin` that passes a `target` without a `navigate` leaves the
document at once, so its parked target is best-effort.
`useLogout` sends a continuation only when a target is given. Omitting it does not leave the session
behind: the token is revoked either way, and the plugin decides what the document does with no
continuation to run — `@owlmeans/web-client`'s redirect plugin reloads the page, because cached auth
reads only forget a session when the application is rebuilt. `useLogin` returns `[path, handler]`
(the path is the dispatcher's, with or without a target) and `useLogout` a bare handler — a logout
control has no address to point at.
Plugin selection, the shipped browser flows and the full invariant list: the `login-plugins` skill.
## Usage
```typescript
import { appendAuthService, setupExternalAuthentication, DEFAULT_ALIAS } from '@owlmeans/client-auth'
import { appendLogin } from '@owlmeans/client-auth/login'
// in makeContext:
appendAuthService(context) // registers under DEFAULT_ALIAS ('auth')
appendLogin(context)
setupExternalAuthentication(MY_WEB_SERVICE) // the service alias the provider redirects into
```
`@owlmeans/web-client`'s own `makeContext` already appends the auth service and the login host, so
a web application only calls these when it builds its context by hand.
## Rules
- The bearer token lives in the `AUTH_RESOURCE` resource under the single id `USER_ID`. Read and
write it through the auth service or through `loginTokenOf(ctx).adoptToken` / `.revokeToken`;
hand-written storage access drifts from the envelope decoding that happens beside it.
- A plugin that persists state across a provider redirect drives the control's own
`persist()` / `restore()` / `hasPersistentState()` / `cleanUpState()`. They keep the type, stage
and allowance in the `FLOW_STATE` resource under an id this package owns and does not export —
restore before submitting the credential, and clean up after.
- After a sign-in, `DispatcherHOC.navigate` calls `loginLandingOf(ctx).landAfterLogin` — a pending
`LoginStep`, else a landing suspended in `@owlmeans/client-flow`
(`flowLandingOf(ctx).resumeSuspendedFlow`, one-shot, an entrypoint alias plus its query — parked
by a flow's `.suspendFlow` or by `useLogin(target)`), else `HOME` — read BEFORE `alias` is
defaulted to `HOME`. Login plugins that navigate on their own do the same — see `login-plugins`.
- A control that leads to a screen through sign-in is `useLogin(target)`. Never navigate to a guarded
screen to start a sign-in, and never hand a plugin a continuation that goes anywhere but the
dispatcher: the landing is the dispatcher's decision.
- The browser ends up holding an ordinary OwlMeans bearer token whichever provider issued the
login. Product authorization stays server-side, in entrypoint gates and handler checks — never in
client-only state.
- The organization entity a token names is its `entitySlug`, the renameable public name. Read it
with `authHelper.entitySlugOf` from `@owlmeans/auth`; the stable `entityId` never reaches the
browser.
## Depends On
- `@owlmeans/auth`, `@owlmeans/auth-common` — types, aliases, `authMiddleware`
- `@owlmeans/client`, `@owlmeans/client-context`, `@owlmeans/client-entrypoint`, `@owlmeans/client-socket`
- `@owlmeans/config` — the login screen, terms and credit configuration
- `@owlmeans/basic-envelope` — decoding the bearer envelope
- `react` (peer)
- `ajv` (peer) — `PinSchema` is typed as `JSONSchemaType<PinForm>`