#What is the secret used to encode the jwt
14 messages · Page 1 of 1 (latest)
Help is on the way! To mark it as solved, use the /solve command. In the meantime, here are some existing threads that may help you:
Documentation:
- Autosave - Autosave API
- Authentication Operations - Refresh
- Authentication Operations - Verify by Email
Community-Help:
#1380835372661211347 message
Thank you, that gets me halve way. With this secret I can verify the jwt but how can I get it inside the middleware? I can't import payload because it's an edge environment in middleware.ts.
I want to create route guards based on roles btw. with the middleware.
I could also just not verify the token and only use the value. It doesn't really matter if the user forges the token because there are anyway checks in all the api calls but would still be interest to know how to do this properly.
If someone is interessed later on. This does not validate the jwt, but as it is only used as a route guard and the queries on the page are validated themself this should not be a problem.
src/middleware.ts
import { NextResponse, type NextRequest } from 'next/server';
import { decodeJwt } from 'jose';
export async function middleware(request: NextRequest) {
const jwt = request.cookies.get('payload-token')?.value;
if (!jwt) {
const signInUrl = `/login?redirect=${encodeURIComponent(request.nextUrl.pathname)}`;
return NextResponse.redirect(new URL(signInUrl, request.url));
}
const claims = decodeJwt(jwt) as {
id: string;
email: string;
roles: string[];
};
const hasRequiredRole = claims.roles.some((role: string) => role === 'admin');
if (!hasRequiredRole) {
const signInUrl = `/login?unauthorized=true&redirect=${encodeURIComponent(request.nextUrl.pathname)}`;
return NextResponse.redirect(new URL(signInUrl, request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/invite'],
};
@nova smelt The payload secret needs to be encoded in a special way before you can verify the JWT. Here is extract from code I use
import { jwtVerify } from 'jose';
import crypto from 'crypto';
const secret = process.env.PAYLOAD_SECRET;
if (!secret) {
throw new Error('PAYLOAD_SECRET is not set');
}
const key = crypto.createHash('sha256').update(secret).digest('hex').slice(0, 32);
const endodedKey = new TextEncoder().encode(key);
const { payload } = await jwtVerify<{ id: string; email: string }>(input, endodedKey, { algorithms: ['HS256'] });
This code should throw an exception if the secret is not correct.
The generic part to the jwtVerify method is just how the payload type will be returned.
Hope this helps.
thank you, that gets me closer but I get "Error: The edge runtime does not support Node.js 'crypto' module." in my middleware.
I guess you could find your key in a node runtime, and the just put that key in an ENV directly instead of calculating it every time. If the rest of the code and Jose works with edge runtime.
As far as I read the ENV vars are set on build and can't be set later on. One other option would be to make a request to an api endpoint and then do the check in node / access the user from the session. I just don't want to make a internel rest request on every page that has a auth guard. I think the decodeJwt version is fine because if someone alters the token, they are just able to load the page and then get an unauthorized from the other api calls. I will bin it under nice to have for later.
Thank you anyway for the help :)
What I mean is that if you just run this code on node.js with the PAYLOAD_SECRET that you use:
const secret = process.env.PAYLOAD_SECRET;
const key = crypto.createHash('sha256').update(secret).digest('hex').slice(0, 32);
console.log('my key', key);
Then you get the key which is used for jwt's. This does not need to be calculated everytime.
You can just store this key in a another env, for example PAYLOAD_JWT_KEY
And then in your edge runtime you can just use:
const endodedKey = new TextEncoder().encode(process.env.PAYLOAD_JWT_KEY);
const { payload } = await jwtVerify<{ id: string; email: string }>(input, endodedKey, { algorithms: ['HS256'] });
without the need to import crypto.
How do I store it in PAYLOAD_JWT_KEY? and where would I best call this code?
Now I get it. That needed to settle a bit 😄
I should run this once and then just hard code it in the .env file.
Implemented it and it works great. Thank you @solar igloo :)
If someone else needs it. My middleware with user and admin checking and how I guard server actions.
middleware.ts
export function getLoginRedirectUrl(currentUrl: string) {
return `/login?unauthorized=true&redirect=${encodeURIComponent(currentUrl)}`;
}
export async function middleware(request: NextRequest) {
const loggedInRoutes = ['/cooking/basket'];
const adminRoutes = ['/invite', '/administration'];
const noAdminNeeded = !adminRoutes.some((route) =>
request.nextUrl.pathname.startsWith(route),
);
const noUserNeeded = !loggedInRoutes.some((route) =>
request.nextUrl.pathname.startsWith(route),
);
if (noAdminNeeded && noUserNeeded) {
return NextResponse.next();
}
const jwt = request.cookies.get('payload-token')?.value;
if (!jwt) {
const signInUrl = getLoginRedirectUrl(request.nextUrl.pathname);
return NextResponse.redirect(new URL(signInUrl, request.url));
}
try {
const encodedKey = new TextEncoder().encode(process.env.PAYLOAD_JWT_KEY);
const { payload: claims } = await jwtVerify<{
id: string;
email: string;
roles: string[];
}>(jwt, encodedKey, { algorithms: ['HS256'] });
if (noAdminNeeded) {
return NextResponse.next();
}
const hasRequiredRole = claims.roles.some(
(role: string) => role === 'admin',
);
if (!hasRequiredRole) {
const signInUrl = getLoginRedirectUrl(request.nextUrl.pathname);
return NextResponse.redirect(new URL(signInUrl, request.url));
}
return NextResponse.next();
} catch (error) {
console.error(error);
const signInUrl = getLoginRedirectUrl(request.nextUrl.pathname);
return NextResponse.redirect(new URL(signInUrl, request.url));
}
}
export const config = {
matcher: ['/((?!api|static|.*\\..*|_next).*)'],
};
And to protect server actions I have:
auth.ts
export async function getCurrentUser(
payload: BasePayload | null = null,
): Promise<User | null> {
if (!payload) {
payload = await getPayload();
}
const authHeader = await headers();
const result = await payload.auth({ headers: authHeader });
return result.user as User | null;
}
export async function redirectIfNotLoggedIn(
payload: BasePayload | null = null,
): Promise<User> {
const user = await getCurrentUser(payload);
const headersList = await headers();
const currentPath =
headersList.get('referer')?.replace(headersList.get('origin') || '', '') ||
'/';
if (!user) {
const redirectUrl = getLoginRedirectUrl(currentPath);
redirect(redirectUrl);
}
return user;
}
export async function redirectIfNotAdmin(
payload: BasePayload | null = null,
): Promise<User> {
const user = await getCurrentUser(payload);
const headersList = await headers();
const currentPath =
headersList.get('referer')?.replace(headersList.get('origin') || '', '') ||
'/';
if (!user || !user?.roles?.includes('admin')) {
const redirectUrl = getLoginRedirectUrl(currentPath);
redirect(redirectUrl);
}
return user;
}
Which then can be called either with await redirectIfNotAdmin(); / await redirectIfNotLoggedIn() or
const payload = await getPayload();
await redirectIfNotAdmin(payload);
if you need payload later in the function. No idea if creating payload twice is a lot of overhead but with the optional parameter it has good DX and we can reuse the payload object.