# Frontend Auth

> Purpose: Guidance for implementing authentication flows using Better Auth and JWT token management.

- Skill: `majiayu000/frontend-auth` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add majiayu000/frontend-auth`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/frontend-auth/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/majiayu000/frontend-auth

---


# Frontend Authentication Skill

**Purpose**: Guidance for implementing authentication flows using Better Auth and JWT token management.

## Overview

Authentication uses Better Auth library with JWT plugin enabled. JWT tokens are stored in localStorage and automatically attached to API requests. Session management handles token expiration and refresh.

## Better Auth Configuration

### Pattern from `frontend/lib/auth.ts`

```typescript
import { createAuthClient } from "better-auth/react";

export const authClient = createAuthClient({
  baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL || "http://localhost:3000",

  // Session configuration
  session: {
    cookieName: "better-auth.session",
    expiresIn: 60 * 60 * 24 * 7, // 7 days
    updateAge: 60 * 60 * 24, // Update session every 24 hours
  },

  // Storage configuration (for client-side token management)
  storage: typeof window !== "undefined" ? localStorage : undefined,
});
```

**Pattern**:
- Use environment variable `NEXT_PUBLIC_BETTER_AUTH_URL` with fallback
- Session expires in 7 days
- Session updates every 24 hours
- Use localStorage for client-side storage
- Check for `window` before accessing localStorage

## Authentication Functions

### 1. Sign Up

```typescript
export async function signUp(email: string, password: string, name: string) {
  try {
    const response = await authClient.signUp.email({
      email,
      password,
      name,
    });

    return response;
  } catch (error: any) {
    throw new Error(error.message || "Signup failed");
  }
}
```

**Pattern**:
- Use `authClient.signUp.email()` method
- Pass email, password, and name
- Handle errors with try-catch
- Throw user-friendly error messages

### 2. Sign In

```typescript
export async function signIn(email: string, password: string) {
  try {
    const response = await authClient.signIn.email({
      email,
      password,
    });

    return response;
  } catch (error: any) {
    throw new Error(error.message || "Signin failed");
  }
}
```

**Pattern**:
- Use `authClient.signIn.email()` method
- Pass email and password
- Handle errors with try-catch

### 3. Sign Out

```typescript
export async function signOut() {
  try {
    await authClient.signOut();

    // Clear auth token from localStorage
    if (typeof window !== "undefined") {
      localStorage.removeItem("auth_token");
    }
  } catch (error) {
    console.error("Signout error:", error);
    throw error;
  }
}
```

**Pattern**:
- Call `authClient.signOut()`
- Remove token from localStorage
- Check for `window` before accessing localStorage

### 4. Check Authentication

```typescript
export async function isAuthenticated(): Promise<boolean> {
  const session = await getSession();
  return session !== null && session.user !== null;
}
```

**Pattern**:
- Get session using `getSession()`
- Check if session and user exist
- Return boolean

### 5. Get Current User

```typescript
export async function getCurrentUser() {
  const session = await getSession();
  return session?.user || null;
}
```

**Pattern**:
- Get session using `getSession()`
- Return user from session or null

### 6. Get Session

```typescript
export async function getSession() {
  try {
    const session = await authClient.getSession();
    return session;
  } catch (error) {
    console.error("Failed to get session:", error);
    return null;
  }
}
```

**Pattern**:
- Use `authClient.getSession()`
- Handle errors gracefully
- Return null on error

## JWT Token Management (from `frontend/lib/api.ts`)

### 1. Token Storage After Successful Auth

```typescript
async signup(userData: UserSignupData): Promise<ApiResponse<AuthResponse>> {
  const response = await apiFetch<AuthResponse>("/api/auth/signup", {
    method: "POST",
    body: JSON.stringify(userData),
  });

  // Store token if signup successful
  if (response.success && response.data?.token) {
    if (typeof window !== "undefined") {
      localStorage.setItem("auth_token", response.data.token);
    }
  }

  return response;
}
```

**Pattern**:
- Check `response.success` and `response.data?.token`
- Store token in localStorage with key `"auth_token"`
- Check for `window` before accessing localStorage

### 2. Token Retrieval

```typescript
const getAuthToken = (): string | null => {
  if (typeof window === "undefined") return null;
  const token = localStorage.getItem("auth_token");
  return token;
};
```

**Pattern**:
- Check for `window` to ensure client-side
- Retrieve token from localStorage
- Return null if token doesn't exist

### 3. Token Attachment to API Requests

```typescript
const headers: HeadersInit = {
  "Content-Type": "application/json",
  ...options.headers,
};

// Attach JWT token if available
if (token) {
  headers["Authorization"] = `Bearer ${token}`;
}
```

**Pattern**:
- Get token using `getAuthToken()`
- Add `Authorization: Bearer ${token}` header if token exists
- Use in all API requests

### 4. Token Removal on Signout

```typescript
async signout(): Promise<ApiResponse<void>> {
  const response = await apiFetch<void>("/api/auth/signout", {
    method: "POST",
  });

  // Remove token on signout
  if (typeof window !== "undefined") {
    localStorage.removeItem("auth_token");
  }

  return response;
}
```

**Pattern**:
- Remove token from localStorage on signout
- Check for `window` before accessing localStorage

### 5. 401 Error Handling

```typescript
const handleApiError = (error: any, statusCode?: number): never => {
  // Redirect to login on 401 Unauthorized
  if (statusCode === 401) {
    if (typeof window !== "undefined") {
      localStorage.removeItem("auth_token");
      window.location.href = "/signin";
    }
  }

  throw {
    message: error.message || "An error occurred",
    code: error.code || "UNKNOWN_ERROR",
    statusCode: statusCode || 500,
  };
};
```

**Pattern**:
- Check for 401 status code
- Remove token from localStorage
- Redirect to `/signin` page
- Throw structured error

## Protected Route Pattern

**From**: `frontend/components/ProtectedRoute.tsx`

```typescript
"use client";

import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { isAuthenticated } from "@/lib/auth";
import LoadingSpinner from "./LoadingSpinner";

export default function ProtectedRoute({ children }: ProtectedRouteProps) {
  const router = useRouter();
  const [isAuthorized, setIsAuthorized] = useState(false);
  const [isChecking, setIsChecking] = useState(true);

  useEffect(() => {
    async function checkAuth() {
      try {
        const authenticated = await isAuthenticated();

        if (!authenticated) {
          // Store the intended destination for redirect after login
          const currentPath = window.location.pathname;
          if (currentPath !== "/signin") {
            sessionStorage.setItem("redirectAfterLogin", currentPath);
          }

          router.push("/signin");
        } else {
          setIsAuthorized(true);
        }
      } catch (error) {
        console.error("Auth check failed:", error);
        router.push("/signin");
      } finally {
        setIsChecking(false);
      }
    }

    checkAuth();
  }, [router]);

  if (isChecking) {
    return (
      <div className="min-h-screen flex items-center justify-center">
        <LoadingSpinner size="large" />
      </div>
    );
  }

  if (!isAuthorized) {
    return null;
  }

  return <>{children}</>;
}
```

**Pattern**:
- Check authentication on mount
- Show loading spinner during check
- Store intended destination in `sessionStorage` with key `"redirectAfterLogin"`
- Redirect to `/signin` if not authenticated
- Only render children if authorized

## Form Validation Pattern

**From**: `frontend/app/signup/page.tsx`

### Email Validation

```typescript
import { isValidEmail } from "@/lib/utils";

if (!formData.email.trim()) {
  errors.email = "Email is required";
} else if (!isValidEmail(formData.email)) {
  errors.email = "Please enter a valid email address";
}
```

**Pattern**:
- Use `isValidEmail()` utility from `@/lib/utils`
- Check for empty email
- Validate email format

### Password Validation

```typescript
import { getPasswordStrength } from "@/lib/utils";

const { strength, message } = getPasswordStrength(formData.password);
if (strength === "weak") {
  errors.password = message || "Password is too weak";
}
```

**Pattern**:
- Use `getPasswordStrength()` utility
- Check password strength
- Show strength message to user

### Form Submission with Auth

```typescript
const handleSubmit = async (e: React.FormEvent) => {
  e.preventDefault();
  setApiError("");

  if (!validateForm()) {
    return;
  }

  setIsLoading(true);

  try {
    const response = await api.signup({
      name: formData.name.trim(),
      email: formData.email.trim().toLowerCase(),
      password: formData.password,
    });

    if (response.success) {
      // Redirect to dashboard or intended destination
      const redirectPath = sessionStorage.getItem("redirectAfterLogin") || "/dashboard";
      sessionStorage.removeItem("redirectAfterLogin");
      router.push(redirectPath);
    } else {
      setApiError(response.message || "Signup failed");
    }
  } catch (error: any) {
    setApiError(error.message || "An error occurred");
  } finally {
    setIsLoading(false);
  }
};
```

**Pattern**:
- Validate form before submission
- Call API method (`api.signup()` or `api.signin()`)
- Check `response.success`
- Redirect to intended destination or dashboard
- Handle errors with user-friendly messages

## Constitution Requirements

- **FR-001**: User account creation ✅
- **FR-002**: User authentication with JWT ✅
- **FR-026**: Better Auth with JWT plugin ✅
- **FR-028**: Auto-attach JWT token ✅
- **FR-029**: Handle 401 errors ✅

## MCP Server Usage

- **Better Auth MCP Server**: Use for Better Auth configuration patterns and best practices

## References

- **Specification**: `specs/002-frontend-todo-app/spec.md` - Authentication requirements
- **Better Auth Config**: `frontend/lib/auth.ts` - Better Auth implementation
- **API Client**: `frontend/lib/api.ts` - JWT token handling
- **Protected Route**: `frontend/components/ProtectedRoute.tsx` - Protected route pattern
- **Signup Page**: `frontend/app/signup/page.tsx` - Signup form pattern
- **Signin Page**: `frontend/app/signin/page.tsx` - Signin form pattern

## Common Patterns Summary

1. ✅ Use Better Auth `createAuthClient()` for configuration
2. ✅ Store JWT token in localStorage with key `"auth_token"`
3. ✅ Attach token to API requests via `Authorization: Bearer ${token}` header
4. ✅ Remove token on signout
5. ✅ Handle 401 errors with redirect to `/signin`
6. ✅ Check authentication before rendering protected routes
7. ✅ Store intended destination in sessionStorage for redirect after login
8. ✅ Validate email with `isValidEmail()` utility
9. ✅ Validate password with `getPasswordStrength()` utility
10. ✅ Always check for `window` before accessing localStorage


