Skip to content

Commit

Permalink
Add cross-account example (#12)
Browse files Browse the repository at this point in the history
* Add cross-account example

* Update README.md
  • Loading branch information
huntharo committed Jul 6, 2023
1 parent af8f6c4 commit 77dbebc
Show file tree
Hide file tree
Showing 5 changed files with 3,810 additions and 1,919 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ Disclaimer: perform your own pricing calculation, monitor your costs during and
3. Load `http://localhost:3001/login` in a browser
4. Observe that a cookie is returned and does not change

## [cross-account](./examples/cross-account)

This example has the DynamoDB in one account and the express app using an IAM role from another account to access the DynamoDB Table using temporary credentials from an STS AssumeRole call (neatly encapsulated by the AWS SDK for JS v3).

This example is more involved than the others as it requires setting up an IAM role that can be assumed by the app account.

[Instructions for Cross-Account DynamoDB Table Example](./examples/CROSS-ACCOUNT.md)

![Session Store with DynamoDB Table in Another Account](https://github.com/pwrdrvr/dynamodb-session-store/assets/5617868/dbc8d07b-b2f3-42c8-96c9-2476007ed24c)

## [express with dynamodb-connect module - for comparison](./examples/other)

1. Create DynamoDB Table using AWS Console or any other method
Expand Down
90 changes: 90 additions & 0 deletions examples/CROSS-ACCOUNT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Cross-Account Example Setup

There are a variety of ways to set this up, such as through the AWS Console, the AWS CLI, SAM, CDK, etc. For the purposes of demonstrating that the session renewal works automatically, we will use the AWS CLI.

## Setup in the Account that Owns the Table

Change `ACCOUNT_USAGE` to the account number that will be using the table.

Copy and paste the whole thing into a shell and it will create the table and the role.

```bash
export TABLE_NAME=dynamodb-session-store-test
export ACCOUNT_USAGE=123456789012
export ROLE_NAME_TABLE=dynamodb-session-store-test

# Create the table
aws dynamodb create-table --table-name ${TABLE_NAME} --attribute-definitions AttributeName=id,AttributeType=S --key-schema AttributeName=id,KeyType=HASH --billing-mode PAY_PER_REQUEST

# Save the table ARN
export TABLE_ARN=$(aws dynamodb describe-table --table-name ${TABLE_NAME} --query Table.TableArn --output text)

# Create the IAM role and allow all roles in the usage account to assume it
# Note: Don't use `root` as the trust policy in anything other than a demo account
aws iam create-role --role-name "${ROLE_NAME_TABLE}" --assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowAssumeRole",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::'"${ACCOUNT_USAGE}"':root"
},
"Action": "sts:AssumeRole"
}
]
}'

# Add inline policy to allow access to the table
aws iam put-role-policy --policy-name "dynamodb-access" --role-name "${ROLE_NAME_TABLE}" --policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowDynamoDBAccess",
"Effect": "Allow",
"Action": [
"dynamodb:DescribeTable",
"dynamodb:GetItem",
"dynamodb:UpdateItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem"
],
"Resource": "'"${TABLE_ARN}"'"
}
]
}'
```

## Run the Example App

In the same shell, change to the Usage Account.

Run the example app:

```bash
# Note: TABLE_ARN will be set to the full ARN of the table in the Table Account
# Note: TABLE_ROLE_ARN will be set to the full ARN of the role in the Table Account
npm run example:cross-account
```

## Hit the Login Route

http://localhost:3001/login

## Observations

- On AWS Console / CloudTrail / Event History
- In Table account
- Set event source to `sts.amazonaws.com`
- You will observe an AssumeRole operation that succeeds from the Usage Account
- When the session is about to expire, you will observe another AssumeRole operation that succeeds from the Usage Account
- On the node app console
- No errors will be observed when the session is renewed

## Cleaning Up Resources

```bash
aws iam delete-role-policy --role-name "${ROLE_NAME_TABLE}" --policy-name "dynamodb-access"
aws iam delete-role --role-name "${ROLE_NAME_TABLE}"
aws dynamodb delete-table --table-name "${TABLE_NAME}"
```
69 changes: 69 additions & 0 deletions examples/cross-account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/* eslint-disable no-console */
import session from 'express-session';
import { DynamoDBStore } from '@pwrdrvr/dynamodb-session-store';
import * as dynamodb from '@aws-sdk/client-dynamodb';
import { fromTemporaryCredentials } from '@aws-sdk/credential-providers';
import express from 'express';

const {
TABLE_ARN = 'dynamodb-session-store-test',
TABLE_ROLE_ARN = '',
PORT = '3001',
} = process.env;

const dynamoDBClient = new dynamodb.DynamoDBClient({
// fromTemporaryCredentials docs:
// https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-credential-providers/Variable/fromTemporaryCredentials/
// Note: this will auto-renew the session either before it expires or when it detects
// an unauthorized error (I haven't check the docs or code to confirm which, but I suspect on error)
credentials: fromTemporaryCredentials({
// Required. Options passed to STS AssumeRole operation.
params: {
// Required. ARN of role to assume.
RoleArn: TABLE_ROLE_ARN,
// Optional. An identifier for the assumed role session. If skipped, it generates a random
// session name with prefix of 'aws-sdk-js-'.
RoleSessionName: 'dynamodb-session-store-cross-account',
// Optional. The duration, in seconds, of the role session.
// Set to 900 seconds (15 minutes) to see the session get refreshed around 10 mins
DurationSeconds: 3600,
// ... For more options see https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
},
}),
});

const app = express();
const port = 3001;

app.use(
session({
store: new DynamoDBStore({
tableName: TABLE_ARN,
dynamoDBClient,
touchAfter: 60 * 5, // 5 minutes in seconds
}),
secret: 'yeah-dont-use-this',
cookie: {
maxAge: 60 * 60 * 1000, // one hour in milliseconds
},
resave: false,
saveUninitialized: false,
}),
);

// Add a fake login route that will set a session cookie
app.get('/login', (req, res) => {
console.log(`Session ID: ${req.session?.id}`);
// @ts-expect-error user is defined
req.session.user = 'test';
res.send('Logged in');
});

// Return a 200 response for all routes
app.get('/*', (req, res) => {
res.status(200).send('Hello world');
});

app.listen(Number.parseInt(PORT, 10), () => {
console.log(`Example app listening on port ${port}`);
});

0 comments on commit 77dbebc

Please sign in to comment.