├── src
├── error.html
└── index.html
├── .gitignore
├── .travis.yml
├── .editorconfig
├── .eslintrc.js
├── package.json
├── handler.js
├── README.md
├── test
└── basicAuth.test.js
└── serverless.yml
/src/error.html:
--------------------------------------------------------------------------------
1 | Error
2 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 | Authenticated Access!!!
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .nyc_output
3 | .serverless
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 6
4 | cache:
5 | directories:
6 | - "node_modules"
7 | script:
8 | - npm run test
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 |
9 | [*.{js}]
10 | indent_style = space
11 | indent_size = 4
12 |
13 | [*.{json,yml}]
14 | indent_style = space
15 | indent_size = 2
16 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "extends": "airbnb-base",
3 | "plugins": [
4 | "import"
5 | ],
6 | "rules": {
7 | "arrow-body-style": "off",
8 | "comma-dangle": ["error", "never"],
9 | "import/no-dynamic-require": "off",
10 | "indent": ["error", 4],
11 | "no-prototype-builtins": "off",
12 | "prefer-destructuring": "off"
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "serverless-static-hosting-with-basic-auth",
3 | "version": "0.2.0",
4 | "description": "Serverless boilerplate for Static website hosting with Basic authentication",
5 | "main": "index.js",
6 | "scripts": {
7 | "deploy": "sls deploy -v",
8 | "remove": "sls remove",
9 | "sync": "sls s3sync",
10 | "test": "nyc ava -v",
11 | "lint": "eslint ."
12 | },
13 | "keywords": [
14 | "serverless"
15 | ],
16 | "author": "k1LoW (https://github.com/k1LoW)",
17 | "license": "MIT",
18 | "devDependencies": {
19 | "ava": "^0.24.0",
20 | "eslint-config-airbnb-base": "^12.1.0",
21 | "nyc": "^11.4.1",
22 | "octopublish": "^0.5.0",
23 | "path": "^0.12.7",
24 | "serverless": "^1.25.0",
25 | "serverless-plugin-cloudfront-lambda-edge": "^1.0.0",
26 | "serverless-s3-sync": "^1.3.0"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/handler.js:
--------------------------------------------------------------------------------
1 | const BASIC_AUTH_USERS = {
2 | user: 'pass'
3 | };
4 |
5 | module.exports.basicAuth = (event, context, callback) => {
6 | const request = event.Records[0].cf.request;
7 | const headers = request.headers;
8 | const authorization = headers.authorization || headers.Authorization;
9 |
10 | if (authorization) {
11 | const encoded = authorization[0].value.split(' ')[1];
12 | const userAndPassword = Buffer.from(encoded, 'base64').toString();
13 | const result = Object.keys(BASIC_AUTH_USERS).filter((user) => {
14 | const password = BASIC_AUTH_USERS[user];
15 | if (`${user}:${password}` === userAndPassword) {
16 | return true;
17 | }
18 | return false;
19 | });
20 | if (result.length > 0) {
21 | callback(null, request);
22 | return;
23 | }
24 | }
25 |
26 | const response = {
27 | status: '401',
28 | statusDescription: 'Authorization Required',
29 | headers: {
30 | 'www-authenticate': [{ key: 'WWW-Authenticate', value: 'Basic' }],
31 | 'content-type': [{ key: 'Content-Type', value: 'text/plain; charset=utf-8' }]
32 | },
33 | body: '401 Authorization Required'
34 | };
35 |
36 | callback(null, response);
37 | };
38 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Serverless boilerplate for Static website hosting with Basic authentication [](https://travis-ci.org/k1LoW/serverless-static-hosting-with-basic-auth)
2 |
3 | ## Architecture
4 |
5 | ```
6 | [CloudFront (with Lambda@Edge)] -Restrict Bucket Access-> [S3 Origin Bucket] <-Sync- [Local src/*]
7 | ```
8 |
9 | ### Using plugin
10 |
11 | - [serverless-plugin-cloudfront-lambda-edge](https://github.com/silvermine/serverless-plugin-cloudfront-lambda-edge)
12 | - [serverless-s3-sync](https://github.com/k1LoW/serverless-s3-sync)
13 |
14 | ## Usage
15 |
16 | ### Install boilerplate
17 |
18 | ```
19 | $ git clone https://github.com/k1LoW/serverless-static-hosting-with-basic-auth.git ./my-static-page
20 | ```
21 |
22 | ### Set Basic authentication USERNAME:PASS
23 |
24 | Set Basic authentication config ( `handler.js` )
25 |
26 | ### Deploy stack
27 |
28 | ```
29 | $ npm install
30 | $ AWS_PROFILE=XxxxxXXX WEBSITE_S3_BUCKET_NAME=sls-static-basic npm run deploy
31 | ```
32 |
33 | ### Synchronize src/* -> Website
34 |
35 | ```
36 | $ AWS_PROFILE=XxxxxXXX WEBSITE_S3_BUCKET_NAME=sls-static-basic npm run sync
37 | ```
38 |
39 | ### Remove stack
40 |
41 | ```
42 | $ AWS_PROFILE=XxxxxXXX WEBSITE_S3_BUCKET_NAME=sls-static-basic npm run remove
43 | ```
44 |
--------------------------------------------------------------------------------
/test/basicAuth.test.js:
--------------------------------------------------------------------------------
1 | const test = require('ava');
2 | const path = require('path');
3 |
4 | const { basicAuth } = require(path.join(__dirname, '..', 'handler.js'));
5 |
6 | test.cb('handler.basicAuth pass valid user/pass', (t) => {
7 | const event = {
8 | Records: [
9 | {
10 | cf: {
11 | request: {
12 | headers: {
13 | authorization: [{ key: 'Authorization', value: 'Basic dXNlcjpwYXNz' }]
14 | }
15 | }
16 | }
17 | }
18 | ]
19 | };
20 | const context = {};
21 | const callback = (error, response) => {
22 | t.deepEqual(response, {
23 | headers: {
24 | authorization: [{ key: 'Authorization', value: 'Basic dXNlcjpwYXNz' }]
25 | }
26 | });
27 | t.end();
28 | };
29 |
30 | basicAuth(event, context, callback);
31 | });
32 |
33 | test.cb('handler.basicAuth fail invalid user/pass', (t) => {
34 | const event = {
35 | Records: [
36 | {
37 | cf: {
38 | request: {
39 | headers: {
40 | authorization: [{ key: 'Authorization', value: 'Basic invalid' }]
41 | }
42 | }
43 | }
44 | }
45 | ]
46 | };
47 | const context = {};
48 | const callback = (error, response) => {
49 | t.deepEqual(response, {
50 | status: '401',
51 | statusDescription: 'Authorization Required',
52 | headers: {
53 | 'www-authenticate': [{ key: 'WWW-Authenticate', value: 'Basic' }],
54 | 'content-type': [{ key: 'Content-Type', value: 'text/plain; charset=utf-8' }]
55 | },
56 | body: '401 Authorization Required'
57 | });
58 | t.end();
59 | };
60 |
61 | basicAuth(event, context, callback);
62 | });
63 |
--------------------------------------------------------------------------------
/serverless.yml:
--------------------------------------------------------------------------------
1 | service: ${self:provider.environment.WEBSITE_S3_BUCKET_NAME}
2 |
3 | provider:
4 | name: aws
5 | runtime: nodejs6.10
6 |
7 | stage: dev
8 | region: us-east-1 # Lambda@Edge function must be us-east-1
9 |
10 | environment:
11 | WEBSITE_S3_BUCKET_NAME: ${env:WEBSITE_S3_BUCKET_NAME, 'sls-static-basic'}
12 |
13 | plugins:
14 | - serverless-plugin-cloudfront-lambda-edge
15 | - serverless-s3-sync
16 |
17 | custom:
18 | s3Sync:
19 | - bucketName: ${self:provider.environment.WEBSITE_S3_BUCKET_NAME}
20 | localDir: src
21 |
22 | package:
23 | exclude:
24 | - src/*
25 | - test/*
26 | - package.json
27 | - README.md
28 |
29 | functions:
30 | basicAuth:
31 | name: '${self:provider.environment.WEBSITE_S3_BUCKET_NAME}-viewer-request'
32 | handler: handler.basicAuth
33 | memorySize: 128
34 | timeout: 1
35 | lambdaAtEdge:
36 | distribution: WebsiteDistribution
37 | eventType: 'viewer-request'
38 |
39 | resources:
40 | Resources:
41 | WebsiteBucket:
42 | Type: AWS::S3::Bucket
43 | Properties:
44 | BucketName: ${self:provider.environment.WEBSITE_S3_BUCKET_NAME}
45 | AccessControl: Private
46 | WebsiteConfiguration:
47 | IndexDocument: index.html
48 | ErrorDocument: error.html
49 | WebsiteBucketPolicy:
50 | Type: AWS::S3::BucketPolicy
51 | Properties:
52 | Bucket: { Ref: WebsiteBucket }
53 | PolicyDocument:
54 | Statement:
55 | -
56 | Action:
57 | - "s3:GetObject"
58 | Effect: Allow
59 | Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { Ref : WebsiteBucket }, "/*" ] ] }
60 | Principal:
61 | AWS: { "Fn::Join" : [" ", ["arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity", { Ref: WebsiteOriginAccessIdentity } ] ] }
62 | WebsiteOriginAccessIdentity:
63 | Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
64 | Properties:
65 | CloudFrontOriginAccessIdentityConfig:
66 | Comment: "CloudFrontOriginAccessIdentity for ${self:service}-${self:provider.stage}"
67 | WebsiteDistribution:
68 | Type: AWS::CloudFront::Distribution
69 | Properties:
70 | DistributionConfig:
71 | DefaultCacheBehavior:
72 | AllowedMethods: [ "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT" ]
73 | CachedMethods: [ "GET", "HEAD", "OPTIONS" ]
74 | TargetOriginId: WebsiteBucketOrigin
75 | ViewerProtocolPolicy: redirect-to-https
76 | DefaultTTL: 0
77 | MaxTTL: 0
78 | MinTTL: 0
79 | Compress: true
80 | ForwardedValues:
81 | QueryString: true
82 | Cookies:
83 | Forward: 'all'
84 | CustomErrorResponses:
85 | -
86 | ErrorCode: '403'
87 | ErrorCachingMinTTL: 1
88 | -
89 | ErrorCode: '404'
90 | ErrorCachingMinTTL: 1
91 | -
92 | ErrorCode: '500'
93 | ErrorCachingMinTTL: 1
94 | -
95 | ErrorCode: '502'
96 | ErrorCachingMinTTL: 1
97 | -
98 | ErrorCode: '503'
99 | ErrorCachingMinTTL: 1
100 | -
101 | ErrorCode: '504'
102 | ErrorCachingMinTTL: 1
103 | DefaultRootObject: 'index.html'
104 | Enabled: true
105 | PriceClass: 'PriceClass_100'
106 | HttpVersion: 'http2'
107 | ViewerCertificate:
108 | CloudFrontDefaultCertificate: true
109 | Origins:
110 | -
111 | Id: 'WebsiteBucketOrigin'
112 | DomainName: { 'Fn::GetAtt': [ WebsiteBucket, DomainName ] }
113 | S3OriginConfig:
114 | OriginAccessIdentity: { "Fn::Join" : ["", ["origin-access-identity/cloudfront/", { Ref: WebsiteOriginAccessIdentity } ] ] }
115 | Outputs:
116 | WebsiteURL:
117 | Value: { "Fn::Join" : ["", ["https://", { "Fn::GetAtt" : [ WebsiteDistribution, DomainName ] } ] ] }
118 | Description: "URL for website via CloudFront"
119 |
--------------------------------------------------------------------------------