agentleFS
kennguyen887kennguyen887/agent-foundation/skills/secure-a-frontend-app/SKILL.md

secure-a-frontend-app

Skill1 starsChanged 5 months agoMITView on GitHub

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.

  • Reads credentials

The file

Files
SKILL.md

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 session resolve, 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 useAuth hook reads the session and calls BaseHttp.saveToken(session.accessToken) (the HTTP client is in write-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 + a permissions: string[]). Don't re-derive roles ad hoc per component.
  • Permission matrix as feature.action strings. 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.ts handles 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 by feature.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 .env locally):
    // 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 use NEXT_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.ts stays thin.
  • grep -rn "NEXT_PUBLIC_" src shows 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 — the BaseHttp 401-refresh queue, SSR guards, config module.
  • structure-a-frontend-app · code-conventions · release-safety (backend release/secrets).

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.