# T2M Authentication & Session Flow Guide ## Overview This guide covers the complete authentication and session management flow for the T2M (Text-to-Media) application using Clerk as the authentication provider. It includes frontend SDK setup, route protection, API token management, and secure file access patterns. ## Table of Contents 1. [Clerk Setup & Configuration](#clerk-setup--configuration) 2. [Frontend SDK Integration](#frontend-sdk-integration) 3. [Route Protection Strategy](#route-protection-strategy) 4. [API Token Management](#api-token-management) 5. [Secure File Access](#secure-file-access) 6. [Session Management](#session-management) 7. [Security Best Practices](#security-best-practices) 8. [Implementation Examples](#implementation-examples) ## Clerk Setup & Configuration ### 1. Clerk Dashboard Configuration **Environment Setup:** - **Development**: `https://dev.t2m-app.com` - **Production**: `https://t2m-app.com` **Required Clerk Settings:** ```javascript // Environment Variables NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_... CLERK_SECRET_KEY=sk_test_... NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/onboarding ``` **Clerk Application Settings:** - **Session token lifetime**: 7 days - **JWT template**: Custom template for API integration - **Allowed origins**: Your frontend domains - **Webhook endpoints**: For user lifecycle events ### 2. JWT Template Configuration Create a custom JWT template in Clerk dashboard: ```json { "aud": "t2m-api", "exp": "{{session.expire_at}}", "iat": "{{session.created_at}}", "iss": "https://clerk.t2m-app.com", "sub": "{{user.id}}", "user_id": "{{user.id}}", "email": "{{user.primary_email_address.email_address}}", "role": "{{user.public_metadata.role}}", "permissions": "{{user.public_metadata.permissions}}", "subscription_tier": "{{user.public_metadata.subscription_tier}}" } ``` ## Frontend SDK Integration ### 1. Next.js Setup (@clerk/nextjs) **Installation:** ```bash npm install @clerk/nextjs ``` **App Router Configuration (app/layout.tsx):** ```typescript import { ClerkProvider } from '@clerk/nextjs' import { Inter } from 'next/font/google' import './globals.css' const inter = Inter({ subsets: ['latin'] }) export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( {children} ) } ``` **Middleware Setup (middleware.ts):** ```typescript import { authMiddleware } from "@clerk/nextjs"; export default authMiddleware({ // Public routes that don't require authentication publicRoutes: [ "/", "/api/auth/health", "/api/system/health", "/pricing", "/about", "/contact" ], // Routes that should be ignored by Clerk ignoredRoutes: [ "/api/webhooks/clerk", "/api/files/secure/(.*)" // Signed URL access ], // API routes that require authentication apiRoutes: ["/api/(.*)"], // Redirect after sign in afterAuth(auth, req, evt) { // Handle users who aren't authenticated if (!auth.userId && !auth.isPublicRoute) { return redirectToSignIn({ returnBackUrl: req.url }); } // Redirect authenticated users away from public-only pages if (auth.userId && auth.isPublicRoute && req.nextUrl.pathname === "/") { const dashboard = new URL("/dashboard", req.url); return NextResponse.redirect(dashboard); } } }); export const config = { matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"], }; ``` ### 2. React Components Setup **Authentication Components:** ```typescript // components/auth/SignInButton.tsx import { SignInButton as ClerkSignInButton } from "@clerk/nextjs"; export function SignInButton() { return ( ); } // components/auth/UserButton.tsx import { UserButton as ClerkUserButton } from "@clerk/nextjs"; export function UserButton() { return ( ); } ``` **Protected Page Component:** ```typescript // components/auth/ProtectedRoute.tsx import { useAuth } from "@clerk/nextjs"; import { useRouter } from "next/navigation"; import { useEffect } from "react"; import { LoadingSpinner } from "@/components/ui/LoadingSpinner"; interface ProtectedRouteProps { children: React.ReactNode; requiredRole?: string; requiredPermissions?: string[]; } export function ProtectedRoute({ children, requiredRole, requiredPermissions }: ProtectedRouteProps) { const { isLoaded, isSignedIn, user } = useAuth(); const router = useRouter(); useEffect(() => { if (isLoaded && !isSignedIn) { router.push("/sign-in"); } }, [isLoaded, isSignedIn, router]); if (!isLoaded) { return ; } if (!isSignedIn) { return null; } // Check role-based access if (requiredRole) { const userRole = user?.publicMetadata?.role as string; if (userRole !== requiredRole) { return
Access denied. Required role: {requiredRole}
; } } // Check permission-based access if (requiredPermissions) { const userPermissions = user?.publicMetadata?.permissions as string[] || []; const hasPermission = requiredPermissions.every(permission => userPermissions.includes(permission) ); if (!hasPermission) { return
Access denied. Missing required permissions.
; } } return <>{children}; } ``` ## Route Protection Strategy ### 1. Public Routes (No Authentication Required) ```typescript // Public routes configuration const PUBLIC_ROUTES = [ "/", // Landing page "/pricing", // Pricing information "/about", // About page "/contact", // Contact page "/api/auth/health", // Auth service health "/api/system/health", // System health check "/legal/privacy", // Privacy policy "/legal/terms" // Terms of service ]; ``` ### 2. Protected Routes (Authentication Required) ```typescript // Protected routes with different access levels const PROTECTED_ROUTES = { // Basic authenticated routes AUTHENTICATED: [ "/dashboard", "/profile", "/files", "/videos", "/jobs" ], // Admin-only routes ADMIN: [ "/admin", "/admin/users", "/admin/system", "/admin/analytics" ], // Premium subscription routes PREMIUM: [ "/premium/advanced-generation", "/premium/batch-processing", "/premium/priority-queue" ] }; ``` ### 3. API Route Protection ```typescript // app/api/auth/route-protection.ts import { auth } from "@clerk/nextjs"; import { NextRequest, NextResponse } from "next/server"; export async function requireAuth(request: NextRequest) { const { userId } = auth(); if (!userId) { return NextResponse.json( { error: "Unauthorized" }, { status: 401 } ); } return userId; } export async function requireRole(request: NextRequest, requiredRole: string) { const { userId, sessionClaims } = auth(); if (!userId) { return NextResponse.json( { error: "Unauthorized" }, { status: 401 } ); } const userRole = sessionClaims?.metadata?.role as string; if (userRole !== requiredRole) { return NextResponse.json( { error: "Forbidden" }, { status: 403 } ); } return userId; } // Usage in API routes // app/api/files/route.ts import { requireAuth } from "@/app/api/auth/route-protection"; export async function GET(request: NextRequest) { const userId = await requireAuth(request); if (userId instanceof NextResponse) return userId; // Error response // Continue with authenticated logic // ... } ``` ## API Token Management ### 1. Token Retrieval in Frontend ```typescript // hooks/useApiToken.ts import { useAuth } from "@clerk/nextjs"; import { useCallback } from "react"; export function useApiToken() { const { getToken } = useAuth(); const getApiToken = useCallback(async () => { try { // Get token with custom JWT template const token = await getToken({ template: "t2m-api" }); return token; } catch (error) { console.error("Failed to get API token:", error); throw new Error("Authentication failed"); } }, [getToken]); return { getApiToken }; } // Usage in components function VideoUpload() { const { getApiToken } = useApiToken(); const uploadVideo = async (file: File) => { const token = await getApiToken(); const formData = new FormData(); formData.append('file', file); const response = await fetch('/api/files/upload', { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: formData }); return response.json(); }; // ... } ``` ### 2. API Client with Automatic Token Management ```typescript // lib/api-client.ts import { useAuth } from "@clerk/nextjs"; class ApiClient { private baseUrl: string; private getToken: () => Promise; constructor(baseUrl: string, getToken: () => Promise) { this.baseUrl = baseUrl; this.getToken = getToken; } private async request( endpoint: string, options: RequestInit = {} ): Promise { const token = await this.getToken(); const config: RequestInit = { ...options, headers: { 'Content-Type': 'application/json', ...(token && { 'Authorization': `Bearer ${token}` }), ...options.headers, }, }; const response = await fetch(`${this.baseUrl}${endpoint}`, config); if (!response.ok) { const error = await response.json(); throw new Error(error.message || 'API request failed'); } return response.json(); } // File operations async uploadFile(file: File, metadata?: any) { const formData = new FormData(); formData.append('file', file); if (metadata) { Object.entries(metadata).forEach(([key, value]) => { formData.append(key, value as string); }); } const token = await this.getToken(); return fetch(`${this.baseUrl}/files/upload`, { method: 'POST', headers: { ...(token && { 'Authorization': `Bearer ${token}` }) }, body: formData }).then(res => res.json()); } async getFiles(params?: any) { const query = params ? `?${new URLSearchParams(params)}` : ''; return this.request(`/files${query}`); } // Video operations async generateVideo(prompt: string, options?: any) { return this.request('/videos/generate', { method: 'POST', body: JSON.stringify({ prompt, ...options }) }); } async getJobStatus(jobId: string) { return this.request(`/jobs/${jobId}`); } } // Hook for using API client export function useApiClient() { const { getToken } = useAuth(); const apiClient = new ApiClient( process.env.NEXT_PUBLIC_API_URL || '/api/v1', () => getToken({ template: "t2m-api" }) ); return apiClient; } ``` ### 3. Backend Token Validation ```typescript // Backend API token validation (if using proxy) // app/api/auth/validate-token.ts import { verifyToken } from "@clerk/backend"; export async function validateClerkToken(token: string) { try { const payload = await verifyToken(token, { jwtKey: process.env.CLERK_JWT_KEY, authorizedParties: [process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY] }); return { userId: payload.sub, email: payload.email, role: payload.role, permissions: payload.permissions, subscriptionTier: payload.subscription_tier }; } catch (error) { throw new Error('Invalid token'); } } // Usage in API routes export async function POST(request: NextRequest) { const authHeader = request.headers.get('authorization'); const token = authHeader?.replace('Bearer ', ''); if (!token) { return NextResponse.json({ error: 'No token provided' }, { status: 401 }); } try { const user = await validateClerkToken(token); // Continue with authenticated logic } catch (error) { return NextResponse.json({ error: 'Invalid token' }, { status: 401 }); } } ``` ## Secure File Access ### 1. Signed URL Generation ```typescript // Backend: Generate signed URLs for secure file access // app/api/files/[fileId]/signed-url/route.ts import { auth } from "@clerk/nextjs"; import { createHmac } from "crypto"; export async function POST( request: NextRequest, { params }: { params: { fileId: string } } ) { const { userId } = auth(); if (!userId) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const { fileId } = params; const { expiresIn = 3600 } = await request.json(); // Default 1 hour // Verify user owns the file const file = await getFileById(fileId); if (!file || file.userId !== userId) { return NextResponse.json({ error: "File not found" }, { status: 404 }); } // Generate signed URL const expires = Math.floor(Date.now() / 1000) + expiresIn; const signature = createHmac('sha256', process.env.FILE_SIGNING_SECRET!) .update(`${fileId}:${userId}:${expires}`) .digest('hex'); const signedUrl = `${process.env.NEXT_PUBLIC_API_URL}/files/secure/${fileId}?` + `user_id=${userId}&expires=${expires}&signature=${signature}`; return NextResponse.json({ success: true, data: { url: signedUrl, expires_at: new Date(expires * 1000).toISOString() } }); } ``` ### 2. Signed URL Validation ```typescript // Backend: Validate signed URLs // app/api/files/secure/[fileId]/route.ts import { createHmac } from "crypto"; import { NextRequest, NextResponse } from "next/server"; export async function GET( request: NextRequest, { params }: { params: { fileId: string } } ) { const { fileId } = params; const { searchParams } = new URL(request.url); const userId = searchParams.get('user_id'); const expires = searchParams.get('expires'); const signature = searchParams.get('signature'); if (!userId || !expires || !signature) { return NextResponse.json({ error: "Invalid signed URL" }, { status: 400 }); } // Check expiration const expiresTimestamp = parseInt(expires); if (Date.now() / 1000 > expiresTimestamp) { return NextResponse.json({ error: "Signed URL expired" }, { status: 410 }); } // Verify signature const expectedSignature = createHmac('sha256', process.env.FILE_SIGNING_SECRET!) .update(`${fileId}:${userId}:${expires}`) .digest('hex'); if (signature !== expectedSignature) { return NextResponse.json({ error: "Invalid signature" }, { status: 403 }); } // Serve file const file = await getFileById(fileId); if (!file || file.userId !== userId) { return NextResponse.json({ error: "File not found" }, { status: 404 }); } // Return file stream const fileStream = await getFileStream(file.path); return new NextResponse(fileStream, { headers: { 'Content-Type': file.contentType, 'Content-Disposition': `attachment; filename="${file.filename}"`, 'Cache-Control': 'private, max-age=3600' } }); } ``` ### 3. Frontend: Secure Video Player ```typescript // components/VideoPlayer.tsx import { useApiClient } from "@/lib/api-client"; import { useEffect, useState } from "react"; interface VideoPlayerProps { jobId: string; autoplay?: boolean; } export function VideoPlayer({ jobId, autoplay = false }: VideoPlayerProps) { const [signedUrl, setSignedUrl] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const apiClient = useApiClient(); useEffect(() => { async function getSignedUrl() { try { setLoading(true); // Get signed URL for video const response = await apiClient.request(`/videos/${jobId}/signed-url`, { method: 'POST', body: JSON.stringify({ expiresIn: 3600 }) // 1 hour }); setSignedUrl(response.data.url); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load video'); } finally { setLoading(false); } } getSignedUrl(); }, [jobId, apiClient]); if (loading) return
Loading video...
; if (error) return
Error: {error}
; if (!signedUrl) return
Video not available
; return ( ); } ``` ## Session Management ### 1. Session Configuration ```typescript // lib/session-config.ts export const SESSION_CONFIG = { // Session duration maxAge: 7 * 24 * 60 * 60, // 7 days in seconds // Token refresh threshold refreshThreshold: 5 * 60, // Refresh if expires in 5 minutes // Automatic logout on inactivity inactivityTimeout: 30 * 60, // 30 minutes // Remember me option rememberMe: { enabled: true, duration: 30 * 24 * 60 * 60 // 30 days } }; ``` ### 2. Session Monitoring Hook ```typescript // hooks/useSessionMonitor.ts import { useAuth } from "@clerk/nextjs"; import { useEffect, useRef } from "react"; export function useSessionMonitor() { const { isSignedIn, signOut } = useAuth(); const lastActivityRef = useRef(Date.now()); const inactivityTimerRef = useRef(); const resetInactivityTimer = () => { lastActivityRef.current = Date.now(); if (inactivityTimerRef.current) { clearTimeout(inactivityTimerRef.current); } inactivityTimerRef.current = setTimeout(() => { if (isSignedIn) { signOut(); alert('You have been logged out due to inactivity.'); } }, SESSION_CONFIG.inactivityTimeout * 1000); }; useEffect(() => { if (!isSignedIn) return; const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart']; events.forEach(event => { document.addEventListener(event, resetInactivityTimer, true); }); resetInactivityTimer(); // Initialize timer return () => { events.forEach(event => { document.removeEventListener(event, resetInactivityTimer, true); }); if (inactivityTimerRef.current) { clearTimeout(inactivityTimerRef.current); } }; }, [isSignedIn]); } ``` ### 3. Token Refresh Management ```typescript // hooks/useTokenRefresh.ts import { useAuth } from "@clerk/nextjs"; import { useEffect, useCallback } from "react"; export function useTokenRefresh() { const { getToken, isSignedIn } = useAuth(); const checkTokenExpiry = useCallback(async () => { if (!isSignedIn) return; try { const token = await getToken({ template: "t2m-api" }); if (!token) return; // Decode JWT to check expiry const payload = JSON.parse(atob(token.split('.')[1])); const expiryTime = payload.exp * 1000; // Convert to milliseconds const currentTime = Date.now(); const timeUntilExpiry = expiryTime - currentTime; // Refresh if token expires within threshold if (timeUntilExpiry < SESSION_CONFIG.refreshThreshold * 1000) { await getToken({ template: "t2m-api", skipCache: true }); } } catch (error) { console.error('Token refresh failed:', error); } }, [getToken, isSignedIn]); useEffect(() => { if (!isSignedIn) return; // Check token expiry every minute const interval = setInterval(checkTokenExpiry, 60 * 1000); return () => clearInterval(interval); }, [isSignedIn, checkTokenExpiry]); } ``` ## Security Best Practices ### 1. Token Security ```typescript // Security guidelines for token handling // ✅ DO: Use secure token storage const { getToken } = useAuth(); const token = await getToken({ template: "t2m-api" }); // ❌ DON'T: Store tokens in localStorage or sessionStorage localStorage.setItem('token', token); // NEVER DO THIS // ✅ DO: Use tokens for API calls only const response = await fetch('/api/files', { headers: { 'Authorization': `Bearer ${token}` } }); // ❌ DON'T: Embed tokens in URLs or HTML const videoUrl = `/video?token=${token}`; // NEVER DO THIS ``` ### 2. CSRF Protection ```typescript // middleware.ts - CSRF protection import { NextRequest, NextResponse } from "next/server"; export function middleware(request: NextRequest) { // CSRF protection for state-changing operations if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(request.method)) { const origin = request.headers.get('origin'); const host = request.headers.get('host'); if (origin && !origin.includes(host!)) { return NextResponse.json( { error: 'CSRF protection: Invalid origin' }, { status: 403 } ); } } return NextResponse.next(); } ``` ### 3. Rate Limiting ```typescript // lib/rate-limiter.ts import { Ratelimit } from "@upstash/ratelimit"; import { Redis } from "@upstash/redis"; const redis = new Redis({ url: process.env.UPSTASH_REDIS_REST_URL!, token: process.env.UPSTASH_REDIS_REST_TOKEN!, }); export const rateLimiter = new Ratelimit({ redis, limiter: Ratelimit.slidingWindow(100, "1 h"), // 100 requests per hour analytics: true, }); // Usage in API routes export async function POST(request: NextRequest) { const { userId } = auth(); if (!userId) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const { success, limit, reset, remaining } = await rateLimiter.limit(userId); if (!success) { return NextResponse.json( { error: "Rate limit exceeded" }, { status: 429, headers: { 'X-RateLimit-Limit': limit.toString(), 'X-RateLimit-Remaining': remaining.toString(), 'X-RateLimit-Reset': reset.toString(), } } ); } // Continue with request processing } ``` ## Implementation Examples ### 1. Complete Authentication Flow ```typescript // app/dashboard/page.tsx import { ProtectedRoute } from "@/components/auth/ProtectedRoute"; import { useSessionMonitor } from "@/hooks/useSessionMonitor"; import { useTokenRefresh } from "@/hooks/useTokenRefresh"; export default function DashboardPage() { useSessionMonitor(); // Monitor for inactivity useTokenRefresh(); // Handle token refresh return (

Welcome to T2M Dashboard

{/* Dashboard content */}
); } ``` ### 2. File Upload with Progress ```typescript // components/FileUpload.tsx import { useApiClient } from "@/lib/api-client"; import { useState } from "react"; export function FileUpload() { const [uploading, setUploading] = useState(false); const [progress, setProgress] = useState(0); const apiClient = useApiClient(); const handleUpload = async (file: File) => { setUploading(true); setProgress(0); try { // Create XMLHttpRequest for progress tracking const formData = new FormData(); formData.append('file', file); const token = await apiClient.getApiToken(); const xhr = new XMLHttpRequest(); xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable) { setProgress((e.loaded / e.total) * 100); } }); xhr.addEventListener('load', () => { if (xhr.status === 200) { const response = JSON.parse(xhr.responseText); console.log('Upload successful:', response); } }); xhr.open('POST', '/api/files/upload'); xhr.setRequestHeader('Authorization', `Bearer ${token}`); xhr.send(formData); } catch (error) { console.error('Upload failed:', error); } finally { setUploading(false); } }; return (
{ const file = e.target.files?.[0]; if (file) handleUpload(file); }} disabled={uploading} /> {uploading && (
{Math.round(progress)}%
)}
); } ``` ### 3. Video Generation with Real-time Updates ```typescript // components/VideoGenerator.tsx import { useApiClient } from "@/lib/api-client"; import { useState, useEffect } from "react"; export function VideoGenerator() { const [prompt, setPrompt] = useState(""); const [jobId, setJobId] = useState(null); const [status, setStatus] = useState("idle"); const [progress, setProgress] = useState(0); const apiClient = useApiClient(); const generateVideo = async () => { try { setStatus("starting"); const response = await apiClient.generateVideo(prompt, { duration: 10, quality: "1080p" }); setJobId(response.data.job_id); setStatus("processing"); } catch (error) { console.error('Video generation failed:', error); setStatus("error"); } }; // Poll for job status useEffect(() => { if (!jobId || status === "completed" || status === "error") return; const pollStatus = async () => { try { const response = await apiClient.getJobStatus(jobId); setStatus(response.data.status); setProgress(response.data.progress || 0); } catch (error) { console.error('Status check failed:', error); } }; const interval = setInterval(pollStatus, 2000); // Poll every 2 seconds return () => clearInterval(interval); }, [jobId, status, apiClient]); return (