graphql-engine icon indicating copy to clipboard operation
graphql-engine copied to clipboard

Allow unsigned (alg: none) JWT tokens

Open siddhatiwari opened this issue 4 years ago • 16 comments

I'm using the firebase auth emulator for local development which produces unsigned tokens. I'm running the firebase auth emulator and hasura (v1.3.3) locally using docker. It seems that hasura views the unsigned tokens using the recommended HASURA_GRAPHQL_JWT_SECRET for firebase as invalid. When I remove the HASURA_GRAPHQL_JWT_SECRET, all requests are defaulted to the anonymous role, which doesn't represent the actual role of the user from the unsigned token.

Is there a flag to allow using unsigned JWT tokens for development purposes? Or am I missing something with my configuration?

Hasura has been a major productivity boost for me! Just having this small issue setting up my local environment

siddhatiwari avatar Dec 13 '20 11:12 siddhatiwari

What do you mean by unsigned tokens? Do you mean the algo is none?

tirumaraiselvan avatar Dec 18 '20 10:12 tirumaraiselvan

I have this issue also with the emulator. It keeps getting back with Could not verify JWT: JWSError JWSNoSignatures

marksyzm avatar Dec 21 '20 16:12 marksyzm

I'm using 1.3.2. This is an issue for me as I want to test my cloud functions with actions as I'm working but I would have to make use of live authentication instead.

Edit: Thinking about it, I can make do with live auth for now.

marksyzm avatar Dec 21 '20 16:12 marksyzm

I'm limited by live auth as it happens... in firebase I can run all via its emulator but as I have to use a key or jwk_url it just breaks. This is less than ideal as I have to extract the JWT data with 3rd party software. I would prefer to be able to use firebaseAdmin.auth().verifyIdToken(idToken) for example.

marksyzm avatar Dec 29 '20 14:12 marksyzm

@tirumaraiselvan Yes, it appears the algorithm is none, which doesn't appear to be specified in the JWT standard, but it'd be super handy if Hasura could support. This would allow for completely local development - without touching a remote auth server at all.

Here's a dump of one of the tokens produced by the mentioned auth emulator:

Headers:

{
  "alg": "none",
  "typ": "JWT"
}

Full payload:

{
  "aud": "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",
  "iat": 1609857856,
  "exp": 1609861456,
  "iss": "[email protected]",
  "sub": "[email protected]",
  "uid": "yRdKtVuhWFShAUm43oT9HhpGMNid",
  "claims": {
    "https://hasura.io/jwt/claims": {
      "...": "..."
    }
  }
}

Given that the token has an issuer and audience, it may be possible to allow a "type" of "none" only when one (or both) of these are specified.

jamesknelson avatar Jan 05 '21 15:01 jamesknelson

I would add an environment variable for that circumstance too so it can only be used in testing and development

marksyzm avatar Jan 05 '21 18:01 marksyzm

@marksyzm I think it should be enough for the existing HASURA_GRAPHQL_JWT_SECRET environment variable to support a none type

jamesknelson avatar Jan 06 '21 06:01 jamesknelson

Yeah probably; save us having user error issues

marksyzm avatar Jan 06 '21 09:01 marksyzm

Is there any solution around this?

@tirumaraiselvan the firebase doc section: https://firebase.google.com/docs/emulator-suite/connect_auth#id_tokens

whollacsek avatar Jan 18 '21 21:01 whollacsek

I also just ran into this issue. I would agree in having the none option on Hasura's JWT secret for local testing and development. Does anyone know a work around for this in the meanwhile?

Update Here is my current work around. I'm using Apollo with NextJS and I just added some middleware to handle the checking of environments. If the environment is local(where I'm using firebase emulator), it signs the token on request.

// Generate a signed token for the request
// since firebase auth emulator tokens are unsigned
const getLocallySignedToken = token => {
  return jwt.sign(jwt.decode(token), "secret");
};

// condition for signing the token
const isLocalEnvironment = () => {
  return location.hostname === "localhost";
};

//middleware to apply signed token on request
const authMiddleware = new ApolloLink((operation, forward) => {
  // add the authorization to the headers
  const token = getAuthToken();
  operation.setContext(() => ({
    headers: token
      ? {
          Authorization: `Bearer ${
            isLocalEnvironment() ? getLocallySignedToken(token) : token
          }`
        }
      : { "X-Hasura-Role": `anonymous` }
  }));
  return forward(operation);
});

Remember to set the secret to use a key instead of the firebase JWK

HASURA_GRAPHQL_JWT_SECRET='{"type":"HS256", "key": "secret"}'

I would still LOVE a type of none on the HASURA_GRAPHQL_JWT_SECRET though :).

Rykuno avatar Jan 19 '21 02:01 Rykuno

That will help for now, thanks... It isn't great having such a public work around in the code of course but hey, it'll be enough for the time being

marksyzm avatar Jan 19 '21 07:01 marksyzm

Remember to set the secret to use a key instead of the firebase JWK

HASURA_GRAPHQL_JWT_SECRET='{"type":"HS256", "key": "secret"}'

This causes errors and I can't run the docker-compose. Normal Firebase JWT secret does work though. Odd!

Update: Nevermind, my "secret" was exactly "secret" which is not long enough! From the docs:

``key``
^^^^^^^
- In case of symmetric key (i.e. HMAC based key), the key as it is. (e.g. -
  "abcdef..."). The key must be long enough for the algorithm chosen,
  (e.g. for HS256 it must be at least 32 characters long).
- In case of asymmetric keys (RSA, EdDSA etc.), only the public key, in a PEM encoded
  string or as a X509 certificate.

HosseinYousefi avatar Dec 13 '21 19:12 HosseinYousefi

In case anyone is using flutter/dart, here is some code that uses the jwt_decoder and jaguar_jwt packages to sign tokens returned by the firebase emulator and have them be trusted by the local instance of Hasura:

String _calcJwtForHasura(String token) {
  if (kDebugMode) {
    try {
      var decoded = JwtDecoder.decode(token);
      var claims = JwtClaim.fromMap(decoded, defaultIatExp: false);
      return issueJwtHS256(
          claims, 'somerandompasswordthatmatcheswhatisinHASURA_GRAPHQL_JWT_SECRET');
    } catch (e) {
      print("Got unexpected exception (will return unmodified token): $e");
      return token;
    }
  } else {
    return token;
  }
}
Future<String> calcJwtForHasura(String token) async {
  return await compute(_calcJwtForHasura, token);
}

klondikedragon avatar Jan 20 '22 03:01 klondikedragon

love you @klondikedragon, if I was a woman, I would have had your kids.

nathikazad avatar Nov 16 '22 18:11 nathikazad

I also just ran into this issue. I would agree in having the none option on Hasura's JWT secret for local testing and development. Does anyone know a work around for this in the meanwhile?

Update Here is my current work around. I'm using Apollo with NextJS and I just added some middleware to handle the checking of environments. If the environment is local(where I'm using firebase emulator), it signs the token on request.

// Generate a signed token for the request
// since firebase auth emulator tokens are unsigned
const getLocallySignedToken = token => {
  return jwt.sign(jwt.decode(token), "secret");
};

// condition for signing the token
const isLocalEnvironment = () => {
  return location.hostname === "localhost";
};

//middleware to apply signed token on request
const authMiddleware = new ApolloLink((operation, forward) => {
  // add the authorization to the headers
  const token = getAuthToken();
  operation.setContext(() => ({
    headers: token
      ? {
          Authorization: `Bearer ${
            isLocalEnvironment() ? getLocallySignedToken(token) : token
          }`
        }
      : { "X-Hasura-Role": `anonymous` }
  }));
  return forward(operation);
});

Remember to set the secret to use a key instead of the firebase JWK

HASURA_GRAPHQL_JWT_SECRET='{"type":"HS256", "key": "secret"}'

I would still LOVE a type of none on the HASURA_GRAPHQL_JWT_SECRET though :).

Yeah, have been using similar workaround. One thing to note is that jsonwebtoken library is not intended for browsers. Latest version does not work and old versions add about 150kb nodejs polyfills https://github.com/auth0/node-jsonwebtoken/issues/885#issuecomment-1423983985

hixus avatar Mar 04 '23 08:03 hixus

I also just ran into this issue. I would agree in having the none option on Hasura's JWT secret for local testing and development. Does anyone know a work around for this in the meanwhile?

Update Here is my current work around. I'm using Apollo with NextJS and I just added some middleware to handle the checking of environments. If the environment is local(where I'm using firebase emulator), it signs the token on request.

// Generate a signed token for the request
// since firebase auth emulator tokens are unsigned
const getLocallySignedToken = token => {
  return jwt.sign(jwt.decode(token), "secret");
};

// condition for signing the token
const isLocalEnvironment = () => {
  return location.hostname === "localhost";
};

//middleware to apply signed token on request
const authMiddleware = new ApolloLink((operation, forward) => {
  // add the authorization to the headers
  const token = getAuthToken();
  operation.setContext(() => ({
    headers: token
      ? {
          Authorization: `Bearer ${
            isLocalEnvironment() ? getLocallySignedToken(token) : token
          }`
        }
      : { "X-Hasura-Role": `anonymous` }
  }));
  return forward(operation);
});

Remember to set the secret to use a key instead of the firebase JWK

HASURA_GRAPHQL_JWT_SECRET='{"type":"HS256", "key": "secret"}'

I would still LOVE a type of none on the HASURA_GRAPHQL_JWT_SECRET though :).

Great info thanks! @Rykuno

i have done similar and got it to work although had to create an endpoint to sign the JWT's as couldnt find a web front end token signer that would do the job and my code for token refreshing is client side.

krisbaum74 avatar May 16 '24 09:05 krisbaum74