Skip to main content

Authentication

Open Genie uses two separate authentication systems:

ContextMethod
Web UI (browser)Google OAuth → server-signed HTTP-only cookie session (jose)
Native devices (phone, tablet, TV)Short-lived access tokens + long-lived refresh tokens (jose)

Device Authentication (JWT)

Native device apps authenticate via access tokens signed with HS256 using GENIE_JWT_SECRET. This is the primary auth system for WebSocket connections and REST API calls from mobile clients.

Token Structure

interface AccessTokenPayload {
deviceId: string;
userId: string;
deviceType: string; // "phone" | "tablet" | "tv"
sessionId: string;
scope: "admin" | "member" | "guest";
}

Default TTLs (configurable via env):

TokenDefault TTLEnv var
Access token15 minutesGENIE_ACCESS_TOKEN_TTL_SECONDS
Refresh token90 daysGENIE_REFRESH_TOKEN_TTL_DAYS

Obtaining Tokens — QR Pairing Flow

The recommended flow for first-time device setup. See docs/pairing.md for the complete protocol.

1. Admin opens Paired Devices page → POST /api/auth/pair/start
← { qr: "opengenie://pair?d=…", pairing: { id, code, scope, expiresAt } }

2. Device scans QR code, chooses best server URL from candidates

3. Device → POST /api/auth/pair/complete { code, secret, deviceInfo }
← { accessToken, refreshToken, expiresIn, device }

4. Device stores accessToken in memory, refreshToken in secure storage

Token Refresh

On 401, call POST /api/auth/refresh with the refresh token:

POST /api/auth/refresh
Authorization: Bearer <refresh-token>

Returns a new { accessToken, refreshToken, expiresIn }.

Using the Access Token

WebSocket — pass as query parameter or subprotocol:

ws://localhost:3000/ws?token=eyJ…

REST API — Bearer header:

Authorization: Bearer eyJ…

Key Files

FileExports
lib/auth/jwt.tssignAccessToken(), verifyAccessToken(), generateRefreshToken()
lib/auth/middleware.tsauthenticateToken(), authenticateRequest()

authenticateRequest(req)

Extracts the Bearer token from the Authorization header, verifies it, and checks the session revocation cache.

const auth = await authenticateRequest(req);
if (!auth) {
return c.json({ error: "Unauthorized" }, 401);
}
// auth = { deviceId, userId, deviceType, sessionId, scope }

Deprecated Routes (v1 → v2)

DeprecatedReplaced by
POST /api/paired-devices/pairPOST /api/auth/pair/start
POST /api/paired-devices/verifyPOST /api/auth/pair/complete
Long-lived 365-day tokensAccess token (15 min) + refresh token (90 days)

Set GENIE_AUTH_V2=0 to re-enable v1 behavior during migration (one release window only).

Web Authentication (Google OAuth + jose)

The web UI uses hand-rolled Google OAuth with jose for browser-based access. There is no NextAuth dependency.

Flow

Browser → GET /api/auth/login → redirects to Google
Google → GET /api/auth/callback?code=… → server exchanges code,
verifies allowed email (GENIE_ALLOWED_EMAILS), signs session cookie
Browser → GET /api/auth/session → returns { user: { email, name, picture } | null }

Configuration

AUTH_SECRET=your-session-signing-secret # min 32 chars; required
AUTH_GOOGLE_ID=your-google-client-id
AUTH_GOOGLE_SECRET=your-google-client-secret

# Optional: restrict sign-in to specific addresses
GENIE_ALLOWED_EMAILS=alice@example.com,bob@example.com

Key Files

FilePurpose
apps/web/server/auth/routes.ts/api/auth/login, /api/auth/callback, /api/auth/session, /api/auth/logout
apps/web/server/auth/config.tsgetRedirectUri(), isAllowedUser()

Usage in Components

import { useSession } from "@/lib/use-session";

function MyComponent() {
const { user, status, isAdmin } = useSession();
// user: { email, name, picture } | null
// status: "loading" | "authenticated" | "unauthenticated"
// isAdmin: true for any signed-in browser session
}

The useSession() hook fetches /api/auth/session once on mount. All signed-in browser sessions are treated as admin (the household owner).

Bootstrap Window

On first boot, while AUTH_SECRET is not yet set, the /setup wizard is accessible only from loopback and requires a one-time PIN printed to the server log. See the Docker deployment guide (docs/deploy-docker.md) for details.