├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── README.md ├── handler.js ├── package.json ├── serverless.yml ├── src ├── error.html └── index.html └── test └── basicAuth.test.js /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless boilerplate for Static website hosting with Basic authentication [![Build Status](https://travis-ci.org/k1LoW/serverless-static-hosting-with-basic-auth.svg?branch=master)](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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/error.html: -------------------------------------------------------------------------------- 1 | Error 2 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | Authenticated Access!!! 2 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------