├── terraform ├── provider.tf ├── iam.tf ├── variables.tf ├── lambda.tf ├── gateway.tf └── rest-api.tf ├── .gitignore ├── examples ├── transport-querystring │ ├── grant │ │ ├── grant.js │ │ ├── config.json │ │ └── package.json │ ├── callback │ │ ├── callback.js │ │ └── package.json │ ├── package.json │ └── serverless.yml ├── dynamic-state │ ├── grant │ │ ├── config.json │ │ ├── package.json │ │ ├── store.js │ │ └── grant.js │ ├── serverless.yml │ └── package.json ├── transport-session │ ├── grant │ │ ├── grant.js │ │ ├── config.json │ │ ├── package.json │ │ └── store.js │ ├── callback │ │ ├── callback.js │ │ ├── package.json │ │ └── store.js │ ├── package.json │ └── serverless.yml └── transport-state │ ├── grant │ ├── grant.js │ ├── config.json │ └── package.json │ ├── serverless.yml │ └── package.json ├── .editorconfig ├── Makefile └── README.md /terraform/provider.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 0.14" 3 | required_providers { 4 | aws = { 5 | source = "hashicorp/aws" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = var.region 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | .nyc_output/ 4 | npm-debug.log 5 | package-lock.json 6 | .vercel/ 7 | 8 | .terraform/ 9 | .terraform.lock.hcl 10 | *.tfstate 11 | *.tfstate.backup 12 | *.tfplan 13 | 14 | *.zip 15 | 16 | examples/grant-oauth/ 17 | -------------------------------------------------------------------------------- /examples/transport-querystring/grant/grant.js: -------------------------------------------------------------------------------- 1 | 2 | var grant = require('grant').aws({ 3 | config: require('./config'), session: {secret: 'grant'} 4 | }) 5 | 6 | exports.handler = async (event) => { 7 | var {redirect} = await grant(event) 8 | return redirect 9 | } 10 | -------------------------------------------------------------------------------- /examples/dynamic-state/grant/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaults": { 3 | "origin": "http://localhost:3000", 4 | "prefix": "/grant/connect", 5 | "transport": "state" 6 | }, 7 | "google": { 8 | "key": "APP_ID", 9 | "secret": "APP_SECRET" 10 | }, 11 | "twitter": {} 12 | } 13 | -------------------------------------------------------------------------------- /examples/transport-querystring/callback/callback.js: -------------------------------------------------------------------------------- 1 | 2 | var qs = require('qs') 3 | 4 | exports.handler = async (event) => ({ 5 | statusCode: 200, 6 | headers: { 7 | 'content-type': 'application/json', 8 | }, 9 | body: JSON.stringify(qs.parse(event.queryStringParameters), null, 2) 10 | }) 11 | -------------------------------------------------------------------------------- /examples/transport-session/grant/grant.js: -------------------------------------------------------------------------------- 1 | 2 | var grant = require('grant').aws({ 3 | config: require('./config'), 4 | session: {secret: 'grant', store: require('./store')} 5 | }) 6 | 7 | exports.handler = async (event) => { 8 | var {redirect} = await grant(event) 9 | return redirect 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [Makefile] 15 | indent_style = tab 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /examples/transport-state/grant/grant.js: -------------------------------------------------------------------------------- 1 | 2 | var grant = require('grant').aws({ 3 | config: require('./config'), session: {secret: 'grant'} 4 | }) 5 | 6 | exports.handler = async (event) => { 7 | var {redirect, response} = await grant(event) 8 | return redirect || { 9 | statusCode: 200, 10 | headers: {'content-type': 'text/plain'}, 11 | body: JSON.stringify(response, null, 2) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /terraform/iam.tf: -------------------------------------------------------------------------------- 1 | 2 | resource "aws_iam_role" "lambda" { 3 | name = var.lambda 4 | assume_role_policy = <<-EOF 5 | { 6 | "Version": "2012-10-17", 7 | "Statement": [ 8 | { 9 | "Effect": "Allow", 10 | "Action": "sts:AssumeRole", 11 | "Principal": { 12 | "Service": "lambda.amazonaws.com" 13 | } 14 | } 15 | ] 16 | } 17 | EOF 18 | } 19 | -------------------------------------------------------------------------------- /examples/transport-state/grant/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaults": { 3 | "origin": "http://localhost:3000", 4 | "prefix": "/grant/connect", 5 | "transport": "state" 6 | }, 7 | "google": { 8 | "key": "APP_ID", 9 | "secret": "APP_SECRET", 10 | "scope": [ 11 | "openid" 12 | ] 13 | }, 14 | "twitter": { 15 | "key": "CONSUMER_KEY", 16 | "secret": "CONSUMER_SECRET" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/dynamic-state/serverless.yml: -------------------------------------------------------------------------------- 1 | service: grant 2 | 3 | provider: 4 | name: aws 5 | runtime: nodejs12.x 6 | region: us-west-2 7 | stage: grant 8 | 9 | functions: 10 | dynamic-state: 11 | handler: grant/grant.handler 12 | events: 13 | - http: 14 | path: /grant/{path+} 15 | method: ANY 16 | 17 | plugins: 18 | - serverless-offline 19 | 20 | custom: 21 | serverless-offline: 22 | port: 3000 23 | noPrependStageInUrl: true 24 | -------------------------------------------------------------------------------- /examples/transport-state/serverless.yml: -------------------------------------------------------------------------------- 1 | service: grant 2 | 3 | provider: 4 | name: aws 5 | runtime: nodejs12.x 6 | region: us-west-2 7 | stage: grant 8 | 9 | functions: 10 | transport-state: 11 | handler: grant/grant.handler 12 | events: 13 | - http: 14 | path: /grant/{path+} 15 | method: ANY 16 | 17 | plugins: 18 | - serverless-offline 19 | 20 | custom: 21 | serverless-offline: 22 | port: 3000 23 | noPrependStageInUrl: true 24 | -------------------------------------------------------------------------------- /examples/transport-session/grant/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaults": { 3 | "origin": "http://localhost:3000", 4 | "prefix": "/grant/connect", 5 | "transport": "session" 6 | }, 7 | "google": { 8 | "key": "APP_ID", 9 | "secret": "APP_SECRET", 10 | "callback": "/grant/hello", 11 | "scope": [ 12 | "openid" 13 | ] 14 | }, 15 | "twitter": { 16 | "key": "CONSUMER_KEY", 17 | "secret": "CONSUMER_SECRET", 18 | "callback": "/grant/hi" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/transport-querystring/grant/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaults": { 3 | "origin": "http://localhost:3000", 4 | "prefix": "/grant/connect", 5 | "transport": "querystring" 6 | }, 7 | "google": { 8 | "key": "APP_ID", 9 | "secret": "APP_SECRET", 10 | "callback": "/grant/hello", 11 | "scope": [ 12 | "openid" 13 | ] 14 | }, 15 | "twitter": { 16 | "key": "CONSUMER_KEY", 17 | "secret": "CONSUMER_SECRET", 18 | "callback": "/grant/hi" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/transport-session/callback/callback.js: -------------------------------------------------------------------------------- 1 | 2 | var Session = require('grant/lib/session')({ 3 | secret: 'grant', store: require('./store') 4 | }) 5 | 6 | exports.handler = async (event) => { 7 | var session = Session(event) 8 | 9 | var {response} = (await session.get()).grant 10 | await session.remove() 11 | 12 | return { 13 | statusCode: 200, 14 | headers: { 15 | 'content-type': 'application/json', 16 | }, 17 | body: JSON.stringify(response, null, 2) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/dynamic-state/grant/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grant-examples", 3 | "version": "0.0.0", 4 | "description": "Grant Examples", 5 | "private": true, 6 | "license": "MIT", 7 | "homepage": "https://github.com/simov/grant", 8 | "author": "Simeon Velichkov (https://simov.github.io)", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/simov/grant.git" 12 | }, 13 | "main": "./grant.js", 14 | "dependencies": { 15 | "grant": "^5.4.9" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/transport-session/grant/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grant-examples", 3 | "version": "0.0.0", 4 | "description": "Grant Examples", 5 | "private": true, 6 | "license": "MIT", 7 | "homepage": "https://github.com/simov/grant", 8 | "author": "Simeon Velichkov (https://simov.github.io)", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/simov/grant.git" 12 | }, 13 | "main": "./grant.js", 14 | "dependencies": { 15 | "grant": "^5.4.9" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/transport-state/grant/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grant-examples", 3 | "version": "0.0.0", 4 | "description": "Grant Examples", 5 | "private": true, 6 | "license": "MIT", 7 | "homepage": "https://github.com/simov/grant", 8 | "author": "Simeon Velichkov (https://simov.github.io)", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/simov/grant.git" 12 | }, 13 | "main": "./grant.js", 14 | "dependencies": { 15 | "grant": "^5.4.9" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/transport-querystring/grant/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grant-examples", 3 | "version": "0.0.0", 4 | "description": "Grant Examples", 5 | "private": true, 6 | "license": "MIT", 7 | "homepage": "https://github.com/simov/grant", 8 | "author": "Simeon Velichkov (https://simov.github.io)", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/simov/grant.git" 12 | }, 13 | "main": "./grant.js", 14 | "dependencies": { 15 | "grant": "^5.4.9" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/transport-querystring/callback/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grant-examples", 3 | "version": "0.0.0", 4 | "description": "Grant Examples", 5 | "private": true, 6 | "license": "MIT", 7 | "homepage": "https://github.com/simov/grant", 8 | "author": "Simeon Velichkov (https://simov.github.io)", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/simov/grant.git" 12 | }, 13 | "main": "./callback.js", 14 | "dependencies": { 15 | "qs": "^6.9.4" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/transport-session/callback/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grant-examples", 3 | "version": "0.0.0", 4 | "description": "Grant Examples", 5 | "private": true, 6 | "license": "MIT", 7 | "homepage": "https://github.com/simov/grant", 8 | "author": "Simeon Velichkov (https://simov.github.io)", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/simov/grant.git" 12 | }, 13 | "main": "./callback.js", 14 | "dependencies": { 15 | "grant": "^5.4.9" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/dynamic-state/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grant-examples", 3 | "version": "0.0.0", 4 | "description": "Grant Examples", 5 | "private": true, 6 | "license": "MIT", 7 | "homepage": "https://github.com/simov/grant", 8 | "author": "Simeon Velichkov (https://simov.github.io)", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/simov/grant.git" 12 | }, 13 | "dependencies": { 14 | "serverless": "^1.73.1", 15 | "serverless-offline": "^6.4.0" 16 | }, 17 | "scripts": { 18 | "start": "serverless offline start" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/transport-state/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grant-examples", 3 | "version": "0.0.0", 4 | "description": "Grant Examples", 5 | "private": true, 6 | "license": "MIT", 7 | "homepage": "https://github.com/simov/grant", 8 | "author": "Simeon Velichkov (https://simov.github.io)", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/simov/grant.git" 12 | }, 13 | "dependencies": { 14 | "serverless": "^1.73.1", 15 | "serverless-offline": "^6.4.0" 16 | }, 17 | "scripts": { 18 | "start": "serverless offline start" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/transport-session/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grant-examples", 3 | "version": "0.0.0", 4 | "description": "Grant Examples", 5 | "private": true, 6 | "license": "MIT", 7 | "homepage": "https://github.com/simov/grant", 8 | "author": "Simeon Velichkov (https://simov.github.io)", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/simov/grant.git" 12 | }, 13 | "dependencies": { 14 | "serverless": "^1.73.1", 15 | "serverless-offline": "^6.4.0" 16 | }, 17 | "scripts": { 18 | "start": "serverless offline start" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/transport-querystring/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grant-examples", 3 | "version": "0.0.0", 4 | "description": "Grant Examples", 5 | "private": true, 6 | "license": "MIT", 7 | "homepage": "https://github.com/simov/grant", 8 | "author": "Simeon Velichkov (https://simov.github.io)", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/simov/grant.git" 12 | }, 13 | "dependencies": { 14 | "serverless": "^1.73.1", 15 | "serverless-offline": "^6.4.0" 16 | }, 17 | "scripts": { 18 | "start": "serverless offline start" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/dynamic-state/grant/store.js: -------------------------------------------------------------------------------- 1 | 2 | var request = require('request-compose').client 3 | 4 | var path = process.env.FIREBASE_PATH 5 | var auth = process.env.FIREBASE_AUTH 6 | 7 | module.exports = { 8 | get: async (sid) => { 9 | var {body} = await request({ 10 | method: 'GET', url: `${path}/${sid}.json`, qs: {auth}, 11 | }) 12 | return body 13 | }, 14 | set: async (sid, json) => { 15 | await request({ 16 | method: 'PATCH', url: `${path}/${sid}.json`, qs: {auth}, json, 17 | }) 18 | }, 19 | remove: async (sid) => { 20 | await request({ 21 | method: 'DELETE', url: `${path}/${sid}.json`, qs: {auth}, 22 | }) 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /examples/transport-session/grant/store.js: -------------------------------------------------------------------------------- 1 | 2 | var request = require('request-compose').client 3 | 4 | var path = process.env.FIREBASE_PATH 5 | var auth = process.env.FIREBASE_AUTH 6 | 7 | module.exports = { 8 | get: async (sid) => { 9 | var {body} = await request({ 10 | method: 'GET', url: `${path}/${sid}.json`, qs: {auth}, 11 | }) 12 | return body 13 | }, 14 | set: async (sid, json) => { 15 | await request({ 16 | method: 'PATCH', url: `${path}/${sid}.json`, qs: {auth}, json, 17 | }) 18 | }, 19 | remove: async (sid) => { 20 | await request({ 21 | method: 'DELETE', url: `${path}/${sid}.json`, qs: {auth}, 22 | }) 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /examples/transport-session/callback/store.js: -------------------------------------------------------------------------------- 1 | 2 | var request = require('request-compose').client 3 | 4 | var path = process.env.FIREBASE_PATH 5 | var auth = process.env.FIREBASE_AUTH 6 | 7 | module.exports = { 8 | get: async (sid) => { 9 | var {body} = await request({ 10 | method: 'GET', url: `${path}/${sid}.json`, qs: {auth}, 11 | }) 12 | return body 13 | }, 14 | set: async (sid, json) => { 15 | await request({ 16 | method: 'PATCH', url: `${path}/${sid}.json`, qs: {auth}, json, 17 | }) 18 | }, 19 | remove: async (sid) => { 20 | await request({ 21 | method: 'DELETE', url: `${path}/${sid}.json`, qs: {auth}, 22 | }) 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /terraform/variables.tf: -------------------------------------------------------------------------------- 1 | 2 | variable "grant" {} 3 | variable "callback" {} 4 | variable "lambda" {} 5 | variable "region" {} 6 | variable "api_type" {} 7 | variable "event_format" {} 8 | variable "example" {} 9 | variable "firebase_path" {} 10 | variable "firebase_auth" {} 11 | 12 | # ----------------------------------------------------------------------------- 13 | 14 | data "aws_caller_identity" "current" {} 15 | 16 | locals { 17 | rest_api = var.api_type == "rest-api" ? 1 : 0 18 | 19 | callback = ( 20 | var.example == "transport-querystring" || 21 | var.example == "transport-session" 22 | ) ? 1 : 0 23 | 24 | rest_api_callback = ( 25 | local.rest_api == 1 && 26 | local.callback == 1 27 | ) ? 1 : 0 28 | } 29 | -------------------------------------------------------------------------------- /examples/transport-session/serverless.yml: -------------------------------------------------------------------------------- 1 | service: grant 2 | 3 | provider: 4 | name: aws 5 | runtime: nodejs12.x 6 | region: us-west-2 7 | stage: grant 8 | 9 | functions: 10 | transport-session: 11 | handler: grant/grant.handler 12 | events: 13 | - http: 14 | path: /grant/{path+} 15 | method: ANY 16 | hello: 17 | handler: callback/callback.handler 18 | events: 19 | - http: 20 | path: /grant/hello 21 | method: GET 22 | hi: 23 | handler: callback/callback.handler 24 | events: 25 | - http: 26 | path: /grant/hi 27 | method: GET 28 | 29 | plugins: 30 | - serverless-offline 31 | 32 | custom: 33 | serverless-offline: 34 | port: 3000 35 | noPrependStageInUrl: true 36 | -------------------------------------------------------------------------------- /examples/transport-querystring/serverless.yml: -------------------------------------------------------------------------------- 1 | service: grant 2 | 3 | provider: 4 | name: aws 5 | runtime: nodejs12.x 6 | region: us-west-2 7 | stage: grant 8 | 9 | functions: 10 | transport-querystring: 11 | handler: grant/grant.handler 12 | events: 13 | - http: 14 | path: /grant/{path+} 15 | method: ANY 16 | hello: 17 | handler: callback/callback.handler 18 | events: 19 | - http: 20 | path: /grant/hello 21 | method: GET 22 | hi: 23 | handler: callback/callback.handler 24 | events: 25 | - http: 26 | path: /grant/hi 27 | method: GET 28 | 29 | plugins: 30 | - serverless-offline 31 | 32 | custom: 33 | serverless-offline: 34 | port: 3000 35 | noPrependStageInUrl: true 36 | -------------------------------------------------------------------------------- /examples/dynamic-state/grant/grant.js: -------------------------------------------------------------------------------- 1 | 2 | var grant = require('grant').aws({ 3 | config: require('./config'), 4 | session: {secret: 'grant', store: require('./store')} 5 | }) 6 | 7 | exports.handler = async (event) => { 8 | if (/\/connect\/google$/.test(event.path)) { 9 | var state = {dynamic: {scope: ['openid']}} 10 | } 11 | else if (/\/connect\/twitter$/.test(event.path)) { 12 | var state = {dynamic: {key: 'CONSUMER_KEY', secret: 'CONSUMER_SECRET'}} 13 | } 14 | 15 | var {redirect, response, session} = await grant(event, state) 16 | 17 | if (redirect) { 18 | return redirect 19 | } 20 | else { 21 | await session.remove() 22 | return { 23 | statusCode: 200, 24 | headers: {'content-type': 'text/plain'}, 25 | body: JSON.stringify(response, null, 2) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /terraform/lambda.tf: -------------------------------------------------------------------------------- 1 | 2 | resource "aws_lambda_function" "grant" { 3 | function_name = var.lambda 4 | description = "OAuth Simplified" 5 | filename = var.grant 6 | handler = "grant.handler" 7 | runtime = "nodejs12.x" 8 | memory_size = 128 9 | timeout = 5 10 | role = aws_iam_role.lambda.arn 11 | source_code_hash = filebase64sha256(var.grant) 12 | environment { 13 | variables = { 14 | FIREBASE_PATH = var.firebase_path 15 | FIREBASE_AUTH = var.firebase_auth 16 | } 17 | } 18 | } 19 | 20 | # ----------------------------------------------------------------------------- 21 | 22 | resource "aws_lambda_function" "callback" { 23 | count = local.callback 24 | function_name = "callback" 25 | description = "Grant Callback" 26 | filename = var.callback 27 | handler = "callback.handler" 28 | runtime = "nodejs12.x" 29 | memory_size = 128 30 | timeout = 5 31 | role = aws_iam_role.lambda.arn 32 | source_code_hash = filebase64sha256(var.callback) 33 | environment { 34 | variables = { 35 | FIREBASE_PATH = var.firebase_path 36 | FIREBASE_AUTH = var.firebase_auth 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | path ?= $(shell pwd) 3 | grant ?= ${path}/grant.zip 4 | callback?= ${path}/callback.zip 5 | tfstate ?= terraform.tfstate 6 | tfplan ?= terraform.tfplan 7 | 8 | lambda ?= grant 9 | 10 | profile ?= ... 11 | region ?= us-west-2 12 | 13 | api_type ?= http-api 14 | event_format ?= 1.0 15 | 16 | firebase_path ?= ... 17 | firebase_auth ?= ... 18 | 19 | example ?= transport-state 20 | 21 | # ----------------------------------------------------------------------------- 22 | 23 | # Develop 24 | 25 | build-dev: 26 | cd ${path}/examples/${example} && \ 27 | npm install --production 28 | 29 | run-dev: 30 | cd ${path}/examples/${example} && \ 31 | FIREBASE_PATH=${firebase_path} \ 32 | FIREBASE_AUTH=${firebase_auth} \ 33 | npx serverless offline start 34 | 35 | # ----------------------------------------------------------------------------- 36 | 37 | # Build 38 | 39 | build-grant: 40 | rm -f ${grant} 41 | cd ${path}/examples/${example}/grant && \ 42 | rm -rf node_modules && \ 43 | npm install --production && \ 44 | zip -r ${grant} node_modules grant.js config.json store.js 45 | 46 | build-callback: 47 | rm -f ${callback} 48 | cd ${path}/examples/${example}/callback && \ 49 | rm -rf node_modules && \ 50 | npm install --production && \ 51 | zip -r ${callback} node_modules callback.js store.js 52 | 53 | # ----------------------------------------------------------------------------- 54 | 55 | # Terraform 56 | 57 | init: 58 | cd ${path}/terraform/ && \ 59 | terraform init 60 | 61 | plan: 62 | cd ${path}/terraform/ && \ 63 | TF_VAR_grant=${grant} \ 64 | TF_VAR_callback=${callback} \ 65 | TF_VAR_lambda=${lambda} \ 66 | TF_VAR_region=${region} \ 67 | TF_VAR_api_type=${api_type} \ 68 | TF_VAR_event_format=${event_format} \ 69 | TF_VAR_example=${example} \ 70 | TF_VAR_firebase_path=${firebase_path} \ 71 | TF_VAR_firebase_auth=${firebase_auth} \ 72 | AWS_PROFILE=${profile} terraform plan \ 73 | -state=${tfstate} \ 74 | -out=${tfplan} 75 | 76 | apply: 77 | cd ${path}/terraform/ && \ 78 | TF_VAR_grant=${grant} \ 79 | TF_VAR_callback=${callback} \ 80 | TF_VAR_lambda=${lambda} \ 81 | TF_VAR_region=${region} \ 82 | TF_VAR_api_type=${api_type} \ 83 | TF_VAR_event_format=${event_format} \ 84 | TF_VAR_example=${example} \ 85 | TF_VAR_firebase_path=${firebase_path} \ 86 | TF_VAR_firebase_auth=${firebase_auth} \ 87 | AWS_PROFILE=${profile} terraform apply \ 88 | -state=${tfstate} \ 89 | ${tfplan} 90 | 91 | destroy: 92 | cd ${path}/terraform/ && \ 93 | TF_VAR_grant=${grant} \ 94 | TF_VAR_callback=${callback} \ 95 | TF_VAR_lambda=${lambda} \ 96 | TF_VAR_region=${region} \ 97 | TF_VAR_api_type=${api_type} \ 98 | TF_VAR_event_format=${event_format} \ 99 | TF_VAR_example=${example} \ 100 | TF_VAR_firebase_path=${firebase_path} \ 101 | TF_VAR_firebase_auth=${firebase_auth} \ 102 | AWS_PROFILE=${profile} terraform destroy \ 103 | -state=${tfstate} 104 | -------------------------------------------------------------------------------- /terraform/gateway.tf: -------------------------------------------------------------------------------- 1 | 2 | # HTTP API Gateway 3 | 4 | resource "aws_apigatewayv2_api" "grant" { 5 | name = "grant-oauth" 6 | description = "OAuth Simplified" 7 | protocol_type = "HTTP" 8 | } 9 | 10 | resource "aws_apigatewayv2_stage" "grant" { 11 | api_id = aws_apigatewayv2_api.grant.id 12 | name = "grant" 13 | auto_deploy = true 14 | } 15 | 16 | # Grant 17 | 18 | resource "aws_apigatewayv2_integration" "proxy" { 19 | api_id = aws_apigatewayv2_api.grant.id 20 | description = "Grant OAuth Proxy" 21 | integration_type = "AWS_PROXY" 22 | integration_method = "POST" 23 | integration_uri = aws_lambda_function.grant.invoke_arn 24 | payload_format_version = var.event_format 25 | # https://github.com/terraform-providers/terraform-provider-aws/issues/11148 26 | lifecycle { 27 | ignore_changes = [passthrough_behavior] 28 | } 29 | } 30 | 31 | resource "aws_apigatewayv2_route" "proxy" { 32 | api_id = aws_apigatewayv2_api.grant.id 33 | route_key = "ANY /{proxy+}" 34 | target = "integrations/${aws_apigatewayv2_integration.proxy.id}" 35 | } 36 | 37 | # Permissions 38 | 39 | resource "aws_lambda_permission" "grant" { 40 | action = "lambda:InvokeFunction" 41 | function_name = aws_lambda_function.grant.function_name 42 | principal = "apigateway.amazonaws.com" 43 | source_arn = "arn:aws:execute-api:${var.region}:${data.aws_caller_identity.current.account_id}:${aws_apigatewayv2_api.grant.id}/*/*/*" 44 | } 45 | 46 | # ----------------------------------------------------------------------------- 47 | 48 | # Callback - Google 49 | 50 | resource "aws_apigatewayv2_integration" "hello" { 51 | count = local.callback 52 | api_id = aws_apigatewayv2_api.grant.id 53 | description = "Grant Callback" 54 | integration_type = "AWS_PROXY" 55 | integration_method = "POST" 56 | integration_uri = aws_lambda_function.callback.0.invoke_arn 57 | payload_format_version = var.event_format 58 | # https://github.com/terraform-providers/terraform-provider-aws/issues/11148 59 | lifecycle { 60 | ignore_changes = [passthrough_behavior] 61 | } 62 | } 63 | 64 | resource "aws_apigatewayv2_route" "hello" { 65 | count = local.callback 66 | api_id = aws_apigatewayv2_api.grant.id 67 | route_key = "GET /hello" 68 | target = "integrations/${aws_apigatewayv2_integration.hello.0.id}" 69 | } 70 | 71 | # Callback - Twitter 72 | 73 | resource "aws_apigatewayv2_integration" "hi" { 74 | count = local.callback 75 | api_id = aws_apigatewayv2_api.grant.id 76 | description = "Grant Callback" 77 | integration_type = "AWS_PROXY" 78 | integration_method = "POST" 79 | integration_uri = aws_lambda_function.callback.0.invoke_arn 80 | payload_format_version = var.event_format 81 | # https://github.com/terraform-providers/terraform-provider-aws/issues/11148 82 | lifecycle { 83 | ignore_changes = [passthrough_behavior] 84 | } 85 | } 86 | 87 | resource "aws_apigatewayv2_route" "hi" { 88 | count = local.callback 89 | api_id = aws_apigatewayv2_api.grant.id 90 | route_key = "GET /hi" 91 | target = "integrations/${aws_apigatewayv2_integration.hi.0.id}" 92 | } 93 | 94 | # Permissions 95 | 96 | resource "aws_lambda_permission" "callback" { 97 | action = "lambda:InvokeFunction" 98 | function_name = aws_lambda_function.grant.function_name 99 | principal = "apigateway.amazonaws.com" 100 | source_arn = "arn:aws:execute-api:${var.region}:${data.aws_caller_identity.current.account_id}:${aws_apigatewayv2_api.grant.id}/*/*/*" 101 | } 102 | -------------------------------------------------------------------------------- /terraform/rest-api.tf: -------------------------------------------------------------------------------- 1 | 2 | # REST API Gateway 3 | 4 | resource "aws_api_gateway_rest_api" "grant" { 5 | count = local.rest_api 6 | name = "grant-oauth" 7 | description = "OAuth Simplified" 8 | endpoint_configuration { 9 | types = ["REGIONAL"] 10 | } 11 | } 12 | 13 | resource "aws_api_gateway_deployment" "grant" { 14 | count = local.rest_api 15 | rest_api_id = aws_api_gateway_rest_api.grant.0.id 16 | stage_name = "grant" 17 | } 18 | 19 | # Grant 20 | 21 | resource "aws_api_gateway_resource" "grant" { 22 | count = local.rest_api 23 | rest_api_id = aws_api_gateway_rest_api.grant.0.id 24 | parent_id = aws_api_gateway_rest_api.grant.0.root_resource_id 25 | path_part = "{proxy+}" 26 | } 27 | 28 | resource "aws_api_gateway_method" "grant" { 29 | count = local.rest_api 30 | rest_api_id = aws_api_gateway_rest_api.grant.0.id 31 | resource_id = aws_api_gateway_resource.grant.0.id 32 | http_method = "ANY" 33 | authorization = "NONE" 34 | } 35 | 36 | resource "aws_api_gateway_integration" "grant" { 37 | count = local.rest_api 38 | rest_api_id = aws_api_gateway_rest_api.grant.0.id 39 | resource_id = aws_api_gateway_resource.grant.0.id 40 | http_method = aws_api_gateway_method.grant.0.http_method 41 | type = "AWS_PROXY" 42 | integration_http_method = "POST" 43 | uri = aws_lambda_function.grant.invoke_arn 44 | } 45 | 46 | # Permissions 47 | 48 | resource "aws_lambda_permission" "rest_api_grant" { 49 | count = local.rest_api 50 | action = "lambda:InvokeFunction" 51 | function_name = aws_lambda_function.grant.function_name 52 | principal = "apigateway.amazonaws.com" 53 | source_arn = "arn:aws:execute-api:${var.region}:${data.aws_caller_identity.current.account_id}:${aws_api_gateway_rest_api.grant.0.id}/*/*/*" 54 | } 55 | 56 | # ----------------------------------------------------------------------------- 57 | 58 | # Callback - Google 59 | 60 | resource "aws_api_gateway_resource" "hello" { 61 | count = local.rest_api_callback 62 | rest_api_id = aws_api_gateway_rest_api.grant.0.id 63 | parent_id = aws_api_gateway_rest_api.grant.0.root_resource_id 64 | path_part = "hello" 65 | } 66 | 67 | resource "aws_api_gateway_method" "hello" { 68 | count = local.rest_api_callback 69 | rest_api_id = aws_api_gateway_rest_api.grant.0.id 70 | resource_id = aws_api_gateway_resource.hello.0.id 71 | http_method = "GET" 72 | authorization = "NONE" 73 | } 74 | 75 | resource "aws_api_gateway_integration" "hello" { 76 | count = local.rest_api_callback 77 | rest_api_id = aws_api_gateway_rest_api.grant.0.id 78 | resource_id = aws_api_gateway_resource.hello.0.id 79 | http_method = aws_api_gateway_method.hello.0.http_method 80 | type = "AWS_PROXY" 81 | integration_http_method = "POST" 82 | uri = aws_lambda_function.callback.0.invoke_arn 83 | } 84 | 85 | # Callback - Twitter 86 | 87 | resource "aws_api_gateway_resource" "hi" { 88 | count = local.rest_api_callback 89 | rest_api_id = aws_api_gateway_rest_api.grant.0.id 90 | parent_id = aws_api_gateway_rest_api.grant.0.root_resource_id 91 | path_part = "hi" 92 | } 93 | 94 | resource "aws_api_gateway_method" "hi" { 95 | count = local.rest_api_callback 96 | rest_api_id = aws_api_gateway_rest_api.grant.0.id 97 | resource_id = aws_api_gateway_resource.hi.0.id 98 | http_method = "GET" 99 | authorization = "NONE" 100 | } 101 | 102 | resource "aws_api_gateway_integration" "hi" { 103 | count = local.rest_api_callback 104 | rest_api_id = aws_api_gateway_rest_api.grant.0.id 105 | resource_id = aws_api_gateway_resource.hi.0.id 106 | http_method = aws_api_gateway_method.hi.0.http_method 107 | type = "AWS_PROXY" 108 | integration_http_method = "POST" 109 | uri = aws_lambda_function.callback.0.invoke_arn 110 | } 111 | 112 | # Permissions 113 | 114 | resource "aws_lambda_permission" "rest_api_callback" { 115 | count = local.rest_api_callback 116 | action = "lambda:InvokeFunction" 117 | function_name = aws_lambda_function.callback.0.function_name 118 | principal = "apigateway.amazonaws.com" 119 | source_arn = "arn:aws:execute-api:${var.region}:${data.aws_caller_identity.current.account_id}:${aws_api_gateway_rest_api.grant.0.id}/*/*/*" 120 | } 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # grant-aws 3 | 4 | > _AWS Lambda handler for **[Grant]**_ 5 | 6 | ```js 7 | var grant = require('grant').aws({ 8 | config: {/*Grant configuration*/}, session: {secret: 'grant'} 9 | }) 10 | 11 | exports.handler = async (event) => { 12 | var {redirect, response} = await grant(event) 13 | return redirect || { 14 | statusCode: 200, 15 | headers: {'content-type': 'application/json'}, 16 | body: JSON.stringify(response) 17 | } 18 | } 19 | ``` 20 | 21 | > _Also available for [Azure], [Google Cloud], [Vercel]_ 22 | 23 | > _[ES Modules and TypeScript][grant-types]_ 24 | 25 | --- 26 | 27 | ## Configuration 28 | 29 | The `config` key expects your [**Grant** configuration][grant-config]. 30 | 31 | ## Routes 32 | 33 | Grant relies on the request path to determine the provider name and any static override being used. The following event keys are being used to determine the request path: 34 | 35 | | Gateway | Event | Key 36 | | :-: | :-: | :- 37 | | rest | - | event.requestContext.path 38 | | http | v1 | event.path 39 | | http | v2 | event.rawPath 40 | 41 | Additionally the `prefix` specified in your Grant configuration is used to generate the correct `redirect_uri` in case it is not configured explicitly. 42 | 43 | However, AWS is inconsistent in the way it sets those values under different circumstances, and you may have to print those event keys and adjust your Grant configuration accordingly. A few known cases: 44 | 45 | ### Default Domain 46 | 47 | ``` 48 | https://[id].execute-api.[region].amazonaws.com/[stage]/connect/google 49 | https://[id].execute-api.[region].amazonaws.com/[stage]/connect/google/callback 50 | ``` 51 | 52 | Gateway | Event | Key | Value 53 | :-: | :-: | :- | :- 54 | rest | - | event.requestContext.path | `/stage/connect/google` 55 | http | v1 | event.path | `/stage/connect/google` 56 | http | v2 | event.rawPath | `/stage/connect/google` 57 | 58 | ```json 59 | { 60 | "defaults": { 61 | "origin": "https://[id].execute-api.[region].amazonaws.com", 62 | "prefix": "/[stage]/connect" 63 | }, 64 | "google": {} 65 | } 66 | ``` 67 | 68 | ### Custom Domain 69 | 70 | ``` 71 | https://amazing.com/connect/google 72 | https://amazing.com/connect/google/callback 73 | ``` 74 | 75 | Gateway | Event | Key | Value 76 | :-: | :-: | :- | :- 77 | rest | - | event.requestContext.path | `/connect/google` 78 | http | v1 | event.path | `/connect/google` 79 | http | v2 | event.rawPath | `/stage/connect/google` 80 | 81 | ##### REST API, HTTP API v1 82 | 83 | ```json 84 | { 85 | "defaults": { 86 | "origin": "https://amazing.com", 87 | "prefix": "/connect" 88 | }, 89 | "google": {} 90 | } 91 | ``` 92 | 93 | ##### HTTP API v2 94 | 95 | ```json 96 | { 97 | "defaults": { 98 | "origin": "https://amazing.com", 99 | "prefix": "/stage/connect" 100 | }, 101 | "google": { 102 | "redirect_uri": "https://amazing.com/connect/google/callback" 103 | } 104 | } 105 | ``` 106 | 107 | ### Custom Domain + Path Mapping 108 | 109 | ``` 110 | https://amazing.com/v1/connect/google 111 | https://amazing.com/v1/connect/google/callback 112 | ``` 113 | 114 | Gateway | Event | Key | Value 115 | :-: | :-: | :- | :- 116 | rest | - | event.requestContext.path | `/v1/connect/google` 117 | http | v1 | event.path | `/v1/connect/google` 118 | http | v2 | event.rawPath | `/stage/connect/google` 119 | 120 | ##### REST API, HTTP API v1 121 | 122 | ```json 123 | { 124 | "defaults": { 125 | "origin": "https://amazing.com", 126 | "prefix": "/v1/connect" 127 | }, 128 | "google": {} 129 | } 130 | ``` 131 | 132 | ##### HTTP API v2 133 | 134 | ```json 135 | { 136 | "defaults": { 137 | "origin": "https://amazing.com", 138 | "prefix": "/stage/connect" 139 | }, 140 | "google": { 141 | "redirect_uri": "https://amazing.com/v1/connect/google/callback" 142 | } 143 | } 144 | ``` 145 | 146 | --- 147 | 148 | ## Local Routes 149 | 150 | When running locally the following routes can be used: 151 | 152 | ``` 153 | http://localhost:3000/[stage]/connect/google 154 | http://localhost:3000/[stage]/connect/google/callback 155 | ``` 156 | 157 | --- 158 | 159 | ## Session 160 | 161 | The `session` key expects your session configuration: 162 | 163 | Option | Description 164 | :- | :- 165 | `name` | Cookie name, defaults to `grant` 166 | `secret` | Cookie secret, **required** 167 | `cookie` | [cookie] options, defaults to `{path: '/', httpOnly: true, secure: false, maxAge: null}` 168 | `store` | External session store implementation 169 | 170 | #### NOTE: 171 | 172 | - The default cookie store is used unless you specify a `store` implementation! 173 | - Using the default cookie store **may leak private data**! 174 | - Implementing an external session store is recommended for production deployments! 175 | 176 | Example session store implementation using [Firebase]: 177 | 178 | ```js 179 | var request = require('request-compose').client 180 | 181 | var path = process.env.FIREBASE_PATH 182 | var auth = process.env.FIREBASE_AUTH 183 | 184 | module.exports = { 185 | get: async (sid) => { 186 | var {body} = await request({ 187 | method: 'GET', url: `${path}/${sid}.json`, qs: {auth}, 188 | }) 189 | return body 190 | }, 191 | set: async (sid, json) => { 192 | await request({ 193 | method: 'PATCH', url: `${path}/${sid}.json`, qs: {auth}, json, 194 | }) 195 | }, 196 | remove: async (sid) => { 197 | await request({ 198 | method: 'DELETE', url: `${path}/${sid}.json`, qs: {auth}, 199 | }) 200 | }, 201 | } 202 | ``` 203 | 204 | --- 205 | 206 | ## Handler 207 | 208 | The AWS Lambda handler for Grant accepts: 209 | 210 | Argument | Type | Description 211 | :- | :- | :- 212 | `event` | **required** | The AWS Lambda event object 213 | `state` | optional | [Dynamic State][grant-dynamic-state] object `{dynamic: {..Grant configuration..}}` 214 | 215 | The AWS Lambda handler for Grant returns: 216 | 217 | Parameter | Availability | Description 218 | :- | :- | :- 219 | `session` | Always | The session store instance, `get`, `set` and `remove` methods can be used to manage the Grant session 220 | `redirect` | On redirect only | HTTP redirect controlled by Grant, your lambda have to return this object when present 221 | `response` | Based on transport | The [response data][grant-response-data], available for [transport-state][example-transport-state] and [transport-session][example-transport-session] only 222 | 223 | --- 224 | 225 | ## Examples 226 | 227 | Example | Session | Callback λ 228 | :- | :- | :- 229 | `transport-state` | Cookie Store | ✕ 230 | `transport-querystring` | Cookie Store | ✓ 231 | `transport-session` | Firebase Session Store | ✓ 232 | `dynamic-state` | Firebase Session Store | ✕ 233 | 234 | > _Different session store types were used for example purposes only._ 235 | 236 | #### Configuration 237 | 238 | All variables at the top of the [`Makefile`][example-makefile] with value set to `...` have to be configured: 239 | 240 | - `profile` - `AWS_PROFILE` to use for managing AWS resources, not used for local development 241 | 242 | - `firebase_path` - [Firebase] path of your database, required for [transport-session][example-transport-session] and [dynamic-state][example-dynamic-state] examples 243 | 244 | ``` 245 | https://[project].firebaseio.com/[prefix] 246 | ``` 247 | 248 | - `firebase_auth` - [Firebase] auth key of your database, required for [transport-session][example-transport-session] and [dynamic-state][example-dynamic-state] examples 249 | 250 | ```json 251 | { 252 | "rules": { 253 | ".read": "auth == '[key]'", 254 | ".write": "auth == '[key]'" 255 | } 256 | } 257 | ``` 258 | 259 | - `api_type` - defaults to `http-api`, available for `rest-api` as well 260 | 261 | - `event_format` - defaults to `1.0`, available for `2.0` as well, applicable for `http-api` 262 | 263 | All variables can be passed as arguments to `make` as well: 264 | 265 | ```bash 266 | make plan example=transport-querystring ... 267 | ``` 268 | 269 | --- 270 | 271 | ## Develop 272 | 273 | ```bash 274 | # build example locally 275 | make build-dev 276 | # run example locally 277 | make run-dev 278 | ``` 279 | 280 | --- 281 | 282 | ## Deploy 283 | 284 | ```bash 285 | # build Grant lambda for deployment 286 | make build-grant 287 | # build callback lambda for transport-querystring and transport-session examples 288 | make build-callback 289 | # execute only once 290 | make init 291 | # plan before every deployment 292 | make plan 293 | # apply plan for deployment 294 | make apply 295 | # cleanup resources 296 | make destroy 297 | ``` 298 | 299 | --- 300 | 301 | [Grant]: https://github.com/simov/grant 302 | [AWS]: https://github.com/simov/grant-aws 303 | [Azure]: https://github.com/simov/grant-azure 304 | [Google Cloud]: https://github.com/simov/grant-gcloud 305 | [Vercel]: https://github.com/simov/grant-vercel 306 | 307 | [cookie]: https://www.npmjs.com/package/cookie 308 | [Firebase]: https://firebase.google.com/ 309 | 310 | [grant-config]: https://github.com/simov/grant#configuration 311 | [grant-dynamic-state]: https://github.com/simov/grant#dynamic-state 312 | [grant-response-data]: https://github.com/simov/grant#callback-data 313 | [grant-types]: https://github.com/simov/grant#misc-es-modules-and-typescript 314 | 315 | [example-makefile]: https://github.com/simov/grant-aws/tree/master/Makefile 316 | [example-transport-state]: https://github.com/simov/grant-aws/tree/master/examples/transport-state 317 | [example-transport-querystring]: https://github.com/simov/grant-aws/tree/master/examples/transport-querystring 318 | [example-transport-session]: https://github.com/simov/grant-aws/tree/master/examples/transport-session 319 | [example-dynamic-state]: https://github.com/simov/grant-aws/tree/master/examples/dynamic-state 320 | --------------------------------------------------------------------------------