Authentication Example
Complete authentication flow implementation with JWT tokens, protected routes, and session management.
📦 View the complete example on GitHub:
storken/examples/auth-appAuth 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