rojects/workspace/outline/auth-connector-rewrite.html

513 lines
21 KiB
HTML
Raw Permalink Normal View History

2026-07-17 11:37:48 +00:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Auth Connector Rewrite — Roject Outline</title>
<link rel="stylesheet" href="../_assets_/styles.css">
<link rel="stylesheet" href="../_assets_/nav.css">
</head>
<body>
<div class="page">
<header>
<p class="date">Plan</p>
<h1>Auth Connector Rewrite</h1>
<p class="subtitle">
Per-repo change list for adopting <code>rokojori-auth-connector</code> 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.
</p>
</header>
<!-- ─── Overview ─────────────────────────────────────────────── -->
<section>
<h2>Overview</h2>
<div class="card">
<h3>What the connector replaces</h3>
<p>
Every service currently has its own copy of auth middleware. They have drifted in
three ways that cause constant breakage:
</p>
<ul style="margin-top:0.75rem;line-height:1.9;font-size:0.9rem;color:var(--muted)">
<li>
<strong style="color:var(--text)">No transparent refresh</strong> — tunnel and
styles only have a bare <code>requireAuth</code>. An expired access token always
returns 401 with no recovery attempt, even when a valid refresh token is present.
</li>
<li>
<strong style="color:var(--text)">Wrong property name</strong> — roject uses
<code>req.user</code>; tunnel, styles, and rokojori-auth use <code>req.auth</code>.
Routes cannot be safely copied between services.
</li>
<li>
<strong style="color:var(--text)">ACCESS_TOKEN_TTL set to 10 seconds</strong>
the single most likely cause of constant auth failures. Every session is broken
10 seconds after login unless the transparent refresh is working perfectly.
</li>
</ul>
</div>
<div class="card">
<h3>Repos in scope</h3>
<pre><code>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</code></pre>
</div>
<div class="card">
<h3>Do this first, before any other change</h3>
<p>
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.
</p>
</div>
</section>
<!-- ─── rokojori-auth ─────────────────────────────────────────── -->
<section>
<h2>rokojori-auth</h2>
<div class="card">
<h3>Does NOT get the submodule</h3>
<p>
rokojori-auth is the auth service itself. It cannot call itself for token
refresh, so <code>jwtMiddleware</code> makes no sense here. Its own
<code>requireAuth.ts</code> is correct for guarding its own API routes
(<code>/api/auth/me</code> etc.) and should not change.
</p>
</div>
<div class="card">
<h3>Change 1 — Fix ACCESS_TOKEN_TTL</h3>
<p>File: <code>source/server/routes/auth.ts</code>, line 13</p>
<pre><code>// Before
const ACCESS_TOKEN_TTL = '10s';
// After
const ACCESS_TOKEN_TTL = '1h';</code></pre>
<p style="margin-top:0.75rem">
Redeploy immediately after this change. Restart the service and confirm with
<code>journalctl -u rokojori-auth -n 20</code> that it came up cleanly.
</p>
</div>
<div class="card">
<h3>Nothing else changes</h3>
<p>
The page-level redirect middleware in <code>source/server/index.ts</code> is
correct for a pure server-rendered auth service — leave it as-is.
The <code>requireAuth.ts</code> middleware is correct for the auth service's own
routes — leave it as-is.
</p>
</div>
</section>
<!-- ─── roject ───────────────────────────────────────────────── -->
<section>
<h2>roject</h2>
<div class="card">
<h3>Current state</h3>
<p>
Roject is the most advanced of the three — it already has
<code>jwtMiddleware</code> with transparent refresh. The problems are:
</p>
<ul style="margin-top:0.75rem;line-height:1.9;font-size:0.9rem;color:var(--muted)">
<li>Uses <code>req.user</code> instead of <code>req.auth</code> (19 call sites across 6 files)</li>
<li>Uses local type <code>JwtUser</code> instead of <code>AuthPayload</code></li>
<li><code>jwtMiddleware</code> still redirects non-API requests to
<code>AUTH_HOST/api/auth/refresh-session</code> — unnecessary because
server-side transparent refresh already handles page requests silently</li>
<li>Missing <code>app.set('trust proxy', 1)</code> in <code>index.ts</code></li>
<li>Electron <code>main.ts</code> intercepts <code>will-redirect</code> to handle
the <code>refresh-session</code> redirect — can be removed once the redirect
branch is gone</li>
</ul>
</div>
<div class="card">
<h3>Change 1 — Add the submodule</h3>
<pre><code>git submodule add git@community.rokojori.com:Rokojori/rokojori-auth-connector.git source/auth-connector
git submodule update --init</code></pre>
</div>
<div class="card">
<h3>Change 2 — tsconfig path alias</h3>
<p>Add to <code>tsconfig.json</code> (or the relevant tsconfig for server compilation):</p>
<pre><code>"paths": {
"auth-connector/*": ["./source/auth-connector/source/*"]
}</code></pre>
</div>
<div class="card">
<h3>Change 3 — Delete source/server/middleware/auth.ts</h3>
<p>
The entire file is replaced by the connector. Delete it after the imports
in all dependents are updated (changes 47 below).
</p>
</div>
<div class="card">
<h3>Change 4 — Update source/server/index.ts</h3>
<ul style="margin-top:0.5rem;line-height:1.9;font-size:0.9rem;color:var(--muted)">
<li>Add <code>app.set( 'trust proxy', 1 );</code> before <code>app.use( express.json() )</code></li>
<li>Change import: <code>from 'auth-connector/server/auth'</code></li>
<li>Line 35: <code>req.user</code><code>req.auth</code></li>
</ul>
<pre><code>// Before
import { jwtMiddleware, requireAuth } from './middleware/auth';
...
app.get( '/api/auth/me', requireAuth, ( req, res ) =&gt; 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 ) =&gt; res.json( req.auth ) );</code></pre>
</div>
<div class="card">
<h3>Change 5 — Update source/server/projectAccess.ts</h3>
<p>
Replace the local <code>JwtUser</code> import with <code>AuthPayload</code>
from the connector. The type shape is identical — this is a rename only.
</p>
<pre><code>// 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</code></pre>
</div>
<div class="card">
<h3>Change 6 — Rename req.user → req.auth in route files</h3>
<p>19 occurrences across 5 files. All are mechanical replacements — the shape of
the object does not change.</p>
<ul style="margin-top:0.5rem;line-height:1.9;font-size:0.9rem;color:var(--muted)">
<li><code>source/server/routes/files.ts</code> — 7 occurrences</li>
<li><code>source/server/routes/projects.ts</code> — 7 occurrences</li>
<li><code>source/server/routes/layout.ts</code> — 2 occurrences</li>
<li><code>source/server/routes/rojos.ts</code> — 2 occurrences</li>
<li><code>source/server/routes/userSettings.ts</code> — 2 occurrences</li>
</ul>
<p style="margin-top:0.75rem">
The <code>checkAccess</code> calls pass <code>req.user!</code> as the second
argument. After change 5, <code>projectAccess.ts</code> expects <code>AuthPayload</code>
— the rename makes the types consistent. Change every <code>req.user</code> to
<code>req.auth</code> and every <code>req.user!</code> to <code>req.auth!</code>.
</p>
</div>
<div class="card">
<h3>Change 7 — Remove the non-API redirect branch from jwtMiddleware</h3>
<p>
In the old local <code>auth.ts</code> (now deleted), <code>jwtMiddleware</code>
redirected non-API requests with an expired token to
<code>AUTH_HOST/api/auth/refresh-session</code>.
The connector's <code>jwtMiddleware</code> 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.
</p>
</div>
<div class="card">
<h3>Change 8 — Remove the will-redirect intercept from electron/main.ts</h3>
<p>
The <code>mainWindow.webContents.on('will-redirect', ...)</code> block
(lines 136158) exists solely to intercept the <code>/api/auth/refresh-session</code>
redirect that the old <code>jwtMiddleware</code> emitted. Once the redirect is
gone, this intercept is dead code and should be removed.
</p>
<pre><code>// Remove this entire block from electron/main.ts:
mainWindow.webContents.on( 'will-redirect', async ( event, url ) =&gt;
{
if ( url.includes( '/api/auth/refresh-session' ) )
{
// ... entire block
}
} );</code></pre>
<p style="margin-top:0.75rem">
The <code>refreshTokens()</code> helper function defined above it can also
be deleted — Electron no longer needs to do its own refresh because the
server-side <code>jwtMiddleware</code> handles it transparently.
</p>
</div>
<div class="card">
<h3>Verify</h3>
<pre><code>journalctl -u roject -f | grep '\[auth\]'</code></pre>
<p style="margin-top:0.5rem">
Log into Roject, wait 65 minutes (or temporarily set <code>ACCESS_TOKEN_TTL=65s</code>
in the test environment), then make an API call. Expect to see the
<code>[auth] tryRefresh → ...</code> sequence and a transparent recovery.
</p>
</div>
</section>
<!-- ─── styles ───────────────────────────────────────────────── -->
<section>
<h2>styles</h2>
<div class="card">
<h3>Current state</h3>
<p>
styles has no <code>jwtMiddleware</code> at all. <code>requireAuth</code> 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.
<code>STYLES_RULES</code> is currently defined inside
<code>middleware/requireAccess.ts</code> — it needs to move to <code>index.ts</code>.
</p>
</div>
<div class="card">
<h3>Change 1 — Add the submodule</h3>
<pre><code>git submodule add git@community.rokojori.com:Rokojori/rokojori-auth-connector.git source/auth-connector
git submodule update --init</code></pre>
</div>
<div class="card">
<h3>Change 2 — tsconfig path alias</h3>
<pre><code>"paths": {
"auth-connector/*": ["./source/auth-connector/source/*"]
}</code></pre>
</div>
<div class="card">
<h3>Change 3 — Delete both middleware files</h3>
<pre><code>source/server/middleware/requireAuth.ts ← delete
source/server/middleware/requireAccess.ts ← delete</code></pre>
<p style="margin-top:0.5rem">
Do this after updating <code>index.ts</code> so the service never references
the deleted files.
</p>
</div>
<div class="card">
<h3>Change 4 — Rewrite source/server/index.ts imports and wiring</h3>
<pre><code>// 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' },
];</code></pre>
<p style="margin-top:0.75rem">
Then add <code>jwtMiddleware</code> as global middleware — place it after
<code>cookieParser()</code> and before the route registrations:
</p>
<pre><code>app.use( cookieParser() );
app.use( jwtMiddleware ); // ← add this line
// rest of routes unchanged...</code></pre>
<p style="margin-top:0.75rem">
The route registrations themselves do not change — they already use
<code>requireAuth</code> and <code>requireAccess( STYLES_RULES )</code>.
</p>
</div>
<div class="card">
<h3>Verify</h3>
<pre><code>journalctl -u styles-rokojori -f | grep '\[auth\]'</code></pre>
<p style="margin-top:0.5rem">
Hit <code>/api/fonts</code> with an expired token and a valid refresh cookie.
Expect transparent recovery in the log.
</p>
</div>
</section>
<!-- ─── tunnel ───────────────────────────────────────────────── -->
<section>
<h2>tunnel</h2>
<div class="card">
<h3>Current state</h3>
<p>
tunnel has no <code>jwtMiddleware</code>. Expired tokens on
<code>/api/tunnels</code> routes always return 401 with no recovery.
Two routes do their own inline JWT handling and must be treated carefully:
</p>
<ul style="margin-top:0.75rem;line-height:1.9;font-size:0.9rem;color:var(--muted)">
<li>
<strong style="color:var(--text)">routes/agent.ts</strong> — handles WebSocket
upgrades; cannot use Express middleware. Has its own <code>jwt.verify()</code>
call. This is correct and does not change — WebSocket upgrades bypass Express
middleware entirely.
</li>
<li>
<strong style="color:var(--text)">routes/proxy.ts</strong><code>softAuth()</code>
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 <code>jwtMiddleware</code>.
</li>
</ul>
</div>
<div class="card">
<h3>Change 1 — Add the submodule</h3>
<pre><code>git submodule add git@community.rokojori.com:Rokojori/rokojori-auth-connector.git source/auth-connector
git submodule update --init</code></pre>
</div>
<div class="card">
<h3>Change 2 — tsconfig path alias</h3>
<pre><code>"paths": {
"auth-connector/*": ["./source/auth-connector/source/*"]
}</code></pre>
</div>
<div class="card">
<h3>Change 3 — Delete source/server/middleware/requireAuth.ts</h3>
<p>Delete after updating all importers below.</p>
</div>
<div class="card">
<h3>Change 4 — Update source/server/index.ts</h3>
<p>
Add <code>jwtMiddleware</code> scoped to the <code>/api/tunnels</code> routes.
The proxy route (<code>/t</code>) must <strong style="color:var(--text)">not</strong>
get <code>jwtMiddleware</code> — it does its own soft auth.
</p>
<pre><code>// 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 );</code></pre>
</div>
<div class="card">
<h3>Change 5 — Update routes/tunnels.ts</h3>
<pre><code>// Before
import { requireAuth } from '../middleware/requireAuth';
// After
import { requireAuth } from 'auth-connector/server/auth';</code></pre>
<p style="margin-top:0.5rem">
All uses of <code>req.auth</code> in this file are already correct — no other
changes needed.
</p>
</div>
<div class="card">
<h3>Change 6 — Update routes/proxy.ts</h3>
<p>
<code>proxy.ts</code> imports <code>AuthPayload</code> and
<code>extractBearer</code> from the old local <code>requireAuth.ts</code>.
<code>extractBearer</code> is internal to the connector and not exported.
Inline it locally — it is three lines.
</p>
<pre><code>// 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;
}</code></pre>
<p style="margin-top:0.75rem">
The <code>softAuth()</code> function and all other logic in proxy.ts stays
unchanged.
</p>
</div>
<div class="card">
<h3>Change 7 — Update routes/agent.ts</h3>
<pre><code>// Before
import { AuthPayload } from '../middleware/requireAuth';
// After
import type { AuthPayload } from 'auth-connector/shared/types';</code></pre>
<p style="margin-top:0.5rem">
The inline <code>jwt.verify()</code> in the WebSocket upgrade handler is
correct and stays — WebSocket upgrades bypass Express middleware.
</p>
</div>
<div class="card">
<h3>Verify</h3>
<pre><code>journalctl -u tunnel-rokojori -f | grep '\[auth\]'</code></pre>
<p style="margin-top:0.5rem">
Make an authenticated API call to <code>/api/tunnels</code> with an expired
access token and a valid refresh cookie. Expect transparent recovery in the log.
Then make a request to a public tunnel (<code>/t/:id/...</code>) without any
token — expect it to pass through without hitting the auth log at all.
</p>
</div>
</section>
<!-- ─── Execution order ───────────────────────────────────────── -->
<section>
<h2>Execution order</h2>
<div class="card">
<ol style="line-height:2.2;font-size:0.9rem;color:var(--muted)">
<li>
<strong style="color:var(--text)">rokojori-auth</strong> — fix
<code>ACCESS_TOKEN_TTL</code> to <code>'1h'</code>, redeploy, verify startup log.
</li>
<li>
<strong style="color:var(--text)">Create the Gitea repo</strong> for
<code>rokojori-auth-connector</code> and push the local directory to it.
</li>
<li>
<strong style="color:var(--text)">roject</strong> — add submodule, make all
changes, redeploy. This is the most complex change (19 renames + Electron
cleanup). Test thoroughly before moving to the others.
</li>
<li>
<strong style="color:var(--text)">styles</strong> — add submodule, rewire
index.ts, delete old middleware files, redeploy.
</li>
<li>
<strong style="color:var(--text)">tunnel</strong> — add submodule, add
jwtMiddleware to API routes, update imports in tunnels/proxy/agent,
delete old requireAuth.ts, redeploy.
</li>
</ol>
</div>
<div class="card">
<h3>After each repo</h3>
<p>
Before moving to the next repo: confirm auth still works end-to-end —
login, wait for the access token to expire (check with
<code>journalctl ... | grep '[auth]'</code>), and confirm the first API call
after expiry succeeds transparently.
</p>
</div>
</section>
<footer>
Roject &mdash; auth connector rewrite plan
</footer>
</div>
<script>var NAV_ROOT = '../';</script>
<script src="../_assets_/nav-data.js"></script>
<script src="../_assets_/nav.js"></script>
</body>
</html>