Skip to content

Instantly share code, notes, and snippets.

@aegrumet
Created December 4, 2022 01:24
Show Gist options
  • Save aegrumet/9ca3e13278b8543348bfdb270133512d to your computer and use it in GitHub Desktop.
Save aegrumet/9ca3e13278b8543348bfdb270133512d to your computer and use it in GitHub Desktop.
Decrypt a NextAuth jwe from somewhere else
package main
import (
"crypto/sha256"
"fmt"
"io"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwe"
"golang.org/x/crypto/hkdf"
)
func main() {
var err error
rawJwe := "raw jwe text"
nextAuthSecret := "next auth secret"
info := "NextAuth.js Generated Encryption Key"
// Step 1: Generate the decryption key with an hdkf lib
hash := sha256.New
kdf := hkdf.New(hash, []byte(nextAuthSecret), []byte(""), []byte(info))
key := make([]byte, 32)
_, _ = io.ReadFull(kdf, key)
// Step 2: Decrypt with a JWE library.
// Here we use lestrrat-go/jwx, which parses the JWE and
// uses the JWE header info to choose the decryption algorithm.
decrypted, err := jwe.Decrypt([]byte(rawJwe),
jwe.WithKey(jwa.DIRECT, key))
if err != nil {
fmt.Printf("failed to decrypt: %s", err)
return
}
fmt.Println(string(decrypted))
}
@jackrdye
Copy link

jackrdye commented Feb 17, 2023

Legend :). I'm wanting to convert this to python. Will give it a crack. Would be cool if you/we could make a repo with a conversion of next-auth token to all popular languages. I think using next-auth with an api from other languages not just JS will only get more prominent :)

@jackrdye
Copy link

Just uploaded a python version to GitHub. let me know if your happy for me to include your go version.
https://github.com/jackrdye/Decrypt-NextAuth-JWE-getToken/tree/main

@aegrumet
Copy link
Author

Go for it!

@nkalupahana
Copy link

nkalupahana commented Nov 13, 2023

const jose = require("jose")
const crypto = require("crypto")

const info = "NextAuth.js Generated Encryption Key";
const nextAuthSecret = "secret"
const jwe = "raw jwe";

(async () => {
    // decrypt
    const key = await new Promise((res) => {
        crypto.hkdf("sha256", nextAuthSecret, "", info, 32, (_, key) => res(new Uint8Array(key)));
    });
    const { plaintext } = await jose.compactDecrypt(jwe, key);
    console.log(plaintext.toString("utf8"))

    // re-encrypt
    const encrypted = await new jose.CompactEncrypt(plaintext).setProtectedHeader({
        "alg": "dir",
        "enc": "A256GCM"
    }).encrypt(key);

    console.log(encrypted)
})();

@Sil1g
Copy link

Sil1g commented Jan 20, 2024

auth.js updated their key generation code:
https://github.com/nextauthjs/next-auth/blob/f4bb790d8041cfbb7fdb2aa37ef45e94f6a027aa/packages/core/src/jwt.ts#L162

    return await hkdf("sha256", keyMaterial, salt, `Auth.js Generated Encryption Key (${salt})`, 32);

Salt is currently generated like that
https://github.com/nextauthjs/next-auth/blob/f4bb790d8041cfbb7fdb2aa37ef45e94f6a027aa/packages/core/src/jwt.ts#L114

cookieName = secureCookie
        ? "__Secure-authjs.session-token"
        : "authjs.session-token"

@SanjoyPator1
Copy link

My backend is separate. So can i send the jwt token that i get from signin callback - account.id_token
I send this token to my backend but how can i decode this in my backend. The token that is send to the backend is:
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjQ1MjljNDA5Zjc3YTEwNmZiNjdlZTFhODVkMTY4ZmQyY2ZiN2MwYjciLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiIyMTcyMTA1MTgwMC12OGp0MzVtNzA0bDVvY201YjlpazlvN3VwdHZsZGpicC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImF1ZCI6IjIxNzIxMDUxODAwLXY4anQzNW03MDRsNW9jbTViOWlrOW83dXB0dmxkamJwLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwic3ViIjoiMTEyMzY3OTUzMDE3ODc2OTUzMzU1IiwiZW1haWwiOiJzYW5qb3lvZmZpY2lhbHBAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImF0X2hhc2giOiI4SDR5WE5zSTk0VlZMMFZRa1NXeWZBIiwibmFtZSI6ImtldGNodXAgUGFydHkiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EvQUNnOG9jSXlCSTJDcEdMMlZENXg0c29xVktCanBURVExanV3YjRIZFhlYl9DZDcyVlUxY0plbz1zOTYtYyIsImdpdmVuX25hbWUiOiJrZXRjaHVwIiwiZmFtaWx5X25hbWUiOiJQYXJ0eSIsImlhdCI6MTcyMzU1OTU0MCwiZXhwIjoxNzIzNTYzMTQwfQ.xzJEkyh2jx5CDUlaLVZT9j35hO0OrpKw4Mh29t77Uma0afWQCpMVyrZAS4_DTnqX_v9E0aBhITd2zI2okgl3mhm0IzLkjD9xgVE9USejJs3EUY7vqngACP7b5wq4z04f9yqrzaBWpHF2xiBVwc060YtoEaEOuKbBmYSYgffvaqJEyCAHimbamhBkeOcEb_WRq4SIcX7li4KpAUZdMzjGewE1aXN969L5iUeQCRM0VHJ7QgSToo7rjGCiSpIfp5BSm0_DK3_t3hHnhrF0fod1jzUGyXONWW1hDqxE9YqCfCaK-i8M-ql0uJEgWNYsQ5DdJE_fGeCtrGVXcq_4IAM6LA"

I am doing this since my backend is on different domain too so I can get the token in cookie. And in my backend i need to authenticate the token once more to know if it is valid
Any help would be appreciated.

@ritmoHung
Copy link

ritmoHung commented Sep 15, 2024

We had a separate backend using Express.js and was trying to let the token be sent with each request to it. I could only find libraries that protects route by verifying JWTs but not JWEs, so I finally figured this out:

import { expressjwt } from "express-jwt";
import * as jose from "jose";
import crypto from "node:crypto";

const COOKIE_NAME = "your-cookie-name";



const JWT = expressjwt({
	secret: process.env.AUTH_SECRET,
	algorithms: ["HS256"],
	getToken: async (req) => {
		const jwe = req.cookies[COOKIE_NAME];
		return jwe ? await jweToJwt(jwe) : null;
	},
});

export default JWT;

function generateEncryptionKey() {
	const hash = "sha256";
	const salt = COOKIE_NAME;
	const info = `Auth.js Generated Encryption Key (${salt})`;
	const length = 64;  // 256 bits

	const keyBuffer = crypto.hkdfSync(hash, process.env.AUTH_SECRET, salt, info, length);
	return new Uint8Array(keyBuffer);
}

async function jweToJwt(jwe) {
	try {
		const key = generateEncryptionKey();
		const { plaintext } = await jose.compactDecrypt(jwe, key);

		const payload = JSON.parse(Buffer.from(plaintext).toString("utf8"));
		const jwt = await new jose.SignJWT(payload)
			.setProtectedHeader({ alg: "HS256" })
			.sign(Buffer.from(process.env.AUTH_SECRET, "utf8"));
		return jwt;
	} catch (error) {
		console.error("ERR::JWE::DECRYPT:", error.message);
		return null;
	}
}

Pretty unefficient, but works with next-auth 5.0.0-beta.20. And ofc you could just return payload if it's all you need.

You might also need to check out the cookie name as Sil1g mentioned, or override the name for session token in NextAuth config:

export const { handlers, signIn, signOut, auth } = NextAuth({
	// ...
	cookies: {
		sessionToken: {
			name: "jwe",
			options: {
				// ...
			}
		}
	},
	// ...
});

@alimorgaan
Copy link

i'm using next-auth 5.0.0-beta.20, i think the best solution is to write your own encode and decode functions for next-auth, instead of depending on a salt that may change in the future, here is what i did.

jwt: {
    encode: async ({ token }) => {
      //create the private key from base64 encoded secret
      const key = Buffer.from(
        process.env.BASE64_ENCODED_JWT_PRIVATE_KEY ?? "",
        "base64"
      ).toString("ascii");

      const secretKey = createPrivateKey(key);

      const result = await new jose.SignJWT(token)
        .setProtectedHeader({ alg: "RS256" })
        .setIssuedAt()
        .setExpirationTime("1d")
        .sign(secretKey);

      return result;
    },

    decode: async ({ token }) => {
      //create the public key from base64 encoded secret
      const key = Buffer.from(
        process.env.BASE64_ENCODED_JWT_PUBLIC_KEY ?? "",
        "base64"
      ).toString("ascii");

      const publicKey = createPublicKey(key);
      const result = await jose.jwtVerify(token ?? "", publicKey, {
        algorithms: ["RS256"],
      });

      return result.payload;
    },
 },

Like this i can share the public key to all my services and verify the JWT anywhere

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment