diff --git a/packages/apollo-server-core/src/__tests__/ApolloServerBase.test.ts b/packages/apollo-server-core/src/__tests__/ApolloServerBase.test.ts index 801cbf4dde9..daa1500d8c0 100644 --- a/packages/apollo-server-core/src/__tests__/ApolloServerBase.test.ts +++ b/packages/apollo-server-core/src/__tests__/ApolloServerBase.test.ts @@ -103,6 +103,43 @@ describe('ApolloServerBase construction', () => { `"Apollo Server requires either an existing schema, modules or typeDefs"`, ); }); + + it('throws when an API key is not a valid header value', () => { + expect(() => { + new ApolloServerBase({ + typeDefs, + resolvers, + apollo: { + key: 'bar▒baz▒', + }, + }); + }).toThrowErrorMatchingInlineSnapshot( + `"The API key provided to Apollo Server contains characters which are invalid as HTTP header values. The following characters found in the key are invalid: ▒, ▒. Valid header values may only contain ASCII visible characters. If you think there is an issue with your key, please contact Apollo support."`, + ); + }); + + it('trims whitespace from incoming API keys and logs a warning', () => { + const logger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + expect(() => { + new ApolloServerBase({ + typeDefs, + resolvers, + apollo: { + key: 'barbaz\n', + }, + logger, + }); + }).not.toThrow(); + expect(logger.warn).toHaveBeenCalledWith( + 'The provided API key has unexpected leading or trailing whitespace. ' + + 'Apollo Server will trim the key value before use.', + ); + }); }); describe('ApolloServerBase start', () => { diff --git a/packages/apollo-server-core/src/determineApolloConfig.ts b/packages/apollo-server-core/src/determineApolloConfig.ts index 4d8ec3ea2c3..9024f068749 100644 --- a/packages/apollo-server-core/src/determineApolloConfig.ts +++ b/packages/apollo-server-core/src/determineApolloConfig.ts @@ -34,21 +34,34 @@ export function determineApolloConfig( // Determine key. if (input?.key) { - apolloConfig.key = input.key; + apolloConfig.key = input.key.trim(); } else if (typeof engine === 'object' && engine.apiKey) { - apolloConfig.key = engine.apiKey; + apolloConfig.key = engine.apiKey.trim(); } else if (APOLLO_KEY) { if (ENGINE_API_KEY) { logger.warn( 'Using `APOLLO_KEY` since `ENGINE_API_KEY` (deprecated) is also set in the environment.', ); } - apolloConfig.key = APOLLO_KEY; + apolloConfig.key = APOLLO_KEY.trim(); } else if (ENGINE_API_KEY) { logger.warn( '[deprecated] The `ENGINE_API_KEY` environment variable has been renamed to `APOLLO_KEY`.', ); - apolloConfig.key = ENGINE_API_KEY; + apolloConfig.key = ENGINE_API_KEY.trim(); + } + + if ((input?.key ?? APOLLO_KEY ?? ENGINE_API_KEY) !== apolloConfig.key) { + logger.warn( + 'The provided API key has unexpected leading or trailing whitespace. ' + + 'Apollo Server will trim the key value before use.', + ); + } + + // Assert API key is a valid header value, since it's going to be used as one + // throughout. + if (apolloConfig.key) { + assertValidHeaderValue(apolloConfig.key); } // Determine key hash. @@ -155,3 +168,17 @@ export function determineApolloConfig( return apolloConfig as ApolloConfig; // can remove cast in AS3 } + +function assertValidHeaderValue(value: string) { + // Ref: node-fetch@2.x `Headers` validation + // https://github.com/node-fetch/node-fetch/blob/9b9d45881e5ca68757077726b3c0ecf8fdca1f29/src/headers.js#L18 + const invalidHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/g; + if (invalidHeaderCharRegex.test(value)) { + const invalidChars = value.match(invalidHeaderCharRegex)!; + throw new Error( + `The API key provided to Apollo Server contains characters which are invalid as HTTP header values. The following characters found in the key are invalid: ${invalidChars.join( + ', ', + )}. Valid header values may only contain ASCII visible characters. If you think there is an issue with your key, please contact Apollo support.`, + ); + } +}