The Problem
You have an SSR application using HTTP-only cookies for authentication, but your NestJS backend expects JWT tokens in Authorization headers.
The mismatch:
- Cookies → SSR server ✅
- Cookies → API server ❌
- Headers → API server ✅
The solution: SSR server extracts JWT from cookies and forwards it in HTTP headers.
The Flow
Implementation
NestJS API (Standard JWT Guard)
@Controller("application")
export class ApplicationController {
@Post(":jobId/upload-resume/:userId")
@UseGuards(JwtAuthGuard) // Check if auth headers are present and valid
async uploadResume(
@Param("jobId") jobId: string,
@Param("userId") userId: string,
@Body() body: ResumeSubmitBody
) {
// Implementation here
}
}
JwtAuthGuard
// auth.guard.ts
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor() {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const authHeader = request.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
throw new UnauthorizedException("No authorization token provided.");
return false;
}
const token = authHeader.split(" ")[1];
const { data: validationResult, error } = await validateJWT(token);
if (error) {
throw new UnauthorizedException("Failed to validate JWT");
return false;
}
return true;
}
}
SSR Server - Cookie Extraction
TanStack ServerFn:
// resume.serverFn.ts
export const submitResumeServerFn = createServerFn({ method: 'POST' })
.middleware([
authMiddleware, // protects the function and provides user context
withApi // wrapper for fetch call to include authorization headers
])
.inputValidator(SubmitResumeRequestSchema)
.handler(async ({ data, context }) => {
const { user, apiClient } = context
return apiClient.fetch({
jobId: data.jobId,
userId: user.id,
// ...
})
})
};
authMiddleware validates the session and provides access_token to context.
// auth.middleware.ts
export const authMiddleware = createMiddleware().server(async ({ next }) => {
const user = await getUser();
if (!user) {
throw redirect({ to: "/signin" });
}
const supabaseClient = getSupabaseServerClient();
const { data: sessionResult, error: sessionError } =
await supabaseClient.auth.getSession();
if (sessionError) return Promise.reject(sessionError);
const accessToken = sessionResult.session?.access_token;
return await next({
context: {
user,
auth: { accessToken },
},
});
});
withApi Provides a wrapper for the fetch call to include authorization headers
export const withApi = createMiddleware().server(async ({ next, context }) => {
const contextWithAuth = context as any;
if (contextWithAuth?.auth?.accessToken) {
// Include Authorization headers in outgoing request
return await next({
context: {
apiClient: {
fetch: (...args) =>
fetch(...args, {
headers: {
Authorization: `Bearer ${contextWithAuth.auth.accessToken}`,
},
}),
},
},
});
}
return await next({
context: {
apiClient: {
fetch: fetch, // default fetch api
},
},
});
});
Note: For this implementation, the order of middlewares in the controller matters.
withApimiddleware can
Key Takeaways
- ✅ HTTP-only cookies prevent XSS attacks
- ✅ SSR server safely bridges cookie auth → JWT headers
- ✅ Client code never touches JWT tokens
- ❌ Don't access HTTP-only cookies from client JavaScript
- ❌ Don't pass JWT tokens as props to client components
Cookie Configuration
// Set cookie with proper security attributes
setCookie("auth_token", jwt, {
httpOnly: true, // Prevent JavaScript access
secure: true, // HTTPS only
sameSite: "strict", // CSRF protection
maxAge: 60 * 60 * 24 * 7, // 7 days
});
Done!
The SSR server is your authentication bridge: it extracts JWTs from HTTP-only cookies and forwards them as headers to your API, keeping tokens away from client-side JavaScript.
JwtAuthGuard
// auth.guard.ts
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor() {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const authHeader = request.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
throw new UnauthorizedException("No authorization token provided.");
return false;
}
const token = authHeader.split(" ")[1];
const { data: validationResult, error } = await validateJWT(token);
if (error) {
throw new UnauthorizedException("Failed to validate JWT");
return false;
}
return true;
}
}
SSR Server - Cookie Extraction
TanStack ServerFn:
// resume.serverFn.ts
export const submitResumeServerFn = createServerFn({ method: 'POST' })
.middleware([
authMiddleware, // protects the function and provides user context
withApi // wrapper for fetch call to include authorization headers
])
.inputValidator(SubmitResumeRequestSchema)
.handler(async ({ data: formData, context }) => {
const { user, apiClient } = context
const file = formData.get('file') as File
if (!(file instanceof File)) throw new Error('[file] not found')
const conferenceFormFields = formDataToJson(formData, {})
return apiClient.fetch({
jobId: formData.get('jobId') as string,
userId: user.id,
file: file,
// ...
})
})
};
authMiddleware validates the session and provides access_token to context.
// auth.middleware.ts
export const authMiddleware = createMiddleware().server(async ({ next }) => {
const user = await getUser();
if (!user) {
throw redirect({ to: "/signin" });
}
const supabaseClient = getSupabaseServerClient();
const { data: sessionResult, error: sessionError } =
await supabaseClient.auth.getSession();
if (sessionError) return Promise.reject(sessionError);
const accessToken = sessionResult.session?.access_token;
return await next({
context: {
user,
auth: { accessToken },
},
});
});
withApi Provides a wrapper for the fetch call to include authorization headers
export const withApi = createMiddleware().server(async ({ next, context }) => {
const contextWithAuth = context as any;
if (contextWithAuth?.auth?.accessToken) {
// Include Authorization headers in outgoing request
return await next({
context: {
apiClient: {
fetch: (...args) =>
fetch(...args, {
headers: {
Authorization: `Bearer ${contextWithAuth.auth.accessToken}`,
},
}),
},
},
});
}
return await next({
context: {
apiClient: {
fetch: fetch, // default fetch api
},
},
});
});
Note: For this implementation, the order of middlewares in the controller matters.
withApimiddleware can
Key Takeaways
- ✅ HTTP-only cookies prevent XSS attacks
- ✅ SSR server safely bridges cookie auth → JWT headers
- ✅ Client code never touches JWT tokens
- ❌ Don't access HTTP-only cookies from client JavaScript
- ❌ Don't pass JWT tokens as props to client components
Cookie Configuration
// Set cookie with proper security attributes
setCookie("auth_token", jwt, {
httpOnly: true, // Prevent JavaScript access
secure: true, // HTTPS only
sameSite: "strict", // CSRF protection
maxAge: 60 * 60 * 24 * 7, // 7 days
});
Done!
The SSR server is your authentication bridge: it extracts JWTs from HTTP-only cookies and forwards them as headers to your API, keeping tokens away from client-side JavaScript.

Top comments (0)