Secure a frontend app
Skill kennguyen887/agent-foundation/skills/secure-a-frontend-app
Claude Code skills marketplace — backend & frontend engineering conventions + step-by-step third-party integration recipes: Stripe, Rapyd, CyberSource, UOB & wallet payments, Singpass/Keycloak OIDC & 3-D Secure, Twilio SMS, Docker & CI/CD. NestJS/TypeScript + React, language-flexible.
npx -y skills add kennguyen887/agent-foundation --skill secure-a-frontend-appAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when wiring auth/session, protecting routes, or handling secrets in a frontend app — OIDC login via NextAuth + server session store + access-token refresh & injection, route guards + permission-matrix RBAC, SSR cookie propagation + hydration, and vault secrets + public/private config split. React/Next.js reference, framework-flexible.
SKILL.md
5.1 KB, as published. Nobody here has run it
Secure a frontend app
Auth, session, access control, and secrets for a web app. App structure: structure-a-frontend-app;
feature code: write-frontend-code.
1. Auth & session
- OIDC via NextAuth. Configure the provider (with PKCE/nonce checks) in the auth route; store the session server-side (a session-store adapter, e.g. Redis) rather than a fat JWT cookie.
- Refresh in the session callback. On each
sessionresolve, check expiry and rotate the access token before handing it to the client:async session({ session, user }) { const acct = await store.getAccount(user.id); if (Date.parse(acct.expires_at) < Date.now()) { const next = await refreshAccessToken(acct); // rotate session.accessToken = next.access_token; } return session; } - One client-side entry injects the token into the HTTP client: a
useAuthhook reads the session and callsBaseHttp.saveToken(session.accessToken)(the HTTP client is inwrite-frontend-code§2). - 401 → one queued refresh + replay at the HTTP layer (the refresh QUEUE in
write-frontend-code§2): concurrent 401s wait on a single refresh, then retry. ▸ Other stacks: any OIDC lib (Auth.js / oidc-client) + a server session store; rotate in the session hook, inject once into the HTTP client, queue-refresh on 401.
2. SSR cookie propagation + hydration
- Pass auth cookies from SSR into the app via the app-init hook, then read them in providers:
MyApp.getInitialProps = async (ctx) => ({ cookies: parseCookies(ctx.req), origin: ctx.req.headers.host }); // _app: <SessionProvider session={pageProps.session}><LocalizationProvider cookies={cookies}>… - Guard token init behind hydration — don't read the session until the router/session is ready:
useEffect(() => { if (!router.isReady || status === 'loading') return; if (session) BaseHttp.saveToken(session.accessToken); }, [router.isReady, status]);
▸ Other stacks: read auth cookies server-side, hand to a provider; gate client token use on "ready/mounted".
3. Route protection & RBAC
- Roles + permissions come from one profile hook (
useMyProfile→ memoized role booleans + apermissions: string[]). Don't re-derive roles ad hoc per component. - Permission matrix as
feature.actionstrings. Gate UI with a checker, routes with a guard:// component-level visibility can({ feature: 'listing', action: 'create' }); // → permissions.includes('listing.create') // route-level (a GuardContainer wrapping the app) const allowed = routesForPermissions(permissions).includes(currentRoute); if (!allowed && role) { confirm('Access denied'); router.replace(Routes.Home); } middleware.tshandles edge redirects/rewrites (e.g. a cookie-driven path → canonical route); keep it thin — real auth/role logic lives in the guard + matrix, not middleware. ▸ Other stacks: a route-guard wrapper + a permission map; render-gate byfeature.action, redirect on deny.
4. Secrets & config safety
- Server secrets from a vault, never the bundle. Load server-only env from a mounted secrets path
at boot (fall back to
.envlocally):// next.config.js const paths = ['/vault/secrets/global', '/vault/secrets/app']; paths.every(fs.existsSync) ? paths.forEach((p) => dotenv.config({ path: p })) : dotenv.config(); - Public vs private split in one Config module — only
NEXT_PUBLIC_*(client-exposed) values go in the public group; secrets (client secret, store password, service keys) stay server-only. Mark server-only vars in.env.example("do not useNEXT_PUBLIC_— never expose to the browser"). - A value reaches the browser bundle only if it is
NEXT_PUBLIC_-prefixed AND in the public Config group — audit both sides. ▸ Other stacks: the framework's public-env prefix + a settings module separating server secrets; pull secrets from a vault/secret-manager, not committed env.
Verification
- Login goes through the OIDC provider; tokens are stored server-side; the session callback refreshes expiring tokens; the client injects the token once; a 401 triggers one queued refresh + replay.
- Roles/permissions come from the profile hook; UI gates on
feature.action; a route guard redirects on deny;middleware.tsstays thin. grep -rn "NEXT_PUBLIC_" srcshows only client-safe values; secrets load from the vault path; no secret sits in the public Config group or the client bundle.
Related
write-frontend-code— theBaseHttp401-refresh queue, SSR guards, config module.structure-a-frontend-app·code-conventions·release-safety(backend release/secrets).