Node-jsonwebtoken: TypeScript: verify callback typings can't assume types

Created on 26 Dec 2020  Â·  4Comments  Â·  Source: auth0/node-jsonwebtoken

jwt.verify(token, secret, (error, data) => { const { userName } = data })

throws

Property 'userName' does not exist on type '{}'.

Environment

"@types/jsonwebtoken": "^8.5.0",
"jsonwebtoken": "^8.5.1",

This is the current typings for VerifyCallback

export type VerifyCallback = (
    err: VerifyErrors | null,
    decoded: object | undefined,
) => void; 

Maybe a generic could be added to extend decoded.

Or someone else has a better idea how to improve TypeScript behaviour here?

Most helpful comment

My suggestion would be something like

export function verify<Decoded>(
    token: string,
    secretOrPublicKey: Secret | GetPublicKeyOrSecret,
    callback?: VerifyCallback<Decoded>,
): void;
export type VerifyCallback<Decoded> = (
    err: VerifyErrors | null,
    decoded: Decoded | undefined ,
) => void;

All 4 comments

My suggestion would be something like

export function verify<Decoded>(
    token: string,
    secretOrPublicKey: Secret | GetPublicKeyOrSecret,
    callback?: VerifyCallback<Decoded>,
): void;
export type VerifyCallback<Decoded> = (
    err: VerifyErrors | null,
    decoded: Decoded | undefined ,
) => void;

Any suggestions on this?

You can't actually type the decoded value, as it comes from an untrusted source; typing it as anything other than Record<string, unknown> would be giving you a false sense of security in your typesafety.

Even if you think the decoded token will include a given property, it may not actually, JWTs don't validate properties beyond a limit set (those you can validate using the options argument).

There's nothing that guarantees the returned decoded value will contain given properties — only those the library explicitly checks (based on the options argument) could be "known" types. decoded would be better represented as Record<string, unknown>

So if you need a specific property, you should actually check if the decoded JWT contains that property.

Example:

const token = jwt.sign({ foo: "fooValue"}, "secret");

jwt.verify<{ bar: boolean }>(token, "secret", (err, decoded) => {
  // decoded is: { foo: "fooValue" }, so if `decoded` was cast as `{ bar: boolean }` then 
  // you'd think `decoded.bar` is always present. As the signer of the token's payload 
  // didn't include the `bar` property, then it won't be there.
})

If you want to guarantee the shape of the decoded object, then you'd need to pass decoded through a validation library like Joi or Yup, or just manually assert properties exist.

This issue can be a particular gotcha for people consuming JWTs from other services/servers.

In fact, in the typings for jws.decode which this module uses, payload of jws.Signature is typed as any.

There's actually no code in jwt.decode that guarantees the decoded value will be an object: https://github.com/auth0/node-jsonwebtoken/blob/master/decode.js#L7-L17

Nor in jwt.verify — payload isn't guaranteed to a parsed JSON Object (it comes from jwt.decode): https://github.com/auth0/node-jsonwebtoken/blob/master/verify.js#L136

To clarify with working code:

const jws = require('jws');
const jwt = require('jsonwebtoken')

const token = jws.sign({ header: { "alg": "HS256" }, payload: true, secret: 'secret'})
// => 'eyJhbGciOiJIUzI1NiJ9.dHJ1ZQ.tNWhzx87CKMgFxL2x9cqZJkzixGWWAkRNbHmbn6d5Dk'

jwt.decode(token, { complete: true })
/* {
  header: { alg: 'HS256' },
  payload: 'true',
  signature: 'tNWhzx87CKMgFxL2x9cqZJkzixGWWAkRNbHmbn6d5Dk'
} */

jwt.verify(token, "secret")
// => 'true'

This is also why you should be really protective over your jsonwebtoken secret/key data, if that gets compromised, then someone can create all manner of random crap to exploit bugs in your app.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dwelle picture dwelle  Â·  3Comments

yvele picture yvele  Â·  4Comments

itamarwe picture itamarwe  Â·  3Comments

glowlabs picture glowlabs  Â·  3Comments

Teebo picture Teebo  Â·  4Comments