Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

jwks-rsa and/or express-jwt not working with bun #10511

Open
henrikericsson opened this issue Apr 25, 2024 · 11 comments
Open

jwks-rsa and/or express-jwt not working with bun #10511

henrikericsson opened this issue Apr 25, 2024 · 11 comments
Labels
bug Something isn't working

Comments

@henrikericsson
Copy link

What version of Bun is running?

1.1.4+fbe2fe0c3

What platform is your computer?

Linux 5.15.146.1-microsoft-standard-WSL2 x86_64 x86_64

What steps can reproduce the bug?

Create an express middleware and apply it to any route served via express using https module.

Middleware:


import { Request, Response, NextFunction } from 'express'
import jwt from 'express-jwt'
import jwksRsa from 'jwks-rsa'

function requireAuth(req: Request, res: Response, next: NextFunction) {
  const jwksUri = 'https://login.microsoftonline.com/<TENANT_ID>/discovery/v2.0/keys'
  const issuer = 'https://login.microsoftonline.com/<TENANT_ID>/v2.0'
  const audience = '<CLIENT_ID>'

  const client = jwksRsa({
    cache: true,
    rateLimit: true,
    jwksRequestsPerMinute: 5,
    jwksUri: jwksUri,
  })

  client
    .getSigningKeys()
    .then(keys => console.log('keys:', keys))
    .catch(err => console.log('error:', err))

  const expressJwtSecret = jwksRsa.expressJwtSecret({
    cache: true,
    rateLimit: true,
    jwksRequestsPerMinute: 5,
    jwksUri: jwksUri,
  }) as jwksRsa.GetVerificationKey

  const expressJwt = jwt.expressjwt({
    secret: expressJwtSecret,
    audience: audience,
    issuer: issuer,
    algorithms: ['RS256'],
  })

  expressJwt(req, res, (error: unknown) => {
    if (error) {
      return next(error)
    }

    return next()
  })
}

export default requireAuth

What is the expected behavior?

A collection of signing keys should be returned from the jwksUri using the jwks-rsa package.

What do you see instead?

No keys are returned and fails with error:

JwksError {
  name: "JwksError",
  message: "The JWKS endpoint did not contain any signing keys",
  toString: [Function: toString],
}

Additional information

The same flow works just fine running it with node

Middleware:


const jwt = require("express-jwt");
const jwksRsa = require("jwks-rsa");

function requireAuth(req, res, next) {
  const jwksUri = "https://login.microsoftonline.com/<TENANT_ID>/discovery/v2.0/keys";
  const issuer = "https://login.microsoftonline.com/<TENANT_ID>/v2.0";
  const audience = "<CLIENT_ID>";

  const client = jwksRsa({
    cache: true,
    rateLimit: true,
    jwksRequestsPerMinute: 5,
    jwksUri: jwksUri,
  });

  client
    .getSigningKeys()
    .then((keys) => console.log("keys:", keys))
    .catch((err) => console.log("error:", err));

  const expressJwtSecret = jwksRsa.expressJwtSecret({
    cache: true,
    rateLimit: true,
    jwksRequestsPerMinute: 5,
    jwksUri: jwksUri,
  });

  const expressJwt = jwt.expressjwt({
    secret: expressJwtSecret,
    audience: audience,
    issuer: issuer,
    algorithms: ["RS256"],
  });

  expressJwt(req, res, (error) => {
    if (error) {
      return next(error);
    }

    return next();
  });
}

module.exports = requireAuth;
@henrikericsson henrikericsson added the bug Something isn't working label Apr 25, 2024
@elliots
Copy link

elliots commented Apr 27, 2024

this is the error jose is throwing that jwks isn't logging... at least in my case

TypeError: CryptoKey is not extractable
      at /opt/core/api/node_modules/jwks-rsa/node_modules/jose/dist/browser/runtime/asn1.js:12:15
      at genericExport (/opt/core/api/node_modules/jwks-rsa/node_modules/jose/dist/browser/runtime/asn1.js:7:30)
      at exportSPKI (/opt/core/api/node_modules/jwks-rsa/node_modules/jose/dist/browser/key/export.js:4:34)
      at /opt/core/api/node_modules/jwks-rsa/src/utils.js:53:30```

@elliots
Copy link

elliots commented Apr 27, 2024

It's using the browser runtime, not node. Not sure if that's whats supposed to be happening? Or maybe theres a bun one. Anyway, when using browser the ext field is looked at in the jwt. This gets past the issue for me. I think.

diff --git a/node_modules/jwks-rsa/src/utils.js b/node_modules/jwks-rsa/src/utils.js
index dcaf146..e40e613 100644
--- a/node_modules/jwks-rsa/src/utils.js
+++ b/node_modules/jwks-rsa/src/utils.js
@@ -43,6 +43,7 @@ async function retrieveSigningKeys(jwks) {
 
   for (const jwk of jwks) {
     try {
+      jwk.ext = true
       const key = await jose.importJWK(jwk, resolveAlg(jwk));
       if (key.type !== 'public') {
         continue;

@henrikericsson
Copy link
Author

Thank you for your responses and for looking into this issue. I appreciate the time and effort you’ve put into investigating this.

I agree that the error seems to be related to the crypto library, specifically with the CryptoKey not being extractable. However, I would like to point out that the same code works perfectly fine when running in a Node.js environment. This suggests that the issue might not be with the jwks-rsa library itself, but rather with how it’s being used or configured in the Bun environment.

The jwks-rsa library is as far as I know designed for Node.js, and it seems to be functioning as expected in that context. Modifying the library directly might not be the best approach, as it could potentially introduce other issues or side effects. Instead, I believe we should focus on understanding why the library is not working as expected in the Bun environment.

It’s possible that there might be some configuration changes needed in Bun, or perhaps some adjustments within the framework itself. I think it would be beneficial to explore these possibilities further.

@elliots
Copy link

elliots commented Apr 28, 2024

No worries, I’m hitting the same issue.

I’m not suggesting the change is an actual fix, just working out where the problem is :)

Looks like it is supposed to use the browser runtime: https://github.com/panva/jose/blob/main/package.json#L80

@elliots
Copy link

elliots commented Apr 28, 2024

Related: auth0/node-jwks-rsa#373

@guidoffm
Copy link

Version 2.1.5 works for me, version 3.1.0 not.

@henrikericsson
Copy link
Author

A few months ago, I experimented with the 2.x version of the jwks-rsa library, using an older version of Bun but encountered the same issue then as I am with the latest version of Bun and the 3.x version of jwks-rsa.

But I haven't tried using the latest version of bun with an older version of jwks-rsa.

@guidoffm
Copy link

guidoffm commented May 1, 2024

It is better to use the Jose library since it is certified for the use with Bun.

@fjdvchain
Copy link

Okay I want to bring attention to this again. I'm having similar issues with Bun v1.1.22. I spending hours tweaking my code in hopes it was application related, I came across this issue and ported my code to node and it verified the jwt just fine. Do you need another repro snippet or is this issue enough? @Jarred-Sumner

@fjdvchain
Copy link

This is what I'm running and azureActiveDirectory fails in bun but succeeds in node. The failure message is

JsonWebTokenError {
  stack: "Error\n    at <anonymous> (.../node_modules/jsonwebtoken/verify.js:103:19)\n    at <anonymous> (.../src/jwt.ts:27:7)\n    at processTicksAndRejections (native)",
  name: "JsonWebTokenError",
  message: "error in secret or public key callback: The JWKS endpoint did not contain any signing keys",
  toString: [Function: toString],
}
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
import { AZURE_AD_TENANT_ID } from './secrets';

const JWKS_URI = `https://login.microsoftonline.com/${AZURE_AD_TENANT_ID}/discovery/v2.0/keys`;
const UNAUTHORIZED = 'Unauthorized';

// Create a JWKS client to retrieve JWKS URI signing keys.
const client = jwksClient({ jwksUri: JWKS_URI });

function getKey(header: any, callback: jwt.SigningKeyCallback) {
  if (typeof callback !== 'function') {
    console.log('Callback is not a function');
    return;
  }

  if (!header) {
    callback(new Error('Header is not provided'));
    return;
  }
  // client.getKeys().then(r => console.log(r))
  client.getSigningKey(header.kid, (err, key) => {
    if (err || !key) {
      console.log("ERROR HERE")
      callback(err);
      return;
    }
    if (!key) return

    const signingKey = key.getPublicKey();
    callback(null, signingKey);
  });
}

export const azureActiveDirectory = (token: string) => {
  // Check if the token is present.
  if (!token) {
    return Promise.reject(`${UNAUTHORIZED}: No token provided`)
  }

  return new Promise((resolve,reject) => {
    jwt.verify(
      token,
      getKey,
      {
        algorithms: ['RS256']
      },
      (err, decode) => {
        if (err) {
          console.log('Error in token verification:', err);
          reject(UNAUTHORIZED)
          return;
        }
        resolve(decode)
      }
    );
  })
};
`

@klippx
Copy link

klippx commented Oct 4, 2024

I am using a Bun backed server that works in Node but not in Bun. This code has different code paths for the runtimes:

  for (const jwk of jwks) {
    try {
      jwk.ext = true
      const key = await jose.importJWK(jwk, resolveAlg(jwk));
      console.log('jwks-rsa key.type:', key.type);
      if (key.type !== 'public') {
        continue;
      }
      let getSpki;
      console.log('jwks-rsa key[Symbol.toStringTag]:', key[Symbol.toStringTag]);

      switch (key[Symbol.toStringTag]) {
        case 'CryptoKey': {
          // 👉 BUN WILL GO THIS CODE PATH
          const spki = await jose.exportSPKI(key);
          getSpki = () => spki;
          break;
        }
        case 'KeyObject':
          // Assume legacy Node.js version without the Symbol.toStringTag backported
          // Fall through
        default: {
          // 👉 NODE WILL GO THIS CODE PATH
          getSpki = () => key.export({ format: 'pem', type: 'spki' });
        }
      }
      console.log('jwks-rsa results.push...', {
        get publicKey() { return getSpki(); },
        get rsaPublicKey() { return getSpki(); },
        getPublicKey() { return getSpki(); },
        ...(typeof jwk.kid === 'string' && jwk.kid ? { kid: jwk.kid } : undefined),
        ...(typeof jwk.alg === 'string' && jwk.alg ? { alg: jwk.alg } : undefined)
      });
      results.push({
        get publicKey() { return getSpki(); },
        get rsaPublicKey() { return getSpki(); },
        getPublicKey() { return getSpki(); },
        ...(typeof jwk.kid === 'string' && jwk.kid ? { kid: jwk.kid } : undefined),
        ...(typeof jwk.alg === 'string' && jwk.alg ? { alg: jwk.alg } : undefined)
      });
    } catch (err) {
      console.error('jwks-rsa error!');
      console.error(err)
      continue;
    }
  }

Debug logs show that for Node:

jwks-rsa key[Symbol.toStringTag]: KeyObject
jwks-rsa fallthrough
jwks-rsa results.push... {
  publicKey: [Getter],
  rsaPublicKey: [Getter],
  getPublicKey: [Function: getPublicKey],
  kid: '***'
}

And for Bun:

jwks-rsa key[Symbol.toStringTag]: CryptoKey
jwks-rsa error!
 7 | const genericExport = async (keyType, keyFormat, key) => {
 8 |     if (!isCryptoKey(key)) {
 9 |         throw new TypeError(invalidKeyInput(key, ...types));
10 |     }
11 |     if (!key.extractable) {
12 |         throw new TypeError('CryptoKey is not extractable');

It's using the browser runtime, not node. Not sure if that's whats supposed to be happening? Or maybe theres a bun one. Anyway, when using browser the ext field is looked at in the jwt. This gets past the issue for me. I think.

This jwk.ext = true "fix" works in Bun backend too:

jwks-rsa key[Symbol.toStringTag]: CryptoKey
jwks-rsa results.push... {
  publicKey: [Getter],
  rsaPublicKey: [Getter],
  getPublicKey: [Function: getPublicKey],
  kid: "***",
}

What exactly does ext = true do that makes this work? Is it insecure to use this "fix", or can it be accepted? 🤔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

5 participants