auth: middleware gates all pages + APIs behind session check
Ship an Astro middleware (src/middleware.ts) that runs on every request
and requires a valid Heady session for anything other than the login
flow itself. Previously any anonymous visitor could load /machines,
/users, /acls, /dns, /settings and the mock /api/users endpoint over
plaintext -- the SSR templates rendered as public shells and only the
front-end auth guard blocked interaction. That's a defense-in-depth
gap now that data endpoints are being wired up.
- src/middleware.ts: onRequest handler validates the heady_session
cookie via HeadySessionManager.validateSession(). Public allowlist:
/api/auth/*, /login, static assets. Unauthenticated /api/* returns
401 JSON; unauthenticated pages redirect to /api/auth/login with a
return_to query param so the OIDC round-trip lands back on the
original URL. Authenticated requests get context.locals.user set so
downstream pages can read Astro.locals.user without a second lookup.
- src/env.d.ts: type App.Locals.user as SessionUser so pages / APIs get
IDE feedback on typos.
- src/pages/{index,machines,acls,dns,users}.astro: 'export const
prerender = false' -- these pages render per-user data, they must
hit middleware on every request rather than being served as a
static file baked at build time. Without this, middleware runs
once during prerender (with no cookie) and writes a redirect as
the static HTML for the route.
- src/pages/api/{acls,users,dns/magic,dns/tailnet,settings/auth-keys}.ts:
same reason. API endpoints must be SSR so the middleware can gate
them at request time.
This commit is contained in:
parent
09b1bae157
commit
4c82487e0e
12 changed files with 105 additions and 0 deletions
65
src/middleware.ts
Normal file
65
src/middleware.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { defineMiddleware } from 'astro:middleware';
|
||||
import { getSessionManager } from './lib/auth/session-manager.js';
|
||||
|
||||
// Public routes — must be reachable WITHOUT a valid session so the auth
|
||||
// flow itself can happen. Everything else requires authentication.
|
||||
// The dashboard at `/` is intentionally NOT public: it renders real
|
||||
// machine + session data. Add explicit landing/marketing paths here if
|
||||
// one is added later.
|
||||
const PUBLIC_API_PREFIX = '/api/auth/';
|
||||
const PUBLIC_PAGE_PATHS = new Set<string>([
|
||||
'/login',
|
||||
'/login/',
|
||||
]);
|
||||
const PUBLIC_ASSET_PREFIXES = ['/assets/', '/_astro/'];
|
||||
const PUBLIC_ASSET_FILES = new Set<string>([
|
||||
'/favicon.svg',
|
||||
'/favicon.ico',
|
||||
'/robots.txt',
|
||||
]);
|
||||
|
||||
function isPublicPath(pathname: string): boolean {
|
||||
if (pathname.startsWith(PUBLIC_API_PREFIX)) return true;
|
||||
if (PUBLIC_PAGE_PATHS.has(pathname)) return true;
|
||||
if (PUBLIC_ASSET_FILES.has(pathname)) return true;
|
||||
for (const prefix of PUBLIC_ASSET_PREFIXES) {
|
||||
if (pathname.startsWith(prefix)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export const onRequest = defineMiddleware(async (context, next) => {
|
||||
const { pathname } = context.url;
|
||||
|
||||
if (isPublicPath(pathname)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const sessionMgr = getSessionManager();
|
||||
const result = await sessionMgr.validateSession(context);
|
||||
|
||||
if (result.valid && result.user) {
|
||||
context.locals.user = result.user;
|
||||
return next();
|
||||
}
|
||||
|
||||
// Unauthenticated: JSON for /api/*, redirect for pages.
|
||||
if (pathname.startsWith('/api/')) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
authenticated: false,
|
||||
error: 'authentication_required',
|
||||
reason: result.reason ?? 'missing_cookie',
|
||||
login_url: '/api/auth/login',
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const loginUrl = new URL('/api/auth/login', context.url);
|
||||
loginUrl.searchParams.set('return_to', pathname + context.url.search);
|
||||
return context.redirect(loginUrl.pathname + loginUrl.search, 302);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue