Submodule
Shared auth integration for all rokojori services. Add as a git submodule. Provides server-side Express middleware, shared TypeScript types, and a browser SPA helper. One source of truth — one fix reaches every app.
rokojori-auth-connector/
source/
shared/
types.ts — AuthPayload, Tokens, AccessRule
server/
auth.ts — jwtMiddleware, requireAuth, requireAuthPage, requireAccess
browser/
apiFetch.ts — SPA fetch wrapper; redirects to login on 401
workspace/
index.html — this document
The three shared types used across every layer. Import from here rather than re-declaring them in each service.
Express middleware for any Node.js service. Handles JWT verification, transparent cookie-based token refresh, and route protection.
Minimal browser fetch wrapper. The server handles transparent token refresh, so the only browser-side concern is reacting to a 401 (both tokens dead) by redirecting to the login page.
git submodule add https://community.rokojori.com/Rokojori/rokojori-auth-connector.git source/auth-connector
git submodule update --init
This creates source/auth-connector/ inside your service repo.
Import directly from that path — no build step, no install step.
# Inside your service repo — pull the latest connector and commit the new SHA
git submodule update --remote source/auth-connector
git add source/auth-connector
git commit -m "update auth-connector"
Each service repo pins a specific commit of the connector. Updates are explicit and deliberate — a change in the connector does not silently affect services that have not pulled it yet.
git clone --recurse-submodules <repo-url>
# Or if you already cloned without --recurse-submodules:
git submodule update --init
Add a path alias so imports read cleanly instead of using relative
../../source/auth-connector/source/... paths.
{
"compilerOptions": {
"paths": {
"auth-connector/*": ["./source/auth-connector/source/*"]
}
}
}
import { jwtMiddleware, requireAuth } from 'auth-connector/server/auth';
import type { AuthPayload } from 'auth-connector/shared/types';
import { jwtMiddleware, requireAuth } from '../../source/auth-connector/source/server/auth';
import type { AuthPayload } from '../../source/auth-connector/source/shared/types';
Add these to your service's .env (and the production
.env on the server). All are read at runtime — no rebuild needed
when they change.
# Required — must match the JWT_SECRET in rokojori-auth exactly
JWT_SECRET=...
# Public URL of the auth service
# Used when redirecting unauthenticated browsers to the login page
AUTH_HOST=https://account.rokojori.com
# Internal URL for server-to-server token refresh calls
# Set to the local port in production to bypass nginx TLS overhead
# Leave unset in dev — falls back to AUTH_HOST
AUTH_INTERNAL_HOST=http://localhost:3001
# Cookie domain — must match COOKIE_DOMAIN in rokojori-auth
COOKIE_DOMAIN=.rokojori.com
import 'dotenv/config';
import express from 'express';
import cookieParser from 'cookie-parser';
import { jwtMiddleware, requireAuth, requireAuthPage } from 'auth-connector/server/auth';
const app = express();
app.set( 'trust proxy', 1 ); // required — real client IPs, secure cookie flag
app.use( express.json() );
app.use( cookieParser() );
app.use( jwtMiddleware ); // runs on every request before routes and static files
app.use( express.static( ... ) );
// API route — returns 401 JSON if not authenticated
app.get( '/api/me', requireAuth, ( req, res ) =>
{
res.json( { userId: req.auth!.userId } );
} );
// Page route — redirects to login if not authenticated
app.get( '/dashboard', requireAuthPage, ( req, res ) =>
{
res.sendFile( ... );
} );
import { jwtMiddleware, requireAuth, requireAccess } from 'auth-connector/server/auth';
import type { AccessRule } from 'auth-connector/shared/types';
const MY_RULES: AccessRule[] = [
{ role: 'admin' },
{ role: 'user', product: 'my-product' },
];
// requireAccess includes its own 401/403 handling — no need to add requireAuth before it
app.get( '/api/premium', jwtMiddleware, requireAccess( MY_RULES ), handler );
The server's jwtMiddleware handles transparent token refresh for
cookie clients — the browser receives rotated cookies on every refreshed response
without doing anything. The only case the browser needs to act on is a
401, which means both tokens are dead and the user must log in again.
import { apiFetch } from 'auth-connector/browser/apiFetch';
// Use apiFetch exactly like fetch — it redirects to login on 401
const res = await apiFetch( '/api/projects' );
const data = await res.json();
Call this once at app startup if your login page is not at the default
https://account.rokojori.com/login.html.
import { setAuthRedirectUrl } from 'auth-connector/browser/apiFetch';
setAuthRedirectUrl( 'https://account.rokojori.com/login.html' );
Understanding this flow is essential for debugging. Watch it live with:
journalctl -u <your-service> -f | grep '\[auth\]'
1. Extract token from accessToken cookie or Authorization: Bearer header
2. jwt.verify() succeeds
3. req.auth = decoded payload
4. next() — route handler runs
[auth] expired token on: GET /api/projects
[auth] tryRefresh → http://localhost:3001/api/auth/refresh
[auth] tryRefresh status: 200
[auth] tryRefresh: succeeded
1. jwt.verify() throws TokenExpiredError
2. Read refreshToken cookie
3. POST AUTH_INTERNAL_HOST/api/auth/refresh with the refresh token
4. Write new accessToken + refreshToken cookies onto the response
5. req.auth = decoded new access token
6. next() — route handler runs, client receives rotated cookies transparently
[auth] expired token on: GET /api/projects
[auth] tryRefresh → http://localhost:3001/api/auth/refresh
[auth] tryRefresh status: 401
[auth] tryRefresh error body: {"error":"Invalid or expired refresh token"}
[auth] refresh failed for: GET /api/projects
1. jwt.verify() throws TokenExpiredError
2. POST to refresh endpoint → 401
3. next() — req.auth remains undefined
4. requireAuth returns 401 JSON
requireAccess returns 401 JSON or redirects to login (page routes)
1. Token extracted from Authorization: Bearer header
2. If expired: no refreshToken cookie → next() with req.auth undefined → 401
3. The Electron main process sees the 401, calls tryRefreshTokens(), retries once
4. This is intentional — Bearer clients manage their own refresh cycle
See the rokojori-auth-connector non-browser guide for the Electron
apiFetch pattern.
git submodule add — add the connector at
source/auth-connector/
tsconfig.json
JWT_SECRET, AUTH_HOST,
AUTH_INTERNAL_HOST, COOKIE_DOMAIN to
.env and the server .env
index.ts: app.set('trust proxy', 1),
app.use(cookieParser()),
app.use(jwtMiddleware) — in that order, before routes
requireAuth on API routes,
requireAuthPage on page routes
requireAccess(rules) where role/product gating is needed
fetch with apiFetch
from auth-connector/browser/apiFetch
journalctl -u <service> -f | grep '\[auth\]'