From e9dcd04e1d289a7e84045191a44ef54495abf840 Mon Sep 17 00:00:00 2001 From: Valentin Cocaud Date: Mon, 24 Jul 2023 11:26:38 +0200 Subject: [PATCH] some request changes from @n1ru4l and @ardatan --- packages/plugins/jwt/package.json | 2 +- .../plugins/jwt/src/__tests__/jwt.spec.ts | 15 ++-- packages/plugins/jwt/src/index.ts | 69 ++++++++++++------- 3 files changed, 55 insertions(+), 31 deletions(-) diff --git a/packages/plugins/jwt/package.json b/packages/plugins/jwt/package.json index 5316804869..a99c5d733d 100644 --- a/packages/plugins/jwt/package.json +++ b/packages/plugins/jwt/package.json @@ -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", diff --git a/packages/plugins/jwt/src/__tests__/jwt.spec.ts b/packages/plugins/jwt/src/__tests__/jwt.spec.ts index 4ab824fcc4..ddfbec0c22 100644 --- a/packages/plugins/jwt/src/__tests__/jwt.spec.ts +++ b/packages/plugins/jwt/src/__tests__/jwt.spec.ts @@ -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') }) @@ -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 } }, }) }) @@ -138,11 +140,12 @@ describe('jwt', () => { }) }) -const createTestServer = (options?: Partial[0]>) => { +const createTestServer = (options?: Partial) => { const yoga = createYoga({ schema, plugins: [ - useJwt({ + // @ts-expect-error testing invalid options fo JS users + useJWT({ issuer: 'http://yoga', signingKey: 'very secret key', ...options, diff --git a/packages/plugins/jwt/src/index.ts b/packages/plugins/jwt/src/index.ts index 960acab9fd..8a7e125a3a 100644 --- a/packages/plugins/jwt/src/index.ts +++ b/packages/plugins/jwt/src/index.ts @@ -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' 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 * @@ -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') } @@ -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() let jwksClient: JwksClient - if (jwksUri) { + if (options.jwksUri) { jwksClient = new JwksClient({ cache: true, rateLimit: true, jwksRequestsPerMinute: 5, - jwksUri, + jwksUri: options.jwksUri, }) } @@ -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, }) } @@ -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) } @@ -140,13 +157,17 @@ async function fetchKey( ): Promise { 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 }