Skip to content

Latest commit

 

History

History
1231 lines (875 loc) · 30 KB

apigateway.md

File metadata and controls

1231 lines (875 loc) · 30 KB

api Event Type:

The api event type is used to handle AWS API Gateway events using the Lambda Proxy based method for handling resource events. The api handler can be used to create sub-handlers for each of the HTTP methods, handle results or errors and ensure that the response is in the correct format for API Gateway.

Typical handler for a GET method request for API Gateway would look like:

const vandium = require( 'vandium' );

const User = require( './user' );

const cache = require( './cache' );

exports.handler = vandium.api()
        .GET( (event) => {

                // handle get request
                return User.get( event.pathParameters.name );
            })
        .finally( () => {

            // close the cache if open - gets executed on every call
            return cache.close();
        });

In the above example, the handler will validate that the event is for API Gateway and that it is a GET HTTP request. Note that the User module used Promises to handle the asynchronous calls, this simplifies the code, enhances readability and reduces complexity.

Vandium allows you to create a compound function for handling different types of HTTP requests.

const vandium = require( 'vandium' );

const User = require( './user' );

const cache = require( './cache' );

exports.handler = vandium.api()
        .GET( (event) => {

                // handle get request
                return User.get( event.pathParameters.name );
            })
        .POST()
            .validation( {

                // validate
                body: {

                    name: vandium.types.string().min(4).max(200).required()
                }
            })
            .handler( (event) => {

                // handle POST request
                return User.create( event.body.name );
            })
        .PATCH()
            .validation({

                // validate
                body: {

                    age: vandium.types.number().min(0).max(130)
                }
            })
            .handler( (event) => {

                // handle PATCH request
                return User.update( event.pathParameters.name, event.body );
            })
        .DELETE( (event) => {

                // handle DELETE request
                return User.delete( event.pathParameters.name );
            })
        .finally( () => {

            // close the cache if open - gets executed on every call
            return cache.close();
        });

The individual HTTP methods have their own independent paths inside the proxied handler, each with their own ability to validate specific event parameters as required. If the value of event.body contains an encoded JSON object, it will be parsed and validated.

HTTP Methods

The api event handler supports the following HTTP methods:

  • GET
  • POST
  • PUT
  • PATCH
  • DELETE
  • HEAD

Each method handler can have an optional validation section, specified before the handler using validation(), to ensure the supplied request information is valid before any logic gets executed. Method handlers can receive additional information to allow them access to more information as needed. Revisiting the example in the "Getting Started" section, we can expand the method handlers to illustrate how one might want to access the context parameter or perform traditional asynchronous calls using a callback function.

const vandium = require( 'vandium' );

exports.handler = vandium.api()
        .GET( (event) => {

                // handle get request
            })
        .POST()
            .validation({

                // validate
                body: {

                    name: vandium.types.string().min(4).max(200).required()
                }
            })
            .handler( (event, context) => {

                // handle POST request
                return User.create( event.body.name );
            });

Note: Although the context parameter is available, Vandium will remove context.succeed(), context.fail() and context.done() methods.

Body Encoding

The default body encoding is set to auto, which will attempt to parse the body as JSON, Form URL Encoded and if unsuccessful the body will be kept "as is".

Form URL Encoded

If you know that your data will be application/x-www-form-urlencoded, then you can set the body encoding as such using the formURLEncoded() method.

const vandium = require( 'vandium' );

exports.handler = vandium.api()
        .POST()
            .formURLEncoded()
            .handler( (event) => {

                // event.body will contain an object from parsing the form URL encoded body
            });

Disable Body Parsing

To disable and skip body parsing, use the skipBodyParse() method, which will set the encoding to none.

const vandium = require( 'vandium' );

exports.handler = vandium.api()
        .POST()
            .skipBodyParse()
            .handler( (event) => {

                // event.body will remain unchanged
            });

Validation of Event Elements

Vandium uses Joi as the engine for validation on specific elements inside the event. Validation schemas are applied to one or more sections of the event object and can be targeted to the headers, queryStringParameters, body and pathParameters elements. Each item inside the element can be validated using one of the build-in types inside Vandium.

Types

The following types are supported for validation.

Type Description
string String value. Automatically trimmed
number Number value (integer or floating point)
boolean Boolean value (true or false)
date Date value
email Email address
uuid Universally unique identifier (UUID)
binary Binary value uncoded using base64
any Any type of value
object Object
array Arrays of values
alternatives Alternative selection of values

Value Conversion

Values will be converted, if needed, and will reduce the amount of code you need to write in your handler. For example, the validation configuration of:

{
    body: {

        age: vandium.types.number().required()
    }
}

with an event of:

{
    // other event elements

    body: {

        age: '42'
    }
}

would be converted to:

{
    // other event elements

    body: {

        age: 42
    }
}

Additionally, binary data with a schema of:

{
    body: {

        data: vandium.types.binary()
    }
}

with an event containing:

{
    // other event elements

    body: {

        data: 'dmFuZGl1bSBsYW1iZGEgd3JhcHBlcg=='
    }
}

would be get converted to a Buffer instance with the data parsed and loaded:

{
    // other event elements

    body: {

        data: Buffer( ... )
    }
}

Type: string

String validators can be created by calling vandium.types.string(), using "string" or "object" notation. The implementation of the string validator is based on Joi, and thus can use most of the functionality available in the Joi library for the string type. All string validators in Vandium will automatically trim leading and trailing whitespace.

Simple validation:

{
    body: {

        name: vandium.types.string().min( 1 ).max( 250 ).required()
    }
}

This could also be writing using a string notation:

{
    body: {

        name: 'string:min=1,max=250,required'
    }
}

Regex expressions can be used:

{
    requestParameters: {

        id: vandium.types.string().regex( /^[abcdef]+$/ )
    }
}

Strings are automatically trimmed. To disable this functionality, add an option when creating the type:

{
    string_not_trimmed: vandium.types.string( { trim: false } )
}

Type: number

Number validators can be created by calling vandium.types.number(), using "string" or "object" notation. The implementation is based on Joi and thus can use functionality available in the Joi library for the number type.

{
    body: {

        // age must be a number, between 0 and 130 and is required
    	age: vandium.types.number().integer().min( 0 ).max( 130 ).required()
    }
}

This could also be writing using a string notation:

{
    body: {

        age: 'number:integer,min=0,max=130,required'
    }
}

Type: boolean

Boolean validators can be created by calling vandium.types.boolean(), using "string" or "object" notation. The implementation is based on Joi and thus can use functionality available in the Joi library for the boolean type.

The boolean validator can accept the following values: true, false, "true", "false", "yes" and "no".

{
    body: {

        allow: vandium.types.boolean().required()
    }
}

This could also be writing using a string notation:

{
    body: {

        allow: 'boolean:required'
    }
}

Type: date

Date validators can be created by calling vandium.types.date(), using "string" or "object" notation. The implementation is based on Joi and thus can use functionality available in the Joi library for the date type.

The date validator can match values against standard Javascript date format as well as the number of milliseconds since the epoch.

{
    queryStringParameters: {

        start: vandium.types.date().min( '1-1-1970' ).required(),
    	end: vandium.types.date().max( 'now' ).required()
    }
}

This could also be writing using a string notation:

{
    queryStringParameters: {

        start: 'date:min=1-1-1970,required',
        end: 'date:max=now,required'
    }
}

Type: email

The email validator matches values that conform to valid email address format. Email validators can be created by calling vandium.types.email(), using "string" or "object" notation. The implementation is based on string type found in Joi.

{
    body: {

        address: vandium.types.email().required()
    }
}

This could also be writing using a string notation:

{
    body: {

        address: 'email:required'
    }
}

Type: uuid

The uuid validator matches values that conform to the Universally unique identifier (UUID) specification. UUID validators can be created by calling vandium.types.uuid(), using "string" or "object" notation. The implementation is based on string type found in Joi.

{
    requestParameters: {

        id: vandium.types.uuid().required()
    }
}

This could also be writing using a string notation:

{
    requestParameters: {

        id: 'uuid:required'
    }
}

Type: binary

Binary validators can be created by calling vandium.types.binary(), using "string" or "object" notation. The implementation is based on Joi and thus can use functionality available in the Joi library for the binary type.

{
    body: {

        data: vandium.types.binary().encoding( 'base64' ).min( 10 ).max( 1000 )
    }
}

This could also be writing using a string notation:

{
    body: {

        data: 'binary:encoding=base64,min=10,max=100'
    }
}

Type: any

the any type can represent any type of value and the validator performs minimal validation. "Any" validators can be created by calling vandium.types.any(), using "string" or "object" notation. The implementation is based on Joi and thus can use functionality available in the Joi library for the any type.

{
    body: {

        name: vandium.types.any().required()
    }
}

This could also be writing using a string notation:

{
    body: {

        name: 'any:required'
    }
}

Type: object

The object validator allows validation of an object and potentially the values within it. Object validators can be created by calling vandium.types.object(), using "string" or "object" notation. The implementation is based on Joi and thus can use functionality available in the Joi library for the object type.

{
    body: {

        job: vandium.types.object().keys({

    			name: vandium.types.string().required(),
    			dept: vandium.types.string().required()
         	}).required();
    }
}

This could also be writing using a string notation:

{

    body: {

        job: {

            name: 'string:required',
            dept: 'string:required',

            '@required': true
        }
    }
}

Type: array

The array can match values the are part of a selection, specific types and/or pattern. Array validators can be created by calling vandium.types.array(), using "string" or "object" notation. The implementation is based on Joi and thus can use functionality available in the Joi library for the array type.

{
    body: {

        // matching seven numbers between 1 and 49:
    	lucky_numbers: vandium.types.array().items( vandium.types.number().min( 1 ).max( 49 ) ).length( 7 )
    }
}

This could also be writing using a string notation:

{
    body: {

        lucky_numbers: {

            '@items': [ 'number:min=1,max=49' ],
            length: 7
        }
    }
}

Type: alternatives

The alternatives validator is used to choose between two types of values. "Alternatives" validators can be created by calling vandium.types.alternatives(), using "string" or "object" notation. The implementation is based on Joi and thus can use functionality available in the Joi library for the alternatives type.

{
    requestParameters: {

        // key can be uuid or name
    	key: vandium.types.alternatives().try( vandium.types.uuid(), vandium.types.string() )
    }
}

This could also be writing using a string notation:

{
    requestParameters: {

        key: [ 'uuid', 'string' ]
    }
}

For more information on how to configure validators, see the joi reference

headers

HTTP response headers can be set using the headers() method on the handler. The following example demonstrates the setting of custom headers:

const vandium = require( 'vandium' );

exports.handler = vandium.api()
        .headers( {

            'x-custom-header': 'header-value-here'
        })
        .GET( (event) => {

                // handle get request
        });

CORS

CORS can be set by using the cors() method on the handler. The CORS implementation in Vandium can accept all request values, including pre-flight ones, as documented by W3C. The following example sets the Access-Control-Allow-Origin and Access-Control-Allow-Credentials CORS values in the response headers:

const vandium = require( 'vandium' );

exports.handler = vandium.api()
        .cors( {

            allowOrigin: 'https://app.example.com',
            allowCredentials: true
        })
        .GET( (event) => {

                // handle get request
        });

The following CORS options are available

Property CORS Header Name
allowOrigin Access-Control-Allow-Origin
allowCredentials Access-Control-Allow-Credentials
exposeHeaders Access-Control-Expose-Headers
maxAge Access-Control-Max-Age
allowHeaders Access-Control-Allow-Headers

Authorization via JSON Web Tokens (JWT)

Vandium can handle validation, enforcement and processing of JSON Web Token (JWT) values. Configuration can be provided either via code or through environment variables.

Supported algorithms

The following JWT signature algorithms are supported:

Algorithm Type
HS256 HMAC SHA256 (shared secret)
HS384 HMAC SHA384 (shared secret)
HS512 HMAC SHA512 (shared secret)
RS256 RSA SHA256 (public-private key)

Enabling JWT Programatically

To enable JWT using code, use the authorization() function on the api handler. By specifying an algorithm, JWT processing is automatically enabled. The jwt configuration format is:

{
    algorithm: 'RS256' | 'HS256' | 'HS384' | 'HS512',
    publicKey: '<public key>',          // if algorithm is 'RS256'
    secret: '<shared secret',           // if algorithm is 'HS256', 'HS384' or 'HS512'
    token: '<token path>',              // path to jwt token. Defaults to 'headers.Authorization'
    xsrf: true | false,                 // enables xsrf verification. Defaults to false unless other xsrf* options are enabled
    xsrfToken: '<xsrf token path>',     // path to xsrf token. Defaults to 'headers.xsrf'
    xsrfClaim: '<xsrf claim path>'      // path to xsrf claim inside jwt. Defaults to 'nonce'
}

The following example enables JWT processing programmatically:

const vandium = require( 'vandium' );

exports.handler = vandium.api()
        .authorization( {

            algorithm: 'RS256'
            key: '<public key goes here>'
        })
        .GET( (event) => {

                // handle get request
            })
        // other method handlers...

Note: Vandium prior to version 6.0 used jwt() to configure support for authorization via JWT. This method will be deprecated in a future version.

Configuration via Environment Variables

Environment variables can be used to configure support for JWT processing. The following environment variables can be used:

Name Description
VANDIUM_JWT_ALGORITHM Specifies the algorithm for JWT verification. Can be HS256, HS384, HS512 or RS256
VANDIUM_JWT_SECRET Secret key value for use with HMAC SHA algorithms: HS256, HS384 and HS512
VANDIUM_JWT_PUBKEY Public key used used with RS256 algorithm
VANDIUM_JWT_KEY Alias for either VANDIUM_JWT_SECRET or VANDIUM_JWT_PUBKEY
VANDIUM_JWT_TOKEN_PATH Name of the token variable in the event object. Defaults to headers.Authorization
VANDIUM_JWT_USE_XSRF Enable or disable Cross Site Request Forgery (XSRF) token. Defaults to false
VANDIUM_JWT_XSRF_TOKEN_PATH Name of the XSRF token in the event. Defaults to headers.xsrf
VANDIUM_JWT_XSRF_CLAIM_PATH XSRF claim path inside JWT. Defaults to nonce

When the VANDIUM_JWT_ALGORITHM value is present, JWT processing is automatically enabled within Vandium.

Configuration via JWK

A valid JWK can supply the algorithm type and key used to validate a JWT. Currently the only supported algorithm type is RSA256. The following example uses a JWK for authorization:

const vandium = require( 'vandium' );

const myJWK = { /* JWK defined here */ };

exports.handler = vandium.api()
        .authorization( {

            jwk: myJWK
        })
        .GET( (event) => {

                // handle get request
            })
        // other method handlers...

Claim enforcement

Tokens will be validated and must conform to the specification, and the following token claims (not required) will be enforced:

Claim Description
iat Issued At (iat) time, in seconds, that the token was created and should only be used after this time.
nbf Not Before (nbf) time, in seconds, that prevents the token from being used before the indicated time.
exp Expiry (exp) time, in seconds, the prevents the token from being used after the indicated time.

Any validation or enforcement issues, including poorly formed or missing tokens, will cause the handler to terminate and return an error message using the proxy message format.

Accessing the Decoded Token

One the JWT has been validated and verified, the decoded contents will be added to the event object under the jwt element. The following code segment shows how to access the token:

const vandium = require( 'vandium' );

exports.handler = vandium.api()
        .authorization( {

            algorithm: 'RS256'      // public key specified using env variable
        })
        .GET( (event) => {

                // user id is stored inside the token
                let userId = event.jwt.id;

                // handle get request
            });

Disabling JWT support when environment/configuration is set

To programmatically disable JWT support for a specific handler when configured using environment variables and/or configuration files, use false as the parameter for the jwt method.

const vandium = require( 'vandium' );

exports.handler = vandium.api()
        .authorization( false )
        .POST( (event) => {

                // JWT  will not be enforced
            });

Additional Reading

For more information about JWT, see RFC 7519 and RFC 7797.

Response and error handling

API Gateway expects all Lambda proxy handlers to return a defined object indicating success or failure, typically via the callback() function. The response format is as follows:

{
    // numeric static code
    "statusCode": status_code,

    // object containing header values
    "headers": {

        "header1": "header 1 value",
        "header2": "header 2 value",

        // etc.
    },

    // body encoded as a string
    "body": "{\"result\":\"json_string_encoded\"}"
}

Since Vandium isolates your code from the Lambda handler, it will ensure that all responses and errors are put into the correct format.

Simple responses

Vandium will automatically try to determine the correct status code (successful or error condition) and process the body section accordingly.

If the following code was executed for a GET request;

const vandium = require( 'vandium' );

exports.handler = vandium.api()
        .GET( (event) => {

            // handle get request
            return {

                id: "12345",
                name: "john.doe"
            };
        });

The response object would be:

{
    // Vandium determined that a successful GET request should return status code of 200
    "statusCode": 200,

    // no headers were set
    "headers": {},

    // Vandium encoded the response object and stored as the body
    "body": "{\"id\":\"12345\",\"name\":\"john.doe\"}"
}

Code specified response object

If the following code was executed for a GET request;

const vandium = require( 'vandium' );

exports.handler = vandium.api()
        .GET( (event) => {

            // handle get request
            return {

                headers: {

                    header1: 'HEADER_1_VALUE',
                    header2: 'HEADER_2_VALUE'
                },

                body: {

                    id: "12345",
                    name: "john.doe"    
                }
            };
        });

The response object would be:

{
    "statusCode": 200,
    "headers": {

        "header1": "HEADER_1_VALUE",
        "header2": "HEADER_2_VALUE"
    },
    "body": "{\"id\":\"12345\",\"name\":\"john.doe\"}"
}

Additionally if the GET method handler returned a statusCode value, it would override the automatic settings:

const vandium = require( 'vandium' );

exports.handler = vandium.api()
        .GET( (event) => {

            // handle get request
            return {

                statusCode: 202,

                headers: {

                    header1: 'HEADER_1_VALUE',
                    header2: 'HEADER_2_VALUE',
                    header3: [ 'HEADER_3_VALUE1', 'HEADER_3_VALUE2'  ]
                },

                body: {

                    id: "12345",
                    name: "john.doe"    
                }
            };
        });

The response object would be:

{
    "statusCode": 202,
    "headers": {

        "header1": "HEADER_1_VALUE",
        "header2": "HEADER_2_VALUE"
    },
    "multValueHeaders": {

        "header3": ["HEADER_3_VALUE1", "HEADER_3_VALUE2"]
    },
    "body": "{\"id\":\"12345\",\"name\":\"john.doe\"}"
}

Set-Cookies

To set a response cookie, supply a setCookie value with either a serialized cookie or an object that contains a name, value and options.

const vandium = require( 'vandium' );

exports.handler = vandium.api()
        .GET( (event) => {

            // handle get request
            return {

                setCookie: {

                    name: 'cartId',
                    value: '1234',
                    options: { /// options here }
                },

                body: {

                    id: "12345",
                    name: "john.doe"    
                }
            };
        });

The setCookie value can have an array of values, which Vanduim will process accordingly.

For more infomration about the option values, see the documentation in the cookie module.

Error Responses

Vandium ensures that if an error gets thrown, it will get caught, processed and formatted into the API Gateway's Lambda proxy response object format.

If the following code was executed for a GET request;

const vandium = require( 'vandium' );

exports.handler = vandium.api()
        .GET( (event) => {

            throw new Error( 'something bad happened' );
        });

The response object would be:

{
    "statusCode": 500,
    "headers": {},
    "body": "{\"message\":\"something bad happened\"}"
}

The statusCode was set to 500 because Vandium examined the exception and did not find a status or statusCode value and used it's default of 500. If we wanted to set the status code properly, we would need to add the status to the exception as in the following example:

const vandium = require( 'vandium' );

exports.handler = vandium.api()
        .GET( (event) => {

            let error = new Error( 'Not Found' );
            error.statusCode = 404;

            throw error;
        });

The response object would be:

{
    "statusCode": 404,
    "headers": {},
    "body": "{\"message\":\"Not Found\"}"
}

Setting the statusCode for errors for all HTTP methods

Vandium provides an onError() function to intercept all errors before they get processed as responses. Using this method, you can examine each exception and set the appropriate status code.

const vandium = require( 'vandium' );

exports.handler = vandium.api()
        .GET( (event) => {

                throw new Error( 'User Not Found' );
            })
        .PUT( (event) => {

                throw new Error( 'Not Found' );
            })
        .onError( (err) => {

            if( err.message.indexOf( 'Not Found' ) > -1 ) {

                err.statusCode = 404;
            }

            return err;
        });

The response object for a GET request would be:

{
    "statusCode": 404,
    "headers": {},
    "body": "{\"message\":\"User Not Found\"}"
}

and the response object for a PUT request would be:

{
    "statusCode": 404,
    "headers": {},
    "body": "{\"message\":\"Not Found\"}"
}

Structured errors (RFC 7807)

The onError() function can be used to provide structured error information by specifying a body as part of the exception:

const vandium = require( 'vandium' );

exports.handler = vandium.api()
        .GET( (event) => {

                throw new Error( 'User Not Found' );
            })
        .onError( (err, event. context ) => {

            if( err.message.indexOf( 'Not Found' ) > -1 ) {

                err.statusCode = 404;
                err.body = {

                    "type": "https://example.com/probs/not-found",
                    "title": "user was not found",
                    "detail": "The user was not found in the database"
                };
            }

            return err;
        });

The response object for a GET request would be:

{
    "statusCode": 404,
    "headers": {},
    "body": "{\"type\":\"https://example.com/probs/not-found\",\"title\":\"user was not found\",\"detail\": \"The user was not found in the database\"}"
}

Setting headers for all HTTP methods

Use the headers() method on the api handler if you would like to set the headers for all responses and errors.

If the following code was executed for a GET request;

const vandium = require( 'vandium' );

exports.handler = vandium.api()
        .headers( {

            header1: 'HEADER_1_VALUE',
            header2: 'HEADER_2_VALUE'
        })
        .GET( (event) => {

            // handle get request
            return {

                id: "12345",
                name: "john.doe"
            };
        });

The response object would be:

{
    "statusCode": 200,
    "headers": {

        "header1": "HEADER_1_VALUE",
        "header2": "HEADER_2_VALUE"
    },
    "body": "{\"id\":\"12345\",\"name\":\"john.doe\"}"
}

Note: that returning a response object that contains a headers section will override the values set in the headers() function.

Injection protection

Vandium will analyze the queryStringParameters, body and pathParameters sections in the event object to determine if potential attacks are present. If attacks are detected, the default mode will be to log the attack. Injection protection can be configured using the protection() method on the api handler. Currently only SQL Injection (SQLi) attacks are detected but future versions will detect other types.

To prevent execution when potential attacks are detected:

const vandium = require( 'vandium' );

exports.handler = vandium.api()
        .protection( {

            // "fail" mode will prevent execution of the method handler
            mode: 'fail'
        })
        .GET( (event) => {

            // handle get request
            return {

                id: "12345",
                name: "john.doe"
            };
        });

To disable protection:

const vandium = require( 'vandium' );

exports.handler = vandium.api()
        .protection( {

            // "fail" mode will prevent execution of the method handler
            mode: 'off'
        })
        .GET( (event) => {

            // handle get request
            return {

                id: "12345",
                name: "john.doe"
            };
        });