From 53c3baddff14caa35800ab236a5c0be7ce7a0788 Mon Sep 17 00:00:00 2001 From: Simon Buchan Date: Tue, 3 Mar 2020 06:50:45 +1300 Subject: [PATCH] =?UTF-8?q?[aws-lambda]=20New=20API=20Gateway=20Authorizer?= =?UTF-8?q?=20types,=20deprecating=20old=E2=80=A6=20(#42420)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [aws-lambda] Deprecate CustomAuthorizer*, new APIGateway*Authorizer* Noticed this testing #42419. When integrating a custom authorizer, you actually have two options, creating a token or a request authorizer, which changes what payload you will get. You nearly certainly know which you will be called with! Just deprecating the old version as it's kinda broken in a way thats hard to fix without breaking someone, but we want to guide devs to the new version. It is possible to fix the existing type by adding a bunch of `foo?: never` fields to each alternative so existing accesses don't, error but this makes things more complex, and confusing for the common case. Other ideas welcome! * [aws-lambda] Add api-gateway authorizer parameters. Fixes #34069, #42418 Ended up a bit messy, might be a bit much. * [aws-lambda] Bump minimum typescript to 3.0 Required to fix failing $ExpectError in tests. Surely nobody is still using pre-3.0? * [aws-lambda] Enforcea API Gateway authorizer context narrowing And implement the changes that API gateway does on the proxy request context for it. Also rename TAuthorizer to TAuthorizerContext to be more clear that they should be the same type across both authorizer and proxy. Some cleanups and fixes for names. --- types/apex.js/index.d.ts | 2 +- types/aws-lambda/common/api-gateway.d.ts | 38 ++- types/aws-lambda/index.d.ts | 2 +- types/aws-lambda/test/api-gateway-tests.ts | 292 +++++++++++++++++- .../trigger/api-gateway-authorizer.d.ts | 93 +++++- .../aws-lambda/trigger/api-gateway-proxy.d.ts | 51 ++- types/cfn-response/index.d.ts | 2 +- 7 files changed, 437 insertions(+), 43 deletions(-) diff --git a/types/apex.js/index.d.ts b/types/apex.js/index.d.ts index edc136a678..3af0de9d72 100644 --- a/types/apex.js/index.d.ts +++ b/types/apex.js/index.d.ts @@ -2,7 +2,7 @@ // Project: https://github.com/apex/node-apex // Definitions by: Yoriki Yamaguchi // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 2.8 +// TypeScript Version: 3.0 /// diff --git a/types/aws-lambda/common/api-gateway.d.ts b/types/aws-lambda/common/api-gateway.d.ts index 49e4b95b8d..15f6e921e6 100644 --- a/types/aws-lambda/common/api-gateway.d.ts +++ b/types/aws-lambda/common/api-gateway.d.ts @@ -1,8 +1,33 @@ -// Used by both APIGatewayProxyEvent and APIGatewayAuthorizerEvent -export interface APIGatewayEventRequestContext { +// Types shared between trigger/api-gateway-authorizer.d.ts and api-gateway-proxy.d.ts + +// Poorly documented, but API Gateway will just fail internally if +// the context type does not match this. +// Note that although non-string types will be accepted, they will be +// coerced to strings on the other side. +export interface APIGatewayAuthorizerResultContext { + [name: string]: string | number | boolean | null | undefined; +} + +// Default authorizer type, prefer using a specific type with the "...WithAuthorizer..." variant types. +// Note that this doesn't have to be a context from a custom lambda outhorizer, AWS also has a cognito +// authorizer type and could add more, so the property won't always be a string. +export type APIGatewayEventDefaultAuthorizerContext = undefined | null | { + [name: string]: any; +}; + +export type APIGatewayEventRequestContext = + APIGatewayEventRequestContextWithAuthorizer; + +// The requestContext property of both request authorizer and proxy integration events. +export interface APIGatewayEventRequestContextWithAuthorizer { accountId: string; apiId: string; - authorizer?: AuthResponseContext | null; + // This one is a bit confusing: it is not actually present in authorizer calls + // and proxy calls without an authorizer. We model this by allowing undefined in the type, + // since it ends up the same and avoids breaking users that are testing the property. + // This lets us allow parameterizing the authorizer for proxy events that know what authorizer + // context values they have. + authorizer: TAuthorizerContext; connectedAt?: number; connectionId?: string; domainName?: string; @@ -40,10 +65,3 @@ export interface APIGatewayEventIdentity { userAgent: string | null; userArn: string | null; } - -/** - * http://docs.aws.amazon.com/apigateway/latest/developerguide/use-custom-authorizer.html#api-gateway-custom-authorizer-output - */ -export interface AuthResponseContext { - [name: string]: any; -} diff --git a/types/aws-lambda/index.d.ts b/types/aws-lambda/index.d.ts index 53d1b819f3..d5a38f51f4 100644 --- a/types/aws-lambda/index.d.ts +++ b/types/aws-lambda/index.d.ts @@ -34,7 +34,7 @@ // Marian Zange // Alexander Pepper // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 2.8 +// TypeScript Version: 3.0 export * from "./handler"; export * from "./common/api-gateway"; diff --git a/types/aws-lambda/test/api-gateway-tests.ts b/types/aws-lambda/test/api-gateway-tests.ts index a91d338883..540e9aee88 100644 --- a/types/aws-lambda/test/api-gateway-tests.ts +++ b/types/aws-lambda/test/api-gateway-tests.ts @@ -1,18 +1,70 @@ import { + APIGatewayAuthorizerHandler, + APIGatewayAuthorizerResult, + APIGatewayAuthorizerResultContext, + APIGatewayAuthorizerWithContextHandler, + APIGatewayAuthorizerWithContextResult, APIGatewayEvent, + APIGatewayEventDefaultAuthorizerContext, + APIGatewayEventLambdaAuthorizerContext, APIGatewayEventRequestContext, + APIGatewayEventRequestContextWithAuthorizer, + APIGatewayProxyEvent, APIGatewayProxyHandler, APIGatewayProxyResult, + APIGatewayProxyWithLambdaAuthorizerEventRequestContext, + APIGatewayProxyWithLambdaAuthorizerHandler, + APIGatewayRequestAuthorizerHandler, + APIGatewayRequestAuthorizerWithContextHandler, + APIGatewayTokenAuthorizerHandler, + APIGatewayTokenAuthorizerWithContextHandler, AuthResponseContext, Context, CustomAuthorizerHandler, CustomAuthorizerResult, PolicyDocument, - ProxyHandler, ProxyCallback, + ProxyHandler, Statement, } from "aws-lambda"; +interface CustomAuthorizerContext extends APIGatewayAuthorizerResultContext { + valid: string | number | boolean | null | undefined; + str: string; + num: number; + bool: boolean; + numOrNull: number | null; + numOrUndefined: number | undefined; + und: undefined; +} + +// Can't serialize objects in the response from an authorizer +interface CustomAuthorizerInvalidResponseContext extends APIGatewayAuthorizerResultContext { + valid: string | number | boolean | null | undefined; + // $ExpectError + invalid: { + id: number; + }; +} + +// Enforce custom response contexts extend APIGatewayAuthorizerResultContext for use in authorizer, +// $ExpectError +type InvalidCustomAuthorizerHandler = APIGatewayAuthorizerWithContextHandler<{ + valid: string | number | boolean | null | undefined; + invalid: { + id: number; + }; +}>; + +// but don't care about in proxy, since it's overkill to force extending an interface if +// its not defined in the same codebase +type ProbablyInvalidCustomProxyHandler = APIGatewayProxyWithLambdaAuthorizerHandler<{ + valid: string | number | boolean | null | undefined; + invalid: { + id: number; + }; +}>; + let proxyHandler: APIGatewayProxyHandler = async (event, context, callback) => { strOrNull = event.body; str = event.headers['example']; @@ -26,12 +78,23 @@ let proxyHandler: APIGatewayProxyHandler = async (event, context, callback) => { str = event.stageVariables!['example']; let requestContext: APIGatewayEventRequestContext; requestContext = event.requestContext; + let requestContextWithCustomAuthorizer: APIGatewayEventRequestContextWithAuthorizer; + // $ExpectError + requestContextWithCustomAuthorizer = event.requestContext; str = event.resource; str = requestContext.protocol; str = requestContext.accountId; str = requestContext.apiId; - const authContext: AuthResponseContext | null | undefined = requestContext.authorizer; + const authContext: APIGatewayEventDefaultAuthorizerContext = requestContext.authorizer; + if (authContext) { + // Anything goes by default + str = authContext.claims[str]; + str = authContext.principalId; + num = authContext.integrationLatency; + // Even probable mistakes: lambda contexts properties are converted to string + num = authContext.num; + } numOrUndefined = requestContext.connectedAt; strOrUndefined = requestContext.connectionId; strOrUndefined = requestContext.domainName; @@ -62,6 +125,55 @@ let proxyHandler: APIGatewayProxyHandler = async (event, context, callback) => { str = requestContext.resourcePath; strOrUndefined = requestContext.routeKey; + const result = createProxyResult(); + + callback(new Error()); + callback(null, result); + return result; +}; + +const proxyHandlerWithCustomAuthorizer: APIGatewayProxyWithLambdaAuthorizerHandler = async (event, context, callback) => { + // standard fields... + strOrNull = event.body; + str = event.headers['example']; + str = event.multiValueHeaders['example'][0]; + + // It seems like it would be easy to make this mistake, but it's still a useful type. + let requestContextWithAuthorizerDirectly: APIGatewayEventRequestContextWithAuthorizer; + // $ExpectError + requestContextWithAuthorizerDirectly = event.requestContext; + + // Check assignable to named types + let requestContext: APIGatewayProxyWithLambdaAuthorizerEventRequestContext; + requestContext = event.requestContext; + + let authorizer: APIGatewayEventLambdaAuthorizerContext; + authorizer = requestContext.authorizer; + + // And it can be converted down to the basic type + const basicEvent: APIGatewayProxyEvent = event; + const basicRequestContext: APIGatewayEventRequestContext = event.requestContext; + + // All non-null or undefined types are converted to string. + str = authorizer.valid; + str = authorizer.str; + str = authorizer.num; + str = authorizer.bool; + strOrNull = authorizer.numOrNull; + strOrUndefined = authorizer.numOrUndefined; + // And these extra properties are added + str = authorizer.principalId; + num = authorizer.integrationLatency; + + const result = createProxyResult(); + + callback(new Error()); + callback(null, result); + + return result; +}; + +function createProxyResult(): APIGatewayProxyResult { let result: APIGatewayProxyResult = { statusCode: num, body: str, @@ -79,13 +191,131 @@ let proxyHandler: APIGatewayProxyHandler = async (event, context, callback) => { isBase64Encoded: true, body: str, }; + return result; +} + +const authorizer: APIGatewayAuthorizerHandler = async (event, context, callback) => { + if (event.type === "TOKEN") { + str = event.methodArn; + str = event.authorizationToken; + str = event.resource; // $ExpectError + } else { + event.type; // $ExpectType "REQUEST" + str = event.methodArn; // $ExpectError + str = event.resource; + } + + let result: APIGatewayAuthorizerResult = createAuthorizerResult(); + // Can convert down to existing type + result = createAuthorizerResultWithCustomContext(); callback(new Error()); callback(null, result); return result; }; -const authorizerHandler: CustomAuthorizerHandler = async (event, context, callback) => { +const authorizerWithCustomContext: APIGatewayAuthorizerWithContextHandler = async (event, context, callback) => { + if (event.type === "TOKEN") { + str = event.methodArn; + str = event.authorizationToken; + str = event.resource; // $ExpectError + } else { + event.type; // $ExpectType "REQUEST" + str = event.methodArn; // $ExpectError + str = event.resource; + } + + let result: APIGatewayAuthorizerWithContextResult; + result = createAuthorizerResultWithCustomContext(); + + // Can't convert up from existing type + // $ExpectError + result = createAuthorizerResult(); + + callback(new Error()); + callback(null, result); + return result; +}; + +const tokenAuthorizer: APIGatewayTokenAuthorizerHandler = async (event, context, callback) => { + event.type; // $ExpectType "TOKEN" + + str = event.type; + str = event.methodArn; + str = event.authorizationToken; + strOrUndefined = event.resource; // $ExpectError + // etc... + + const result = createAuthorizerResult(); + + callback(new Error()); + callback(null, result); + return result; +}; + +const tokenAuthorizerWithCustomContext: APIGatewayTokenAuthorizerWithContextHandler = async (event, context, callback) => { + event.type; // $ExpectType "TOKEN" + + str = event.type; + str = event.methodArn; + str = event.authorizationToken; + strOrUndefined = event.resource; // $ExpectError + // etc... + + const result = createAuthorizerResultWithCustomContext(); + + callback(new Error()); + callback(null, result); + return result; +}; + +const requestAuthorizer: APIGatewayRequestAuthorizerHandler = async (event, context, callback) => { + event.type; // $ExpectType "REQUEST" + + str = event.type; + str = event.methodArn; // $ExpectError + str = event.authorizationToken; // $ExpectError + str = event.resource; + str = event.path; + str = event.httpMethod; + if (event.headers !== null) + str = event.headers[str]; + if (event.multiValueHeaders !== null) + str = event.multiValueHeaders[str][num]; + if (event.pathParameters !== null) + str = event.pathParameters[str]; + if (event.queryStringParameters !== null) + str = event.queryStringParameters[str]; + if (event.multiValueQueryStringParameters !== null) + str = event.multiValueQueryStringParameters[str][num]; + if (event.stageVariables !== null) + str = event.stageVariables[str]; + const requestContext: APIGatewayEventRequestContext = event.requestContext; + str = event.domainName; + str = event.apiId; + + const result = createAuthorizerResult(); + + callback(new Error()); + callback(null, result); + return result; +}; + +const requestAuthorizerWithCustomContext: APIGatewayRequestAuthorizerWithContextHandler = async (event, context, callback) => { + event.type; // $ExpectType "REQUEST" + + str = event.type; + str = event.methodArn; // $ExpectError + str = event.authorizationToken; // $ExpectError + + const result = createAuthorizerResultWithCustomContext(); + + callback(new Error()); + callback(null, result); + return result; +}; + +const legacyAuthorizerHandler: CustomAuthorizerHandler = async (event, context, callback) => { str = event.type; str = event.methodArn; strOrUndefined = event.authorizationToken; @@ -103,6 +333,14 @@ const authorizerHandler: CustomAuthorizerHandler = async (event, context, callba strOrUndefined = event.domainName; strOrUndefined = event.apiId; + const result = createAuthorizerResult(); + + callback(new Error()); + callback(null, result); + return result; +}; + +function createPolicyDocument(): PolicyDocument { let statement: Statement = { Action: str, Effect: str, @@ -159,16 +397,60 @@ const authorizerHandler: CustomAuthorizerHandler = async (event, context, callba policyDocument = { Version: str, Statement: [statement, statement] }; + return policyDocument; +} + +function createAuthorizerResultWithCustomContext(): APIGatewayAuthorizerWithContextResult { + let result: APIGatewayAuthorizerWithContextResult; + + // Requires context + // $ExpectError + result = { + principalId: str, + policyDocument: createPolicyDocument(), + usageIdentifierKey: strOrUndefinedOrNull, + }; + + // Invalid context + result = { + principalId: str, + policyDocument: createPolicyDocument(), + context: {}, // $ExpectError + usageIdentifierKey: strOrUndefinedOrNull, + }; + + result = { + principalId: str, + policyDocument: createPolicyDocument(), + context: { + valid: [str, num, bool, null, undefined][num], + str, + num, + bool, + numOrNull: [num, null][num], + numOrUndefined: [num, undefined][num], + und: undefined, + }, + }; + + return result; +} + +function createAuthorizerResult(): APIGatewayAuthorizerResult { + const policyDocument = createPolicyDocument(); + const authResponseContext: AuthResponseContext = { stringKey: str, numberKey: num, booleanKey: bool, + [str]: [str, num, bool][num], // string | number | bool }; let result: CustomAuthorizerResult = { principalId: str, policyDocument, context: authResponseContext, + usageIdentifierKey: strOrUndefinedOrNull, }; result = { @@ -176,10 +458,8 @@ const authorizerHandler: CustomAuthorizerHandler = async (event, context, callba policyDocument, }; - callback(new Error()); - callback(null, result); return result; -}; +} // Test old names const oldNameProxyHandler: ProxyHandler = (event: APIGatewayEvent, context: Context, cb: ProxyCallback) => {}; diff --git a/types/aws-lambda/trigger/api-gateway-authorizer.d.ts b/types/aws-lambda/trigger/api-gateway-authorizer.d.ts index 2f09a93ce3..f5a004075e 100644 --- a/types/aws-lambda/trigger/api-gateway-authorizer.d.ts +++ b/types/aws-lambda/trigger/api-gateway-authorizer.d.ts @@ -1,10 +1,79 @@ -import { APIGatewayEventRequestContext, AuthResponseContext } from "../common/api-gateway"; +import { + APIGatewayAuthorizerResultContext, + APIGatewayEventDefaultAuthorizerContext, + APIGatewayEventRequestContextWithAuthorizer, +} from "../common/api-gateway"; import { Callback, Handler } from "../handler"; -export type CustomAuthorizerHandler = Handler; -export type CustomAuthorizerCallback = Callback; +export type APIGatewayAuthorizerHandler = Handler; +export type APIGatewayAuthorizerWithContextHandler = + Handler>; -// API Gateway CustomAuthorizer "event" +export type APIGatewayAuthorizerCallback = Callback; +export type APIGatewayAuthorizerWithContextCallback = + Callback>; + +export type APIGatewayTokenAuthorizerHandler = + Handler; +export type APIGatewayTokenAuthorizerWithContextHandler = + Handler>; + +export type APIGatewayRequestAuthorizerHandler = + Handler; +export type APIGatewayRequestAuthorizerWithContextHandler = + Handler>; + +export type APIGatewayAuthorizerEvent = APIGatewayTokenAuthorizerEvent | APIGatewayRequestAuthorizerEvent; + +export interface APIGatewayTokenAuthorizerEvent { + type: "TOKEN"; + methodArn: string; + authorizationToken: string; +} + +// Note, when invoked by the tester in the AWS web console, the map values can be null, +// but they will be empty objects in the real object. +// Worse, it will include "body" and "isBase64Encoded" properties, unlike the real call! +export interface APIGatewayRequestAuthorizerEvent { + type: "REQUEST"; + resource: string; + path: string; + httpMethod: string; + headers: { [name: string]: string } | null; + multiValueHeaders: { [name: string]: string[] } | null; + pathParameters: { [name: string]: string } | null; + queryStringParameters: { [name: string]: string } | null; + multiValueQueryStringParameters: { [name: string]: string[] } | null; + stageVariables: { [name: string]: string } | null; + requestContext: APIGatewayEventRequestContextWithAuthorizer; + domainName: string; + apiId: string; +} + +export interface APIGatewayAuthorizerResult { + principalId: string; + policyDocument: PolicyDocument; + context?: APIGatewayAuthorizerResultContext | null; + usageIdentifierKey?: string | null; +} + +// Separate type so the context property is required, without pulling complex type magic. +export interface APIGatewayAuthorizerWithContextResult { + principalId: string; + policyDocument: PolicyDocument; + context: TAuthorizerContext; + usageIdentifierKey?: string | null; +} + +// Legacy event / names + +/** @deprecated Use APIGatewayAuthorizerHandler or a subtype */ +export type CustomAuthorizerHandler = Handler; + +// This one is actually fine. +export type CustomAuthorizerCallback = APIGatewayAuthorizerCallback; + +/** @deprecated Use APIGatewayAuthorizerEvent or a subtype */ export interface CustomAuthorizerEvent { type: string; methodArn: string; @@ -18,22 +87,14 @@ export interface CustomAuthorizerEvent { queryStringParameters?: { [name: string]: string } | null; multiValueQueryStringParameters?: { [name: string]: string[] } | null; stageVariables?: { [name: string]: string }; - requestContext?: APIGatewayEventRequestContext; + requestContext?: APIGatewayEventRequestContextWithAuthorizer; domainName?: string; apiId?: string; } -/** - * API Gateway CustomAuthorizer AuthResponse. - * http://docs.aws.amazon.com/apigateway/latest/developerguide/use-custom-authorizer.html#api-gateway-custom-authorizer-output - */ -export interface CustomAuthorizerResult { - principalId: string; - policyDocument: PolicyDocument; - context?: AuthResponseContext; - usageIdentifierKey?: string; -} -export type AuthResponse = CustomAuthorizerResult; +export type CustomAuthorizerResult = APIGatewayAuthorizerResult; +export type AuthResponse = APIGatewayAuthorizerResult; +export type AuthResponseContext = APIGatewayAuthorizerResultContext; /** * API Gateway CustomAuthorizer AuthResponse.PolicyDocument. diff --git a/types/aws-lambda/trigger/api-gateway-proxy.d.ts b/types/aws-lambda/trigger/api-gateway-proxy.d.ts index 40f1fe6551..b072956c63 100644 --- a/types/aws-lambda/trigger/api-gateway-proxy.d.ts +++ b/types/aws-lambda/trigger/api-gateway-proxy.d.ts @@ -1,16 +1,45 @@ -import { APIGatewayEventRequestContext } from "../common/api-gateway"; +import { + APIGatewayEventDefaultAuthorizerContext, + APIGatewayEventRequestContextWithAuthorizer, +} from "../common/api-gateway"; import { Callback, Handler } from "../handler"; export type APIGatewayProxyHandler = Handler; export type APIGatewayProxyCallback = Callback; -export type ProxyHandler = APIGatewayProxyHandler; // Old name -export type ProxyCallback = APIGatewayProxyCallback; // Old name -export type APIGatewayEvent = APIGatewayProxyEvent; // Old name -export type ProxyResult = APIGatewayProxyResult; // Old name +export type APIGatewayProxyEvent = APIGatewayProxyEventBase; -// API Gateway "event" -export interface APIGatewayProxyEvent { +export type APIGatewayProxyWithLambdaAuthorizerHandler = + Handler, APIGatewayProxyResult>; + +export type APIGatewayProxyWithCognitoAuthorizerHandler = + Handler; + +export type APIGatewayProxyWithLambdaAuthorizerEvent = + APIGatewayProxyEventBase>; + +export type APIGatewayProxyWithLambdaAuthorizerEventRequestContext = + APIGatewayEventRequestContextWithAuthorizer>; + +// API Gateway proxy integration mangles the context from a custom authorizer, +// converting all number or boolean properties to string, and adding some extra properties. +export type APIGatewayEventLambdaAuthorizerContext = { + [P in keyof TAuthorizerContext]: TAuthorizerContext[P] extends null ? null : string; +} & { + principalId: string; + integrationLatency: number; +}; + +export type APIGatewayProxyWithCognitoAuthorizerEvent = APIGatewayProxyEventBase; + +// All claims are coerced into strings. +export interface APIGatewayProxyCognitoAuthorizer { + claims: { + [name: string]: string; + }; +} + +export interface APIGatewayProxyEventBase { body: string | null; headers: { [name: string]: string }; multiValueHeaders: { [name: string]: string[] }; @@ -21,7 +50,7 @@ export interface APIGatewayProxyEvent { queryStringParameters: { [name: string]: string } | null; multiValueQueryStringParameters: { [name: string]: string[] } | null; stageVariables: { [name: string]: string } | null; - requestContext: APIGatewayEventRequestContext; + requestContext: APIGatewayEventRequestContextWithAuthorizer; resource: string; } @@ -36,3 +65,9 @@ export interface APIGatewayProxyResult { body: string; isBase64Encoded?: boolean; } + +// Legacy names +export type ProxyHandler = APIGatewayProxyHandler; +export type ProxyCallback = APIGatewayProxyCallback; +export type APIGatewayEvent = APIGatewayProxyEvent; +export type ProxyResult = APIGatewayProxyResult; diff --git a/types/cfn-response/index.d.ts b/types/cfn-response/index.d.ts index f3e3893f07..05dfc86402 100644 --- a/types/cfn-response/index.d.ts +++ b/types/cfn-response/index.d.ts @@ -2,7 +2,7 @@ // Project: https://github.com/LukeMizuhashi/cfn-response // Definitions by: Ivo Murrell // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 2.8 +// TypeScript Version: 3.0 import { CloudFormationCustomResourceEvent, Context } from "aws-lambda";