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))
}
@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

@shamun-khatri
Copy link

@alimorgaan, how do you get a decoded token? In simple how to use decode method?

Currently, I am getting this accessToken ya29.a0AcM612wgVtxPKK_s_YkCZxdgTQ8z4QEtMcIiAf87IvUP_uAvt04skTbPlbk8VsC2c0UDKGbhzLkxv0bV9_WFTfX3wAjXVPwOg8hw1hzmK2iYYDREpXZitf9Fsw-2vT640HmycqqH_8mShS0fUap1RTX77VxmV9UoKpsdfDh_aCgYKAd8SARASFQHGX2Min3G9lxVnC1aQxRE1xXXrAQ0175

So, is this an encoded token, or is this incomplete or half-taken?

@Martin-Hayot
Copy link

updated code for go :

package main

import (
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"

	"github.com/joho/godotenv"
	"github.com/lestrrat-go/jwx/v3/jwa"
	"github.com/lestrrat-go/jwx/v3/jwe"
	"github.com/lestrrat-go/jwx/v3/jwt"
	"golang.org/x/crypto/hkdf"
)

func generateEncryptionKey() ([]byte, error) {
	authSecret := os.Getenv("AUTH_SECRET")
	if authSecret == "" {
		return nil, fmt.Errorf("AUTH_SECRET not set")
	}

	salt := "authjs.session-token"
	info := fmt.Sprintf("Auth.js Generated Encryption Key (%s)", salt)

	// HKDF with SHA-256
	hash := sha256.New
	kdf := hkdf.New(hash, []byte(authSecret), []byte(salt), []byte(info))

	key := make([]byte, 64)
	if _, err := io.ReadFull(kdf, key); err != nil {
		return nil, fmt.Errorf("failed to generate key: %w", err)
	}

	return key, nil
}

func jweToJwt(encryptedToken string) (string, error) {
	key, err := generateEncryptionKey()
	if err != nil {
		return "", fmt.Errorf("key generation failed: %w", err)
	}

	// Decrypt JWE using DIRECT key encryption and A256GCM content encryption
	decrypted, err := jwe.Decrypt([]byte(encryptedToken),
		jwe.WithKey(jwa.DIRECT(), key))
	if err != nil {
		return "", fmt.Errorf("JWE decryption failed: %w", err)
	}

	var payload map[string]interface{}
	if err := json.Unmarshal(decrypted, &payload); err != nil {
		return "", fmt.Errorf("failed to parse payload: %w", err)
	}

	token := jwt.New()
	for k, v := range payload {
		token.Set(k, v)
	}

	signed, err := jwt.Sign(token, jwt.WithKey(jwa.HS256(), []byte(os.Getenv("AUTH_SECRET"))))
	if err != nil {
		return "", fmt.Errorf("JWT signing failed: %w", err)
	}

	return string(signed), nil
}

func validateTokenFromCookie(r *http.Request) (jwt.Token, error) {
	cookie, err := r.Cookie("authjs.session-token")
	if err != nil {
		return nil, fmt.Errorf("no session cookie: %w", err)
	}

	// Convert JWE to JWT
	jwtString, err := jweToJwt(cookie.Value)
	if err != nil {
		log.Error("Failed to convert JWE to JWT", "error", err)
		return nil, err
	}

	// Verify JWT
	token, err := jwt.Parse([]byte(jwtString),
		jwt.WithKey(jwa.HS256(), []byte(os.Getenv("AUTH_SECRET"))),
		jwt.WithValidate(true))
	if err != nil {
		return nil, fmt.Errorf("invalid JWT: %w", err)
	}

	// Check expiration
	if exp, ok := token.Expiration(); ok && exp.Before(time.Now()) {
		return nil, fmt.Errorf("token expired")
	}

	return token, nil
}

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