Authentication Example

Complete authentication flow implementation with JWT tokens, protected routes, and session management.

📦 View the complete example on GitHub:

storken/examples/auth-app

Auth Store Setup

// auth-store.ts
import { create } from 'storken'

interface User {
  id: string
  email: string
  name: string
  avatar?: string
  role: 'user' | 'admin'
}

interface AuthState {
  user: User | null
  isAuthenticated: boolean
  isLoading: boolean
  token: string | null
  refreshToken: string | null
  error: string | null
}

export const [useAuth, getAuth, setAuth, sky] = create<AuthState>({
  initialValues: {
    user: null,
    isAuthenticated: false,
    isLoading: true,
    token: null,
    refreshToken: null,
    error: null
  },
  
  plugins: {
    // Persist auth tokens
    persist: {
      name: 'auth-persist',
      init(storken) {
        const token = localStorage.getItem('token')
        const refreshToken = localStorage.getItem('refreshToken')
        
        if (token && refreshToken) {
          storken.set('token', token)
          storken.set('refreshToken', refreshToken)
          // Validate token on init
          validateToken(token)
        } else {
          storken.set('isLoading', false)
        }
      },
      afterSet(key, value) {
        if (key === 'token') {
          if (value) {
            localStorage.setItem('token', value)
          } else {
            localStorage.removeItem('token')
          }
        }
        if (key === 'refreshToken') {
          if (value) {
            localStorage.setItem('refreshToken', value)
          } else {
            localStorage.removeItem('refreshToken')
          }
        }
      }
    }
  }
})

Auth Service

// auth-service.ts
import { sky } from './auth-store'

const API_URL = process.env.NEXT_PUBLIC_API_URL

export const authService = {
  async login(email: string, password: string) {
    sky.set('isLoading', true)
    sky.set('error', null)
    
    try {
      const response = await fetch(`${API_URL}/auth/login`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password })
      })
      
      if (!response.ok) {
        throw new Error('Invalid credentials')
      }
      
      const data = await response.json()
      
      // Update auth state
      sky.set('user', data.user)
      sky.set('token', data.token)
      sky.set('refreshToken', data.refreshToken)
      sky.set('isAuthenticated', true)
      
      // Setup token refresh
      this.setupTokenRefresh()
      
      return data.user
    } catch (error) {
      sky.set('error', error.message)
      throw error
    } finally {
      sky.set('isLoading', false)
    }
  },
  
  async register(email: string, password: string, name: string) {
    sky.set('isLoading', true)
    sky.set('error', null)
    
    try {
      const response = await fetch(`${API_URL}/auth/register`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password, name })
      })
      
      if (!response.ok) {
        const error = await response.json()
        throw new Error(error.message || 'Registration failed')
      }
      
      const data = await response.json()
      
      // Auto-login after registration
      sky.set('user', data.user)
      sky.set('token', data.token)
      sky.set('refreshToken', data.refreshToken)
      sky.set('isAuthenticated', true)
      
      return data.user
    } catch (error) {
      sky.set('error', error.message)
      throw error
    } finally {
      sky.set('isLoading', false)
    }
  },
  
  async logout() {
    const token = sky.get('token')
    
    // Notify backend
    if (token) {
      await fetch(`${API_URL}/auth/logout`, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${token}`
        }
      }).catch(() => {})
    }
    
    // Clear auth state
    sky.reset('user')
    sky.reset('token')
    sky.reset('refreshToken')
    sky.set('isAuthenticated', false)
    
    // Clear refresh timer
    this.clearTokenRefresh()
  },
  
  async refreshToken() {
    const refreshToken = sky.get('refreshToken')
    
    if (!refreshToken) {
      throw new Error('No refresh token')
    }
    
    try {
      const response = await fetch(`${API_URL}/auth/refresh`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ refreshToken })
      })
      
      if (!response.ok) {
        throw new Error('Token refresh failed')
      }
      
      const data = await response.json()
      
      sky.set('token', data.token)
      sky.set('refreshToken', data.refreshToken)
      
      return data.token
    } catch (error) {
      // Refresh failed, logout user
      await this.logout()
      throw error
    }
  },
  
  refreshTimer: null as NodeJS.Timeout | null,
  
  setupTokenRefresh() {
    // Refresh token every 15 minutes
    this.refreshTimer = setInterval(() => {
      this.refreshToken().catch(console.error)
    }, 15 * 60 * 1000)
  },
  
  clearTokenRefresh() {
    if (this.refreshTimer) {
      clearInterval(this.refreshTimer)
      this.refreshTimer = null
    }
  }
}

Login Component

// LoginForm.tsx
import { useState } from 'react'
import { useAuth } from './auth-store'
import { authService } from './auth-service'
import { useRouter } from 'next/navigation'

export function LoginForm() {
  const router = useRouter()
  const [, , , isLoading, , , error] = useAuth('isLoading')
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    
    try {
      await authService.login(email, password)
      router.push('/dashboard')
    } catch (error) {
      // Error is handled in auth service
    }
  }
  
  return (
    <form onSubmit={handleSubmit} className="space-y-4 max-w-md mx-auto">
      <h2 className="text-2xl font-bold text-center">Login</h2>
      
      {error && (
        <div className="p-3 bg-red-100 text-red-700 rounded">
          {error}
        </div>
      )}
      
      <div>
        <label htmlFor="email" className="block text-sm font-medium mb-2">
          Email
        </label>
        <input
          id="email"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
          className="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
        />
      </div>
      
      <div>
        <label htmlFor="password" className="block text-sm font-medium mb-2">
          Password
        </label>
        <input
          id="password"
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
          className="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
        />
      </div>
      
      <button
        type="submit"
        disabled={isLoading}
        className="w-full py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
      >
        {isLoading ? 'Logging in...' : 'Login'}
      </button>
      
      <p className="text-center text-sm">
        Don't have an account?{' '}
        <a href="/register" className="text-blue-600 hover:underline">
          Register
        </a>
      </p>
    </form>
  )
}

Protected Route Component

// ProtectedRoute.tsx
import { useEffect } from 'react'
import { useAuth } from './auth-store'
import { useRouter } from 'next/navigation'

interface ProtectedRouteProps {
  children: React.ReactNode
  requiredRole?: 'user' | 'admin'
  fallback?: React.ReactNode
}

export function ProtectedRoute({ 
  children, 
  requiredRole,
  fallback = <div>Loading...</div>
}: ProtectedRouteProps) {
  const router = useRouter()
  const [isAuthenticated] = useAuth('isAuthenticated')
  const [user] = useAuth('user')
  const [isLoading] = useAuth('isLoading')
  
  useEffect(() => {
    if (!isLoading && !isAuthenticated) {
      router.push('/login')
    }
  }, [isLoading, isAuthenticated, router])
  
  // Check role if required
  if (requiredRole && user?.role !== requiredRole) {
    return (
      <div className="text-center py-8">
        <h2 className="text-2xl font-bold text-red-600">Access Denied</h2>
        <p className="mt-2">You don't have permission to view this page.</p>
      </div>
    )
  }
  
  if (isLoading) {
    return <>{fallback}</>
  }
  
  if (!isAuthenticated) {
    return null
  }
  
  return <>{children}</>
}

User Profile Component

// UserProfile.tsx
import { useAuth } from './auth-store'
import { authService } from './auth-service'

export function UserProfile() {
  const [user] = useAuth('user')
  const [isAuthenticated] = useAuth('isAuthenticated')
  
  if (!isAuthenticated || !user) {
    return null
  }
  
  return (
    <div className="flex items-center gap-4 p-4 bg-white dark:bg-gray-800 rounded-lg shadow">
      {user.avatar ? (
        <img
          src={user.avatar}
          alt={user.name}
          className="w-12 h-12 rounded-full"
        />
      ) : (
        <div className="w-12 h-12 rounded-full bg-blue-500 flex items-center justify-center text-white font-bold">
          {user.name[0].toUpperCase()}
        </div>
      )}
      
      <div className="flex-1">
        <h3 className="font-semibold">{user.name}</h3>
        <p className="text-sm text-gray-600 dark:text-gray-400">
          {user.email}
        </p>
        <span className="inline-block px-2 py-1 text-xs bg-blue-100 text-blue-700 rounded mt-1">
          {user.role}
        </span>
      </div>
      
      <button
        onClick={() => authService.logout()}
        className="px-4 py-2 text-red-600 hover:bg-red-50 rounded"
      >
        Logout
      </button>
    </div>
  )
}

Auth Hook Utilities

// useAuthHelpers.ts
import { useAuth } from './auth-store'
import { authService } from './auth-service'

export function useCurrentUser() {
  const [user] = useAuth('user')
  const [isAuthenticated] = useAuth('isAuthenticated')
  
  return {
    user,
    isAuthenticated,
    isGuest: !isAuthenticated,
    isAdmin: user?.role === 'admin'
  }
}

export function useAuthActions() {
  return {
    login: authService.login.bind(authService),
    logout: authService.logout.bind(authService),
    register: authService.register.bind(authService),
    refreshToken: authService.refreshToken.bind(authService)
  }
}

export function useAuthStatus() {
  const [isLoading] = useAuth('isLoading')
  const [error] = useAuth('error')
  const [isAuthenticated] = useAuth('isAuthenticated')
  
  return {
    isLoading,
    error,
    isAuthenticated,
    isReady: !isLoading
  }
}

API Interceptor

// api-client.ts
import { sky } from './auth-store'
import { authService } from './auth-service'

class ApiClient {
  private baseURL: string
  
  constructor(baseURL: string) {
    this.baseURL = baseURL
  }
  
  private async request(
    endpoint: string,
    options: RequestInit = {}
  ) {
    const token = sky.get('token')
    
    const config: RequestInit = {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        ...options.headers,
        ...(token && { Authorization: `Bearer ${token}` })
      }
    }
    
    let response = await fetch(`${this.baseURL}${endpoint}`, config)
    
    // Handle token expiration
    if (response.status === 401) {
      try {
        // Try to refresh token
        const newToken = await authService.refreshToken()
        
        // Retry request with new token
        config.headers = {
          ...config.headers,
          Authorization: `Bearer ${newToken}`
        }
        
        response = await fetch(`${this.baseURL}${endpoint}`, config)
      } catch (error) {
        // Refresh failed, user will be logged out
        throw new Error('Session expired')
      }
    }
    
    if (!response.ok) {
      throw new Error(`API Error: ${response.status}`)
    }
    
    return response.json()
  }
  
  get(endpoint: string) {
    return this.request(endpoint, { method: 'GET' })
  }
  
  post(endpoint: string, data: any) {
    return this.request(endpoint, {
      method: 'POST',
      body: JSON.stringify(data)
    })
  }
  
  put(endpoint: string, data: any) {
    return this.request(endpoint, {
      method: 'PUT',
      body: JSON.stringify(data)
    })
  }
  
  delete(endpoint: string) {
    return this.request(endpoint, { method: 'DELETE' })
  }
}

export const api = new ApiClient(process.env.NEXT_PUBLIC_API_URL)

Social Login Integration

// SocialLogin.tsx
import { authService } from './auth-service'

export function SocialLogin() {
  const handleSocialLogin = async (provider: 'google' | 'github') => {
    // Redirect to OAuth provider
    window.location.href = `${process.env.NEXT_PUBLIC_API_URL}/auth/${provider}`
  }
  
  return (
    <div className="space-y-3">
      <div className="relative">
        <div className="absolute inset-0 flex items-center">
          <div className="w-full border-t border-gray-300"></div>
        </div>
        <div className="relative flex justify-center text-sm">
          <span className="px-2 bg-white text-gray-500">Or continue with</span>
        </div>
      </div>
      
      <div className="grid grid-cols-2 gap-3">
        <button
          onClick={() => handleSocialLogin('google')}
          className="flex items-center justify-center px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
        >
          <svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
            {/* Google Icon SVG */}
          </svg>
          Google
        </button>
        
        <button
          onClick={() => handleSocialLogin('github')}
          className="flex items-center justify-center px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
        >
          <svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
            {/* GitHub Icon SVG */}
          </svg>
          GitHub
        </button>
      </div>
    </div>
  )
}

🔐 Security Best Practices

  • • Store tokens securely (httpOnly cookies preferred over localStorage)
  • • Implement token refresh mechanism
  • • Use HTTPS for all authentication endpoints
  • • Validate tokens on both client and server
  • • Implement rate limiting for login attempts
  • • Clear sensitive data on logout
  • • Use secure password requirements
  • • Implement CSRF protection