Authentication

Login, refresh, SSO, and token stores with @lunnoa/client.

Use @lunnoa/client auth helpers so Pattern B apps do not reinvent login, refresh, or token storage. Pattern A (API keys) still uses createServerClient or apiKey on the constructor.

Choose a client factory

HelperPatternCredential
createServerClient({ apiKey })AWorkspace API key (lna_…), server / BFF only
createBrowserClient({ … })BUser JWTs in a token store (default: localStorage)
createServerClient({ accessToken })B on serverUser JWT held by your backend
browser.ts
server.ts
import { createBrowserClient, isLoginRequires2FA } from '@lunnoa/client';const lunnoa = createBrowserClient({  baseUrl: 'https://lunnoa.your-company.example',});const result = await lunnoa.auth.login({ email, password });if (isLoginRequires2FA(result)) {  await lunnoa.auth.verify2faLogin({    sessionToken: result.sessionToken,    token: totpOrBackupCode,  });}const me = await lunnoa.auth.me();
import { createServerClient } from '@lunnoa/client';const lunnoa = createServerClient({  baseUrl: process.env.LUNNOA_URL!,  apiKey: process.env.LUNNOA_API_KEY!,});

createBrowserClient defaults to localStorage keys accessToken and refreshToken (same keys as the built-in Automate UI). Pass createMemoryTokenStore() for Node scripts or tests.

For Pattern B in the browser, allow your origin with CORS_ALLOWED_ORIGINS on the deployment.

React hooks (@lunnoa/client/react)

Opt-in React helpers for Pattern B portals. Peer dependency: react ≥ 18. For the full hook set (progress, needs-input, agent chat, approvals, entities), see React hooks.

app.tsx
import { LunnoaAuthProvider, useLunnoaAuth } from '@lunnoa/client/react';import { isLoginRequires2FA } from '@lunnoa/client';export function App() {  return (    <LunnoaAuthProvider baseUrl={import.meta.env.VITE_LUNNOA_URL}>      <Shell />    </LunnoaAuthProvider>  );}function Shell() {  const { user, status, login, logout, error } = useLunnoaAuth();  if (status === 'loading') return <p>Loading…</p>;  if (!user) {    return (      <button        onClick={async () => {          const result = await login({ email, password });          if (isLoginRequires2FA(result)) {            // show TOTP form → verify2faLogin({ sessionToken, token })          }        }}      >        Sign in      </button>    );  }  return (    <div>      <p>{user.email}</p>      {error ? <p>{error}</p> : null}      <button onClick={() => logout()}>Sign out</button>    </div>  );}
ExportRole
LunnoaAuthProviderContext: pass client={createBrowserClient(...)} or baseUrl (+ optional token store)
useLunnoaAuth(){ user, status, login, verify2faLogin, loginWithToken, logout, listSsoProviders, getSsoLoginUrl, … }
useLunnoaClient()Same LunnoaClient instance for entities, actions.run, watchProgress, …
useAccessToken(){ getAccessToken, status } for AI SDK chat Authorization headers

More hooks (useExecutionProgress, useNeedsInput, useAgentChat, useApprovalsInbox, useEntityList) are documented in React hooks.

What lunnoa.auth wraps

MethodHTTPNotes
login({ email, password })POST /api/auth/loginPublic. May return { requires2FA, sessionToken }
verify2faLogin({ sessionToken, token })POST /api/auth/2fa/verify-loginPublic. Completes 2FA login
loginWithToken({ token })POST /api/auth/login-with-tokenPublic. Exchanges SSO / email hidden JWT
refresh({ refreshToken? })POST /api/auth/refresh-tokenPublic. Returns a new access token (refresh is not rotated)
me()GET /api/users/meRequires Bearer JWT
logout()(local only)Clears the token store
sso.listProviders()GET /api/auth/sso/providersPublic discovery
sso.getLoginUrl(providerId)Builds /api/auth/sso/:id/login

Successful login helpers write camelCase tokens into the store (accessToken / refreshToken). Wire responses still use access_token / refresh_token.

Auto-refresh on 401

When a tokenStore is configured and autoRefresh is true (default for createBrowserClient), a 401 on an authenticated request triggers one POST /api/auth/refresh-token using the stored refresh token, then retries the original call. If refresh fails, the store is cleared.

You can still call auth.refresh() yourself (for example after catching LunnoaApiError.isUnauthorized when auto-refresh is off).

SSO for custom portals

  1. Call auth.sso.listProviders() to render buttons (and to respect ssoEnforced / passwordAuthDisabled).
  2. Navigate the browser to auth.sso.getLoginUrl(providerId).
  3. The IdP callback hits Automate at /api/auth/sso/:providerId/callback, which redirects to CLIENT_URL/verify-token?token=… (the deployment's configured client URL, not an arbitrary custom origin).
  4. Exchange the hidden token with auth.loginWithToken({ token }).

Token stores

typescript
import {
  createMemoryTokenStore,
  createLocalStorageTokenStore,
  LunnoaClient,
} from '@lunnoa/client';

const lunnoa = new LunnoaClient({
  baseUrl,
  tokenStore: createMemoryTokenStore(), // or createLocalStorageTokenStore()
});

Implement TokenStore yourself when tokens live in httpOnly cookies on your BFF: your store reads/writes via your backend; Lunnoa still receives Bearer JWTs.

Related