Plan
Per-repo change list for adopting rokojori-auth-connector as a git
submodule. Read in full before touching any file. Changes are listed in dependency
order within each repo — do them top to bottom.
Every service currently has its own copy of auth middleware. They have drifted in three ways that cause constant breakage:
requireAuth. An expired access token always
returns 401 with no recovery attempt, even when a valid refresh token is present.
req.user; tunnel, styles, and rokojori-auth use req.auth.
Routes cannot be safely copied between services.
rokojori-auth — fix ACCESS_TOKEN_TTL only; does NOT get the submodule
roject — add submodule; delete local auth.ts; rename req.user → req.auth (19 places)
styles — add submodule; delete local requireAuth.ts + requireAccess.ts; add jwtMiddleware
tunnel — add submodule; delete local requireAuth.ts; add jwtMiddleware for API routes
Fix the TTL bug in rokojori-auth and redeploy. Every other fix depends on transparent refresh working, and transparent refresh being tested every 10 seconds instead of every hour makes everything harder to reason about.
rokojori-auth is the auth service itself. It cannot call itself for token
refresh, so jwtMiddleware makes no sense here. Its own
requireAuth.ts is correct for guarding its own API routes
(/api/auth/me etc.) and should not change.
File: source/server/routes/auth.ts, line 13
// Before
const ACCESS_TOKEN_TTL = '10s';
// After
const ACCESS_TOKEN_TTL = '1h';
Redeploy immediately after this change. Restart the service and confirm with
journalctl -u rokojori-auth -n 20 that it came up cleanly.
The page-level redirect middleware in source/server/index.ts is
correct for a pure server-rendered auth service — leave it as-is.
The requireAuth.ts middleware is correct for the auth service's own
routes — leave it as-is.
Roject is the most advanced of the three — it already has
jwtMiddleware with transparent refresh. The problems are:
req.user instead of req.auth (19 call sites across 6 files)JwtUser instead of AuthPayloadjwtMiddleware still redirects non-API requests to
AUTH_HOST/api/auth/refresh-session — unnecessary because
server-side transparent refresh already handles page requests silentlyapp.set('trust proxy', 1) in index.tsmain.ts intercepts will-redirect to handle
the refresh-session redirect — can be removed once the redirect
branch is gonegit submodule add git@community.rokojori.com:Rokojori/rokojori-auth-connector.git source/auth-connector
git submodule update --init
Add to tsconfig.json (or the relevant tsconfig for server compilation):
"paths": {
"auth-connector/*": ["./source/auth-connector/source/*"]
}
The entire file is replaced by the connector. Delete it after the imports in all dependents are updated (changes 4–7 below).
app.set( 'trust proxy', 1 ); before app.use( express.json() )from 'auth-connector/server/auth'req.user → req.auth// Before
import { jwtMiddleware, requireAuth } from './middleware/auth';
...
app.get( '/api/auth/me', requireAuth, ( req, res ) => res.json( req.user ) );
// After
import { jwtMiddleware, requireAuth } from 'auth-connector/server/auth';
...
app.set( 'trust proxy', 1 );
...
app.get( '/api/auth/me', requireAuth, ( req, res ) => res.json( req.auth ) );
Replace the local JwtUser import with AuthPayload
from the connector. The type shape is identical — this is a rename only.
// Before
import { JwtUser } from './middleware/auth';
export function isOwner( project: Project, user: JwtUser ): boolean { ... }
// ... all function signatures use JwtUser
// After
import type { AuthPayload } from 'auth-connector/shared/types';
export function isOwner( project: Project, user: AuthPayload ): boolean { ... }
// ... replace JwtUser with AuthPayload in all 4 function signatures
19 occurrences across 5 files. All are mechanical replacements — the shape of the object does not change.
source/server/routes/files.ts — 7 occurrencessource/server/routes/projects.ts — 7 occurrencessource/server/routes/layout.ts — 2 occurrencessource/server/routes/rojos.ts — 2 occurrencessource/server/routes/userSettings.ts — 2 occurrences
The checkAccess calls pass req.user! as the second
argument. After change 5, projectAccess.ts expects AuthPayload
— the rename makes the types consistent. Change every req.user to
req.auth and every req.user! to req.auth!.
In the old local auth.ts (now deleted), jwtMiddleware
redirected non-API requests with an expired token to
AUTH_HOST/api/auth/refresh-session.
The connector's jwtMiddleware does not do this — it handles
the refresh server-side and never redirects. This is correct behavior.
No explicit action needed here once the old file is deleted.
The mainWindow.webContents.on('will-redirect', ...) block
(lines 136–158) exists solely to intercept the /api/auth/refresh-session
redirect that the old jwtMiddleware emitted. Once the redirect is
gone, this intercept is dead code and should be removed.
// Remove this entire block from electron/main.ts:
mainWindow.webContents.on( 'will-redirect', async ( event, url ) =>
{
if ( url.includes( '/api/auth/refresh-session' ) )
{
// ... entire block
}
} );
The refreshTokens() helper function defined above it can also
be deleted — Electron no longer needs to do its own refresh because the
server-side jwtMiddleware handles it transparently.
journalctl -u roject -f | grep '\[auth\]'
Log into Roject, wait 65 minutes (or temporarily set ACCESS_TOKEN_TTL=65s
in the test environment), then make an API call. Expect to see the
[auth] tryRefresh → ... sequence and a transparent recovery.
styles has no jwtMiddleware at all. requireAuth is
wired directly onto routes. An expired access token causes an immediate 401 with
no recovery attempt, regardless of whether the refresh token is valid.
STYLES_RULES is currently defined inside
middleware/requireAccess.ts — it needs to move to index.ts.
git submodule add git@community.rokojori.com:Rokojori/rokojori-auth-connector.git source/auth-connector
git submodule update --init
"paths": {
"auth-connector/*": ["./source/auth-connector/source/*"]
}
source/server/middleware/requireAuth.ts ← delete
source/server/middleware/requireAccess.ts ← delete
Do this after updating index.ts so the service never references
the deleted files.
// Before
import { requireAuth } from './middleware/requireAuth';
import { requireAccess, STYLES_RULES } from './middleware/requireAccess';
// After
import { jwtMiddleware, requireAuth, requireAccess } from 'auth-connector/server/auth';
import type { AccessRule } from 'auth-connector/shared/types';
const STYLES_RULES: AccessRule[] = [
{ role: 'admin' },
{ role: 'user', product: 'styles' },
{ role: 'user', product: 'premium' },
];
Then add jwtMiddleware as global middleware — place it after
cookieParser() and before the route registrations:
app.use( cookieParser() );
app.use( jwtMiddleware ); // ← add this line
// rest of routes unchanged...
The route registrations themselves do not change — they already use
requireAuth and requireAccess( STYLES_RULES ).
journalctl -u styles-rokojori -f | grep '\[auth\]'
Hit /api/fonts with an expired token and a valid refresh cookie.
Expect transparent recovery in the log.
tunnel has no jwtMiddleware. Expired tokens on
/api/tunnels routes always return 401 with no recovery.
Two routes do their own inline JWT handling and must be treated carefully:
jwt.verify()
call. This is correct and does not change — WebSocket upgrades bypass Express
middleware entirely.
softAuth()
does a non-blocking JWT check for the proxy route. Public tunnels pass even
without a token. This intentional soft auth stays local to proxy.ts — do not
replace it with jwtMiddleware.
git submodule add git@community.rokojori.com:Rokojori/rokojori-auth-connector.git source/auth-connector
git submodule update --init
"paths": {
"auth-connector/*": ["./source/auth-connector/source/*"]
}
Delete after updating all importers below.
Add jwtMiddleware scoped to the /api/tunnels routes.
The proxy route (/t) must not
get jwtMiddleware — it does its own soft auth.
// Before
import { requireAuth } from './middleware/requireAuth'; // (was unused at index level)
// After
import { jwtMiddleware } from 'auth-connector/server/auth';
// Add jwtMiddleware only for the API routes section:
app.use( '/api/tunnels', jwtMiddleware, express.json(), tunnelsRouter );
// The /t proxy route stays unchanged — no jwtMiddleware
app.use( '/t', proxyRouter );
// Before
import { requireAuth } from '../middleware/requireAuth';
// After
import { requireAuth } from 'auth-connector/server/auth';
All uses of req.auth in this file are already correct — no other
changes needed.
proxy.ts imports AuthPayload and
extractBearer from the old local requireAuth.ts.
extractBearer is internal to the connector and not exported.
Inline it locally — it is three lines.
// Before
import { AuthPayload, extractBearer } from '../middleware/requireAuth';
// After
import type { AuthPayload } from 'auth-connector/shared/types';
function extractBearer( req: Request ): string | undefined
{
const h = req.headers.authorization;
return h?.startsWith( 'Bearer ' ) ? h.slice( 7 ) : undefined;
}
The softAuth() function and all other logic in proxy.ts stays
unchanged.
// Before
import { AuthPayload } from '../middleware/requireAuth';
// After
import type { AuthPayload } from 'auth-connector/shared/types';
The inline jwt.verify() in the WebSocket upgrade handler is
correct and stays — WebSocket upgrades bypass Express middleware.
journalctl -u tunnel-rokojori -f | grep '\[auth\]'
Make an authenticated API call to /api/tunnels with an expired
access token and a valid refresh cookie. Expect transparent recovery in the log.
Then make a request to a public tunnel (/t/:id/...) without any
token — expect it to pass through without hitting the auth log at all.
ACCESS_TOKEN_TTL to '1h', redeploy, verify startup log.
rokojori-auth-connector and push the local directory to it.
Before moving to the next repo: confirm auth still works end-to-end —
login, wait for the access token to expire (check with
journalctl ... | grep '[auth]'), and confirm the first API call
after expiry succeeds transparently.