Authentication API Reference
Complete guide to authentication flows, session management, and security in Izri.
📋 Overview
Izri uses Better Auth for authentication with GitHub OAuth as the primary authentication method.
Authentication Methods:
- ✅ GitHub OAuth (implemented)
- 📋 API Tokens (planned)
- 📋 Email/Password (planned)
Session Management:
- Cookie-based sessions
- 7-day expiration (default)
- Automatic renewal on activity
- Multi-device support
🔐 GitHub OAuth Flow
1. Initiate Sign-In
Frontend:
import { authClient } from '@izri/auth/client'
// Redirect to GitHub
await authClient.signIn.social({
provider: 'github',
callbackURL: '/dashboard'
})
What Happens:
- Client calls Better Auth sign-in endpoint
- Better Auth generates OAuth state
- User redirected to GitHub authorization page
- User approves access
2. GitHub Callback
URL: http://localhost:4000/api/auth/callback/github?code=...&state=...
What Happens:
- GitHub redirects back with authorization code
- Better Auth exchanges code for access token
- Better Auth fetches user profile from GitHub
- User created or updated in database
- Session created
- Session cookie set
- User redirected to callbackURL
Database Changes:
-- Insert or update user
INSERT INTO users (id, email, name, avatar, email_verified)
VALUES (...) ON CONFLICT (email) DO UPDATE ...
-- Create account link (GitHub)
INSERT INTO accounts (user_id, provider, provider_account_id, access_token, ...)
VALUES (...)
-- Create session
INSERT INTO sessions (id, user_id, expires_at)
VALUES (...)
3. Session Cookie
Cookie Name: better-auth.session_token
Attributes:
HttpOnly: Prevents JavaScript accessSecure: HTTPS only (production)SameSite=Lax: CSRF protectionMax-Age: 7 days (604800 seconds)
Example:
Set-Cookie: better-auth.session_token=session_abc123xyz;
HttpOnly; Secure; SameSite=Lax; Max-Age=604800; Path=/
🔑 Session Management
Get Current Session
tRPC:
const { data } = await trpc.auth.getSession.useQuery()
console.log(data?.user) // User object or null
console.log(data?.isAuthenticated) // boolean
REST:
curl http://localhost:4000/api/auth/get-session \
--cookie "better-auth.session_token=..."
Response:
{
"session": {
"id": "session_abc",
"userId": "user_xyz",
"expiresAt": "2024-12-31T23:59:59.999Z"
},
"user": {
"id": "user_xyz",
"email": "user@example.com",
"name": "John Doe",
"avatar": "https://avatars.githubusercontent.com/...",
"emailVerified": true
}
}
List All Sessions
View all active sessions for current user.
tRPC:
const { data } = await trpc.auth.listSessions.useQuery()
console.log(data) // Array of sessions
Response:
[
{
id: 'session_current',
userId: 'user_xyz',
expiresAt: Date,
createdAt: Date,
ipAddress: '192.168.1.1',
userAgent: 'Chrome/120.0...'
},
{
id: 'session_mobile',
// ... other session
}
]
Revoke Session (Logout)
Current Session:
const { mutate: logout } = trpc.auth.invalidateSession.useMutation({
onSuccess: () => {
window.location.href = '/login'
}
})
logout()
Specific Session:
const { mutate } = trpc.auth.revokeSession.useMutation()
mutate({ sessionToken: 'session_to_revoke' })
All Other Sessions (keep current):
const { mutate } = trpc.auth.revokeOtherSessions.useMutation()
mutate()
👤 User Profile
Get Profile
tRPC:
const { data: profile } = await trpc.auth.getProfile.useQuery()
Response:
{
id: string
email: string
name: string
avatar?: string
emailVerified: boolean
createdAt: Date
updatedAt: Date
}
Update Profile
tRPC:
const { mutate } = trpc.auth.updateProfile.useMutation()
mutate({
name: 'New Name',
avatar: 'https://example.com/avatar.jpg'
})
Updatable Fields:
name- Display nameavatar- Avatar URL
Non-updatable:
id,email,emailVerified,createdAt
🔒 Protected Routes
Frontend Route Protection
React Router Loader:
// app/routes/_authenticated.tsx
export async function loader() {
const session = await trpc.auth.getSession.query()
if (!session.isAuthenticated) {
throw redirect('/login')
}
return { user: session.user }
}
Component:
import { useAuth } from '@/hooks/useAuth'
export function ProtectedPage() {
const { user, isLoading } = useAuth()
if (isLoading) return <LoadingSpinner />
if (!user) return <Navigate to="/login" />
return <div>Welcome {user.name}</div>
}
Backend Procedure Protection
tRPC Context:
// packages/trpc/src/context.ts
export async function createContext({ req, res }: Context) {
const session = await auth.api.getSession({ headers: req.headers })
return {
req,
res,
db,
session: session?.session,
user: session?.user,
isAuthenticated: !!session?.session
}
}
Protected Procedure:
export const protectedProcedure = t.procedure.use(async ({ ctx, next }) => {
if (!ctx.isAuthenticated || !ctx.user) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Not authenticated'
})
}
return next({
ctx: {
...ctx,
user: ctx.user,
session: ctx.session
}
})
})
Usage:
export const projectsRouter = router({
getProjects: protectedProcedure
.input(z.object({ ... }))
.query(async ({ ctx, input }) => {
// ctx.user is guaranteed to exist
const userId = ctx.user.id
// ...
})
})
🔑 API Token Authentication (Planned)
Token Structure
Format:
izri_v1_k7n9p2q5r8t1u4v7w0x3y6z9_c4a8
Components:
izri_- Prefixv1- Versionk7n9...- Random 32 chars (secret)c4a8- Checksum (verification)
Token Storage
Database (hashed):
INSERT INTO api_tokens (
id,
user_id,
name,
token_hash, -- SHA-256 hash
scopes,
expires_at
) VALUES (...)
Never store plain tokens - only hashes.
Token Verification
import crypto from 'node:crypto'
function hashToken(token: string): string {
return crypto.createHash('sha256').update(token).digest('hex')
}
async function verifyToken(token: string): Promise<User | null> {
const hash = hashToken(token)
const tokenRecord = await db
.select()
.from(apiTokens)
.where(eq(apiTokens.tokenHash, hash))
.limit(1)
if (!tokenRecord.length) return null
if (tokenRecord[0].expiresAt < new Date()) return null
const user = await db
.select()
.from(users)
.where(eq(users.id, tokenRecord[0].userId))
.limit(1)
return user[0] || null
}
Usage in Requests
Authorization Header:
curl http://localhost:4000/api/v1/projects \
-H "Authorization: Bearer izri_v1_..."
Middleware:
app.use('/api/v1/*', async (c, next) => {
const auth = c.req.header('Authorization')
if (!auth?.startsWith('Bearer ')) {
return c.json({ error: 'Unauthorized' }, 401)
}
const token = auth.slice(7)
const user = await verifyToken(token)
if (!user) {
return c.json({ error: 'Invalid token' }, 401)
}
c.set('user', user)
await next()
})
🛡️ Security Best Practices
Session Security
Cookie Attributes:
{
httpOnly: true, // No JavaScript access
secure: true, // HTTPS only
sameSite: 'lax', // CSRF protection
maxAge: 7 * 24 * 60 * 60 // 7 days
}
Session Rotation:
- Sessions refreshed on every request
- Old session tokens invalidated
- Prevents session fixation attacks
Session Timeout:
- 7-day absolute timeout
- 30-minute idle timeout (planned)
- Automatic cleanup of expired sessions
Token Security
Generation:
import crypto from 'node:crypto'
function generateToken(): string {
const random = crypto.randomBytes(24).toString('base64url')
const checksum = crypto.createHash('sha256')
.update(random)
.digest('base64url')
.slice(0, 4)
return `izri_v1_${random}_${checksum}`
}
Storage:
- ✅ Store hashed tokens in database
- ❌ Never log tokens
- ❌ Never return tokens after creation
- ✅ Show tokens once on creation
Rotation:
- Rotate tokens periodically (90 days recommended)
- Revoke unused tokens
- Alert on suspicious activity
CORS Configuration
Development:
cors({
origin: ['http://localhost:5173'],
credentials: true
})
Production:
cors({
origin: [
'https://yourdomain.com',
'https://www.yourdomain.com'
],
credentials: true,
maxAge: 86400 // 24 hours
})
📊 Audit Logging
Auth Audit Logs
Track authentication events for security monitoring.
Events Logged:
login- Successful loginlogout- User logoutsession_revoked- Session revokedtoken_created- API token createdtoken_revoked- API token revokedfailed_auth- Failed authentication attempt
Schema:
{
id: string
userId: string
action: string
ipAddress?: string
userAgent?: string
metadata?: Record<string, any>
createdAt: Date
}
Query Logs:
const { data: logs } = await trpc.auth.getAuditLogs.useQuery({
limit: 50,
offset: 0
})
Example Log Entry:
{
"id": "log_abc123",
"userId": "user_xyz",
"action": "login",
"ipAddress": "192.168.1.1",
"userAgent": "Mozilla/5.0...",
"metadata": {
"provider": "github",
"success": true
},
"createdAt": "2024-01-01T00:00:00.000Z"
}
🚨 Error Handling
Common Errors
UNAUTHORIZED (401):
{
"code": "UNAUTHORIZED",
"message": "Not authenticated"
}
Causes:
- No session cookie
- Expired session
- Invalid session token
Solution: Redirect to login
FORBIDDEN (403):
{
"code": "FORBIDDEN",
"message": "Insufficient permissions"
}
Causes:
- Valid session but insufficient role
- API token with limited scopes
Solution: Request higher permissions or different token
SESSION_EXPIRED:
{
"code": "SESSION_EXPIRED",
"message": "Your session has expired. Please log in again."
}
Frontend Handling:
trpc.useContext().client.setDefaultOptions({
onError: (error) => {
if (error.data?.code === 'UNAUTHORIZED') {
// Clear local state
queryClient.clear()
// Redirect to login
window.location.href = '/login'
}
}
})
🔗 Related Documentation
- tRPC Routers - API endpoints using auth
- REST Endpoints - REST auth endpoints
- Error Codes - Complete error reference
- Auth Package - Implementation details
- Backend Authentication - Server-side auth