agentsclimarketplace

Secure a frontend app

Skill kennguyen887/agent-foundation/skills/secure-a-frontend-app

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.From its SKILL.md

Install
npx -y skills add kennguyen887/agent-foundation --skill secure-a-frontend-app

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

3 things to look at

  • reads credentialsReads from 3 credential sources: `/vault/secrets/global` and 2 more.
  • 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.
  • runs commandsInstructs the agent to run 1 command, including `grep -rn "NEXT_PUBLIC_" src`.

SKILL.md

5.1 KB, ~1.2k tokens by cl100k_base, 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 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).

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.