Warning
This project is experimental and under active development. APIs are subject to change without notice. Use with caution in production environments.
@algorandfoundation/liquid-auth-core is a modular, environment-agnostic middleware suite for implementing Liquid Auth. It provides authentication, FIDO (WebAuthn), and WebRTC signaling capabilities, built on top of itty-router to run anywhere from Node.js Express to Cloudflare Workers.
npm install @algorandfoundation/liquid-auth-coreNote: This package is ESM-only. Ensure your
package.jsonincludes"type": "module"or use.mjsextensions.
The middleware is configured using a common LiquidAuthOptions object:
import { LiquidAuthOptions } from '@algorandfoundation/liquid-auth-core';
const options: LiquidAuthOptions = {
origin: 'https://your-domain.com', // Base URL of your app
storage: myStorageAdapter, // Required: user and session persistence
events: myEventsAdapter, // Optional: for cross-device signaling
logger: myLoggingAdapter, // Optional: structured logging
rpID: 'your-domain.com', // WebAuthn Relying Party ID
rpName: 'Your App Name', // WebAuthn Relying Party Name
extensions: [myCustomExtension], // Optional: custom signature verifiers
sessionCookieName: 'liquid_sid' // Optional: defaults to 'liquid_sid'
};Liquid Auth Core is built using itty-router, making it compatible with various environments.
For Express applications, we provide an adapter and a composed middleware.
Mounts both auth and fido capabilities.
import express from 'express';
import { liquidAuthMiddleware } from '@algorandfoundation/liquid-auth-core';
const app = express();
app.use(express.json());
// Options as defined above
app.use('/auth', liquidAuthMiddleware(options));If you need more control, you can mount them individually:
import { auth, fido, createExpressMiddleware } from '@algorandfoundation/liquid-auth-core';
app.use('/auth', createExpressMiddleware(auth(options)));
app.use('/auth/fido', createExpressMiddleware(fido(options)));You can use the routers directly in any environment that supports Request/Response (Fetch API).
import { auth, fido } from '@algorandfoundation/liquid-auth-core';
import { Router } from 'itty-router';
const router = Router();
// Mount the liquid auth routers
router.all('/auth/*', auth(options).handle);
router.all('/auth/fido/*', fido(options).handle);
export default {
fetch: (req, env, ctx) => router.handle(req)
};Handles WebAuthn attestation (registration) and assertion (login) flows.
POST /attestation/request: Generates WebAuthn registration options.- Body:
{ username: string, extensions?: { liquid?: boolean } } - If
extensions.liquidistrue, it returnsverificationMethods(e.g.["ed25519"]) supported by the service.
- Body:
POST /attestation/response: Verifies registration and creates/updates the user.- Body: WebAuthn response + optional
walletoraddressfor the linked identifier.
- Body: WebAuthn response + optional
POST /assertion/request/:credId: Generates authentication options for a specific credential.- Body:
{ extensions?: { liquid?: boolean } }
- Body:
POST /assertion/response: Verifies authentication and starts the user session.
Handles user profile retrieval, session status, and logout.
GET /user: Returns the profile of the currently authenticated user.GET /session: Returns the current session state and associated user info.GET /logout: Destroys the authentication session and redirects to/.DELETE /keys/:id: Deletes a specific WebAuthn credential from the user's account.
Provides a WebSocket handler for WebRTC signaling between devices (e.g., browser and mobile wallet).
import { signal } from '@algorandfoundation/liquid-auth-core/signal';
import { WebSocketAdapter } from '@algorandfoundation/liquid-auth-core/adapters';
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ server });
const handler = signal(options);
wss.on('connection', (ws, req) => {
const adapter = new WebSocketAdapter(ws);
handler(adapter, req);
});To enable cross-device linking, the signaling handler needs access to the user's session. If you are using express-session, you can apply it to the WebSocket upgrade request:
wss.on('connection', (ws, req) => {
// sessionMiddleware is your express-session instance
sessionMiddleware(req, {}, () => {
const adapter = new WebSocketAdapter(ws);
handler(adapter, req);
});
});link: Orchestrates the connection between a browser session and a wallet device.offer-description/answer-description: Forwards WebRTC SDP messages to the linked peer.offer-candidate/answer-candidate: Forwards WebRTC ICE candidates to the linked peer.
Liquid Auth Core is environment-agnostic regarding sessions. It prioritizes existing session objects but can manage its own if needed.
- Express Session (Recommended for Express): If
req.sessionis present (e.g., viaexpress-session), the middleware will use it to store authentication state. - Managed Sessions (Agnostic): If no session object is found on the request, the core will:
- Look for a session ID in the
liquid_sidcookie (configurable). - Retrieve session data using
storage.findSession(sid). - If missing, create a new session and persist it via
storage.saveSession(sid, data). - Automatically set the
Set-Cookieheader in the response.
- Look for a session ID in the
This allows the same code to work in Express, Cloudflare Workers, and other environments without changing the core logic.
Liquid Auth Core uses adapters to remain platform-agnostic. Each adapter has a specific interface you must implement or use a provided one.
Handles persistence for users, credentials, and session associations. Required by all middleware components.
export interface StorageAdapter {
findUserByWallet(wallet: string): Promise<LiquidUser | null>;
findUserByCredId(credId: string): Promise<LiquidUser | null>;
createUser(wallet: string): Promise<LiquidUser>;
updateUser(user: LiquidUser): Promise<void>;
findSession(sid: string): Promise<any | null>;
saveSession?(sid: string, data: any): Promise<void>;
updateSessionWallet(sid: string, wallet: string): Promise<void>;
getAccountAuthAddress?(address: string): Promise<string | null>; // Optional rekeying check
}
export interface LiquidUser {
id: string; // Internal user ID
wallet: string; // Primary wallet address
credentials: LiquidCredential[];
}
export interface LiquidCredential {
credId: string;
publicKey: string;
prevCounter: number;
device?: string;
}Used to bridge authentication events (like a successful FIDO login) to other parts of the system, such as the signaling handler.
export interface EventsAdapter {
emit(event: string, data: any): void;
on(event: string, callback: (data: any) => void): void;
off(event: string, callback: (data: any) => void): void;
}An abstraction over different WebSocket implementations (e.g., native ws). The core package provides a WebSocketAdapter for the standard ws library.
export interface SocketAdapter {
id?: string;
on(event: string, listener: (...args: any[]) => void): this;
once(event: string, listener: (...args: any[]) => void): this;
emit(event: string, ...args: any[]): this;
disconnect(): this;
removeAllListeners(event?: string): this;
}Provides a structured way to handle logs. If not provided, a no-op logger is used.
export interface LoggingAdapter {
info(message: string, ...args: any[]): void;
error(message: string, ...args: any[]): void;
warn?(message: string, ...args: any[]): void;
debug?(message: string, ...args: any[]): void;
}Allows for custom verification logic of the liquid extension signatures during WebAuthn registration.
export interface LiquidExtensionAdapter {
type: string; // The identifier for the signature type (e.g., 'ed25519')
verify(
challenge: string,
signature: string,
address: string,
storage: StorageAdapter,
): Promise<boolean>;
}For a complete implementation example using Express, sessions, and WebSockets, check the examples/express directory.
Apache-2.0