@vulog/auth
v1.0.6
Published
Browser-only, framework-agnostic authentication for Vulog apps. Ships a Service Worker that owns the tokens and transparently injects the Bearer + refreshes it on AIMA/BO API calls.
Readme
@vulog/auth
Browser-only, framework-agnostic authentication for Vulog apps.
@vulog/auth ships a Service Worker that owns the tokens. The worker
stores the access/refresh tokens in IndexedDB (never in localStorage,
never in a JS variable the page can read), transparently injects
Authorization: Bearer … (and X-API-Key) on your API calls, and
refreshes the token silently — including a one-shot retry on a 401.
It supports the three Vulog authentication modes and, when a client has
one enabled, the Service Worker handles token injection and refresh for
every request to /boapi/proxy/ and /apiv5/ (configurable).
ropc— username/password login (grant_type=password) with optional 2FA.oauth2— OAuth2 Authorization Code + PKCE, refreshed silently via the httpOnly session cookie (no refresh token in the browser).microsoft— Microsoft / Azure AD SSO.
Works with vanilla JS, React, or any other framework. Written in TypeScript, fully tested.
Why a Service Worker?
The tokens live inside the worker. The page asks the worker to log in,
log out, or read the (token-free) auth state; it never touches the raw
token itself. When the page fetch()es an API URL, the worker intercepts
the request and attaches the credentials. This keeps tokens out of reach
of most XSS payloads and centralizes refresh + retry in one place shared
across all tabs.
page (React / vanilla) Service Worker
───────────────────── ──────────────
createAuthClient(...) ── INIT ───────────▶ persist config
client.loginRopc(...) ── LOGIN ──────────▶ POST IDP, store tokens (IndexedDB)
◀── AUTH_OK ───────── broadcast (claims only)
fetch('/apiv5/user') ─────────────────────▶ intercept:
+ Authorization: Bearer …
+ X-API-Key
on 401 → refresh + retry onceInstall
npm install @vulog/authreact and zustand are optional peer dependencies, needed only if
you use the @vulog/auth/react bindings.
Serve the worker file
The package ships a pre-built, self-contained worker at
@vulog/auth/auth-sw (→ node_modules/@vulog/auth/dist/auth-sw.js).
Serve it from your app root and register it at scope /.
The server must send the Service-Worker-Allowed: / header for the root
scope to be granted. With Vite, a small plugin serves it in dev/preview
and emits it at the build root:
// vite.config.js
import path from 'path';
import fs from 'fs';
const vulogAuthWorkerPlugin = () => {
const workerPath = path.resolve('node_modules/@vulog/auth/dist/auth-sw.js');
const serve = (server) => {
server.middlewares.use('/auth-sw.js', (_req, res) => {
res.setHeader('Content-Type', 'application/javascript');
res.setHeader('Service-Worker-Allowed', '/');
res.setHeader('Cache-Control', 'no-cache');
res.end(fs.readFileSync(workerPath));
});
};
return {
name: 'vulog-auth-worker',
configureServer: serve,
configurePreviewServer: serve,
generateBundle() {
this.emitFile({
type: 'asset',
fileName: 'auth-sw.js',
source: fs.readFileSync(workerPath),
});
},
};
};
// plugins: [vulogAuthWorkerPlugin(), ...]For a static server / CDN: serve auth-sw.js from the root with
Service-Worker-Allowed: / and no long cache. The full guides
(getting-started, architecture, security) ship in the package's docs/
folder.
Quickstart (vanilla JS)
import { createAuthClient } from '@vulog/auth';
const auth = createAuthClient({
serviceWorker: { scriptUrl: '/auth-sw.js' },
config: {
clientId: 'F0001_Secure',
clientSecret: '…', // ROPC only
apiKey: '…', // sent as X-API-Key
tokenEndpoint: 'https://…/auth/realms/F0001/protocol/openid-connect/token',
// interceptPrefixes defaults to ['/boapi/proxy/', '/apiv5/']
},
});
// Register the SW + push the config. Call once at boot.
await auth.start();
// React to session changes (login / logout / refresh) from any tab.
auth.onEvent((event) => console.log('[auth]', event.type));
// Log in with a password.
const result = await auth.loginRopc({ username, password });
if (!result.ok && result.code === 'TWO_FA_REQUIRED') {
const pin = await promptUser(result.details.pinCodeFormat);
await auth.check2fa({ pin, url: result.details.url, sessionId: result.details.sessionId });
}
// From now on, just fetch — the worker attaches the token.
const user = await fetch('/apiv5/user').then((r) => r.json());
await auth.logout();Microsoft SSO
// URLs come from discovery (`.well-known/method`).
await auth.loginMicrosoft({ loginUrl, refreshUrl, logoutUrl }); // logoutUrl optional (server-side logout)OAuth2 Authorization Code + PKCE
// On the login page — generates PKCE + redirects to the IDP:
await auth.startOAuth2Login();
// On your redirect/callback page (config.redirectUri):
const result = await auth.completeOAuth2Login(); // reads ?code&state, exchanges itDiscover which modes a tenant has enabled with
auth.discoverAuthMethods(methodsDiscoveryUrl(baseUrl, realm, clientId)).
Quickstart (React)
import { createAuthClient } from '@vulog/auth';
import { AuthProvider, useAuth, useLogout } from '@vulog/auth/react';
const auth = createAuthClient({
serviceWorker: { scriptUrl: '/auth-sw.js' },
config: { clientId: 'F0001_Secure', tokenEndpoint: '…', apiKey: '…' },
});
function App() {
return (
<AuthProvider client={auth}>
<Home />
</AuthProvider>
);
}
function Home() {
const view = useAuth();
const { logout } = useLogout();
if (view.isPreInit) return <Spinner />;
if (!view.isAuthed) return <LoginForm />;
return <button onClick={() => logout()}>Sign out ({String(view.claims.sub)})</button>;
}The React hooks are useAuth, useLogout, useAuthMethods, and the
useAuthStore observer — see the docs/ folder in the package.
API surface
| Import | What |
| --------------------- | ------------------------------------------------------------------------------------------------- |
| @vulog/auth | createAuthClient, low-level client pieces, PKCE + OIDC URL helpers, the typed message protocol. |
| @vulog/auth/react | AuthProvider, useAuth, useLogout, useAuthMethods, useAuthStore. |
| @vulog/auth/auth-sw | The pre-built Service Worker to serve at your app root. |
Configuration (AuthConfig)
| Field | For | Description |
| ------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| clientId | all | OAuth2 client id. |
| clientSecret | ropc | Confidential-client secret. Omit for public/PKCE clients. |
| apiKey | optional | Value for the X-API-Key header on intercepted requests. |
| interceptPrefixes | optional | Prefixes to intercept. Default ['/boapi/proxy/', '/apiv5/']. Path prefixes match same-origin pathname; http(s) prefixes match the full URL. |
| tokenEndpoint | ropc | grant_type=password / refresh_token endpoint. |
| logoutEndpoint | optional (ropc) | Best-effort IDP logout. |
| oauth2AuthorizeEndpoint | oauth2 | Authorize endpoint (same-origin proxy for silent refresh). |
| oauth2TokenEndpoint | oauth2 | Code-exchange endpoint (falls back to tokenEndpoint). |
| endSessionEndpoint | optional (oauth2) | End-session redirect target for logout. |
| redirectUri | oauth2 | Registered redirect URI. |
Documentation
The full guides — getting started, vanilla, react,
architecture, and the security model — ship in the package's
docs/ folder (node_modules/@vulog/auth/docs/).
Development
npm install
npm run type-check # tsc --noEmit
npm run lint # eslint
npm test # vitest run
npm run test:coverage
npm run build # tsdown → dist/{index,react,auth-sw}.jsReleasing
Publishing to the npm registry is maintainer-only, enforced by GitLab:
- Bump the version + update
CHANGELOG.md, merge tomain. - A maintainer creates a protected tag
vX.Y.Z(protected tags: Allowed to create = Maintainers). - The CI
publishjob (manual, on the protectednpmenvironment, using the protected+maskedNPM_TOKEN) is triggered by a maintainer.
See the header of .gitlab-ci.yml for the exact project settings that lock this down.
License
MIT © Vulog.
