-
-
Save aegrumet/9ca3e13278b8543348bfdb270133512d to your computer and use it in GitHub Desktop.
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)) | |
} |
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: {
// ...
}
}
},
// ...
});
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
@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?
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
}
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.