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

feat: Replacing jsonwebtoken with fast-jwt #184

Merged
merged 29 commits into from
Nov 25, 2021
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
897cf93
feat: Replaced jsonwebtoken with fast-jwt
radomird Oct 19, 2021
b06b07a
Removed jsowebtoken library from the source and package.json;
radomird Oct 19, 2021
09b8e68
Updated tests to use new option names;
radomird Oct 22, 2021
9d284bf
Enabled all passing tests
radomird Oct 25, 2021
ac38fc6
Fixed all unit tests
radomird Oct 25, 2021
ba0e155
Added missing code coverage
radomird Oct 25, 2021
dfc1562
Updated types
radomird Oct 25, 2021
34148a4
Updated docs
radomird Oct 25, 2021
6a6d3b4
Removing unnecessary comment in jwt.d.ts
radomird Oct 25, 2021
df87c80
Added TODO task for disabled tests
radomird Oct 25, 2021
bc36ce1
Extracting repeated code to a new function
radomird Oct 26, 2021
2aa247b
Refactored repeating code
radomird Oct 26, 2021
cc97627
Removed console.log statements
radomird Oct 27, 2021
b482963
Updated fast-jwt to v1.3.2 and enabled tests
radomird Nov 10, 2021
ebc8b3f
Updated the min required fastify to a stable version
radomird Nov 10, 2021
e7c754f
docs: Updated README with a note about migration
radomird Nov 10, 2021
36df788
Update README.md
radomird Nov 10, 2021
00f3065
Added conversion to ms
radomird Nov 16, 2021
f0f4706
chore: Removed non compatible Node v10 from the CI build
radomird Nov 16, 2021
e6da278
Extracted signer/verifier/decoder init for reusability
radomird Nov 16, 2021
18d9d74
Apply suggestions from code review
radomird Nov 17, 2021
3665579
Update README.md
radomird Nov 17, 2021
e18137f
Apply suggestions from code review
radomird Nov 18, 2021
3832bff
Added type definitions for node - fix failing CI build
radomird Nov 18, 2021
c38ef95
chore: Updated docs with migration breaking changes and updated examples
radomird Nov 19, 2021
c9ad90a
fix: typo in README
radomird Nov 19, 2021
5c9ae34
Added missing option in migration section in README
radomird Nov 19, 2021
bd7e2b5
chore: Moved migration notes to separate file
radomird Nov 19, 2021
bb6dd06
Added few more migration points to UPGRADING.md
radomird Nov 22, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:

strategy:
matrix:
node-version: [10, 12, 14, 16]
node-version: [12, 14, 16]
climba03003 marked this conversation as resolved.
Show resolved Hide resolved
os: [macos-latest, ubuntu-latest, windows-latest]

steps:
Expand Down
86 changes: 46 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
[![Known Vulnerabilities](https://snyk.io/test/github/fastify/fastify-jwt/badge.svg)](https://snyk.io/test/github/fastify/fastify-jwt)
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/)

JWT utils for Fastify, internally uses [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken).
JWT utils for Fastify, internally it uses [fast-jwt](https://github.com/nearform/fast-jwt).

**NOTE:** The plugin has been migrated from using `jsonwebtoken` to `fast-jwt`. Even though `fast-jwt` has 1:1 feature implementation with `jsonwebtoken`, some _exotic_ implementations might break. In that case please open an issue with details of your implementation.
radomird marked this conversation as resolved.
Show resolved Hide resolved

`fastify-jwt` supports Fastify@3.
`fastify-jwt` [v1.x](https://github.com/fastify/fastify-jwt/tree/1.x)
Expand All @@ -17,7 +19,7 @@ npm i fastify-jwt --save
```

## Usage
Register as a plugin. This will decorate your `fastify` instance with the standard [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) methods `decode`, `sign`, and `verify`; refer to their documentation to find how to use the utilities. It will also register `request.jwtVerify` and `reply.jwtSign`. You must pass a `secret` when registering the plugin.
Register as a plugin. This will decorate your `fastify` instance with the following methods: `decode`, `sign`, and `verify`; refer to their documentation to find how to use the utilities. It will also register `request.jwtVerify` and `reply.jwtSign`. You must pass a `secret` when registering the plugin.

```js
const fastify = require('fastify')()
Expand Down Expand Up @@ -411,43 +413,39 @@ fastify.register(require('fastify-jwt'), {

### `decode`

* `json`: force JSON.parse on the payload even if the header doesn't contain `"typ":"JWT"`.
* `complete`: return an object with the decoded payload and header.
* `complete`: Return an object with the decoded header, payload, signature and input (the token part before the signature), instead of just the content of the payload. Default is `false`.
* `checkTyp`: When validating the decoded header, setting this option forces the check of the typ property against this value. Example: `checkTyp: 'JWT'`. Default is `undefined`.

### `sign`

* `algorithm` (default: `HS256`)
* `expiresIn`: expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms). E.g.: `60`, `"2 days"`, `"10h"`, `"7d"`. A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc.), otherwise milliseconds unit is used by default (`"120"` is equal to `"120ms"`).
* `notBefore`: expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms). E.g.: `60`, `"2 days"`, `"10h"`, `"7d"`. A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc.), otherwise milliseconds unit is used by default (`"120"` is equal to `"120ms"`).
* `audience`
* `issuer`
* `jwtid`
* `subject`
* `noTimestamp`
* `header`
* `keyid`
* `mutatePayload`: if true, the sign function will modify the payload object directly. This is useful if you need a raw reference to the payload after claims have been applied to it but before it has been encoded into a token.
* `key`: A string or a buffer containing the secret for `HS*` algorithms or the PEM encoded public key for `RS*`, `PS*`, `ES*` and `EdDSA` algorithms. The key can also be a function accepting a Node style callback or a function returning a promise. If provided, it will override the value of [secret](#secret-required) provided in the options.
* `algorithm`: The algorithm to use to sign the token. The default is autodetected from the key, using RS256 for RSA private keys, HS256 for plain secrets and the correspondent ES or EdDSA algorithms for EC or Ed* private keys.
* `mutatePayload`: If set to `true`, the original payload will be modified in place (via `Object.assign`) by the signing function. This is useful if you need a raw reference to the payload after claims have been applied to it but before it has been encoded into a token.
* `expiresIn`: Time span after which the token expires, added as the `exp` claim in the payload. It is expressed in seconds or a string describing a time span (E.g.: `60`, `"2 days"`, `"10h"`, `"7d"`). A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc.), otherwise milliseconds unit is used by default (`"120"` is equal to `"120ms"`). This will override any existing value in the claim.
* `notBefore`: Time span before the token is active, added as the `nbf` claim in the payload. It is expressed in seconds or a string describing a time span (E.g.: `60`, `"2 days"`, `"10h"`, `"7d"`). A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc.), otherwise milliseconds unit is used by default (`"120"` is equal to `"120ms"`). This will override any existing value in the claim.

* ... the rest of the **sign** options can be found [here](https://github.com/nearform/fast-jwt#createsigner).

### `verify`

* `algorithms`: List of strings with the names of the allowed algorithms. For instance, `["HS256", "HS384"]`.
* `audience`: if you want to check audience (`aud`), provide a value here. The audience can be checked against a string, a regular expression or a list of strings and/or regular expressions. E.g.: `"urn:foo"`, `/urn:f[o]{2}/`, `[/urn:f[o]{2}/, "urn:bar"]`
* `issuer` (optional): string or array of strings of valid values for the `iss` field.
* `ignoreExpiration`: if `true` do not validate the expiration of the token.
* `ignoreNotBefore`...
* `subject`: if you want to check subject (`sub`), provide a value here
* `clockTolerance`: number of seconds to tolerate when checking the `nbf` and `exp` claims, to deal with small clock differences among different servers
* `maxAge`: the maximum allowed age for tokens to still be valid. It is expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms). E.g.: `1000`, `"2 days"`, `"10h"`, `"7d"`. A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc.), otherwise milliseconds unit is used by default (`"120"` is equal to `"120ms"`).
* `clockTimestamp`: the time in seconds that should be used as the current time for all necessary comparisons.
* `extractToken(request): token`: Callback function allowing to use custom logic to extract the JWT token from the request.
* `key`: A string or a buffer containing the secret for `HS*` algorithms or the PEM encoded public key for `RS*`, `PS*`, `ES*` and `EdDSA` algorithms. The key can also be a function accepting a Node style callback or a function returning a promise. If provided, it will override the value of [secret](#secret-required) provided in the options.
* `algorithms`: List of strings with the names of the allowed algorithms. By default, all algorithms are accepted.
* `complete`: Return an object with the decoded header, payload, signature and input (the token part before the signature), instead of just the content of the payload. Default is `false`.
* `cache`: A positive number specifying the size of the verified tokens cache (using LRU strategy). Setting this to `true` is equivalent to provide the size 1000. When enabled the performance is dramatically improved. By default the cache is disabled.
mcollina marked this conversation as resolved.
Show resolved Hide resolved
* `cacheTTL`: The maximum time to live of a cache entry (in milliseconds). If the token has a earlier expiration or the verifier has a shorter `maxAge`, the earlier takes precedence. The default is `600000`, which is 10 minutes.
radomird marked this conversation as resolved.
Show resolved Hide resolved
* `maxAge`: The maximum allowed age for tokens to still be valid. It is expressed in seconds or a string describing a time span (E.g.: `60`, `"2 days"`, `"10h"`, `"7d"`). A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc.), otherwise milliseconds unit is used by default (`"120"` is equal to `"120ms"`). By default this is not checked.
* ... the rest of the **verify** options can be found [here](https://github.com/nearform/fast-jwt#createverifier).

## API Spec

### fastify.jwt.sign(payload [,options] [,callback])
The `sign` method is an implementation of [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken#jwtsignpayload-secretorprivatekey-options-callback) `.sign()`. Can be used asynchronously by passing a callback function; synchronously without a callback.
This method is used to sign the provided `payload`. It returns the token.
The `payload` must be an `Object`. Can be used asynchronously by passing a callback function; synchronously without a callback.
`options` must be an `Object` and can contain [sign](#sign) options.

### fastify.jwt.verify(token, [,options] [,callback])
The `verify` method is an implementation of [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback) `.verify()`. Can be used asynchronously by passing a callback function; synchronously without a callback.
This method is used to verify provided token. It accepts a `token` (as `Buffer` or a `string`) and returns the payload or the sections of the token. Can be used asynchronously by passing a callback function; synchronously without a callback.
`options` must be an `Object` and can contain [verify](#verify) options.

#### Example
```js
Expand All @@ -462,7 +460,9 @@ fastify.jwt.verify(token, (err, decoded) => {
```

### fastify.jwt.decode(token [,options])
The `decode` method is an implementation of [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken#jwtdecodetoken--options) `.decode()`. Can only be used synchronously.
This method is used to decode the provided token. It accepts a token (as a `Buffer` or a `string`) and returns the payload or the sections of the token.
`options` must be an `Object` and can contain [decode](#decode) options.
Can only be used synchronously.

#### Example
```js
Expand Down Expand Up @@ -555,20 +555,26 @@ As of 3.2.0, decorated when `options.jwtDecode` is truthy. Will become non-condi

### Algorithms supported

The following algorithms are currently supported by [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken#algorithms-supported) that is internally used by `fastify-jwt`.
The following algorithms are currently supported by [fast-jwt](https://github.com/nearform/fast-jwt) that is internally used by `fastify-jwt`.

algorithm(s) Parameter Value | Digital Signature or MAC Algorithm
**Name** | **Description**
----------------|----------------------------
HS256 | HMAC using SHA-256 hash algorithm
HS384 | HMAC using SHA-384 hash algorithm
HS512 | HMAC using SHA-512 hash algorithm
RS256 | RSASSA using SHA-256 hash algorithm
RS384 | RSASSA using SHA-384 hash algorithm
RS512 | RSASSA using SHA-512 hash algorithm
ES256 | ECDSA using P-256 curve and SHA-256 hash algorithm
ES384 | ECDSA using P-384 curve and SHA-384 hash algorithm
ES512 | ECDSA using P-521 curve and SHA-512 hash algorithm
none | No digital signature or MAC value included
none | Empty algorithm - The token signature section will be empty
HS256 | HMAC using SHA-256 hash algorithm
HS384 | HMAC using SHA-384 hash algorithm
HS512 | HMAC using SHA-512 hash algorithm
ES256 | ECDSA using P-256 curve and SHA-256 hash algorithm
ES384 | ECDSA using P-384 curve and SHA-384 hash algorithm
ES512 | ECDSA using P-521 curve and SHA-512 hash algorithm
RS256 | RSASSA-PKCS1-v1_5 using SHA-256 hash algorithm
RS384 | RSASSA-PKCS1-v1_5 using SHA-384 hash algorithm
RS512 | RSASSA-PKCS1-v1_5 using SHA-512 hash algorithm
PS256 | RSASSA-PSS using SHA-256 hash algorithm
PS384 | RSASSA-PSS using SHA-384 hash algorithm
PS512 | RSASSA-PSS using SHA-512 hash algorithm
EdDSA | EdDSA tokens using Ed25519 or Ed448 keys, only supported on Node.js 12+

You can find the list [here](https://github.com/nearform/fast-jwt#algorithms-supported).

### Examples

Expand Down
91 changes: 55 additions & 36 deletions jwt.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DecoderOptions, KeyFetcher, SignerCallback, SignerOptions, VerifierCallback, VerifierOptions } from 'fast-jwt'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given the many changes in typings, let's make sure we are running typing checks in CI and that we have tests in place for typing

import * as fastify from 'fastify'
import * as jwt from 'jsonwebtoken'

/**
* for declaration merging
Expand Down Expand Up @@ -37,29 +37,48 @@ export type UserType = FastifyJWT extends { user: infer T }
? T
: SignPayloadType

export type TokenOrHeader = jwt.JwtHeader | { header: jwt.JwtHeader; payload: any }
// standard names https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1
// same interface as in jsonwebtoken lib - https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/jsonwebtoken/index.d.ts
export interface JwtHeader {
radomird marked this conversation as resolved.
Show resolved Hide resolved
alg: string | Algorithm;
typ?: string | undefined;
cty?: string | undefined;
crit?: Array<string | Exclude<keyof JwtHeader, 'crit'>> | undefined;
kid?: string | undefined;
jku?: string | undefined;
x5u?: string | string[] | undefined;
'x5t#S256'?: string | undefined;
x5t?: string | undefined;
x5c?: string | string[] | undefined;
}

export type TokenOrHeader = JwtHeader | { header: JwtHeader; payload: any }

export type Secret = jwt.Secret
export type Secret = string | Buffer | KeyFetcher
| ((request: fastify.FastifyRequest, tokenOrHeader: TokenOrHeader, cb: (e: Error | null, secret: string | undefined) => void) => void)
| ((request: fastify.FastifyRequest, tokenOrHeader: TokenOrHeader) => Promise<string>)

export type VerifyPayloadType = object | string

export type DecodePayloadType = object | string

export interface VerifyCallback<Decoded extends VerifyPayloadType> extends jwt.VerifyCallback {
(err: jwt.VerifyErrors, decoded: Decoded): void
}

export interface DecodeCallback<Decoded extends DecodePayloadType> {
(err: Error, decoded: Decoded): void
}

radomird marked this conversation as resolved.
Show resolved Hide resolved
export interface SignOptions extends Omit<SignerOptions, "expiresIn" | "notBefore"> {
expiresIn: number | string;
notBefore: number | string;
}

export interface VerifyOptions extends Omit<VerifierOptions, "maxAge"> {
maxAge: number | string;
}

export interface FastifyJWTOptions {
secret: Secret | { public: Secret; private: Secret }
decode?: jwt.DecodeOptions
sign?: jwt.SignOptions
verify?: jwt.VerifyOptions & { extractToken?: (request: fastify.FastifyRequest) => string | void }
decode?: Partial<DecoderOptions>
sign?: Partial<SignOptions>
verify?: Partial<VerifyOptions> & { extractToken?: (request: fastify.FastifyRequest) => string | void }
cookie?: {
cookieName: string,
signed: boolean
Expand All @@ -81,42 +100,42 @@ export interface FastifyJWTOptions {

export interface JWT {
options: {
decode: jwt.DecodeOptions
sign: jwt.SignOptions
verify: jwt.VerifyOptions
decode: Partial<DecoderOptions>
sign: Partial<SignOptions>
verify: Partial<VerifyOptions>
}
cookie?: {
cookieName: string,
signed: boolean
}

sign(payload: SignPayloadType, options?: jwt.SignOptions): string
sign(payload: SignPayloadType, callback: jwt.SignCallback): void
sign(payload: SignPayloadType, options: jwt.SignOptions, callback: jwt.SignCallback): void
sign(payload: SignPayloadType, options?: Partial<SignOptions>): string
sign(payload: SignPayloadType, callback: SignerCallback): void
sign(payload: SignPayloadType, options: Partial<SignOptions>, callback: SignerCallback): void

verify<Decoded extends VerifyPayloadType>(token: string, options?: jwt.VerifyOptions): Decoded
verify<Decoded extends VerifyPayloadType>(token: string, callback: VerifyCallback<Decoded>): void
verify<Decoded extends VerifyPayloadType>(token: string, options: jwt.VerifyOptions, callback: VerifyCallback<Decoded>): void
verify<Decoded extends VerifyPayloadType>(token: string, options?: Partial<VerifyOptions>): Decoded
verify<Decoded extends VerifyPayloadType>(token: string, callback: VerifierCallback): void
verify<Decoded extends VerifyPayloadType>(token: string, options: Partial<VerifyOptions>, callback: VerifierCallback): void

decode<Decoded extends DecodePayloadType>(token: string, options?: jwt.DecodeOptions): null | Decoded
decode<Decoded extends DecodePayloadType>(token: string, options?: Partial<DecoderOptions>): null | Decoded
}

export const fastifyJWT: fastify.FastifyPluginCallback<FastifyJWTOptions>

export default fastifyJWT

export interface FastifyJwtSignOptions {
sign?: jwt.SignOptions
sign?: Partial<SignOptions>
}

export interface FastifyJwtVerifyOptions {
decode: jwt.DecodeOptions
verify: jwt.VerifyOptions
decode: Partial<DecoderOptions>
verify: Partial<VerifyOptions>
}

export interface FastifyJwtDecodeOptions {
decode: jwt.DecodeOptions
verify: jwt.VerifyOptions
decode: Partial<DecoderOptions>
verify: Partial<VerifyOptions>
}

declare module 'fastify' {
Expand All @@ -126,20 +145,20 @@ declare module 'fastify' {

interface FastifyReply {
jwtSign(payload: SignPayloadType, options?: FastifyJwtSignOptions): Promise<string>
jwtSign(payload: SignPayloadType, callback: jwt.SignCallback): void
jwtSign(payload: SignPayloadType, options: FastifyJwtSignOptions, callback: jwt.SignCallback): void
jwtSign(payload: SignPayloadType, options?: jwt.SignOptions): Promise<string>
jwtSign(payload: SignPayloadType, callback: jwt.SignCallback): void
jwtSign(payload: SignPayloadType, options: jwt.SignOptions, callback: jwt.SignCallback): void
jwtSign(payload: SignPayloadType, callback: SignerCallback): void
jwtSign(payload: SignPayloadType, options: FastifyJwtSignOptions, callback: SignerCallback): void
jwtSign(payload: SignPayloadType, options?: Partial<SignOptions>): Promise<string>
jwtSign(payload: SignPayloadType, callback: SignerCallback): void
jwtSign(payload: SignPayloadType, options: Partial<SignOptions>, callback: SignerCallback): void
}

interface FastifyRequest {
jwtVerify<Decoded extends VerifyPayloadType>(options?: FastifyJwtVerifyOptions): Promise<Decoded>
jwtVerify<Decoded extends VerifyPayloadType>(callback: VerifyCallback<Decoded>): void
jwtVerify<Decoded extends VerifyPayloadType>(options: FastifyJwtVerifyOptions, callback: VerifyCallback<Decoded>): void
jwtVerify<Decoded extends VerifyPayloadType>(options?: jwt.VerifyOptions): Promise<Decoded>
jwtVerify<Decoded extends VerifyPayloadType>(callback: VerifyCallback<Decoded>): void
jwtVerify<Decoded extends VerifyPayloadType>(options: jwt.VerifyOptions, callback: VerifyCallback<Decoded>): void
jwtVerify<Decoded extends VerifyPayloadType>(callback: VerifierCallback): void
jwtVerify<Decoded extends VerifyPayloadType>(options: FastifyJwtVerifyOptions, callback: VerifierCallback): void
jwtVerify<Decoded extends VerifyPayloadType>(options?: Partial<VerifyOptions>): Promise<Decoded>
jwtVerify<Decoded extends VerifyPayloadType>(callback: VerifierCallback): void
jwtVerify<Decoded extends VerifyPayloadType>(options: Partial<VerifyOptions>, callback: VerifierCallback): void
jwtDecode<Decoded extends DecodePayloadType>(options?: FastifyJwtDecodeOptions): Promise<Decoded>
jwtDecode<Decoded extends DecodePayloadType>(callback: DecodeCallback<Decoded>): void
jwtDecode<Decoded extends DecodePayloadType>(options: FastifyJwtDecodeOptions, callback: DecodeCallback<Decoded>): void
Expand Down
Loading