@filen/aws4-express
Version:
Express middleware handlers for validation AWS Signature V4
276 lines (206 loc) • 10.6 kB
Markdown
# aws4-express
This library is about to create on security layer of your API using well defined comunication standard known as AWS Signature V4.
We provide Express middleware handler `awsVerify` for validation your `AWS Signature V4` with your access and secret pair of key. So, your web app can mimic AWS services, and you can use benefits from already well-defined standard to securing your web API.
At this moment, library is based on general version of aws4 signature:
[Authenticating Requests (AWS Signature Version 4)](https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html)
### Beta stage
Until we hit >=1.x.x, there could be possible breaking changes on minor changes.
Use at least 0.8.0 version.
## Install
```shell
npm install aws4-express
```
## Use
There are prepared helpers to handle the original body with any changes, because if you change a single character, your request won't be valid anymore.
If you use express parsers like `express.raw()` or `express.json()` or `express.urlencoded` you can attach with handler `rawBodyFromVerify`. You can also write own stream parser or use our `rawBodyFromStream`.
```typescript
import express from 'express';
import { awsVerify, rawBodyFromVerify, rawBodyFromStream } from 'aws4-express';
const app = express();
// whenever you may need to get original body string and you case
// when json parser u may use like this
app.use(
express.json({
type: '*/*',
verify: rawBodyFromVerify,
}),
);
// or when json parser u may use like this
app.use(
express.raw({
type: '*/*',
verify: rawBodyFromVerify,
})
);
// or when url encoded body u may use like this
app.use(
express.urlencoded({
extended: true,
type: '*/*',
verify: rawBodyFromVerify,
}),
);
// or events on when json parser u may use like this
app.use(rawBodyFromStream);
// main handler to authorization incomming requests:
app.use(awsVerify({
secretKey: (message, req, res, next) => {
// fetch secret key from your storage key/secret pairs (sql, nosql, memory)
// you have to provide your own secret provider here.
// retrun string | undefined
return getMySecretByKey(message.accessKey),
}
}));
// your routers ...
app.all('*', ...);
app.get('/get', ...);
app.post('/post', ...);
return app;
```
## Example:
- [simpleIntegration.ts](/src/examples/simpleIntegration.ts)
## Features:
- [x] General implementation standard: aws4 signature.
- [x] Fully customized.
- [x] No strict rules on services, regions you can name it as you want as long your signing client support this.
- [x] Single chunk request
- [x] Tests with client: [aws4](https://www.npmjs.com/package/aws4)
- [ ] Query headers x-amz-*,
- [ ] Multiple chunks (no x-amz-decoded-content-length)
## Supported headers:
- `authorization` - [required] must have in proper format: **Authorization: AWS4-HMAC-SHA256
Credential=`ACCESS_KEY`/`DATE`/`REGION`/`SERVICE`/`TYPE_REQUEST`,
SignedHeaders=< SIGNED_HEADERS>,
Signature=`SIGNATURE`** :
* `ACCESS_KEY` - any text without whitespaces and slashes (/) - Only have to do is handle distribution of access_key, secret_key and these keys have to be accessible on the server side.
* `DATE` - is part of X-AMZ-DATE: in format YYYYMMDD.
* `REGION` - any thing you need in this place or use something from [amz regions](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html).
* `SERVICE` - any thing you need in this place or 'execute-api' for sake of simplicity.
* `TYPE_REQUEST` - you can use your variations instead of standard 'aws4_request'.
* `SIGNED_HEADERS` - all signed headers - more headers mean harder to temper request. Required headers at this moment: *host:x-amz-date*
* `SIGNATURE` - calculated signature based on [Authenticating Requests (AWS Signature Version 4)](https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html)
- `x-amz-date` - [required] must have in request to valid request signature
- `x-amz-content-sha256` - [optional] you can attach precalculated hash. When X-Amz-Content-Sha256 is sent we skip calculating hash from body. This way is less secure and recommended use at least with `X-Amz-Expires`.
* There can provide your validation whenever you want handle this header `onBeforeParse` or `onAfterParse`.
* You can also send `UNSIGNED-PAYLOAD` instead of sha-256 signature - this cloud speed up your bigger request, but signature will be same as long as headers remain same.
* You can put your signature (most client don't include these headers) - should be calculated in this way:
```
crypto.createHash('sha256').update(data, 'utf8').digest('hex')
```
- `x-amz-expires` - [optional] - format: `YYYY-mm-ddTHH:MM:SS`. If you want valid your request for a period of time and don't want to reuse signature when time is up.
Pull Requests are welcome.
## Documentation
# awsVerify Function Documentation
## Overview
The `awsVerify` function is an Express.js middleware that verifies AWS signatures in incoming requests.
## Usage
```javascript
import { awsVerify } from 'aws4-express';
app.use(awsVerify(options));
```
## Parameters
- `options`: An object of type `AwsVerifyOptions`
## Return Value
Returns an Express.js middleware function.
## Configuration: AwsVerifyOptions
The `AwsVerifyOptions` object can contain the following fields:
### secretKey (required)
- Type: `(message: AwsIncomingMessage, req: Request, res: Response, next: NextFunction) => Promise<string | undefined> | string | undefined`
- Description: Callback for retrieving the secret key. Should return the secret key based on incoming parameters or `undefined` if the key is not available.
### headers (optional)
- Type: `(headers: Dictionary) => Promise<Dictionary> | Dictionary`
- Description: Callback for modifying incoming headers before the parsing process.
### enabled (optional)
- Type: `(req: Request) => Promise<boolean> | boolean`
- Description: Function determining whether AWS signature validation should be performed. If it returns `false`, validation will be skipped.
### onMissingHeaders (optional)
- Type: `(req: Request, res: Response, next: NextFunction) => Promise<void> | void`
- Description: Custom response when required headers are missing.
- Default: Sends a 400 status with the message "Required headers are missing".
### onSignatureMismatch (optional)
- Type: `(req: Request, res: Response, next: NextFunction) => Promise<void> | void`
- Description: Custom response when the signature doesn't match.
- Default: Sends a 401 status with the message "The signature does not match".
### onExpired (optional)
- Type: `(req: Request, res: Response, next: NextFunction) => Promise<void> | void`
- Description: Custom response when the signature has expired.
- Default: Sends a 401 status with the message "Request is expired".
### onBeforeParse (optional)
- Type: `(req: Request, res: Response, next: NextFunction) => Promise<boolean> | boolean`
- Description: Callback invoked before the standard parser. If it returns `false`, validation will be stopped.
- Default: Always returns `true`.
### onAfterParse (optional)
- Type: `(message: AwsIncomingMessage, req: Request, res: Response, next: NextFunction) => Promise<boolean> | boolean`
- Description: Callback invoked after the standard parser completes. If it returns `false`, validation will be stopped.
- Default: Always returns `true`.
### onSuccess (optional)
- Type: `(message: AwsIncomingMessage | undefined, req: Request, res: Response, next: NextFunction) => Promise<void> | void`
- Description: Callback invoked after successful signature validation.
- Default: Calls `next()`.
## Example
```javascript
import express from 'express';
import { awsVerify, rawBodyFromVerify } from 'aws4-express';
const app = express();
app.use(express.json({ type: '*/*', verify: rawBodyFromVerify }));
app.use(awsVerify({
secretKey: async (message) => {
// Implement secret key retrieval logic
return 'your_secret_key';
},
onSignatureMismatch: (req, res) => {
res.status(403).send('Unauthorized');
}
}));
app.get('/', (req, res) => {
res.send('Hello, AWS-verified world!');
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
```
In this example, the `awsVerify` middleware is used to verify the AWS signature for all incoming requests. A custom `secretKey` function is used to provide the secret key, and `onSignatureMismatch` defines a custom response for signature mismatch cases.
### awsVerify:
#### Complete options configuration for `awsVerify`:
```typescript
express.use(awsVerify({
secretKey: (message: AwsIncomingMessage, req: Request, res: Response, next: NextFunction) => Promise<string | undefined> | string | undefined;
headers?: (headers: Dictionary) => Promise<Dictionary> | Dictionary;
enabled?: (req: Request) => Promise<boolean> | boolean;
onMissingHeaders?: (req: Request, res: Response, next: NextFunction) => Promise<void> | void;
onSignatureMismatch?: (req: Request, res: Response, next: NextFunction) => Promise<void> | void;
onExpired?: (req: Request, res: Response, next: NextFunction) => Promise<void> | void;
onBeforeParse?: (req: Request, res: Response, next: NextFunction) => Promise<boolean> | boolean;
onAfterParse?: (
message: AwsIncomingMessage,
req: Request,
res: Response,
next: NextFunction,
) => Promise<boolean> | boolean;
onSuccess?: (
message: AwsIncomingMessage | undefined,
req: Request,
res: Response,
next: NextFunction,
) => Promise<void> | void;
}))
```
#### Default values for all optional configuration for `awsVerify`:
```typescript
{
enabled: () => true,
headers: (req) => req.headers,
onExpired: (res) => {
res.status(401).send('Request is expired');
},
onMissingHeaders: (res) => {
res.status(400).send('Required headers are missing');
},
onSignatureMismatch: (res) => {
res.status(401).send('The signature does not match');
},
onBeforeParse: () => true,
onAfterParse: () => true,
onSuccess: () => next()
}
```