Skip to main content
Implement authentication flows with protected routes, redirects, and context-based access control.

Setup auth context

Create an authentication context for your router:
src/auth.tsx
export interface AuthContext {
  isAuthenticated: boolean
  user: User | null
  login: (username: string, password: string) => Promise<void>
  logout: () => Promise<void>
}

export function createAuthContext(): AuthContext {
  let user: User | null = null
  
  return {
    get isAuthenticated() {
      return user !== null
    },
    get user() {
      return user
    },
    async login(username, password) {
      const response = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify({ username, password }),
      })
      user = await response.json()
    },
    async logout() {
      await fetch('/api/logout', { method: 'POST' })
      user = null
    },
  }
}

Router with auth context

Provide auth context to all routes:
src/main.tsx
import { createRouter, RouterProvider } from '@tanstack/react-router'
import { createAuthContext } from './auth'
import { routeTree } from './routeTree.gen'

const auth = createAuthContext()

const router = createRouter({
  routeTree,
  context: {
    auth,
  },
})

// Register for type safety
declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router
  }
}

function App() {
  return <RouterProvider router={router} />
}

Protected routes

Guard routes with authentication checks:
src/routes/_authenticated.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'

export const Route = createFileRoute('/_authenticated')({
  beforeLoad: async ({ context, location }) => {
    if (!context.auth.isAuthenticated) {
      throw redirect({
        to: '/login',
        search: {
          // Redirect back after login
          redirect: location.href,
        },
      })
    }
  },
})
src/routes/
  _authenticated.tsx        -> Auth layout (checks auth)
  _authenticated/
    dashboard.tsx           -> /dashboard (protected)
    profile.tsx             -> /profile (protected)
    settings.tsx            -> /settings (protected)

Login page

Implement login with redirect:
src/routes/login.tsx
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { z } from 'zod'

const loginSearchSchema = z.object({
  redirect: z.string().optional(),
})

export const Route = createFileRoute('/login')({
  validateSearch: loginSearchSchema,
  component: LoginPage,
})

function LoginPage() {
  const navigate = useNavigate()
  const { redirect } = Route.useSearch()
  const { auth } = Route.useRouteContext()
  const [error, setError] = React.useState<string>()
  
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    const formData = new FormData(e.currentTarget)
    const username = formData.get('username') as string
    const password = formData.get('password') as string
    
    try {
      await auth.login(username, password)
      
      // Redirect to original destination or dashboard
      navigate({ to: redirect || '/dashboard' })
    } catch (err) {
      setError('Invalid credentials')
    }
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <input name="username" type="text" required />
      <input name="password" type="password" required />
      {error && <p className="error">{error}</p>}
      <button type="submit">Login</button>
    </form>
  )
}

Role-based access

Protect routes based on user roles:
src/routes/_authenticated/_admin.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'

export const Route = createFileRoute('/_authenticated/_admin')({
  beforeLoad: async ({ context }) => {
    if (context.auth.user?.role !== 'admin') {
      throw redirect({
        to: '/unauthorized',
      })
    }
  },
})
src/routes/
  _authenticated/
    _admin/
      users.tsx             -> /users (admin only)
      settings.tsx          -> /settings (admin only)

Conditional navigation

Show links based on permissions:
import { Link } from '@tanstack/react-router'

function Navigation() {
  const { auth } = Route.useRouteContext()
  
  return (
    <nav>
      <Link to="/">Home</Link>
      
      {auth.isAuthenticated ? (
        <>
          <Link to="/dashboard">Dashboard</Link>
          <Link to="/profile">Profile</Link>
          {auth.user?.role === 'admin' && (
            <Link to="/admin">Admin</Link>
          )}
          <button onClick={auth.logout}>Logout</button>
        </>
      ) : (
        <Link to="/login">Login</Link>
      )}
    </nav>
  )
}

Session management

Check and refresh authentication:
src/routes/__root.tsx
import { createRootRouteWithContext } from '@tanstack/react-router'
import type { AuthContext } from '@/auth'

interface RouterContext {
  auth: AuthContext
}

export const Route = createRootRouteWithContext<RouterContext>()({
  beforeLoad: async ({ context }) => {
    // Check session on every navigation
    try {
      const response = await fetch('/api/session')
      if (!response.ok) {
        context.auth.user = null
      }
    } catch (err) {
      context.auth.user = null
    }
  },
})

Token-based auth

Store and use JWT tokens:
src/auth.tsx
export function createAuthContext() {
  const getToken = () => localStorage.getItem('auth_token')
  const setToken = (token: string) => localStorage.setItem('auth_token', token)
  const clearToken = () => localStorage.removeItem('auth_token')
  
  return {
    get isAuthenticated() {
      return !!getToken()
    },
    async login(username: string, password: string) {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username, password }),
      })
      
      const { token } = await response.json()
      setToken(token)
    },
    async logout() {
      clearToken()
    },
    async fetch(url: string, options?: RequestInit) {
      const token = getToken()
      return fetch(url, {
        ...options,
        headers: {
          ...options?.headers,
          Authorization: `Bearer ${token}`,
        },
      })
    },
  }
}

OAuth/Social login

Implement OAuth flows:
src/routes/auth/callback.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'
import { z } from 'zod'

const callbackSearchSchema = z.object({
  code: z.string(),
  state: z.string().optional(),
})

export const Route = createFileRoute('/auth/callback')({
  validateSearch: callbackSearchSchema,
  loader: async ({ search, context }) => {
    // Exchange code for token
    const response = await fetch('/api/oauth/token', {
      method: 'POST',
      body: JSON.stringify({ code: search.code }),
    })
    
    const { token } = await response.json()
    localStorage.setItem('auth_token', token)
    
    // Redirect to app
    throw redirect({ to: '/dashboard' })
  },
})

Logout handling

Clear session and redirect:
function LogoutButton() {
  const navigate = useNavigate()
  const { auth } = Route.useRouteContext()
  
  const handleLogout = async () => {
    await auth.logout()
    navigate({ to: '/login' })
  }
  
  return <button onClick={handleLogout}>Logout</button>
}

Persisting auth state

Restore auth on page reload:
src/main.tsx
const auth = createAuthContext()

// Restore session on app load
const restoreSession = async () => {
  try {
    const response = await fetch('/api/session')
    if (response.ok) {
      const user = await response.json()
      auth.user = user
    }
  } catch (err) {
    console.error('Failed to restore session', err)
  }
}

restoreSession().then(() => {
  const router = createRouter({ routeTree, context: { auth } })
  // ... render app
})

Best practices

Wrap protected routes in _authenticated layout for clean URLs.
Use beforeLoad instead of loader for auth checks to avoid data fetching for unauthorized users.
Only store essential auth state - fetch user details when needed.
Implement token refresh logic and redirect to login on expiration.
Always use HTTPS to protect authentication tokens and credentials.

Security considerations

Never store sensitive data like passwords in localStorage. Use secure, httpOnly cookies for tokens when possible.
Always validate user permissions on the server. Client-side checks are for UX only.
Implement CSRF protection for state-changing operations.

Next steps

Data loading

Load user data in protected routes

beforeLoad hook

Learn more about beforeLoad

Build docs developers (and LLMs) love