payloadcms/payload

Add back documentation for JWT Token encryption / decryption

Closed this issue · 8 comments

Documentation Issue

I was attempting to use the JWT access token generated by Payload CMS in another service that also validates JWTs. However, I kept encountering an "invalid signature" error when trying to verify the token externally.

After some investigation, I found an older GitHub issue (#2441) that addressed this exact problem and clarified that Payload does not use the configured secret directly to sign the JWT. This was further confirmed by a helpful article: https://medium.com/@nirjalprajapati/custom-jwt-token-generation-in-payload-cms-a-step-by-step-guide-ccb1546fa8bf.

The original issue was resolved in 2023, and the correct behavior is still documented in the v2 documentation. However, the current version of the documentation no longer includes this crucial detail.

Additional Details

I strongly suggest adding this back to the documentation, ideally with a warning or callout box, to prevent confusion for developers trying to integrate with other JWT-based services.

Thanks for your work on Payload!

Running into this now too - mega thanks for finding this earlier reference!
Was about to re-read the jwt.io handbook doubting my knowledge on how this works. Thank god I'm not crazy (or that dumb)

So there's no way to verify the JWT token validity including it's secret?

@philipgher Haha, I was also quite confused about this! Then it turns out it was because Payload processes the secret key before using it. (Kudos to the author of this article)

We just need to reverse this process so that your external service uses the resolved secret that payload uses. Here's an example snippet to process your secret the same way Payload does:

import crypto from "node:crypto";

/**
 * Hash the secret the same way Payload CMS does
 * 1. SHA-256 hash the secret string.
 * 2. Take the first 32 characters.
 *
 * @see https://github.com/payloadcms/payload/blob/main/packages/payload/src/index.ts#L757
 */
export function getPayloadSecret(secret: string) {
  const hash = crypto.createHash("sha256").update(secret).digest("hex");
  return hash.slice(0, 32);
}
...
experimental_auth: new MastraJwtAuth({
  secret: getPayloadSecret(process.env.PAYLOAD_SECRET),
}),
...

Alternatively, you can precompute the processed secret and store it as a separate environment variable to avoid runtime computation - just remember to update it if you change your Payload secret.

This is part of Payload's internal implementation, so it's totally understandable that it wasn't immediately obvious! I've created a PR that add this into their documentation and hopefully this helps others who might run into the same thing. 😊

Awesome! Good timing this is. I was working in middleware and do plan to host on vercel, so personally I would be avoiding node:crypto module. Do you know of an alternative? Maybe the https://www.npmjs.com/package/jose package as that is designed to work accross runtimes? This is really not my expertise

Do you have access to payload in these instances? The hashed secret is already on the payload class https://github.com/payloadcms/payload/blob/main/packages/payload/src/index.ts#L757

In Next middleware.ts accessing the payload instance results in error's - that's why I wanted to validate the token in there on my own accord.
I think it's okay to accept not validating along with the secret in the middleware though. This is only ever off when someone has fiddled and tries to hack. At later stages (upon further restricted requests) the secret will be validated, but that user will not get a quick redirect. Not an issue.

That the computed secret is exposed (and not the same as the env variable) is useful to know when implementing outside of middleware and needing the token, thanks!

🚀 This is included in version v3.57.0

This issue has been automatically locked.
Please open a new issue if this issue persists with any additional detail.