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

513 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>