6.2.1. Asymmetric Encryption
In this chapter, you'll learn how to configure asymmetric encryption in Medusa using public/private key pairs instead of a shared secret.
What is Asymmetric Encryption?#
By default, Medusa uses symmetric JWT authentication, where the same secret signs and verifies tokens. With asymmetric encryption, you use a private key to sign tokens and a public key to verify them.
This approach provides better security, supports key rotation, and enables distributed systems where multiple services can verify tokens without needing access to the signing key.
When to Use Asymmetric Encryption#
Asymmetric encryption is useful in several scenarios:
| Scenario | Description | Benefits | 
|---|---|---|
| Multi-Instance Deployments | Running multiple Medusa instances behind a load balancer. | Centralized signing, reduced risk if an instance is compromised. | 
| Microservices Architecture | Medusa as part of a larger microservices ecosystem. | Independent token verification across services. | 
| JWKS Support | Dynamic key rotation using JSON Web Key Sets. | Seamless key rotation without service disruption. | 
How to Configure Asymmetric Encryption#
Step 1: Set Asymmetric JWT Configuration#
To configure asymmetric encryption, you need to set up both signing and verification options in your medusa-config.ts file.
In medusa-config.ts, create a helper function to load the JWT configuration, and use it in the exported configuration:
1// other imports...2import jwt from "jsonwebtoken"3 4export const getJwtConfig = () => {5 return {6 jwtSecret: process.env.JWT_SECRET_KEY,7 jwtPublicKey: process.env.JWT_PUBLIC_KEY,8 jwtExpiresIn: process.env.JWT_EXPIRES_IN || "1d",9 jwtOptions: {10 algorithm: (process.env.JWT_ALGORITHM || "RS256") as jwt.Algorithm,11 audience: process.env.JWT_AUDIENCE12 ? process.env.JWT_AUDIENCE.split(",")13 : undefined,14 issuer: process.env.JWT_ISSUER,15 keyid: process.env.JWT_KEYID,16 },17 jwtVerifyOptions: {18 algorithms: [(process.env.JWT_ALGORITHM || "RS256") as jwt.Algorithm],19 audience: process.env.JWT_AUDIENCE20 ? process.env.JWT_AUDIENCE.split(",")21 : undefined,22 issuer: process.env.JWT_ISSUER,23 },24 }25}26 27const jwtConfig = getJwtConfig()28 29module.exports = defineConfig({30 projectConfig: {31 http: {32 // ...33 jwtSecret: jwtConfig.jwtSecret,34 jwtPublicKey: jwtConfig.jwtPublicKey,35 jwtExpiresIn: jwtConfig.jwtExpiresIn,36 jwtOptions: jwtConfig.jwtOptions,37 jwtVerifyOptions: jwtConfig.jwtVerifyOptions,38 },39 // ...40 },41 modules: [42 {43 resolve: "@medusajs/medusa/user",44 options: {45 jwt_secret: {46 key: jwtConfig.jwtSecret,47 },48 jwt_public_key: jwtConfig.jwtPublicKey,49 jwt_options: jwtConfig.jwtOptions,50 },51 },52 ],53})
You set the JWT configurations in the following options:
- http options: You set the global JWT options for Medusa's HTTP layer, which are used to sign and verify JWT authentication tokens.
- Refer to the Medusa Configuration chapter for more details on these options and their default values.
 
- User Module options: You set the JWT options specific to the User Module, which are used to sign and verify invite tokens.
Step 2: Generate Key Pair#
Next, generate an RSA key pair (private and public keys) for signing and verifying tokens. You can use OpenSSL to generate the keys:
Make sure not to commit your private key to Git or any public repository. Add it to your .gitignore file to prevent accidental commits:
Step 3: Set Environment Variables#
Finally, set the following environment variables using the generated keys:
❯# JWT Configuration❯JWT_SECRET_KEY="-----BEGIN RSA PRIVATE KEY-----❯MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC...❯-----END RSA PRIVATE KEY-----"❯ ❯JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----❯MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvTtLGDIK...❯-----END PUBLIC KEY-----"❯ ❯JWT_ALGORITHM=RS256❯JWT_EXPIRES_IN=1d❯JWT_ISSUER=medusa❯JWT_AUDIENCE=medusa-api❯JWT_KEYID=medusa-key-1
Where:
- JWT_SECRET_KEY: Your RSA private key for signing tokens.
- JWT_PUBLIC_KEY: Your RSA public key for verifying tokens.
- JWT_ALGORITHM: The signing algorithm.
- JWT_EXPIRES_IN: The token expiration time.
- JWT_ISSUER: (Optional) The issuer claim for tokens.
- JWT_AUDIENCE: (Optional) The audience claim for tokens.
- JWT_KEYID: (Optional) The key ID for JWKS support.
.env files, wrap the key with double quotes and use \n for newlines.Using JWKS (JSON Web Key Set)#
JWKS (JSON Web Key Set) is a set of public keys used to verify JWT tokens. By exposing a JWKS endpoint, you allow clients to dynamically fetch your public keys for token verification, enabling key rotation without requiring clients to update their configurations.
This section explains how to set up JWKS support in Medusa and verify tokens using JWKS.
Step 1: Install Required Packages#
First, install the packages for handling JWKS and JWT verification. Run the following command in your Medusa application:
You install the following packages:
- jwks-rsa: A library to create a JWKS client that can fetch and cache public keys.
- jsonwebtoken: A library to handle JWT token creation and verification.
- @types/jsonwebtoken: TypeScript types for the- jsonwebtokenpackage. Make sure to install version- 8.5.9for compatibility.
Step 2: Expose JWKS Endpoint#
To allow clients to fetch the JWKS, expose it in an API route.
In the API route, return the JWKS content containing your public keys. You can set the JWKS content using an environment variable or by manually converting your public key to JWK format.
Option 1: Environment Variable
The first option is to set the JWKS content using an environment variable. Convert your public key to JWK format using online tools or libraries like node-jose.
For example, add the following environment variable:
Then, create the API route at src/api/.well-known/jwks.json/route.ts with the following content:
1import type { 2 MedusaRequest, 3 MedusaResponse, 4} from "@medusajs/framework/http"5 6export const GET = async (7 req: MedusaRequest, 8 res: MedusaResponse9) => {10 if (!process.env.JWKS_CONTENT) {11 return res.status(500).json({ error: "JWKS_CONTENT not configured" })12 }13 14 res.status(200).json(JSON.parse(process.env.JWKS_CONTENT))15}
This exposes your public key at /.well-known/jwks.json, which clients can fetch to verify tokens.
Option 2: Manual JWK Conversion
If you prefer not to use an environment variable, manually convert your public key to JWK format using the JWT configurations you set in medusa-config.ts.
For example, create the API route at src/api/.well-known/jwks.json/route.ts with the following content:
1import type { 2 MedusaRequest, 3 MedusaResponse, 4} from "@medusajs/framework/http"5import crypto from "crypto"6 7export const GET = async (8 req: MedusaRequest, 9 res: MedusaResponse10) => {11 const configModule = req.scope.resolve("configModule")12 const { projectConfig } = configModule13 14 // If JWKS_CONTENT is set, use it15 if (process.env.JWKS_CONTENT) {16 return res.status(200).json(JSON.parse(process.env.JWKS_CONTENT))17 }18 19 // Otherwise, generate from public key20 const publicKey = projectConfig.http.jwtPublicKey21 if (!publicKey) {22 return res.status(500).json({ error: "No public key configured" })23 }24 25 try {26 // Convert PEM to JWK27 const jwk = crypto.createPublicKey(publicKey).export({ format: "jwk" })28 29 const jwks = {30 keys: [{31 ...jwk,32 use: "sig",33 kid: projectConfig.http.jwtOptions?.keyid || "medusa-key-1",34 alg: projectConfig.http.jwtOptions?.algorithm || "RS256",35 }],36 }37 38 res.status(200).json(jwks)39 } catch (error: any) {40 return res.status(500).json({ 41 error: "Failed to generate JWKS", 42 message: error.message, 43 })44 }45}
In the above example:
- Check if the JWKS_CONTENTenvironment variable is set and return it if available.
- If not, retrieve the public key from the Medusa configuration and convert it to JWK format using Node's cryptomodule.
- Construct the JWKS response and return it.
The public key will be available at /.well-known/jwks.json for clients to fetch.
Step 3: Verify Tokens Using JWKS#
Finally, verify JWT tokens from incoming requests using the JWKS API route.
Create a middleware function at src/api/middlewares/jwks-auth.ts that uses the jwks-rsa package to fetch the public key and verify the token:
1import {2 MedusaRequest,3 MedusaNextFunction,4 MedusaResponse,5} from "@medusajs/framework/http"6import jwt from "jsonwebtoken"7import { JwksClient } from "jwks-rsa"8 9const MEDUSA_BACKEND_URL = process.env.MEDUSA_BACKEND_URL || "http://localhost:9000"10 11// Create JWKS client with caching12const jwksClient = new JwksClient({13 // This is the API route where your JWKS is exposed14 jwksUri: `${MEDUSA_BACKEND_URL}/.well-known/jwks.json`,15 cache: true,16 rateLimit: true,17 jwksRequestsPerMinute: 5,18 cacheMaxAge: 60 * 60 * 1000, // 1 hour in ms19})20 21// Helper to get signing key22async function getKey(header: any) {23 try {24 const key = await jwksClient.getSigningKey(header.kid)25 26 return key.getPublicKey()27 } catch (err: any) {28 throw new Error(`Failed to get signing key: ${err.message}`)29 }30}31 32// Function that validates JWT token33export async function isValidJWTToken(token: string): Promise<boolean> {34 if (!token) {35 return false36 }37 38 try {39 // Decode the token to get the header40 const decoded = jwt.decode(token, { complete: true }) as {41 header: { kid: string }42 payload: {43 actor_id: string44 }45 } | null46 47 if (48 !decoded ||49 !decoded.header ||50 !decoded.header.kid ||51 !decoded.payload.actor_id52 ) {53 return false54 }55 56 const publicKey = await getKey(decoded.header)57 58 return new Promise((resolve) => {59 jwt.verify(60 token,61 publicKey,62 {63 ignoreExpiration: false,64 ignoreNotBefore: false,65 },66 (err) => {67 if (err) {68 console.error("Error verifying JWT token:", err)69 resolve(false)70 }71 72 resolve(true)73 }74 )75 })76 } catch (err: any) {77 console.error("Error validating JWT token:", err)78 return false79 }80}81 82// Authentication middleware83export const jwtAuthMiddleware = async (84 req: MedusaRequest,85 res: MedusaResponse,86 next: MedusaNextFunction87) => {88 const authHeader = req.headers.authorization89 const jwtToken = authHeader?.split(" ")[1]90 91 // If we should check login and the JWT token is invalid, return 40192 if (!jwtToken || !(await isValidJWTToken(jwtToken))) {93 const error = new Error(94 "Invalid auth token provided"95 )96 97 return next(error)98 }99 100 return next()101}
The jwtAuthMiddleware function extracts the JWT token from the Authorization header, fetches the appropriate public key from the JWKS endpoint, and verifies the token.
Apply this middleware to your protected API routes to ensure that only requests with valid JWT tokens are allowed.
For example, apply the middleware in src/api/middlewares.ts:
Test JWKS Verification#
To test the JWKS verification, start the Medusa application:
Next, obtain a valid JWT token by authenticating a user. For example, to authenticate an admin user, send a POST request to /auth/user/emailpass:
Make sure to replace the email and password with valid credentials for your Medusa application.
The response will include a token field, which is your JWT token.
Finally, make a request to your protected route using the obtained JWT token:
If the token is valid, the middleware will successfully verify it using the public key fetched from the JWKS endpoint, and you'll receive a successful response. If the token is invalid or expired, you'll receive an error.


