serverless-offline 11.2.3 → 11.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -4
- package/package.json +8 -8
- package/src/events/http/HttpServer.js +38 -3
- package/src/events/http/createAuthScheme.js +108 -35
- package/src/utils/getRawQueryParams.js +11 -0
- package/src/utils/index.js +1 -0
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ This plugin is updated by its users, I just do maintenance and ensure that PRs a
|
|
|
43
43
|
- [Installation](#installation)
|
|
44
44
|
- [Usage and command line options](#usage-and-command-line-options)
|
|
45
45
|
- [Run modes](#run-modes)
|
|
46
|
-
- [
|
|
46
|
+
- [Invoke Lambda](#invoke-lambda)
|
|
47
47
|
- [The `process.env.IS_OFFLINE` variable](#the-processenvis_offline-variable)
|
|
48
48
|
- [Docker and Layers](#docker-and-layers)
|
|
49
49
|
- [Authorizers](#authorizers)
|
|
@@ -295,7 +295,7 @@ Lambda handlers for the `node.js` runtime can run in different execution modes w
|
|
|
295
295
|
|
|
296
296
|
the Lambda handler process is running in a child process.
|
|
297
297
|
|
|
298
|
-
##
|
|
298
|
+
## Invoke Lambda
|
|
299
299
|
|
|
300
300
|
To use `Lambda.invoke` you need to set the lambda endpoint to the `serverless-offline` endpoint:
|
|
301
301
|
|
|
@@ -326,14 +326,20 @@ const lambda = new aws.Lambda({
|
|
|
326
326
|
})
|
|
327
327
|
|
|
328
328
|
export async function handler() {
|
|
329
|
-
const clientContextData = stringify({
|
|
329
|
+
const clientContextData = stringify({
|
|
330
|
+
foo: 'foo',
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
const payload = stringify({
|
|
334
|
+
data: 'foo',
|
|
335
|
+
})
|
|
330
336
|
|
|
331
337
|
const params = {
|
|
332
338
|
ClientContext: Buffer.from(clientContextData).toString('base64'),
|
|
333
339
|
// FunctionName is composed of: service name - stage - function name, e.g.
|
|
334
340
|
FunctionName: 'myServiceName-dev-invokedHandler',
|
|
335
341
|
InvocationType: 'RequestResponse',
|
|
336
|
-
Payload:
|
|
342
|
+
Payload: payload,
|
|
337
343
|
}
|
|
338
344
|
|
|
339
345
|
const response = await lambda.invoke(params).promise()
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"dedicatedTo": "Blue, a great migrating bird.",
|
|
3
3
|
"name": "serverless-offline",
|
|
4
|
-
"version": "11.
|
|
4
|
+
"version": "11.3.0",
|
|
5
5
|
"description": "Emulate AWS λ and API Gateway locally when developing your Serverless project",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"exports": {
|
|
@@ -82,11 +82,11 @@
|
|
|
82
82
|
]
|
|
83
83
|
},
|
|
84
84
|
"dependencies": {
|
|
85
|
-
"@aws-sdk/client-lambda": "^3.
|
|
85
|
+
"@aws-sdk/client-lambda": "^3.204.0",
|
|
86
86
|
"@hapi/boom": "^10.0.0",
|
|
87
87
|
"@hapi/h2o2": "^10.0.0",
|
|
88
|
-
"@hapi/hapi": "^
|
|
89
|
-
"@serverless/utils": "^6.8.
|
|
88
|
+
"@hapi/hapi": "^21.0.0",
|
|
89
|
+
"@serverless/utils": "^6.8.2",
|
|
90
90
|
"boxen": "^7.0.0",
|
|
91
91
|
"chalk": "^5.1.2",
|
|
92
92
|
"execa": "^6.1.0",
|
|
@@ -101,16 +101,16 @@
|
|
|
101
101
|
"luxon": "^3.1.0",
|
|
102
102
|
"node-fetch": "^3.2.10",
|
|
103
103
|
"node-schedule": "^2.1.0",
|
|
104
|
-
"object.hasown": "^1.1.
|
|
104
|
+
"object.hasown": "^1.1.2",
|
|
105
105
|
"p-memoize": "^7.1.1",
|
|
106
106
|
"p-retry": "^5.1.1",
|
|
107
107
|
"velocityjs": "^2.0.6",
|
|
108
|
-
"ws": "^8.
|
|
108
|
+
"ws": "^8.11.0"
|
|
109
109
|
},
|
|
110
110
|
"devDependencies": {
|
|
111
111
|
"@istanbuljs/esm-loader-hook": "^0.2.0",
|
|
112
112
|
"archiver": "^5.3.1",
|
|
113
|
-
"eslint": "^8.
|
|
113
|
+
"eslint": "^8.27.0",
|
|
114
114
|
"eslint-config-airbnb-base": "^15.0.0",
|
|
115
115
|
"eslint-config-prettier": "^8.5.0",
|
|
116
116
|
"eslint-plugin-import": "^2.25.4",
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
"mocha": "^10.1.0",
|
|
122
122
|
"nyc": "^15.1.0",
|
|
123
123
|
"prettier": "^2.7.1",
|
|
124
|
-
"serverless": "^3.
|
|
124
|
+
"serverless": "^3.24.1",
|
|
125
125
|
"standard-version": "^9.5.0"
|
|
126
126
|
},
|
|
127
127
|
"peerDependencies": {
|
|
@@ -253,6 +253,16 @@ export default class HttpServer {
|
|
|
253
253
|
return null
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
if (
|
|
257
|
+
(endpoint.authorizer.name &&
|
|
258
|
+
this.#serverless.service.provider?.httpApi?.authorizers?.[
|
|
259
|
+
endpoint.authorizer.name
|
|
260
|
+
]?.type === 'request') ||
|
|
261
|
+
endpoint.authorizer.type === 'request'
|
|
262
|
+
) {
|
|
263
|
+
return null
|
|
264
|
+
}
|
|
265
|
+
|
|
256
266
|
const jwtSettings = this.#extractJWTAuthSettings(endpoint)
|
|
257
267
|
if (!jwtSettings) {
|
|
258
268
|
return null
|
|
@@ -303,11 +313,36 @@ export default class HttpServer {
|
|
|
303
313
|
log.error(`Authorization function ${authFunctionName} does not exist`)
|
|
304
314
|
return null
|
|
305
315
|
}
|
|
316
|
+
const serverlessAuthorizerOptions =
|
|
317
|
+
this.#serverless.service.provider.httpApi &&
|
|
318
|
+
this.#serverless.service.provider.httpApi.authorizers &&
|
|
319
|
+
this.#serverless.service.provider.httpApi.authorizers[authFunctionName]
|
|
306
320
|
|
|
307
321
|
const authorizerOptions = {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
322
|
+
enableSimpleResponses:
|
|
323
|
+
(endpoint.isHttpApi &&
|
|
324
|
+
serverlessAuthorizerOptions?.enableSimpleResponses) ||
|
|
325
|
+
false,
|
|
326
|
+
identitySource:
|
|
327
|
+
serverlessAuthorizerOptions?.identitySource ||
|
|
328
|
+
'method.request.header.Authorization',
|
|
329
|
+
identityValidationExpression:
|
|
330
|
+
serverlessAuthorizerOptions?.identityValidationExpression || '(.*)',
|
|
331
|
+
payloadVersion: !endpoint.isHttpApi
|
|
332
|
+
? '1.0'
|
|
333
|
+
: serverlessAuthorizerOptions?.payloadVersion || '2.0',
|
|
334
|
+
resultTtlInSeconds:
|
|
335
|
+
serverlessAuthorizerOptions?.resultTtlInSeconds || '300',
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (
|
|
339
|
+
authorizerOptions.enableSimpleResponses &&
|
|
340
|
+
authorizerOptions.payloadVersion === '1.0'
|
|
341
|
+
) {
|
|
342
|
+
log.error(
|
|
343
|
+
`Cannot create Authorization function '${authFunctionName}' if payloadVersion is '1.0' and enableSimpleResponses is true`,
|
|
344
|
+
)
|
|
345
|
+
return null
|
|
311
346
|
}
|
|
312
347
|
|
|
313
348
|
if (typeof endpoint.authorizer === 'string') {
|
|
@@ -3,6 +3,7 @@ import { log } from '@serverless/utils/log.js'
|
|
|
3
3
|
import authCanExecuteResource from '../authCanExecuteResource.js'
|
|
4
4
|
import authValidateContext from '../authValidateContext.js'
|
|
5
5
|
import {
|
|
6
|
+
getRawQueryParams,
|
|
6
7
|
nullIfEmpty,
|
|
7
8
|
parseHeaders,
|
|
8
9
|
parseMultiValueHeaders,
|
|
@@ -14,18 +15,22 @@ export default function createAuthScheme(authorizerOptions, provider, lambda) {
|
|
|
14
15
|
const authFunName = authorizerOptions.name
|
|
15
16
|
let identityHeader = 'authorization'
|
|
16
17
|
|
|
17
|
-
if (
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
if (
|
|
19
|
+
authorizerOptions.type !== 'request' ||
|
|
20
|
+
authorizerOptions.identitySource
|
|
21
|
+
) {
|
|
22
|
+
const identitySourceMatch =
|
|
23
|
+
/^(method.|\$)request.header.((?:\w+-?)+\w+)$/.exec(
|
|
24
|
+
authorizerOptions.identitySource,
|
|
25
|
+
)
|
|
21
26
|
|
|
22
|
-
if (!identitySourceMatch || identitySourceMatch.length !==
|
|
27
|
+
if (!identitySourceMatch || identitySourceMatch.length !== 3) {
|
|
23
28
|
throw new Error(
|
|
24
|
-
`Serverless Offline only supports retrieving tokens from
|
|
29
|
+
`Serverless Offline only supports retrieving tokens from headers (λ: ${authFunName})`,
|
|
25
30
|
)
|
|
26
31
|
}
|
|
27
32
|
|
|
28
|
-
identityHeader = identitySourceMatch[
|
|
33
|
+
identityHeader = identitySourceMatch[2].toLowerCase()
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
// Create Auth Scheme
|
|
@@ -36,8 +41,7 @@ export default function createAuthScheme(authorizerOptions, provider, lambda) {
|
|
|
36
41
|
`Running Authorization function for ${request.method} ${request.path} (λ: ${authFunName})`,
|
|
37
42
|
)
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
const { req } = request.raw
|
|
44
|
+
const { rawHeaders, url } = request.raw.req
|
|
41
45
|
|
|
42
46
|
// Get path params
|
|
43
47
|
// aws doesn't auto decode path params - hapi does
|
|
@@ -45,6 +49,8 @@ export default function createAuthScheme(authorizerOptions, provider, lambda) {
|
|
|
45
49
|
|
|
46
50
|
const accountId = 'random-account-id'
|
|
47
51
|
const apiId = 'random-api-id'
|
|
52
|
+
const requestId = 'random-request-id'
|
|
53
|
+
|
|
48
54
|
const httpMethod = request.method.toUpperCase()
|
|
49
55
|
const resourcePath = request.route.path.replace(
|
|
50
56
|
new RegExp(`^/${provider.stage}`),
|
|
@@ -53,53 +59,96 @@ export default function createAuthScheme(authorizerOptions, provider, lambda) {
|
|
|
53
59
|
|
|
54
60
|
let event = {
|
|
55
61
|
enhancedAuthContext: {},
|
|
56
|
-
|
|
62
|
+
headers: parseHeaders(rawHeaders),
|
|
57
63
|
requestContext: {
|
|
58
64
|
accountId,
|
|
59
65
|
apiId,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
requestId
|
|
63
|
-
resourceId: 'random-resource-id',
|
|
64
|
-
resourcePath,
|
|
66
|
+
domainName: `${apiId}.execute-api.us-east-1.amazonaws.com`,
|
|
67
|
+
domainPrefix: apiId,
|
|
68
|
+
requestId,
|
|
65
69
|
stage: provider.stage,
|
|
66
70
|
},
|
|
67
|
-
|
|
71
|
+
version: authorizerOptions.payloadVersion,
|
|
68
72
|
}
|
|
69
73
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
const protocol = `${request.server.info.protocol.toUpperCase()}/${
|
|
75
|
+
request.raw.req.httpVersion
|
|
76
|
+
}`
|
|
77
|
+
const currentDate = new Date()
|
|
78
|
+
const resourceId = `${httpMethod} ${resourcePath}`
|
|
79
|
+
const methodArn = `arn:aws:execute-api:${provider.region}:${accountId}:${apiId}/${provider.stage}/${httpMethod}${resourcePath}`
|
|
80
|
+
|
|
81
|
+
const authorization = request.raw.req.headers[identityHeader]
|
|
82
|
+
|
|
83
|
+
const identityValidationExpression = new RegExp(
|
|
84
|
+
authorizerOptions.identityValidationExpression,
|
|
85
|
+
)
|
|
86
|
+
const matchedAuthorization =
|
|
87
|
+
identityValidationExpression.test(authorization)
|
|
88
|
+
const finalAuthorization = matchedAuthorization ? authorization : ''
|
|
89
|
+
|
|
90
|
+
log.debug(`Retrieved ${identityHeader} header "${finalAuthorization}"`)
|
|
75
91
|
|
|
92
|
+
if (authorizerOptions.payloadVersion === '1.0') {
|
|
76
93
|
event = {
|
|
77
94
|
...event,
|
|
78
|
-
|
|
95
|
+
authorizationToken: finalAuthorization,
|
|
79
96
|
httpMethod: request.method.toUpperCase(),
|
|
97
|
+
identitySource: finalAuthorization,
|
|
98
|
+
methodArn,
|
|
80
99
|
multiValueHeaders: parseMultiValueHeaders(rawHeaders),
|
|
81
100
|
multiValueQueryStringParameters:
|
|
82
101
|
parseMultiValueQueryStringParameters(url),
|
|
83
102
|
path: request.path,
|
|
84
103
|
pathParameters: nullIfEmpty(pathParams),
|
|
85
104
|
queryStringParameters: parseQueryStringParameters(url),
|
|
86
|
-
|
|
105
|
+
requestContext: {
|
|
106
|
+
extendedRequestId: requestId,
|
|
107
|
+
httpMethod,
|
|
108
|
+
path: request.path,
|
|
109
|
+
protocol,
|
|
110
|
+
requestTime: currentDate.toString(),
|
|
111
|
+
requestTimeEpoch: currentDate.getTime(),
|
|
112
|
+
resourceId,
|
|
113
|
+
resourcePath,
|
|
114
|
+
stage: provider.stage,
|
|
115
|
+
},
|
|
116
|
+
resource: resourcePath,
|
|
87
117
|
}
|
|
88
|
-
}
|
|
89
|
-
const authorization = req.headers[identityHeader]
|
|
90
|
-
|
|
91
|
-
const identityValidationExpression = new RegExp(
|
|
92
|
-
authorizerOptions.identityValidationExpression,
|
|
93
|
-
)
|
|
94
|
-
const matchedAuthorization =
|
|
95
|
-
identityValidationExpression.test(authorization)
|
|
96
|
-
const finalAuthorization = matchedAuthorization ? authorization : ''
|
|
118
|
+
}
|
|
97
119
|
|
|
98
|
-
|
|
120
|
+
if (authorizerOptions.payloadVersion === '2.0') {
|
|
121
|
+
event = {
|
|
122
|
+
...event,
|
|
123
|
+
identitySource: [finalAuthorization],
|
|
124
|
+
rawPath: request.path,
|
|
125
|
+
rawQueryString: getRawQueryParams(url),
|
|
126
|
+
requestContext: {
|
|
127
|
+
http: {
|
|
128
|
+
method: httpMethod,
|
|
129
|
+
path: resourcePath,
|
|
130
|
+
protocol,
|
|
131
|
+
},
|
|
132
|
+
routeKey: resourceId,
|
|
133
|
+
time: currentDate.toString(),
|
|
134
|
+
timeEpoch: currentDate.getTime(),
|
|
135
|
+
},
|
|
136
|
+
routeArn: methodArn,
|
|
137
|
+
routeKey: resourceId,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
99
140
|
|
|
141
|
+
// methodArn is the ARN of the function we are running we are authorizing access to (or not)
|
|
142
|
+
// Account ID and API ID are not simulated
|
|
143
|
+
if (authorizerOptions.type === 'request') {
|
|
144
|
+
event = {
|
|
145
|
+
...event,
|
|
146
|
+
type: 'REQUEST',
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
// This is safe since type: 'TOKEN' cannot have payload format 2.0
|
|
100
150
|
event = {
|
|
101
151
|
...event,
|
|
102
|
-
authorizationToken: finalAuthorization,
|
|
103
152
|
type: 'TOKEN',
|
|
104
153
|
}
|
|
105
154
|
}
|
|
@@ -109,6 +158,25 @@ export default function createAuthScheme(authorizerOptions, provider, lambda) {
|
|
|
109
158
|
|
|
110
159
|
try {
|
|
111
160
|
const result = await lambdaFunction.runHandler()
|
|
161
|
+
|
|
162
|
+
if (authorizerOptions.enableSimpleResponses) {
|
|
163
|
+
if (result.isAuthorized) {
|
|
164
|
+
const authorizer = {
|
|
165
|
+
integrationLatency: '42',
|
|
166
|
+
...result.context,
|
|
167
|
+
}
|
|
168
|
+
return h.authenticated({
|
|
169
|
+
credentials: {
|
|
170
|
+
authorizer,
|
|
171
|
+
context: result.context || {},
|
|
172
|
+
},
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
return Boom.forbidden(
|
|
176
|
+
'User is not authorized to access this resource',
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
112
180
|
if (result === 'Unauthorized') return Boom.unauthorized('Unauthorized')
|
|
113
181
|
|
|
114
182
|
// Validate that the policy document has the principalId set
|
|
@@ -120,7 +188,12 @@ export default function createAuthScheme(authorizerOptions, provider, lambda) {
|
|
|
120
188
|
return Boom.forbidden('No principalId set on the Response')
|
|
121
189
|
}
|
|
122
190
|
|
|
123
|
-
if (
|
|
191
|
+
if (
|
|
192
|
+
!authCanExecuteResource(
|
|
193
|
+
result.policyDocument,
|
|
194
|
+
event.methodArn || event.routeArn,
|
|
195
|
+
)
|
|
196
|
+
) {
|
|
124
197
|
log.notice(
|
|
125
198
|
`Authorization response didn't authorize user to access resource: (λ: ${authFunName})`,
|
|
126
199
|
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import parseQueryStringParameters from './parseQueryStringParameters.js'
|
|
2
|
+
|
|
3
|
+
export default function getRawQueryParams(url) {
|
|
4
|
+
const queryParams = parseQueryStringParameters(url) || {}
|
|
5
|
+
return Object.keys(queryParams)
|
|
6
|
+
.reduce(function reducer(accumulator, currentKey) {
|
|
7
|
+
accumulator.push(`${currentKey}=${queryParams[currentKey]}`)
|
|
8
|
+
return accumulator
|
|
9
|
+
}, [])
|
|
10
|
+
.join('&')
|
|
11
|
+
}
|
package/src/utils/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export { default as formatToClfTime } from './formatToClfTime.js'
|
|
|
7
7
|
export { default as generateHapiPath } from './generateHapiPath.js'
|
|
8
8
|
export { default as getApiKeysValues } from './getApiKeysValues.js'
|
|
9
9
|
export { default as getHttpApiCorsConfig } from './getHttpApiCorsConfig.js'
|
|
10
|
+
export { default as getRawQueryParams } from './getRawQueryParams.js'
|
|
10
11
|
export { default as jsonPath } from './jsonPath.js'
|
|
11
12
|
export { default as lowerCaseKeys } from './lowerCaseKeys.js'
|
|
12
13
|
export { default as parseHeaders } from './parseHeaders.js'
|