Skip to content
This repository was archived by the owner on Aug 8, 2024. It is now read-only.

Commit dcb03db

Browse files
authored
Adds customizable CORS configurations (#52)
* Add new CORS functionality, confirm original tests still work, update readme. * Update readme and allow origin to be passed a function * Add example to docs, as per code comment * Closes #51
1 parent 1cd933e commit dcb03db

File tree

4 files changed

+550
-21
lines changed

4 files changed

+550
-21
lines changed

README.md

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ See the following example:
147147
148148
149149
<details>
150-
<summary>CORS example</summary>
150+
<summary>Default CORS example</summary>
151151
152152
```js
153153
import * as router from 'aws-lambda-router'
@@ -176,6 +176,49 @@ If CORS is activated, these default headers will be sent on every response:
176176
"Access-Control-Allow-Methods" = "'GET,POST,PUT,DELETE,HEAD,PATCH'"
177177
"Access-Control-Allow-Headers" = "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
178178
179+
### Customizing CORS
180+
181+
To customize CORS for all routes pass any of the following options to the `proxyIntegration` `cors` property. If a property is not set then it will default to the above default CORS headers.
182+
183+
* `origin`: Configures the **Access-Control-Allow-Origin** CORS header. Possible values:
184+
- `Boolean` - set `origin` to `true` to reflect the request origin or set it to `false` to disable CORS.
185+
- `String` - set `origin` to a specific origin. For example if you set it to `"http://example.com"` only requests from "http://example.com" will be allowed.
186+
- `RegExp` - set `origin` to a regular expression pattern which will be used to test the request origin. If it's a match, the request origin will be reflected. For example the pattern `/example\.com$/` will reflect any request that is coming from an origin ending with "example.com".
187+
- `Array` - set `origin` to an array of valid origins. Each origin can be a `String` or a `RegExp`. For example `["http://example1.com", /\.example2\.com$/]` will accept any request from "http://example1.com" or from a subdomain of "example2.com".
188+
- `Function` - set `origin` to a function to be evaluated. The function will get passed the `APIGatewayProxyEvent` and must return the allowed origin or `false`
189+
* `methods`: Configures the **Access-Control-Allow-Methods** CORS header. Expects a comma-delimited string (ex: 'GET,PUT,POST') or an array (ex: `['GET', 'PUT', 'POST']`).
190+
* `allowedHeaders`: Configures the **Access-Control-Allow-Headers** CORS header. Expects a comma-delimited string (ex: 'Content-Type,Authorization') or an array (ex: `['Content-Type', 'Authorization']`). If not specified, defaults to reflecting the headers specified in the request's **Access-Control-Request-Headers** header.
191+
* `exposedHeaders`: Configures the **Access-Control-Expose-Headers** CORS header. Expects a comma-delimited string (ex: 'Content-Range,X-Content-Range') or an array (ex: `['Content-Range', 'X-Content-Range']`). If not specified, no custom headers are exposed.
192+
* `credentials`: Configures the **Access-Control-Allow-Credentials** CORS header. Set to `true` to pass the header, otherwise it is omitted.
193+
* `maxAge`: Configures the **Access-Control-Max-Age** CORS header. Set to an integer to pass the header, otherwise it is omitted.
194+
195+
196+
<details>
197+
<summary>Customize CORS example</summary>
198+
199+
```js
200+
import * as router from 'aws-lambda-router'
201+
202+
export const handler = router.handler({
203+
// for handling an http-call from an AWS Apigateway proxyIntegration we provide the following config:
204+
proxyIntegration: {
205+
cors: {
206+
origin: 'https://test.example.com', // Only allow CORS request from this url
207+
methods: ['GET', 'POST', 'PUT'] // Only allow these HTTP methods to make requests
208+
},
209+
routes: [
210+
{
211+
path: '/graphql',
212+
method: 'POST',
213+
// provide a function to be called with the appropriate data
214+
action: (request, context) => doAnything(request.body)
215+
}
216+
]
217+
}
218+
})
219+
```
220+
221+
</details>
179222
180223
## Error mapping
181224

lib/cors.ts

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { APIGatewayProxyEvent } from 'aws-lambda'
2+
3+
type HeaderKeyValue = {
4+
key: string,
5+
value: any
6+
}
7+
8+
type HeaderObject = Array<HeaderKeyValue>
9+
10+
type CorsOrigin = string | boolean | RegExp | Array<RegExp | string> | Function| undefined
11+
12+
export interface CorsOptions {
13+
origin?: CorsOrigin
14+
methods?: string | string[]
15+
allowedHeaders?: string | string[]
16+
exposedHeaders?: string | string[]
17+
maxAge?: number
18+
credentials?: boolean
19+
}
20+
21+
const defaults = {
22+
origin: '*',
23+
methods: 'GET,POST,PUT,DELETE,HEAD,PATCH',
24+
allowedHeaders: 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'
25+
}
26+
27+
function isString(s: any) {
28+
return typeof s === 'string' || s instanceof String
29+
}
30+
31+
const isOriginAllowed = (origin: string, allowedOrigin: CorsOrigin): boolean => {
32+
if (Array.isArray(allowedOrigin)) {
33+
for (var i = 0; i < allowedOrigin.length; ++i) {
34+
if (isOriginAllowed(origin, allowedOrigin[i])) {
35+
return true
36+
}
37+
}
38+
return false
39+
} else if (isString(allowedOrigin)) {
40+
return origin === allowedOrigin
41+
} else if (allowedOrigin instanceof RegExp) {
42+
return allowedOrigin.test(origin)
43+
} else {
44+
return !!allowedOrigin
45+
}
46+
}
47+
48+
const configureOrigin = (options: CorsOptions, event: APIGatewayProxyEvent): HeaderObject => {
49+
const { origin } = options
50+
const headers: HeaderObject = []
51+
52+
if (origin === true || origin === '*') {
53+
headers.push({
54+
key: 'Access-Control-Allow-Origin',
55+
value: '*'
56+
})
57+
} else if(isString(origin)) {
58+
headers.push({
59+
key: 'Access-Control-Allow-Origin',
60+
value: origin
61+
}, {
62+
key: 'Vary',
63+
value: 'Origin'
64+
})
65+
} else if(typeof origin === 'function') {
66+
headers.push({
67+
key: 'Access-Control-Allow-Origin',
68+
value: origin(event)
69+
}, {
70+
key: 'Vary',
71+
value: 'Origin'
72+
})
73+
} else {
74+
const requestOrigin: string = event.headers.origin
75+
const isAllowed: boolean = isOriginAllowed(requestOrigin, origin)
76+
77+
headers.push({
78+
key: 'Access-Control-Allow-Origin',
79+
value: isAllowed ? requestOrigin : false
80+
}, {
81+
key: 'Vary',
82+
value: 'Origin'
83+
})
84+
}
85+
86+
return headers
87+
}
88+
89+
const configureMethods = (options: CorsOptions): HeaderObject => {
90+
const { methods } = options
91+
92+
return [{
93+
key: 'Access-Control-Allow-Methods',
94+
value: Array.isArray(methods) ? methods.join(',') : methods
95+
}]
96+
}
97+
98+
const configureAllowedHeaders = (options: CorsOptions, event: APIGatewayProxyEvent): HeaderObject => {
99+
let { allowedHeaders } = options
100+
const headers = []
101+
102+
if (!allowedHeaders) {
103+
allowedHeaders = event.headers['Access-Control-Request-Headers']
104+
headers.push({
105+
key: 'Vary',
106+
value: 'Access-Control-Request-Headers'
107+
})
108+
} else if(Array.isArray(allowedHeaders)) {
109+
allowedHeaders = allowedHeaders.join(',')
110+
}
111+
112+
if(allowedHeaders && allowedHeaders.length) {
113+
headers.push({
114+
key: 'Access-Control-Allow-Headers',
115+
value: allowedHeaders
116+
})
117+
}
118+
119+
return headers
120+
}
121+
122+
const configureExposedHeaders = (options: CorsOptions): HeaderObject => {
123+
let { exposedHeaders } = options
124+
125+
if (!exposedHeaders) {
126+
return []
127+
} else if(Array.isArray(exposedHeaders)){
128+
exposedHeaders = exposedHeaders.join(',')
129+
}
130+
if (exposedHeaders) {
131+
return [{
132+
key: 'Access-Control-Expose-Headers',
133+
value: exposedHeaders
134+
}]
135+
}
136+
return []
137+
}
138+
139+
140+
const configureAllowMaxAge = (options: CorsOptions): HeaderObject => {
141+
const { maxAge } = options
142+
143+
return !maxAge ? [] : [
144+
{
145+
key: 'Access-Control-Max-Age',
146+
value: `${maxAge}`
147+
}
148+
]
149+
}
150+
151+
152+
const configureCredentials = (options: CorsOptions): HeaderObject => {
153+
const { credentials } = options
154+
155+
return credentials === true
156+
? [{
157+
key: 'Access-Control-Allow-Credentials',
158+
value: 'true'
159+
}] : []
160+
}
161+
162+
const generateHeaders = (headersArray: Array<HeaderObject> ) => {
163+
const vary: string[] = []
164+
const headers: any = {}
165+
166+
headersArray.forEach((header: HeaderObject) => {
167+
header.forEach((h: HeaderKeyValue) => {
168+
if (h.key === 'Vary' && h.value) {
169+
vary.push(h.value)
170+
} else {
171+
headers[h.key] = h.value
172+
}
173+
})
174+
})
175+
176+
return {
177+
...headers,
178+
...(vary.length && { 'Vary': vary.join(',') })
179+
}
180+
}
181+
182+
export const addCorsHeaders = (options: CorsOptions | boolean, event: APIGatewayProxyEvent) => {
183+
if (options === false) {
184+
return {}
185+
}
186+
187+
const corsOptions = Object.assign({}, defaults, typeof options === 'object' ? options : {})
188+
189+
return generateHeaders([
190+
configureOrigin(corsOptions, event),
191+
configureExposedHeaders(corsOptions),
192+
configureCredentials(corsOptions),
193+
configureMethods(corsOptions),
194+
configureAllowedHeaders(corsOptions, event),
195+
configureAllowMaxAge(corsOptions)
196+
])
197+
}

lib/proxyIntegration.ts

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { APIGatewayEventRequestContext, APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'
22

33
import { ProcessMethod } from './EventProcessor'
4+
import { addCorsHeaders, CorsOptions } from './cors';
45

56
type ProxyIntegrationParams = {
67
paths?: { [paramId: string]: string }
@@ -34,7 +35,7 @@ export type ProxyIntegrationError = {
3435
}
3536

3637
export interface ProxyIntegrationConfig {
37-
cors?: boolean
38+
cors?: CorsOptions | boolean
3839
routes: ProxyIntegrationRoute[]
3940
debug?: boolean
4041
errorMapping?: ProxyIntegrationErrorMapping
@@ -49,13 +50,6 @@ const NO_MATCHING_ACTION = (request: ProxyIntegrationEvent) => {
4950
}
5051
}
5152

52-
const addCorsHeaders = (toAdd: APIGatewayProxyResult['headers'] = {}) => {
53-
toAdd['Access-Control-Allow-Origin'] = '*'
54-
toAdd['Access-Control-Allow-Methods'] = 'GET,POST,PUT,DELETE,HEAD,PATCH'
55-
toAdd['Access-Control-Allow-Headers'] = 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'
56-
return toAdd
57-
}
58-
5953
const processActionAndReturn = async (actionConfig: Pick<ProxyIntegrationRoute, 'action'>, event: ProxyIntegrationEvent,
6054
context: APIGatewayEventRequestContext, headers: APIGatewayProxyResult['headers']) => {
6155

@@ -97,17 +91,15 @@ export const process: ProcessMethod<ProxyIntegrationConfig, APIGatewayProxyEvent
9791
return null
9892
}
9993

100-
const headers: APIGatewayProxyResult['headers'] = {}
101-
if (proxyIntegrationConfig.cors) {
102-
addCorsHeaders(headers)
103-
if (event.httpMethod === 'OPTIONS') {
104-
return Promise.resolve({
105-
statusCode: 200,
106-
headers,
107-
body: ''
108-
})
109-
}
94+
if (event.httpMethod === 'OPTIONS') {
95+
return Promise.resolve({
96+
statusCode: 200,
97+
headers: proxyIntegrationConfig.cors ? addCorsHeaders(proxyIntegrationConfig.cors, event) : {},
98+
body: ''
99+
})
110100
}
101+
102+
const headers: APIGatewayProxyResult['headers'] = proxyIntegrationConfig.cors ? addCorsHeaders(proxyIntegrationConfig.cors, event) : {};
111103
Object.assign(headers, { 'Content-Type': 'application/json' }, proxyIntegrationConfig.defaultHeaders)
112104

113105
// assure necessary values have sane defaults:
@@ -188,14 +180,14 @@ const convertError = (error: ProxyIntegrationError | Error, errorMapping?: Proxy
188180
return {
189181
statusCode: error.statusCode,
190182
body: JSON.stringify({ message: error.message, error: error.statusCode }),
191-
headers: addCorsHeaders({})
183+
headers: addCorsHeaders({}, {} as APIGatewayProxyEvent)
192184
}
193185
}
194186
try {
195187
return {
196188
statusCode: 500,
197189
body: JSON.stringify({ error: 'ServerError', message: `Generic error:${JSON.stringify(error)}` }),
198-
headers: addCorsHeaders({})
190+
headers: addCorsHeaders({}, {} as APIGatewayProxyEvent)
199191
}
200192
} catch (stringifyError) { }
201193

0 commit comments

Comments
 (0)