diff --git a/.changeset/slow-ads-dress.md b/.changeset/slow-ads-dress.md new file mode 100644 index 0000000000..fa63ccd4b4 --- /dev/null +++ b/.changeset/slow-ads-dress.md @@ -0,0 +1,5 @@ +--- +'@graphql-yoga/plugin-jwt': patch +--- + +Fix typo of the option `singingKeyProviders` => `signingKeyProviders`. diff --git a/packages/plugins/jwt/src/__tests__/jwt.spec.ts b/packages/plugins/jwt/src/__tests__/jwt.spec.ts index 498c950812..f2f2ba86a6 100644 --- a/packages/plugins/jwt/src/__tests__/jwt.spec.ts +++ b/packages/plugins/jwt/src/__tests__/jwt.spec.ts @@ -45,7 +45,7 @@ I3OrgFkoqk03cpX4AL2GYC2ejytAqboL6pFTfmTgg2UtvKIeaTyF describe('jwt plugin', () => { test('incoming http request is reject when auth token is not present', async () => { const test = createTestServer({ - singingKeyProviders: [createInlineSigningKeyProvider('topsecret')], + signingKeyProviders: [createInlineSigningKeyProvider('topsecret')], }); const response = await test.queryWithoutAuth(); expect(response.status).toBe(401); @@ -62,7 +62,7 @@ describe('jwt plugin', () => { test('should allow to continue if reject.missingToken is set to false', async () => { const test = createTestServer({ - singingKeyProviders: [createInlineSigningKeyProvider('topsecret')], + signingKeyProviders: [createInlineSigningKeyProvider('topsecret')], reject: { missingToken: false, invalidToken: true, @@ -75,7 +75,7 @@ describe('jwt plugin', () => { test('any prefix is supported when strict prefix validation is not configured', async () => { const secret = 'topsecret'; const test = createTestServer({ - singingKeyProviders: [createInlineSigningKeyProvider(secret)], + signingKeyProviders: [createInlineSigningKeyProvider(secret)], tokenLookupLocations: [ extractFromHeader({ name: 'Authorization', @@ -89,7 +89,7 @@ describe('jwt plugin', () => { test('incoming http has a token but prefix does not match or missing', async () => { const test = createTestServer({ - singingKeyProviders: [createInlineSigningKeyProvider('topsecret')], + signingKeyProviders: [createInlineSigningKeyProvider('topsecret')], }); // does not match prefix let response = await test.queryWithAuth('Basic 123'); @@ -120,7 +120,7 @@ describe('jwt plugin', () => { test('token provided but jwt token is not valid for decoding', async () => { const test = createTestServer({ - singingKeyProviders: [createInlineSigningKeyProvider('topsecret')], + signingKeyProviders: [createInlineSigningKeyProvider('topsecret')], }); const response = await test.queryWithAuth('Bearer BadJwt'); expect(response.status).toBe(400); @@ -137,7 +137,7 @@ describe('jwt plugin', () => { test('invalid token can be accepted when reject.invalidToken=false is set', async () => { const test = createTestServer({ - singingKeyProviders: [createInlineSigningKeyProvider('topsecret')], + signingKeyProviders: [createInlineSigningKeyProvider('topsecret')], reject: { invalidToken: false, }, @@ -149,7 +149,7 @@ describe('jwt plugin', () => { it('should not allow non matching issuer', async () => { const secret = 'topsecret'; const server = createTestServer({ - singingKeyProviders: [createInlineSigningKeyProvider(secret)], + signingKeyProviders: [createInlineSigningKeyProvider(secret)], tokenVerification: { issuer: ['http://yoga'], }, @@ -168,7 +168,7 @@ describe('jwt plugin', () => { it('should allow matching issuer', async () => { const secret = 'topsecret'; const server = createTestServer({ - singingKeyProviders: [createInlineSigningKeyProvider(secret)], + signingKeyProviders: [createInlineSigningKeyProvider(secret)], tokenVerification: { issuer: ['http://yoga'], }, @@ -182,7 +182,7 @@ describe('jwt plugin', () => { it('should not allow non matching audience', async () => { const secret = 'topsecret'; const server = createTestServer({ - singingKeyProviders: [createInlineSigningKeyProvider(secret)], + signingKeyProviders: [createInlineSigningKeyProvider(secret)], tokenVerification: { audience: 'my.app', }, @@ -203,7 +203,7 @@ describe('jwt plugin', () => { it('should allow matching audience', async () => { const secret = 'topsecret'; const server = createTestServer({ - singingKeyProviders: [createInlineSigningKeyProvider(secret)], + signingKeyProviders: [createInlineSigningKeyProvider(secret)], tokenVerification: { audience: 'my.app', }, @@ -234,7 +234,7 @@ describe('jwt plugin', () => { try { const server = createTestServer({ - singingKeyProviders: [ + signingKeyProviders: [ createRemoteJwksSigningKeyProvider({ jwksUri: `http://localhost:${(jwksServer.address() as any).port}`, }), @@ -259,7 +259,7 @@ describe('jwt plugin', () => { it('should not accept token without algorithm', async () => { const secret = 'topsecret'; const server = createTestServer({ - singingKeyProviders: [createInlineSigningKeyProvider(secret)], + signingKeyProviders: [createInlineSigningKeyProvider(secret)], }); const response = await server.queryWithAuth(buildJWTWithoutAlg()); @@ -276,7 +276,7 @@ describe('jwt plugin', () => { test('valid token is injected to the GraphQL context', async () => { const secret = 'topsecret'; const test = createTestServer({ - singingKeyProviders: [createInlineSigningKeyProvider(secret)], + signingKeyProviders: [createInlineSigningKeyProvider(secret)], }); const token = buildJWT({ sub: '123', scopes: ['users.read'] }, { key: secret }); const response = await test.queryWithAuth(token); @@ -302,7 +302,7 @@ describe('jwt plugin', () => { test('valid token is injected to the GraphQL context (custom field)', async () => { const secret = 'topsecret'; const test = createTestServer({ - singingKeyProviders: [createInlineSigningKeyProvider(secret)], + signingKeyProviders: [createInlineSigningKeyProvider(secret)], extendContext: 'my_jwt', }); const token = buildJWT({ sub: '123', scopes: ['users.read'] }, { key: secret }); @@ -327,6 +327,31 @@ describe('jwt plugin', () => { }); test('auth is passing when token is valid (HS256)', async () => { + const secret = 'topsecret'; + const test = createTestServer({ + signingKeyProviders: [createInlineSigningKeyProvider(secret)], + }); + const token = buildJWT({ sub: '123' }, { key: secret }); + const response = await test.queryWithAuth(token); + expect(response.status).toBe(200); + expect(await response.json()).toMatchObject({ + data: { + ctx: { + jwt: { + payload: { + sub: '123', + }, + token: { + prefix: 'Bearer', + value: expect.any(String), + }, + }, + }, + }, + }); + }); + + test('should allow to use deprecated `singingKeyProviders` option', async () => { const secret = 'topsecret'; const test = createTestServer({ singingKeyProviders: [createInlineSigningKeyProvider(secret)], @@ -351,9 +376,24 @@ describe('jwt plugin', () => { }); }); + test('should not allow to use both deprecated `singingKeyProviders` option and its replacement', async () => { + const secret = 'topsecret'; + try { + // @ts-expect-error This should not be allowed by TS + createTestServer({ + singingKeyProviders: [createInlineSigningKeyProvider(secret)], + signingKeyProviders: [createInlineSigningKeyProvider(secret)], + }); + } catch (err) { + expect((err as Error).message).toBe( + 'You are using both deprecated `singingKeyProviders` and its new replacement `signingKeyProviders` configuration. Please use only `signingKeyProviders`', + ); + } + }); + test('auth is passing when token is valid (RS256)', async () => { const test = createTestServer({ - singingKeyProviders: [createInlineSigningKeyProvider(JWKS_RSA512_PRIVATE_PEM)], + signingKeyProviders: [createInlineSigningKeyProvider(JWKS_RSA512_PRIVATE_PEM)], }); const token = buildJWT({ sub: '123' }, { key: JWKS_RSA512_PRIVATE_PEM, algorithm: 'RS256' }); const response = await test.queryWithAuth(token); @@ -397,7 +437,7 @@ describe('jwt plugin', () => { try { const test = createTestServer({ - singingKeyProviders: [ + signingKeyProviders: [ createRemoteJwksSigningKeyProvider({ jwksUri: `http://localhost:${(jwksServer.address() as any).port}`, }), @@ -438,7 +478,7 @@ describe('jwt plugin', () => { try { const test = createTestServer({ - singingKeyProviders: [ + signingKeyProviders: [ // Remote, invalid createRemoteJwksSigningKeyProvider({ jwksUri: `http://localhost:${(jwksServer.address() as any).port}`, @@ -476,7 +516,7 @@ describe('jwt plugin', () => { test('should throw when lookup is configured for cookie but no cookie store available', async () => { const secret = 'topsecret'; const test = createTestServer({ - singingKeyProviders: [createInlineSigningKeyProvider(secret)], + signingKeyProviders: [createInlineSigningKeyProvider(secret)], tokenLookupLocations: [extractFromCookie({ name: 'auth' })], }); const token = buildJWT({ sub: '123' }, { key: secret }); @@ -495,7 +535,7 @@ describe('jwt plugin', () => { const secret = 'topsecret'; const test = createTestServer( { - singingKeyProviders: [createInlineSigningKeyProvider(secret)], + signingKeyProviders: [createInlineSigningKeyProvider(secret)], tokenLookupLocations: [extractFromCookie({ name: 'auth' })], }, [useCookies()], @@ -508,7 +548,7 @@ describe('jwt plugin', () => { test('custom getToken functiFailed to verify authentication token. Verifon', async () => { const secret = 'topsecret'; const test = createTestServer({ - singingKeyProviders: [createInlineSigningKeyProvider(secret)], + signingKeyProviders: [createInlineSigningKeyProvider(secret)], tokenLookupLocations: [ async payload => { expect(payload.request).toBeDefined(); @@ -539,7 +579,7 @@ describe('jwt plugin', () => { const secret = 'topsecret'; const test = createTestServer( { - singingKeyProviders: [createInlineSigningKeyProvider(secret)], + signingKeyProviders: [createInlineSigningKeyProvider(secret)], tokenLookupLocations: [ extractFromHeader({ name: 'Authorization', diff --git a/packages/plugins/jwt/src/config.ts b/packages/plugins/jwt/src/config.ts index 8eeac0e8d9..654f2aea76 100644 --- a/packages/plugins/jwt/src/config.ts +++ b/packages/plugins/jwt/src/config.ts @@ -12,18 +12,30 @@ export type ExtractTokenFunction = (params: { export type GetSigningKeyFunction = (kid?: string) => Promise | string; -export type JwtPluginOptions = { - /** - * List of configurations for the signin-key providers. You can configure multiple signin-key providers to allow for key rotation, fallbacks, etc. - * - * In addition, you can use the `remote` variant and configure [`jwks-rsa`'s JWKS client](https://github.com/auth0/node-jwks-rsa/tree/master). - * - * The plugin will try to fetch the keys from the providers in the order they are defined in this array. - * - * If the first provider fails to fetch the keys, the plugin will try the next provider in the list. - * - */ - singingKeyProviders: AtleastOneItem; +export type JwtPluginOptions = ( + | { + /** + * List of configurations for the signin-key providers. You can configure multiple signin-key providers to allow for key rotation, fallbacks, etc. + * + * In addition, you can use the `remote` variant and configure [`jwks-rsa`'s JWKS client](https://github.com/auth0/node-jwks-rsa/tree/master). + * + * The plugin will try to fetch the keys from the providers in the order they are defined in this array. + * + * If the first provider fails to fetch the keys, the plugin will try the next provider in the list. + * + */ + signingKeyProviders: AtleastOneItem; + singingKeyProviders?: never; + } + | { + /** + * @deprecated: please use `signingKeyProviders` instead. + */ + + singingKeyProviders: AtleastOneItem; + signingKeyProviders?: never; + } +) & { /** * List of locations to look for the token in the incoming request. * @@ -77,9 +89,19 @@ export type JwtPluginOptions = { }; export function normalizeConfig(input: JwtPluginOptions) { - if (input.singingKeyProviders.length === 0) { + // TODO: remove this on next major version. + if (input.singingKeyProviders) { + if (input.signingKeyProviders) { + throw new TypeError( + 'You are using both deprecated `singingKeyProviders` and its new replacement `signingKeyProviders` configuration. Please use only `signingKeyProviders`', + ); + } + (input.signingKeyProviders as unknown) = input.singingKeyProviders; + } + + if (!input.signingKeyProviders) { throw new TypeError( - 'You must provide at least one signing key provider. Please verify your `singingKeyProviders` configuration.', + 'You must provide at least one signing key provider. Please verify your `signingKeyProviders` configuration.', ); } @@ -102,7 +124,7 @@ export function normalizeConfig(input: JwtPluginOptions) { } return { - singingKeyProviders: input.singingKeyProviders, + signingKeyProviders: input.signingKeyProviders, tokenLookupLocations, tokenVerification: input.tokenVerification ?? { algorithms: ['RS256', 'HS256'], diff --git a/packages/plugins/jwt/src/plugin.ts b/packages/plugins/jwt/src/plugin.ts index 6517fec688..019892c8d1 100644 --- a/packages/plugins/jwt/src/plugin.ts +++ b/packages/plugins/jwt/src/plugin.ts @@ -42,7 +42,7 @@ export function useJWT(options: JwtPluginOptions): Plugin<{ }; const getSigningKey = async (kid?: string) => { - for (const provider of normalizedOptions.singingKeyProviders) { + for (const provider of normalizedOptions.signingKeyProviders) { try { const key = await provider(kid); diff --git a/website/src/pages/docs/features/jwt.mdx b/website/src/pages/docs/features/jwt.mdx index 691ddf60f7..4aa230bb9f 100644 --- a/website/src/pages/docs/features/jwt.mdx +++ b/website/src/pages/docs/features/jwt.mdx @@ -60,7 +60,7 @@ const yoga = createYoga({ plugins: [ useJWT({ // Configure your signing providers: either a local signing-key or a remote JWKS are supported. - singingKeyProviders: [ + signingKeyProviders: [ createInlineSigningKeyProvider(signingKey), createRemoteJwksSigningKeyProvider({ jwksUri: 'https://example.com/.well-known/jwks.json' }) ] @@ -230,7 +230,7 @@ const yoga = createYoga({ // ... plugins: [ useJWT({ - singingKeyProviders: [createInlineSigningKeyProvider(process.env.MY_JWT_SECRET)] + signingKeyProviders: [createInlineSigningKeyProvider(process.env.MY_JWT_SECRET)] }) ] }) @@ -253,7 +253,7 @@ const yoga = createYoga({ // ... plugins: [ useJWT({ - singingKeyProviders: [ + signingKeyProviders: [ createRemoteJwksSigningKeyProvider({ jwksUri: 'https://example.com/.well-known/jwks.json' }) @@ -278,7 +278,7 @@ const yoga = createYoga({ // ... plugins: [ useJWT({ - singingKeyProviders: [ + signingKeyProviders: [ // In case your remote provider is not available, the plugin will try use the inline provider. createRemoteJwksSigningKeyProvider({ jwksUri: 'https://example.com/.well-known/jwks.json'