├── .gitattributes
├── docs
├── multi-region.png
└── single-region.png
├── src
├── utils
│ └── response.js
└── lambdas
│ ├── decode
│ └── handler.js
│ ├── login
│ └── handler.js
│ └── register
│ └── handler.js
├── .github
└── workflows
│ └── main.yml
├── LICENSE
├── package.json
├── .gitignore
├── CONTRIBUTING.md
├── serverless.yml
└── README.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/docs/multi-region.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GagePielsticker/AWS-Auth-Template/HEAD/docs/multi-region.png
--------------------------------------------------------------------------------
/docs/single-region.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GagePielsticker/AWS-Auth-Template/HEAD/docs/single-region.png
--------------------------------------------------------------------------------
/src/utils/response.js:
--------------------------------------------------------------------------------
1 |
2 | function formatResponse (data, stat) {
3 | let statusCode
4 |
5 | if (!stat) statusCode = 200
6 | else statusCode = stat
7 |
8 | return {
9 | statusCode,
10 | headers: {
11 | 'Content-Type': 'application/json',
12 | 'Access-Control-Allow-Origin':'*'
13 | },
14 | body: JSON.stringify(
15 | {
16 | region: process.env.AWS_REGION,
17 | data
18 | }
19 | )
20 | }
21 | }
22 |
23 | module.exports = {
24 | formatResponse
25 | }
26 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Deployer
2 |
3 | on:
4 | push:
5 | branches:
6 | - main # Change this to your desired branch name
7 |
8 | jobs:
9 | build-and-deploy:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout repository
14 | uses: actions/checkout@v2
15 |
16 | - name: Set up Node.js
17 | uses: actions/setup-node@v2
18 | with:
19 | node-version: '16' # Change this to your desired Node.js version
20 |
21 | - name: Configure AWS Credentials
22 | uses: aws-actions/configure-aws-credentials@v2
23 | with:
24 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
25 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
26 | aws-region: us-east-1
27 |
28 | - name: Install dependencies with pnpm
29 | run: npm i
30 |
31 | - name: Run npm deploy
32 | run: npm run deploy
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 James Gage Pielsticker
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/lambdas/decode/handler.js:
--------------------------------------------------------------------------------
1 | /* Coldstart dependency loading */
2 | const { formatResponse } = require('../../utils/response')
3 | const { string, object } = require('yup')
4 | const jwt = require('jsonwebtoken')
5 |
6 | /* Invoke handler */
7 | exports.handler = async (event, context, callback) => {
8 | const params = event?.queryStringParameters
9 | const body = JSON.parse(event.body)
10 | console.log(`Lambda Invoked with params:\n${params ? JSON.stringify(params, null, 4) : 'NONE'}`)
11 |
12 | // Input Validation
13 | const inputSchema = object({
14 | jwt: string()
15 | })
16 |
17 | try {
18 | await inputSchema.validate(body)
19 | } catch (error) {
20 | console.log(`Error validating input :: ${error}.`)
21 | return callback(null, formatResponse({ error: `Invalid Input. ${error}` }, 409))
22 | }
23 |
24 | // Decode our JWT
25 | try {
26 | var decodedToken = jwt.verify(body.jwt, process.env.JWT_KEY)
27 | } catch (error) {
28 | console.log(`Invalid JWT :: ${error}`)
29 | return callback(null, formatResponse({ error: 'Could not validate JWT token.' }, 403))
30 | }
31 |
32 | // Return data
33 | const data = {
34 | status: 'Successfully validated JWT.',
35 | decodedJWT: decodedToken
36 | }
37 |
38 | return callback(null, formatResponse(data))
39 | }
40 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "aws-auth-template",
3 | "version": "1.0.0",
4 | "description": "A serverless template created as an example of user signup & JWT authentication in AWS using DynamoDB, Lambdas, WAFv2, and API-Gateway",
5 | "main": "index.js",
6 | "scripts": {
7 | "build": "serverless package",
8 | "deploy": "serverless deploy --stage prod --region us-east-1"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/GagePielsticker/AWS-Auth-Template.git"
13 | },
14 | "keywords": [
15 | "aws",
16 | "serverless",
17 | "api",
18 | "service",
19 | "template",
20 | "boilerplate",
21 | "jwt",
22 | "jwt-tokens"
23 | ],
24 | "author": "James Gage Pielsticker",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/GagePielsticker/AWS-Auth-Template/issues"
28 | },
29 | "homepage": "https://github.com/GagePielsticker/AWS-Auth-Template#readme",
30 | "devDependencies": {
31 | "serverless": "^3.30.0",
32 | "serverless-associate-waf": "^1.2.1",
33 | "serverless-deployment-bucket": "^1.6.0",
34 | "serverless-offline": "^12.0.4",
35 | "standard": "^17.0.0"
36 | },
37 | "dependencies": {
38 | "aws-sdk": "^2.1428.0",
39 | "bcryptjs": "^2.4.3",
40 | "jsonwebtoken": "^9.0.1",
41 | "uuid": "^9.0.0",
42 | "yup": "^1.2.0"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 |
107 | # Serverless directories
108 | .serverless/
109 |
110 | # FuseBox cache
111 | .fusebox/
112 |
113 | # DynamoDB Local files
114 | .dynamodb/
115 |
116 | # TernJS port file
117 | .tern-port
118 |
119 | # Stores VSCode versions used for testing VSCode extensions
120 | .vscode-test
121 |
122 | # yarn v2
123 | .yarn/cache
124 | .yarn/unplugged
125 | .yarn/build-state.yml
126 | .yarn/install-state.gz
127 | .pnp.*
128 |
--------------------------------------------------------------------------------
/src/lambdas/login/handler.js:
--------------------------------------------------------------------------------
1 | /* Coldstart dependency loading */
2 | const { formatResponse } = require('../../utils/response')
3 |
4 | const { DynamoDBClient, QueryCommand } = require('@aws-sdk/client-dynamodb')
5 | const { marshall, unmarshall } = require('@aws-sdk/util-dynamodb')
6 | const dbClient = new DynamoDBClient({ region: process.env.AWS_REGION })
7 |
8 | const { string, object } = require('yup')
9 |
10 | const bcrypt = require('bcryptjs')
11 | const jwt = require('jsonwebtoken')
12 |
13 | /* Invoke handler */
14 | exports.handler = async (event, context, callback) => {
15 | const params = event?.queryStringParameters
16 | const body = JSON.parse(event.body)
17 | console.log(`Lambda Invoked with params:\n${params ? JSON.stringify(params, null, 4) : 'NONE'}`)
18 |
19 | // Input Validation
20 | const inputSchema = object({
21 | email: string().email(),
22 | password: string()
23 | })
24 |
25 | try {
26 | await inputSchema.validate(body)
27 | } catch (error) {
28 | console.log(`Error validating input :: ${error}.`)
29 | return callback(null, formatResponse({ error: `Invalid Input. ${error}` }, 409))
30 | }
31 |
32 | body.email = body.email.toLowerCase() // Change our email to lowercase since we check against it for pre-existing users
33 |
34 | // Get user from database
35 | const dbQuery = {
36 | TableName: process.env.DYNAMO_TABLE_NAME,
37 | IndexName: process.env.USER_INDEX,
38 | KeyConditionExpression: 'email = :email',
39 | ExpressionAttributeValues: marshall({
40 | ':email': body.email
41 | })
42 | }
43 |
44 | try {
45 | const queryCommand = new QueryCommand(dbQuery)
46 | const queryResponse = await dbClient.send(queryCommand)
47 |
48 | if (queryResponse.Items && queryResponse.Items.length > 0) {
49 | var userObject = unmarshall(queryResponse.Items[0])
50 | console.log('Found email in database.')
51 | } else {
52 | return callback(null, formatResponse({ error: 'No user with that email/password combination exist.' }, 403))
53 | }
54 | } catch (error) {
55 | console.log(`Error checking user in database:: ${error}.`)
56 | return callback(null, formatResponse({ error: 'Internal Service Error.' }, 500))
57 | }
58 |
59 | // Validate Password Is Correct
60 | try {
61 | const bcryptResult = bcrypt.compareSync(body.password, userObject.password)
62 | if (!bcryptResult) return callback(null, formatResponse({ error: 'No user with that email/password combination exist.' }, 403))
63 | console.log('validated Password.')
64 | } catch (error) {
65 | console.log(`Error checking password:: ${error}.`)
66 | return callback(null, formatResponse({ error: 'Internal Service Error' }, 500))
67 | }
68 |
69 | // Create JWT
70 | try {
71 | var userToken = jwt.sign(
72 | {
73 | userid: userObject.userid,
74 | email: userObject.email
75 | },
76 | process.env.JWT_KEY, // Random string our app will encrypt/decrypt jwt with. Technically sensitive so should be in secrets manager and auto-rotate.
77 | {
78 | expiresIn: process.env.JWT_EXPIRY
79 | }
80 | )
81 | console.log('Successfully generated JWT.')
82 | } catch (error) {
83 | console.log(`Error when generating JWT :: ${error}.`)
84 | return callback(null, formatResponse({ error: 'Internal Service Error.' }, 500))
85 | }
86 |
87 | // Return data
88 | const data = {
89 | status: 'Successfully logged in.',
90 | jwt: `${userToken}`
91 | }
92 |
93 | return callback(null, formatResponse(data))
94 | }
95 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | When contributing to this repository, please first discuss the change you wish to make via issue,
4 | email, or any other method with the owners of this repository before making a change.
5 |
6 | Please note we have a code of conduct, please follow it in all your interactions with the project.
7 |
8 | ## Pull Request Process
9 |
10 | 1. Ensure that you are following [StandardJS](https://standardjs.com/) principles. This can be quickly achieved running `standard --fix`
11 | 2. Increase the version numbers in any examples files and the README.md to the new version that this
12 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
13 | 3. All Pull Request will be merged by myself.
14 |
15 | ## Code of Conduct
16 |
17 | ### Our Pledge
18 |
19 | In the interest of fostering an open and welcoming environment, we as
20 | contributors and maintainers pledge to making participation in our project and
21 | our community a harassment-free experience for everyone, regardless of age, body
22 | size, disability, ethnicity, gender identity and expression, level of experience,
23 | nationality, personal appearance, race, religion, or sexual identity and
24 | orientation.
25 |
26 | ### Our Standards
27 |
28 | Examples of behavior that contributes to creating a positive environment
29 | include:
30 |
31 | * Using welcoming and inclusive language
32 | * Being respectful of differing viewpoints and experiences
33 | * Gracefully accepting constructive criticism
34 | * Focusing on what is best for the community
35 | * Showing empathy towards other community members
36 |
37 | Examples of unacceptable behavior by participants include:
38 |
39 | * The use of sexualized language or imagery and unwelcome sexual attention or
40 | advances
41 | * Trolling, insulting/derogatory comments, and personal or political attacks
42 | * Public or private harassment
43 | * Publishing others' private information, such as a physical or electronic
44 | address, without explicit permission
45 | * Other conduct which could reasonably be considered inappropriate in a
46 | professional setting
47 |
48 | ### Our Responsibilities
49 |
50 | Project maintainers are responsible for clarifying the standards of acceptable
51 | behavior and are expected to take appropriate and fair corrective action in
52 | response to any instances of unacceptable behavior.
53 |
54 | Project maintainers have the right and responsibility to remove, edit, or
55 | reject comments, commits, code, wiki edits, issues, and other contributions
56 | that are not aligned to this Code of Conduct, or to ban temporarily or
57 | permanently any contributor for other behaviors that they deem inappropriate,
58 | threatening, offensive, or harmful.
59 |
60 | ### Scope
61 |
62 | This Code of Conduct applies both within project spaces and in public spaces
63 | when an individual is representing the project or its community. Examples of
64 | representing a project or community include using an official project e-mail
65 | address, posting via an official social media account, or acting as an appointed
66 | representative at an online or offline event. Representation of a project may be
67 | further defined and clarified by project maintainers.
68 |
69 | ### Enforcement
70 |
71 | All
72 | complaints will be reviewed and investigated and will result in a response that
73 | is deemed necessary and appropriate to the circumstances. The project team is
74 | obligated to maintain confidentiality with regard to the reporter of an incident.
75 | Further details of specific enforcement policies may be posted separately.
76 |
77 | Project maintainers who do not follow or enforce the Code of Conduct in good
78 | faith may face temporary or permanent repercussions as determined by other
79 | members of the project's leadership.
80 |
81 | ### Attribution
82 |
83 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
84 | available at [http://contributor-covenant.org/version/1/4][version]
85 |
86 | [homepage]: http://contributor-covenant.org
87 | [version]: http://contributor-covenant.org/version/1/4/
--------------------------------------------------------------------------------
/src/lambdas/register/handler.js:
--------------------------------------------------------------------------------
1 | /* Coldstart dependency loading */
2 | const { formatResponse } = require('../../utils/response')
3 |
4 | const { DynamoDBClient, PutItemCommand, QueryCommand } = require('@aws-sdk/client-dynamodb')
5 | const { marshall } = require('@aws-sdk/util-dynamodb')
6 | const dbClient = new DynamoDBClient({ region: process.env.AWS_REGION })
7 |
8 | const { v4: uuidv4 } = require('uuid')
9 | const bcrypt = require('bcryptjs')
10 | const { string, object } = require('yup')
11 |
12 | const jwt = require('jsonwebtoken')
13 |
14 | /* Invoke handler */
15 | exports.handler = async (event, context, callback) => {
16 | const params = event?.queryStringParameters
17 | const body = JSON.parse(event.body)
18 | console.log(`Lambda Invoked with params:\n${params ? JSON.stringify(params, null, 4) : 'NONE'}`)
19 |
20 | // Input Validation
21 | const usernameMaxLength = 20
22 |
23 | const inputSchema = object({
24 | email: string().email(),
25 | username: string().max(20),
26 | password: string()
27 | })
28 |
29 | try {
30 | await inputSchema.validate(body)
31 | } catch (error) {
32 | console.log(`Error validating input :: ${error}.`)
33 | return callback(null, formatResponse({ error: `Invalid Input. ${error}` }, 409))
34 | }
35 |
36 | body.email = body.email.toLowerCase() // Change our email to lowercase since we check against it for pre-existing users
37 |
38 | // Validate email doesnt already exist in database
39 | const dbQuery = {
40 | TableName: process.env.DYNAMO_TABLE_NAME,
41 | IndexName: process.env.USER_INDEX,
42 | KeyConditionExpression: 'email = :email',
43 | ExpressionAttributeValues: marshall({
44 | ':email': body.email
45 | })
46 | }
47 |
48 | try {
49 | const queryCommand = new QueryCommand(dbQuery)
50 | const queryResponse = await dbClient.send(queryCommand)
51 |
52 | if (queryResponse.Items && queryResponse.Items.length > 0) {
53 | return callback(null, formatResponse({ error: 'User with this email already exist.' }, 409))
54 | }
55 | } catch (error) {
56 | console.log(`Error checking user in database:: ${error}.`)
57 | return callback(null, formatResponse({ error: 'Internal Service Error.' }, 500))
58 | }
59 |
60 | // Hash our password
61 | try {
62 | var passwordHash = await bcrypt.hash(body.password, 10)
63 | console.log('Password Successfully Hashed.')
64 | } catch (error) {
65 | console.log(`Error hashing password :: ${error}.`)
66 | return callback(null, formatResponse({ error: 'Internal Service Error.' }, 500))
67 | }
68 |
69 | // Create DynamoDB Object
70 | const dbInput = {
71 | TableName: process.env.DYNAMO_TABLE_NAME,
72 | Item: {
73 | userid: { S: uuidv4() }, // primary key (unchangeable)
74 | email: { S: body.email }, // secondary index key
75 | username: { S: body.username },
76 | password: { S: passwordHash },
77 | createdOn: { N: `${+new Date()}` } // You must send numbers to Dynamo as strings, however dynamo will treat it as a number for maths
78 | }
79 | }
80 |
81 | // Insert into DynamoDB
82 | try {
83 | const putCommand = new PutItemCommand(dbInput)
84 | await dbClient.send(putCommand)
85 | console.log(`Successfully added user ${body.username} to database.`)
86 | } catch (error) {
87 | console.log(`Error when adding user to database :: ${error}.`)
88 | return callback(null, formatResponse({ error: 'Internal Service Error.' }, 500))
89 | }
90 |
91 | // Generate our JWT to send to the user
92 | try {
93 | var userToken = jwt.sign(
94 | {
95 | userid: dbInput.Item.userid.S,
96 | email: dbInput.Item.email.S
97 | },
98 | process.env.JWT_KEY, // Random string our app will encrypt/decrypt jwt with. Technically sensitive so should be in secrets manager and auto-rotate.
99 | {
100 | expiresIn: process.env.JWT_EXPIRY
101 | }
102 | )
103 | console.log('Successfully generated JWT.')
104 | } catch (error) {
105 | console.log(`Error when generating JWT :: ${error}.`)
106 | return callback(null, formatResponse({ error: 'Internal Service Error.' }, 500))
107 | }
108 |
109 | // Form Response
110 | const data = {
111 | status: 'Successfully created user!',
112 | jwt: `${userToken}`
113 | }
114 |
115 | // Send response
116 | return callback(null, formatResponse(data))
117 | }
118 |
--------------------------------------------------------------------------------
/serverless.yml:
--------------------------------------------------------------------------------
1 | service: auth-service
2 |
3 | ###################
4 | # General Configuration
5 | ###################
6 |
7 | plugins:
8 | - serverless-offline
9 | - serverless-deployment-bucket
10 | - serverless-associate-waf
11 |
12 | provider:
13 |
14 | ### General Deployment Settings
15 | name: aws
16 | stage: ${opt:stage, 'dev'}
17 | region: ${opt:region, 'us-east-1'}
18 |
19 | stackTags:
20 | project: ${self:service}
21 | tags:
22 | project: ${self:service}
23 |
24 | deploymentBucket:
25 | name: ${self:service}-${self:provider.stage}-deployment-641 ##MUST BE UNIQUE IN EACH REGION
26 |
27 | ### Lambda Settings
28 |
29 | ### Global environment Vars for lambdas
30 | environment:
31 | DYNAMO_TABLE_NAME: 'userTable'
32 | USER_INDEX: 'EmailIndex' #Secondary Index we can search users by. (We want to be able to pull up user by their email)
33 | JWT_KEY: 'q35ZQKcD5HXK8ZAIv1wVR91EEhFeQnPs' #Random string to generate JWT with
34 | JWT_EXPIRY: '1d'
35 |
36 | runtime: nodejs18.x #node version
37 | memorySize: 512 # optional, in MB, default is 1024
38 | timeout: 20 # optional, in seconds, default is 6
39 | architecture: arm64 #architecture to run the lambda (graviton2)
40 | logRetentionInDays: 7 #time in days to keep lambda logs in cloudwatch
41 | tracing: # X-Ray tracing on lambda & apigateway
42 | lambda: true
43 | apiGateway: true
44 |
45 | ### Give Lambda permission to dynamodb
46 | iamRoleStatements:
47 | - Effect: Allow
48 | Action:
49 | - dynamodb:DescribeTable
50 | - dynamodb:Query
51 | - dynamodb:Scan
52 | - dynamodb:GetItem
53 | - dynamodb:PutItem
54 | - dynamodb:UpdateItem
55 | - dynamodb:DeleteItem
56 | Resource:
57 | - !Sub arn:aws:dynamodb:*:${AWS::AccountId}:table/${self:provider.environment.DYNAMO_TABLE_NAME}
58 | - !Sub arn:aws:dynamodb:*:${AWS::AccountId}:table/${self:provider.environment.DYNAMO_TABLE_NAME}/index/${self:provider.environment.USER_INDEX}
59 |
60 | ### API Gateway creation/Settings & usage plan
61 | endpointType: REGIONAL
62 | apiGateway:
63 | description: Api Gateway for ${self:service}-${self:provider.stage}
64 |
65 | # attach waf to api gateway
66 | custom:
67 | associateWaf:
68 | name: ${self:service}-${self:provider.stage}-WAF
69 | version: V2 #(optional) Regional | V2
70 | deploymentBucket:
71 | blockPublicAccess: true
72 | tags:
73 | - Key: project
74 | Value: ${self:service}
75 |
76 |
77 | ###################
78 | # Lambda Functions to provision
79 | ###################
80 | # Lambda Packaging Options
81 | package:
82 | individually: true
83 | patterns:
84 | - '!**/*'
85 | - node_modules/**
86 | - src/utils/** #Global lambda utils
87 | - package.json
88 |
89 | # Actual lambda functions
90 | functions:
91 | register:
92 | handler: src/lambdas/register/handler.handler #fileName.exportName in code
93 | maximumRetryAttempts: 1 #Retry attempts by lambda on failure
94 | package: # Only package code relevant to this specific lambda
95 | patterns:
96 | - src/lambdas/register/**
97 | events: #API Gateway integration etc
98 | - http:
99 | method: post
100 | path: user/register
101 | private: true #require api key
102 | cors: #cors settings
103 | origin: '*'
104 |
105 | login:
106 | handler: src/lambdas/login/handler.handler
107 | maximumRetryAttempts: 1
108 | package:
109 | patterns:
110 | - src/lambdas/login/**
111 | events:
112 | - http:
113 | method: post
114 | path: user/login
115 | private: true
116 | cors:
117 | origin: '*'
118 |
119 | decode:
120 | handler: src/lambdas/decode/handler.handler
121 | maximumRetryAttempts: 1
122 | package:
123 | patterns:
124 | - src/lambdas/decode/**
125 | events:
126 | - http:
127 | method: post
128 | path: user/decode
129 | private: true
130 | cors:
131 | origin: '*'
132 |
133 | ###################
134 | # Additional Resource Provisioning
135 | ###################
136 | resources:
137 | Resources:
138 |
139 | # DynamoDB for user data
140 | # Check out https://github.com/sbstjn/serverless-dynamodb-autoscaling for autoscaling the capacity units
141 | # Check out https://www.serverless.com/plugins/serverless-create-global-dynamodb-table/ for multi-region
142 | # We keep put our email as a secondary index so that we can preserve the ability to change it if the user wishes.
143 | usersTable:
144 | Type: AWS::DynamoDB::Table
145 | Properties:
146 | TableName: ${self:provider.environment.DYNAMO_TABLE_NAME}
147 | AttributeDefinitions:
148 | - AttributeName: userid
149 | AttributeType: S
150 | - AttributeName: email
151 | AttributeType: S # Define 'email' attribute with the desired type (S: String, N: Number, etc.)
152 | KeySchema:
153 | - AttributeName: userid
154 | KeyType: HASH
155 | ProvisionedThroughput:
156 | ReadCapacityUnits: 1
157 | WriteCapacityUnits: 1
158 | GlobalSecondaryIndexes: # Define the secondary index
159 | - IndexName: ${self:provider.environment.USER_INDEX}
160 | KeySchema:
161 | - AttributeName: email
162 | KeyType: HASH
163 | Projection:
164 | ProjectionType: ALL # Change this to specify what attributes to include in the index
165 | ProvisionedThroughput:
166 | ReadCapacityUnits: 1
167 | WriteCapacityUnits: 1
168 |
169 | # WAF for our api gateway
170 | wafv2:
171 | Type: AWS::WAFv2::WebACL
172 | Properties:
173 | DefaultAction:
174 | Allow: {}
175 | Name: ${self:service}-${self:provider.stage}-WAF
176 | Scope: REGIONAL
177 | VisibilityConfig:
178 | CloudWatchMetricsEnabled: False
179 | MetricName: ${self:service}-${self:provider.stage}
180 | SampledRequestsEnabled: False
181 | Tags:
182 | - Key: project
183 | Value: ${self:service}
184 | Rules:
185 | - Name: Core-Rule-Set
186 | OverrideAction:
187 | None: {}
188 | Priority: 0
189 | VisibilityConfig:
190 | CloudWatchMetricsEnabled: False
191 | MetricName: ${self:service}-${self:provider.stage}-WAF
192 | SampledRequestsEnabled: False
193 | Statement:
194 | ManagedRuleGroupStatement:
195 | VendorName: AWS
196 | Name: AWSManagedRulesCommonRuleSet
197 | - Name: Known-Bad-Input
198 | OverrideAction:
199 | None: {}
200 | Priority: 1
201 | VisibilityConfig:
202 | CloudWatchMetricsEnabled: False
203 | MetricName: ${self:service}-${self:provider.stage}-WAF
204 | SampledRequestsEnabled: False
205 | Statement:
206 | ManagedRuleGroupStatement:
207 | VendorName: AWS
208 | Name: AWSManagedRulesKnownBadInputsRuleSet
209 | - Name: Anonymous-IP
210 | OverrideAction:
211 | None: {}
212 | Priority: 2
213 | VisibilityConfig:
214 | CloudWatchMetricsEnabled: False
215 | MetricName: ${self:service}-${self:provider.stage}-WAF
216 | SampledRequestsEnabled: False
217 | Statement:
218 | ManagedRuleGroupStatement:
219 | VendorName: AWS
220 | Name: AWSManagedRulesAnonymousIpList
221 | - Name: IP-Reputation
222 | OverrideAction:
223 | None: {}
224 | Priority: 3
225 | VisibilityConfig:
226 | CloudWatchMetricsEnabled: False
227 | MetricName: ${self:service}-${self:provider.stage}-WAF
228 | SampledRequestsEnabled: False
229 | Statement:
230 | ManagedRuleGroupStatement:
231 | VendorName: AWS
232 | Name: AWSManagedRulesAmazonIpReputationList
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AWS Serverless JWT Template
2 | 
3 |
4 | > A template for starting an AWS based serverless api which integrates JWT authorization and user registration. This utilizes Lambda, api-gateway, WAFv2, and DynamoDB to build a scalable and highly-available rest api. Deployment of all infrastructure fully automated via serverless framework :)
5 |
6 | - [AWS Serverless JWT Template](#aws-serverless-jwt-template)
7 | - [Architecture](#architecture)
8 | - [REST Documentation](#rest-documentation)
9 | - [`POST /user/register`](#post-userregister)
10 | - [`POST /user/login`](#post-userlogin)
11 | - [`POST /user/decode`](#post-userdecode)
12 | - [Adding new lambdas](#adding-new-lambdas)
13 | - [Adding Environment Variables](#adding-environment-variables)
14 | - [Caching](#caching)
15 | - [Configuring API Gateway \& Limits](#configuring-api-gateway--limits)
16 | - [Deployment](#deployment)
17 | - [Dev Dependencies](#dev-dependencies)
18 | - [App Dependencies](#app-dependencies)
19 | - [Contributing](#contributing)
20 |
21 | ## Architecture
22 | Below is a single region deployment example. All resources here are automatically provisioned and configured via serverless framework and deployed to the configured region.
23 |
24 |
25 |
26 | Below is a multi-region deployment for high availability in the unlikely event of an AWS regional failure. This also reduces geographic latency to each region deployed. To achieve this active-active behavior we must manually configure a `route53 zone` & `api gateway custom domains`. You then apply 2 `latency based` routing policies to point at each api gateaway via `A record`. Then attach health-checks to each policy to ensure that the services are healthy in that region. You will also need to configure DynamoDB to use a [global table](https://aws.amazon.com/dynamodb/global-tables/)
27 |
28 |
29 |
30 | Alternatively you could setup a cloudfront distribution with an origin group that contains both api-gateways that fails over on 4xx-5xx. This active-passive pattern would also allow multi-region failover without the need of route53 or a custom domain.
31 |
32 | ## REST Documentation
33 | ### `POST /user/register`
34 |
35 | Input: `body (json)`
36 |
37 | ```json
38 | {
39 | "email": "String (must be valid format)",
40 | "username": "String (under 20 characters)",
41 | "password": "String"
42 | }
43 | ```
44 | Example Output
45 | ```json
46 | {
47 | "status": "Successfully created user!",
48 | "jwt": "xxxxxxxxxxxxxxxxx"
49 | }
50 | ```
51 | ### `POST /user/login`
52 |
53 | Input: `body (json)`
54 |
55 | ```json
56 | {
57 | "email": "String (must be valid format)",
58 | "password": "String"
59 | }
60 | ```
61 | Example Output
62 | ```json
63 | {
64 | "status": "Successfully logged in.",
65 | "jwt": "xxxxxxxxxxxxxxxxx"
66 | }
67 | ```
68 |
69 | ### `POST /user/decode`
70 |
71 | Input: `body (json)`
72 |
73 | ```json
74 | {
75 | "jwt": "String (must be valid jwt)"
76 | }
77 | ```
78 | Example Output
79 | ```json
80 | {
81 | "status": "Successfully validated JWT.",
82 | "decodedJWT": {
83 | "userid": "xxxxx-xxxx-xxx-x",
84 | "email": "test@test.com",
85 | "iat": 1691089483,
86 | "exp": 1691175883
87 | }
88 | }
89 | ```
90 |
91 | ## Adding new lambdas
92 | Adding lambdas is fairly straight forward. Simply copy one of the lambda folders inside of `/src/lambdas/` and modify the handler code as needed. Note that the handler must return the callback and the data should be in a `json` format. Once that is complete open `serverless.yml` and navigate to the `functions:` section. This is where we specify the file to use in that lambda as well as how it connects to api-gateway.
93 |
94 | Example config:
95 | ```yaml
96 | functions:
97 | hello:
98 | handler: src/lambdas/hello/handler.handler #fileName.exportName in code
99 | maximumRetryAttempts: 1 #Retry attempts by lambda on failure
100 | package: # Only package code relevant to this specific lambda
101 | patterns:
102 | - src/lambdas/hello/**
103 | events: #API Gateway integration etc
104 | - http:
105 | method: get
106 | path: hello
107 | private: true #require an api key (look below to usage-plans)
108 | cors: #cors settings
109 | origin: '*'
110 | ```
111 |
112 | For more documentation on how its configured, visit [HERE](https://www.serverless.com/framework/docs/providers/aws/guide/functions).
113 |
114 | ## Adding Environment Variables
115 | Environment variables can be added to our lambdas via `serverless.yml` under the `provider:`. An example is as follow:
116 | ```yaml
117 | provider:
118 | environment:
119 | MY_SECRET: hi there :)
120 | ```
121 | This will be accessible in the lambda via `process.env.MY_SECRET`. There are defaultly some environment variables accessible from AWS such as `process.env.AWS_REGION` for the current region
122 |
123 | **Note**: Sensitive secrets should be stored via [Secret Manager](https://aws.amazon.com/secrets-manager/) which should be manually configured by you. To request secrets and data from the service you can utilize the [AWS SDK for JS](https://www.npmjs.com/package/aws-sdk) and follow this [example](https://www.internetkatta.com/how-to-use-secrets-manager-in-aws-lambda-node-js).
124 |
125 | ## Caching
126 | There are a few methods to cache request. Api-Gateway has a built in caching mechanism & you can alternatively use something like elasticache for an app cache. For database caching there is also [DAX](https://aws.amazon.com/dynamodb/dax/) for a fairly high price.
127 |
128 | ## Configuring API Gateway & Limits
129 | By default, our api-gateway service is public and is throttled through default burst and limits attached to the stage. However, you can alternatively create a `usage plan` for finer control and optionally attach it to specific api keys. A usage plan is a set of limits such as a `rate limit`, `burst limit`, and `quota`. You can modify the limits of this usage plan via the `serverless.yml` under the `apiGateway:` section.
130 |
131 | Example config:
132 | ```yaml
133 | apiKeys:
134 | - private_key: # Used to tie key to usage plan
135 | - my amazing api key
136 | usagePlan:
137 | - private_key: # Used to tie key to usage plan. Key will abide by the following
138 | quota:
139 | limit: 5000 #maximum 5000 request per month, refreshes on 2nd day
140 | offset: 2
141 | period: MONTH
142 | throttle:
143 | burstLimit: 200 #maximum number of concurrent requests that API gateway will serve at any given point
144 | rateLimit: 100 #request per second limit
145 | ```
146 | More info on configuration options found [HERE](https://www.serverless.com/framework/docs/providers/aws/events/apigateway).
147 |
148 | ## Deployment
149 | To deploy the application to AWS you will first need to install the [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html). Then proceed with the following steps.
150 |
151 | 1. Authenticate with your AWS CLI. Whether that be entering access keys in the [credential file](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html) or alternatively entering [environment variables](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html).
152 | 2. Ensure all your unit test are passing with `npm test`
153 | 3. Run `npm deploy` which will use the default of stage `dev` and region `us-east-1`. If you wish to manually configure the stage and region, you can do so by modifying the `package.json` script or alternatively running `serverless deploy --stage dev --region us-east-1` manually with your respective changes to that commands parameters.
154 |
155 | **IMPORTANT** : Due to the universal nature of s3, the deployment may fail with a "bucket already exist" error. This is because s3 buckets must have unique names _per region_. To resolve this ensure your service name is unique to your project or modify the deployment bucket name in the `serverless.yml`
156 |
157 | **IMPORTANT** : Upon first deployment, the script may not be able to attach the WAF to the API-Gateway. Simply rerun the deployment script and it should attach now that its created.
158 |
159 | ## Dev Dependencies
160 | - serverless
161 | - serverless-associate-waf
162 | - serverless-deployment-bucket
163 | - serverless-offline
164 | - standard
165 |
166 | ## App Dependencies
167 | - aws-sdk
168 | - bcryptjs
169 | - jsonwebtoken
170 | - uuid
171 | - yup
172 |
173 | ## Contributing
174 | Find the contribution document [HERE](/CONTRIBUTING.md) if you wish to work on the repo!
--------------------------------------------------------------------------------