Hasura Integration
Overview
Section titled “Overview”The auth service integrates with Hasura via a proxy pattern rather than Hasura’s built-in JWT mode. Your app verifies the JWT locally, then forwards GraphQL requests to Hasura with admin-secret headers that include the user’s identity.
Architecture
Section titled “Architecture”Browser → App's /api/graphql → JWT verify → Hasura (with admin headers)This approach gives you:
- Full control over JWT verification logic
- No JWT configuration needed in Hasura
- Ability to refresh tokens transparently
- CSRF protection via Origin header validation
GraphQL Proxy Route
Section titled “GraphQL Proxy Route”const secret = new TextEncoder().encode(process.env.JWT_SECRET!);
async function getAuthUser() { const jar = await cookies(); const token = jar.get('yourapp_access_token')?.value; if (!token) return null;
try { const { payload } = await jwtVerify(token, secret); if (typeof payload.sub !== 'string') return null; return { userId: payload.sub, role: (payload.role as string) || 'user', }; } catch { return null; }}
export async function POST(request: Request) { // CSRF check const origin = request.headers.get('origin'); if (!origin || !ALLOWED_ORIGINS.includes(origin)) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); }
// Auth check const auth = await getAuthUser(); if (!auth) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); }
const body = await request.json();
// Forward to Hasura with admin secret + user context const hasuraRes = await fetch( `${process.env.HASURA_GRAPHQL_ENDPOINT}/v1/graphql`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-hasura-admin-secret': process.env.HASURA_GRAPHQL_ADMIN_SECRET!, 'x-hasura-role': 'user', 'x-hasura-user-id': auth.userId, }, body: JSON.stringify({ query: body.query, variables: body.variables, }), }, );
const data = await hasuraRes.json(); return NextResponse.json(data);}Hasura Permissions
Section titled “Hasura Permissions”Since the proxy sends x-hasura-role: user and x-hasura-user-id, configure Hasura permissions to use session variables:
select_permissions: - role: user permission: filter: user_id: _eq: X-Hasura-User-Id columns: [id, name, finished_at, created_at]This ensures users can only access their own data.
CSRF Protection
Section titled “CSRF Protection”Always validate the Origin header before processing GraphQL requests:
const ALLOWED_ORIGINS = [ 'https://yourapp.beshoy.ai', ...(process.env.NODE_ENV !== 'production' ? ['http://localhost:3006'] : []),];
function validateOrigin(request: Request): boolean { const origin = request.headers.get('origin'); if (!origin) return false; return ALLOWED_ORIGINS.includes(origin);}Admin Queries
Section titled “Admin Queries”For server-side operations (seeding, background jobs), bypass the proxy and call Hasura directly with the admin secret:
// lib/graphql/client.ts (server-only)export async function graphql<T>( query: string, variables?: Record<string, unknown>,) { const res = await fetch( `${process.env.HASURA_GRAPHQL_ENDPOINT}/v1/graphql`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-hasura-admin-secret': process.env.HASURA_GRAPHQL_ADMIN_SECRET!, }, body: JSON.stringify({ query, variables }), }, ); return res.json() as Promise<{ data: T; errors?: unknown[] }>;}