Skip to content
Back to skills

Set Up Auth

ASecurity

Use when adding authentication to a frontend SPA — treats the current user as server state (TanStack Query), keeps tokens out of localStorage (httpOnly cookies or in-memory access + refresh), wires login/logout mutations, a transparent 401→refresh retry in the fetcher, and route guards that read the user query.

  • 5 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 9, 2026
developmentgobashreactvueapifrontend

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned October 9, 2026

npx -y skills add VelimirMueller/vlm-code-context-mcp --skill set-up-auth --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Set Up Auth?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Set Up Auth
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/velimirmueller-set-up-auth/badge)](https://www.skillsdirectory.com/skills/velimirmueller-set-up-auth)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: set-up-auth
description: Use when adding authentication to a frontend SPA — treats the current user as server state (TanStack Query), keeps tokens out of localStorage (httpOnly cookies or in-memory access + refresh), wires login/logout mutations, a transparent 401→refresh retry in the fetcher, and route guards that read the user query.
---

# Set Up Auth

## 1. Audit current state

```bash
grep -rn "localStorage.*token\|sessionStorage.*token" src/ 2>/dev/null   # red flag — see step 4
grep -rn "auth/me\|currentUser\|useAuth" src/ 2>/dev/null | head
```

**Prerequisites:** `set-up-state-management` (the `fetcher` seam + query-key factory) and `set-up-routing` (guards live in `beforeLoad`/`beforeEach`). Finding tokens in `localStorage` is itself an audit finding — fix it (step 4).

## 2. Decide what to do
- No auth → full setup.
- Tokens in `localStorage` → migrate to cookies / in-memory (step 4) — that's an XSS hole.
- Auth present, no route guards → wire the guard (step 7).

## 3. Detect framework
React → hooks + TanStack Router guard. Vue → composables + Vue Router guard. The token strategy and "user is server state" rule are framework-agnostic.

## 4. Token strategy — never `localStorage`

| Strategy | Where the token lives | Use when |
|---|---|---|
| **httpOnly cookie** (preferred) | server-set cookie, JS can't read it | you control the API/domain — XSS-safe by construction |
| **In-memory access + httpOnly refresh** | access token in a JS variable; refresh token in an httpOnly cookie | cross-domain token API; access token never touches storage |

**Never** `localStorage`/`sessionStorage` for tokens — any XSS reads them. With cookies, the `fetcher` sends credentials automatically; add CSRF protection (SameSite=Lax + a CSRF token on unsafe methods).

Upgrade `src/libs/fetcher.ts` to the **auth version** in [`../_shared/fetcher.md`](../_shared/fetcher.md): `credentials: 'include'` for the cookie, an `X-CSRF-Token` header on unsafe methods, and the 401 refresh from step 8. It builds headers from `new Headers(init?.headers)` *after* spreading `init`, so a caller's own headers can never erase the CSRF token.

## 5. The current user is server state

Define shared query options (usable by both the hook and the route guard). First extend the query-key factory with an `auth` group:

```ts
// src/libs/queryKeys.ts — add alongside the existing `todos` group
export const queryKeys = {
  todos: { /* …existing… */ },
  auth: {
    all: ['auth'] as const,
    me: () => [...queryKeys.auth.all, 'me'] as const,
  },
} as const;
```

```ts
// src/libs/auth.ts
import { queryOptions } from '@tanstack/react-query';
import { fetcher, HttpError } from '@/libs/fetcher';
import { queryKeys } from '@/libs/queryKeys';

export type User = { id: string; name: string };

export const currentUserQueryOptions = queryOptions({
  queryKey: queryKeys.auth.me(),
  // 401 = signed out, not an error: the guard needs `null` to redirect.
  queryFn: () =>
    fetcher<User>('/auth/me').catch((e: unknown) => {
      if (e instanceof HttpError && e.status === 401) return null;
      throw e;
    }),
  retry: false,
  staleTime: Infinity, // session rarely changes; invalidate on login/logout
});
```
```ts
// src/hooks/useCurrentUser.ts
import { useQuery } from '@tanstack/react-query';
import { currentUserQueryOptions } from '@/libs/auth';

export function useCurrentUser() {
  const query = useQuery(currentUserQueryOptions);
  return { ...query, isAuthenticated: !!query.data };
}
```

## 6. Login / logout as mutations

```ts
// src/hooks/useLogin.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { fetcher } from '@/libs/fetcher';
import { queryKeys } from '@/libs/queryKeys';

export function useLogin() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (creds: { email: string; password: string }) =>
      fetcher('/auth/login', { method: 'POST', body: JSON.stringify(creds) }),
    onSuccess: () => queryClient.invalidateQueries({ queryKey: queryKeys.auth.all }),
  });
}
// useLogout: POST /auth/logout, then queryClient.clear() (drop all cached user data)
```

## 7. Guard routes via the user query

```tsx
// React (TanStack Router): src/routes/dashboard.tsx
import { createFileRoute, redirect } from '@tanstack/react-router';
import { currentUserQueryOptions } from '@/libs/auth';

export const Route = createFileRoute('/dashboard')({
  beforeLoad: async ({ context, location }) => {
    const user = await context.queryClient.ensureQueryData(currentUserQueryOptions);
    if (!user) throw redirect({ to: '/login', search: { redirect: location.href } });
  },
  component: () => <h1>Dashboard</h1>,
});
```
Vue: in `router.beforeEach`, `await queryClient.ensureQueryData(currentUserQueryOptions)` (or read a Pinia auth store hydrated from it) and redirect if absent.

## 8. Transparent refresh (in-memory token strategy)

On a 401, the `fetcher` calls `/auth/refresh` once, retries the original request, and on failure throws `HttpError(401)` — the guard, not the fetcher, redirects to login. Keep the refresh in the fetcher so call sites never handle expiry — see `auth-patterns.md`.

The code is the `refreshSession` / `retried` part of the auth version in [`../_shared/fetcher.md`](../_shared/fetcher.md). Concurrent 401s share one in-flight refresh promise instead of stampeding, and the original request is retried once. When the refresh fails, the fetcher throws `HttpError(401)`; `currentUserQueryOptions` maps that to `null`, so the guard in step 7 redirects instead of crashing.

## 9. Verify
```bash
pnpm tsc --noEmit
```
Log in → protected route loads and `/auth/me` is cached; log out → `queryClient.clear()` empties it and the guard bounces to `/login`. Confirm no token is in `localStorage`/`sessionStorage` (DevTools → Application).

## References
- ./auth-patterns.md — token storage threat model, user-is-server-state, guard-at-the-boundary, 401 refresh, CSRF, third-party providers.
- ../_shared/fetcher.md — the canonical `fetcher`, base and auth versions.
- ../set-up-state-management/SKILL.md — the `fetcher` seam + query-key factory this extends.
- ../set-up-routing/SKILL.md — the guard hook points.

Files in this skill

  • SKILL.md6.1 KB
  • auth-patterns.md3.9 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.