[aws-lambda] New API Gateway Authorizer types, deprecating old… (#42420)

* [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.
This commit is contained in:
Simon Buchan
2020-03-02 09:50:45 -08:00
committed by GitHub
parent 6208bf89ac
commit 53c3baddff
7 changed files with 437 additions and 43 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
// Project: https://github.com/apex/node-apex
// Definitions by: Yoriki Yamaguchi <https://github.com/y13i>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.8
// TypeScript Version: 3.0
/// <reference types="aws-lambda" />
+28 -10
View File
@@ -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<APIGatewayEventDefaultAuthorizerContext>;
// The requestContext property of both request authorizer and proxy integration events.
export interface APIGatewayEventRequestContextWithAuthorizer<TAuthorizerContext> {
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;
}
+1 -1
View File
@@ -34,7 +34,7 @@
// Marian Zange <https://github.com/marianzange>
// Alexander Pepper <https://github.com/apepper>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.8
// TypeScript Version: 3.0
export * from "./handler";
export * from "./common/api-gateway";
+286 -6
View File
@@ -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<CustomAuthorizerContext>;
// $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<CustomAuthorizerContext> = 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<CustomAuthorizerContext>;
// $ExpectError
requestContextWithAuthorizerDirectly = event.requestContext;
// Check assignable to named types
let requestContext: APIGatewayProxyWithLambdaAuthorizerEventRequestContext<CustomAuthorizerContext>;
requestContext = event.requestContext;
let authorizer: APIGatewayEventLambdaAuthorizerContext<CustomAuthorizerContext>;
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<CustomAuthorizerContext> = 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<CustomAuthorizerContext>;
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<CustomAuthorizerContext> = 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<CustomAuthorizerContext> = 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<CustomAuthorizerContext> {
let result: APIGatewayAuthorizerWithContextResult<CustomAuthorizerContext>;
// 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) => {};
+77 -16
View File
@@ -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<CustomAuthorizerEvent, CustomAuthorizerResult>;
export type CustomAuthorizerCallback = Callback<CustomAuthorizerResult>;
export type APIGatewayAuthorizerHandler = Handler<APIGatewayAuthorizerEvent, APIGatewayAuthorizerResult>;
export type APIGatewayAuthorizerWithContextHandler<TAuthorizerContext extends APIGatewayAuthorizerResultContext> =
Handler<APIGatewayAuthorizerEvent, APIGatewayAuthorizerWithContextResult<TAuthorizerContext>>;
// API Gateway CustomAuthorizer "event"
export type APIGatewayAuthorizerCallback = Callback<APIGatewayAuthorizerResult>;
export type APIGatewayAuthorizerWithContextCallback<TAuthorizerContext extends APIGatewayAuthorizerResultContext> =
Callback<APIGatewayAuthorizerWithContextResult<TAuthorizerContext>>;
export type APIGatewayTokenAuthorizerHandler =
Handler<APIGatewayTokenAuthorizerEvent, APIGatewayAuthorizerResult>;
export type APIGatewayTokenAuthorizerWithContextHandler<TAuthorizerContext extends APIGatewayAuthorizerResultContext> =
Handler<APIGatewayTokenAuthorizerEvent, APIGatewayAuthorizerWithContextResult<TAuthorizerContext>>;
export type APIGatewayRequestAuthorizerHandler =
Handler<APIGatewayRequestAuthorizerEvent, APIGatewayAuthorizerResult>;
export type APIGatewayRequestAuthorizerWithContextHandler<TAuthorizerContext extends APIGatewayAuthorizerResultContext> =
Handler<APIGatewayRequestAuthorizerEvent, APIGatewayAuthorizerWithContextResult<TAuthorizerContext>>;
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<undefined>;
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<TAuthorizerContext extends APIGatewayAuthorizerResultContext> {
principalId: string;
policyDocument: PolicyDocument;
context: TAuthorizerContext;
usageIdentifierKey?: string | null;
}
// Legacy event / names
/** @deprecated Use APIGatewayAuthorizerHandler or a subtype */
export type CustomAuthorizerHandler = Handler<CustomAuthorizerEvent, APIGatewayAuthorizerResult>;
// 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<APIGatewayEventDefaultAuthorizerContext>;
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.
+43 -8
View File
@@ -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<APIGatewayProxyEvent, APIGatewayProxyResult>;
export type APIGatewayProxyCallback = Callback<APIGatewayProxyResult>;
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<APIGatewayEventDefaultAuthorizerContext>;
// API Gateway "event"
export interface APIGatewayProxyEvent {
export type APIGatewayProxyWithLambdaAuthorizerHandler<TAuthorizerContext> =
Handler<APIGatewayProxyWithLambdaAuthorizerEvent<TAuthorizerContext>, APIGatewayProxyResult>;
export type APIGatewayProxyWithCognitoAuthorizerHandler =
Handler<APIGatewayProxyWithCognitoAuthorizerEvent, APIGatewayProxyResult>;
export type APIGatewayProxyWithLambdaAuthorizerEvent<TAuthorizerContext> =
APIGatewayProxyEventBase<APIGatewayEventLambdaAuthorizerContext<TAuthorizerContext>>;
export type APIGatewayProxyWithLambdaAuthorizerEventRequestContext<TAuthorizerContext> =
APIGatewayEventRequestContextWithAuthorizer<APIGatewayEventLambdaAuthorizerContext<TAuthorizerContext>>;
// 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<TAuthorizerContext> = {
[P in keyof TAuthorizerContext]: TAuthorizerContext[P] extends null ? null : string;
} & {
principalId: string;
integrationLatency: number;
};
export type APIGatewayProxyWithCognitoAuthorizerEvent = APIGatewayProxyEventBase<APIGatewayProxyCognitoAuthorizer>;
// All claims are coerced into strings.
export interface APIGatewayProxyCognitoAuthorizer {
claims: {
[name: string]: string;
};
}
export interface APIGatewayProxyEventBase<TAuthorizerContext> {
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<TAuthorizerContext>;
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;
+1 -1
View File
@@ -2,7 +2,7 @@
// Project: https://github.com/LukeMizuhashi/cfn-response
// Definitions by: Ivo Murrell <https://github.com/ivoisbelongtous>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.8
// TypeScript Version: 3.0
import { CloudFormationCustomResourceEvent, Context } from "aws-lambda";