Skip to content

Commit

Permalink
some request changes from @n1ru4l and @ardatan
Browse files Browse the repository at this point in the history
  • Loading branch information
EmrysMyrddin committed Jul 24, 2023
1 parent b849d8f commit aa70be3
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 31 deletions.
2 changes: 1 addition & 1 deletion packages/plugins/jwt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"engines": {
"node": ">=16.0.0"
},
"version": "1.0.0-next.0",
"version": "0.0.0",
"description": "jwt plugin for GraphQL Yoga.",
"repository": {
"type": "git",
Expand Down
15 changes: 9 additions & 6 deletions packages/plugins/jwt/src/__tests__/jwt.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@ import { createSchema, createYoga } from 'graphql-yoga'
import jwt from 'jsonwebtoken'
import crypto from 'node:crypto'

import { useJwt } from '..'
import { JwtPluginOptions, useJWT } from '@graphql-yoga/plugin-jwt'

describe('jwt', () => {
it('should throw if no signing key or jwksUri is provided', () => {
expect(() => useJwt({ issuer: 'yoga' })).toThrow(
// @ts-expect-error testing invalid options fo JS users
expect(() => useJWT({ issuer: 'yoga' })).toThrow(
'You need to provide either a signingKey or a jwksUri',
)
})

it('should throw if both signing key and jwksUri are provided', () => {
expect(() =>
useJwt({ signingKey: 'test', jwksUri: 'test', issuer: 'yoga' }),
// @ts-expect-error testing invalid options fo JS users
useJWT({ signingKey: 'test', jwksUri: 'test', issuer: 'yoga' }),
).toThrow('You need to provide either a signingKey or a jwksUri, not both')
})

Expand Down Expand Up @@ -82,7 +84,7 @@ describe('jwt', () => {
await expect(
server.queryWithAuth(buildJWT({}, { keyid: 'unknown' })),
).rejects.toMatchObject({
message: 'Failed to decode authentication token',
message: 'Failed to decode authentication token. Unknown key id.',
extensions: { http: { status: 401 } },
})
})
Expand Down Expand Up @@ -138,11 +140,12 @@ describe('jwt', () => {
})
})

const createTestServer = (options?: Partial<Parameters<typeof useJwt>[0]>) => {
const createTestServer = (options?: Partial<JwtPluginOptions>) => {
const yoga = createYoga({
schema,
plugins: [
useJwt({
// @ts-expect-error testing invalid options fo JS users
useJWT({
issuer: 'http://yoga',
signingKey: 'very secret key',
...options,
Expand Down
69 changes: 45 additions & 24 deletions packages/plugins/jwt/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
import { JwksClient } from 'jwks-rsa'
import jsonwebtoken, { JwtPayload, Algorithm } from 'jsonwebtoken'
import { Plugin, createGraphQLError } from 'graphql-yoga'
import { GraphQLErrorOptions } from 'graphql'

Check failure on line 4 in packages/plugins/jwt/src/index.ts

View workflow job for this annotation

GitHub Actions / leaks / nodejs v16 / graphql v15.8.0

'"graphql"' has no exported member named 'GraphQLErrorOptions'. Did you mean 'GraphQLErrorExtensions'?

Check failure on line 4 in packages/plugins/jwt/src/index.ts

View workflow job for this annotation

GitHub Actions / integration / nodejs v16 / graphql v15.8.0

'"graphql"' has no exported member named 'GraphQLErrorOptions'. Did you mean 'GraphQLErrorExtensions'?

Check failure on line 4 in packages/plugins/jwt/src/index.ts

View workflow job for this annotation

GitHub Actions / unit / nodejs v18 / graphql v15.8.0

'"graphql"' has no exported member named 'GraphQLErrorOptions'. Did you mean 'GraphQLErrorExtensions'?

Check failure on line 4 in packages/plugins/jwt/src/index.ts

View workflow job for this annotation

GitHub Actions / unit / nodejs v16 / graphql v15.8.0

'"graphql"' has no exported member named 'GraphQLErrorOptions'. Did you mean 'GraphQLErrorExtensions'?

Check failure on line 4 in packages/plugins/jwt/src/index.ts

View workflow job for this annotation

GitHub Actions / leaks / nodejs v18 / graphql v15.8.0

'"graphql"' has no exported member named 'GraphQLErrorOptions'. Did you mean 'GraphQLErrorExtensions'?

Check failure on line 4 in packages/plugins/jwt/src/index.ts

View workflow job for this annotation

GitHub Actions / integration / nodejs v18 / graphql v15.8.0

'"graphql"' has no exported member named 'GraphQLErrorOptions'. Did you mean 'GraphQLErrorExtensions'?

Check failure on line 4 in packages/plugins/jwt/src/index.ts

View workflow job for this annotation

GitHub Actions / unit / nodejs v20 / graphql v15.8.0

'"graphql"' has no exported member named 'GraphQLErrorOptions'. Did you mean 'GraphQLErrorExtensions'?

const { decode } = jsonwebtoken

export interface JwtPluginOptions {
/**
* The endpoint to fetch keys from.
*
* For example: https://example.com/.well-known/jwks.json
*/
jwksUri?: string
/**
* Signing key to be used to verify the token
* You can also use the jwks option to fetch the key from a JWKS endpoint
*/
signingKey?: string
export type JwtPluginOptions =
| JwtPluginOptionsWithJWKS
| JwtPluginOptionsWithSigningKey

export interface JwtPluginOptionsBase {
/**
* List of the algorithms used to verify the token
*
Expand Down Expand Up @@ -46,7 +40,26 @@ export interface JwtPluginOptions {
}) => string | undefined
}

export function useJwt(options: JwtPluginOptions): Plugin {
export interface JwtPluginOptionsWithJWKS extends JwtPluginOptionsBase {
/**
* The endpoint to fetch keys from.
*
* For example: https://example.com/.well-known/jwks.json
*/
jwksUri: string
signingKey?: never
}

export interface JwtPluginOptionsWithSigningKey extends JwtPluginOptionsBase {
/**
* Signing key to be used to verify the token
* You can also use the jwks option to fetch the key from a JWKS endpoint
*/
signingKey: string
jwksUri?: never
}

export function useJWT(options: JwtPluginOptions): Plugin {
if (!options.signingKey && !options.jwksUri) {
throw new TypeError('You need to provide either a signingKey or a jwksUri')
}
Expand All @@ -57,21 +70,17 @@ export function useJwt(options: JwtPluginOptions): Plugin {
)
}

const {
jwksUri,
extendContextField = 'jwt',
getToken = defaultGetToken,
} = options
const { extendContextField = 'jwt', getToken = defaultGetToken } = options

const payloadByRequest = new WeakMap<Request, JwtPayload | string>()

let jwksClient: JwksClient
if (jwksUri) {
if (options.jwksUri) {
jwksClient = new JwksClient({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri,
jwksUri: options.jwksUri,
})
}

Expand Down Expand Up @@ -107,13 +116,17 @@ export function useJwt(options: JwtPluginOptions): Plugin {
}
}

function unauthorizedError(message: string) {
function unauthorizedError(
message: string,
options?: GraphQLErrorOptions | undefined,
) {
return createGraphQLError(message, {
extensions: {
http: {
status: 401,
},
},
...options,
})
}

Expand All @@ -126,7 +139,11 @@ function verify(
jsonwebtoken.verify(token, signingKey, options, (err, result) => {
if (err) {
// Should we expose the error message? Perhaps only in development mode?
reject(unauthorizedError('Failed to decode authentication token'))
reject(
unauthorizedError('Failed to decode authentication token', {
originalError: err,
}),
)
} else {
resolve(result as JwtPayload)
}
Expand All @@ -140,13 +157,17 @@ async function fetchKey(
): Promise<string> {
const decodedToken = decode(token, { complete: true })
if (decodedToken?.header?.kid == null) {
throw unauthorizedError(`Failed to decode authentication token`)
throw unauthorizedError(
`Failed to decode authentication token. Missing key id.`,
)
}

const secret = await jwksClient.getSigningKey(decodedToken.header.kid)
const signingKey = secret?.getPublicKey()
if (!signingKey) {
throw unauthorizedError(`Failed to decode authentication token`)
throw unauthorizedError(
`Failed to decode authentication token. Unknown key id.`,
)
}
return signingKey
}
Expand Down

0 comments on commit aa70be3

Please sign in to comment.