├── .editorconfig ├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── MIGRATION.md ├── README.md ├── config ├── oauth.json ├── profile.json └── reserved.json ├── examples ├── README.md ├── dynamic-http │ ├── config.json │ ├── express.js │ ├── fastify.js │ ├── form.html │ ├── hapi.js │ ├── koa.js │ └── package.json ├── dynamic-proxy │ ├── express.js │ └── package.json ├── dynamic-state │ ├── config.json │ ├── express.js │ ├── fastify.js │ ├── hapi.js │ ├── koa.js │ └── package.json ├── request-options │ ├── config.json │ ├── package.json │ ├── proxy-agent.js │ └── tunnel-agent.js ├── response-profile │ ├── config.json │ ├── express.js │ └── package.json ├── session-store │ ├── config.json │ ├── express-cookie.js │ ├── express-mongo.js │ ├── express-redis.js │ ├── express-session.js │ ├── fastify-secure-session.js │ ├── fastify-session.js │ ├── hapi-redis.js │ ├── hapi-session.js │ ├── koa-redis.js │ ├── koa-session.js │ └── package.json ├── static-overrides │ ├── config.json │ ├── express.js │ └── package.json ├── token-endpoint │ ├── config.json │ ├── express.js │ └── package.json ├── transport-querystring │ ├── config.json │ ├── express.js │ ├── fastify.js │ ├── hapi.js │ ├── koa.js │ └── package.json ├── transport-session │ ├── config.json │ ├── express.js │ ├── fastify.js │ ├── hapi.js │ ├── koa.js │ └── package.json └── transport-state │ ├── config.json │ ├── express.js │ ├── fastify.js │ ├── hapi.js │ ├── koa.js │ └── package.json ├── grant.d.ts ├── grant.js ├── lib ├── client.js ├── config.js ├── flow │ ├── oauth1.js │ └── oauth2.js ├── grant.js ├── handler │ ├── aws.js │ ├── azure.js │ ├── curveball.js │ ├── express-4.js │ ├── fastify.js │ ├── gcloud.js │ ├── hapi-16.js │ ├── hapi-17.js │ ├── koa-1.js │ ├── koa-2.js │ ├── node.js │ └── vercel.js ├── oidc.js ├── profile.js ├── request.js ├── response.js ├── session.js └── util.js ├── package.json └── test ├── client.js ├── config.js ├── flow ├── oauth1.js └── oauth2.js ├── handler.js ├── handler ├── handler.js ├── oidc.js ├── profile.js └── session.js ├── response.js ├── session.js └── util ├── client.js ├── keys.json └── provider.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | test: 11 | name: Node.js 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: 16 | - ubuntu-latest 17 | node: 18 | - 22 19 | - 20 20 | - 18 21 | - 16 22 | - 14 23 | steps: 24 | # ----------------------------------------------------------------------- 25 | - name: Clone default branch 26 | uses: actions/checkout@v2 27 | with: 28 | persist-credentials: false 29 | # ----------------------------------------------------------------------- 30 | - name: Set Node.js version 31 | uses: actions/setup-node@v2 32 | with: 33 | node-version: ${{ matrix.node }} 34 | # ----------------------------------------------------------------------- 35 | - name: Cache node_modules 36 | id: cache-modules 37 | uses: actions/cache@v3 38 | with: 39 | path: node_modules 40 | key: ${{ matrix.os }}-${{ matrix.node }}-${{ hashFiles('package.json') }} 41 | # ----------------------------------------------------------------------- 42 | - name: Install deps 43 | if: steps.cache-modules.outputs.cache-hit != 'true' 44 | run: | 45 | npm install 46 | # ----------------------------------------------------------------------- 47 | - name: Run Tests 48 | run: | 49 | npm test 50 | # ----------------------------------------------------------------------- 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | .nyc_output/ 4 | npm-debug.log 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-present, Simeon Velichkov (https://github.com/simov/grant) 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 | -------------------------------------------------------------------------------- /MIGRATION.md: -------------------------------------------------------------------------------- 1 | 2 | # Grant v5 3 | 4 | 5 | ## Breaking: Return `id_token` as string by default 6 | 7 | In Grant v4 the `id_token` was returned decoded by default: 8 | 9 | ```js 10 | { 11 | id_token: {header: {}, payload: {}, signature: '...'}, 12 | access_token: '...', 13 | refresh_token: '...' 14 | } 15 | ``` 16 | 17 | In Grant v5 the `id_token` is returned as string instead: 18 | 19 | ```js 20 | { 21 | id_token: 'abc.abc.abc', 22 | access_token: '...', 23 | refresh_token: '...' 24 | } 25 | ``` 26 | 27 | #### Documentation 28 | 29 | - [response data](https://github.com/simov/grant#callback-data) 30 | 31 | 32 | ## Breaking: Change in `response` configuration 33 | 34 | In Grant v4 the following `response` configuration: 35 | 36 | ```json 37 | { 38 | "google": { 39 | "response": ["jwt"] 40 | } 41 | } 42 | ``` 43 | 44 | Was returning the decoded JWT as `id_token_jwt`: 45 | 46 | ```js 47 | { 48 | id_token: '...', 49 | access_token: '...', 50 | refresh_token: '...', 51 | id_token_jwt: {header: {}, payload: {}, signature: '...'} 52 | } 53 | ``` 54 | 55 | In Grant v5 the decoded JWT can only be returned by using the `response` configuration explicitly: 56 | 57 | ```json 58 | { 59 | "google": { 60 | "response": ["tokens", "raw", "jwt"] 61 | } 62 | } 63 | ``` 64 | 65 | The decoded JWT will be available as `jwt.id_token` instead: 66 | 67 | ```js 68 | { 69 | id_token: '...', 70 | access_token: '...', 71 | refresh_token: '...', 72 | raw: { 73 | id_token: '...', 74 | access_token: '...', 75 | refresh_token: '...', 76 | some: 'other data' 77 | }, 78 | jwt: {id_token: {header: {}, payload: {}, signature: '...'}} 79 | } 80 | ``` 81 | 82 | #### Documentation 83 | 84 | - [`response`](https://github.com/simov/grant#callback-response) configuration 85 | 86 | 87 | ## Deprecate: `protocol` and `host` configuration 88 | 89 | In Grant v4 the `protocol` and the `host` were used to construct the origin of your client server: 90 | 91 | ```json 92 | { 93 | "defaults": { 94 | "protocol": "http", 95 | "host": "localhost:3000" 96 | } 97 | } 98 | ``` 99 | 100 | In Grant v5 it is reommended to use the `origin` configuration instead: 101 | 102 | ```json 103 | { 104 | "defaults": { 105 | "origin": "http://localhost:3000" 106 | } 107 | } 108 | ``` 109 | 110 | #### Documentation 111 | 112 | - [`origin`](https://github.com/simov/grant#connect-origin) configuration 113 | 114 | 115 | ## Deprecate: `path` configuration 116 | 117 | In Grant v4 it was possible to set a `path` prefix: 118 | 119 | ```json 120 | { 121 | "defaults": { 122 | "protocol": "http", 123 | "host": "localhost:3000", 124 | "path": "/oauth" 125 | } 126 | } 127 | ``` 128 | 129 | The equivalent of the above in Grant v5 is: 130 | 131 | ```json 132 | { 133 | "defaults": { 134 | "origin": "http://localhost:3000", 135 | "prefix": "/oauth/connect" 136 | } 137 | } 138 | ``` 139 | 140 | #### Documentation 141 | 142 | - [`prefix`](https://github.com/simov/grant#connect-prefix) configuration 143 | - [path prefix](https://github.com/simov/grant#misc-path-prefix) for a middleware 144 | 145 | 146 | ## Deprecate: Meta modules 147 | 148 | In Grant v4 it was possible to require Express, Koa and Hapi using: 149 | 150 | ```js 151 | var grant = require('grant-express') 152 | var grant = require('grant-koa') 153 | var grant = require('grant-hapi') 154 | ``` 155 | 156 | In Grant v5 it is recommended to use one of the following: 157 | 158 | ```js 159 | var grant = require('grant').express()(config) 160 | var grant = require('grant').express()({config, ...}) 161 | var grant = require('grant').express(config) 162 | var grant = require('grant').express({config, ...}) 163 | var grant = require('grant')({handler: 'express', config}) 164 | ``` 165 | 166 | #### Documentation 167 | 168 | - [handler constructors](https://github.com/simov/grant#misc-handler-constructors) configuration 169 | -------------------------------------------------------------------------------- /config/reserved.json: -------------------------------------------------------------------------------- 1 | [ 2 | "request_url", 3 | "authorize_url", 4 | "access_url", 5 | "oauth", 6 | "scope_delimiter", 7 | "token_endpoint_auth_method", 8 | "token_endpoint_auth_signing_alg", 9 | 10 | "origin", 11 | "prefix", 12 | "state", 13 | "nonce", 14 | "pkce", 15 | "response", 16 | "transport", 17 | "callback", 18 | "overrides", 19 | "dynamic", 20 | "public_key", 21 | "private_key", 22 | 23 | "protocol", 24 | "host", 25 | "path", 26 | 27 | "key", 28 | "secret", 29 | "consumer_key", 30 | "consumer_secret", 31 | "client_id", 32 | "client_secret", 33 | "scope", 34 | "custom_params", 35 | "subdomain", 36 | 37 | "name", 38 | "redirect_uri", 39 | "profile_url" 40 | ] 41 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Examples 3 | 4 | Most of the examples are using Google and Twitter for showcasing the OAuth 2.0 and OAuth 1.0a flow respectively. 5 | 6 | ## App 7 | 8 | Create OAuth Apps for Google and Twitter (or any provider you want), and set their `redirect_uri` accordingly: 9 | 10 | - `http://localhost:3000/connect/google/callback` 11 | - `http://localhost:3000/connect/twitter/callback` 12 | 13 | ## Configuration 14 | 15 | Add your OAuth App credentials in `config.json` 16 | 17 | ## Server 18 | 19 | Start the server: 20 | 21 | ```bash 22 | $ npm install 23 | $ node app.js 24 | ``` 25 | 26 | ## Login 27 | 28 | Navigate to the following URLs in your browser: 29 | 30 | - `http://localhost:3000/connect/google` 31 | - `http://localhost:3000/connect/twitter` 32 | -------------------------------------------------------------------------------- /examples/dynamic-http/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaults": { 3 | "origin": "http://localhost:3000", 4 | "transport": "session" 5 | }, 6 | "google": { 7 | "key": "APP_ID", 8 | "secret": "APP_SECRET", 9 | "dynamic": ["scope"], 10 | "response": ["tokens", "raw", "jwt"], 11 | "callback": "/hello" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/dynamic-http/express.js: -------------------------------------------------------------------------------- 1 | 2 | var express = require('express') 3 | var session = require('express-session') 4 | var parser = require('body-parser') 5 | var grant = require('../../').express() 6 | var fs = require('fs') 7 | 8 | 9 | express() 10 | .use(session({secret: 'grant', saveUninitialized: true, resave: false})) 11 | .use(parser.urlencoded({extended: true})) 12 | .use(grant(require('./config.json'))) 13 | .get('/login', (req, res) => { 14 | res.writeHead(200, {'content-type': 'text/html'}) 15 | res.end(fs.readFileSync('./form.html', 'utf8')) 16 | }) 17 | .get('/hello', (req, res) => { 18 | res.end(JSON.stringify(req.session.grant.response, null, 2)) 19 | }) 20 | .listen(3000) 21 | -------------------------------------------------------------------------------- /examples/dynamic-http/fastify.js: -------------------------------------------------------------------------------- 1 | 2 | var fastify = require('fastify') 3 | var cookie = require('@fastify/cookie') 4 | var session = require('@fastify/session') 5 | var parser = require('@fastify/formbody') 6 | var grant = require('../../').fastify() 7 | var fs = require('fs') 8 | 9 | 10 | fastify() 11 | .register(cookie) 12 | .register(session, {secret: '01234567890123456789012345678912', cookie: {secure: false}}) 13 | .register(parser) 14 | .register(grant(require('./config'))) 15 | .route({method: 'GET', path: '/login', handler: async (req, res) => { 16 | res.header('content-type', 'text/html') 17 | res.send(fs.readFileSync('./form.html', 'utf8')) 18 | }}) 19 | .route({method: 'GET', path: '/hello', handler: async (req, res) => { 20 | res.send(JSON.stringify(req.session.grant.response, null, 2)) 21 | }}) 22 | .listen({port: 3000}) 23 | -------------------------------------------------------------------------------- /examples/dynamic-http/form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Grant - Dynamic HTTP Override 5 | 6 | 7 |
8 |
9 |
10 | 13 |
14 |
15 | 18 |
19 |
20 | 23 |
24 |
25 | 26 |
27 |
28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /examples/dynamic-http/hapi.js: -------------------------------------------------------------------------------- 1 | 2 | var Hapi = require('@hapi/hapi') 3 | var yar = require('@hapi/yar') 4 | var grant = require('../../').hapi() 5 | var fs = require('fs') 6 | 7 | 8 | var server = new Hapi.Server({host: 'localhost', port: 3000}) 9 | 10 | server.route({method: 'GET', path: '/login', handler: (req, res) => { 11 | return res 12 | .response(fs.readFileSync('./form.html', 'utf8')) 13 | .header('content-type', 'text/html') 14 | }}) 15 | server.route({method: 'GET', path: '/hello', handler: (req, res) => { 16 | return res 17 | .response(JSON.stringify(req.yar.get('grant').response, null, 2)) 18 | .header('content-type', 'text/plain') 19 | }}) 20 | 21 | ;(async () => { 22 | await server.register([ 23 | {plugin: yar, options: { 24 | name: 'grant', 25 | cookieOptions: {password: '01234567890123456789012345678901', isSecure: false}}}, 26 | {plugin: grant(require('./config.json'))} 27 | ]) 28 | await server.start() 29 | })() 30 | -------------------------------------------------------------------------------- /examples/dynamic-http/koa.js: -------------------------------------------------------------------------------- 1 | 2 | var Koa = require('koa') 3 | var session = require('koa-session') 4 | var parser = require('koa-bodyparser') 5 | var Router = require('koa-router') 6 | var grant = require('../../').koa() 7 | var fs = require('fs') 8 | 9 | 10 | var app = new Koa() 11 | app.keys = ['grant'] 12 | 13 | app 14 | .use(session(app)) 15 | .use(parser()) 16 | .use(grant(require('./config.json'))) 17 | .use(new Router() 18 | .get('/login', (ctx) => { 19 | ctx.body = fs.readFileSync('./form.html', 'utf8') 20 | }) 21 | .get('/hello', (ctx) => { 22 | ctx.body = JSON.stringify(ctx.session.grant.response, null, 2) 23 | }) 24 | .routes()) 25 | .listen(3000) 26 | -------------------------------------------------------------------------------- /examples/dynamic-http/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 | "@fastify/cookie": "^9.1.0", 15 | "@fastify/formbody": "^7.4.0", 16 | "@fastify/session": "^10.5.0", 17 | "@hapi/hapi": "^21.3.2", 18 | "@hapi/yar": "^11.0.1", 19 | "body-parser": "^1.20.2", 20 | "express": "^4.18.2", 21 | "express-session": "^1.17.3", 22 | "fastify": "^4.23.2", 23 | "koa": "^2.14.2", 24 | "koa-bodyparser": "^4.4.1", 25 | "koa-router": "^9.4.0", 26 | "koa-session": "^6.4.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/dynamic-proxy/express.js: -------------------------------------------------------------------------------- 1 | 2 | var express = require('express') 3 | var qs = require('querystring') 4 | 5 | 6 | express() 7 | .use('/login/:provider', (req, res) => { 8 | var provider = req.params.provider 9 | var params = qs.stringify({ 10 | transport: 'querystring', 11 | response: 'tokens', 12 | callback: 'http://localhost:3000/hello', 13 | // pass any other configuration here 14 | }) 15 | res.redirect(`https://grant.outofindex.com/connect/${provider}?${params}`) 16 | }) 17 | .get('/hello', (req, res) => { 18 | res.end(JSON.stringify(req.query, null, 2)) 19 | }) 20 | .listen(3000) 21 | -------------------------------------------------------------------------------- /examples/dynamic-proxy/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 | "express": "^4.17.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/dynamic-state/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaults": { 3 | "origin": "http://localhost:3000", 4 | "transport": "session" 5 | }, 6 | "google": { 7 | "key": "APP_ID", 8 | "secret": "APP_SECRET", 9 | "callback": "/hello" 10 | }, 11 | "twitter": { 12 | "callback": "/hi" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/dynamic-state/express.js: -------------------------------------------------------------------------------- 1 | 2 | var express = require('express') 3 | var session = require('express-session') 4 | var grant = require('../../').express() 5 | 6 | 7 | express() 8 | .use(session({secret: 'grant', saveUninitialized: true, resave: false})) 9 | .use('/connect/google', (req, res, next) => { 10 | res.locals.grant = {dynamic: {scope: ['openid']}} 11 | next() 12 | }) 13 | .use('/connect/twitter', (req, res, next) => { 14 | res.locals.grant = {dynamic: {key: 'CONSUMER_KEY', secret: 'CONSUMER_SECRET'}} 15 | next() 16 | }) 17 | .use(grant(require('./config.json'))) 18 | .get('/hello', (req, res) => { 19 | res.end(JSON.stringify(req.session.grant.response, null, 2)) 20 | }) 21 | .get('/hi', (req, res) => { 22 | res.end(JSON.stringify(req.session.grant.response, null, 2)) 23 | }) 24 | .listen(3000) 25 | -------------------------------------------------------------------------------- /examples/dynamic-state/fastify.js: -------------------------------------------------------------------------------- 1 | 2 | var fastify = require('fastify') 3 | var cookie = require('@fastify/cookie') 4 | var session = require('@fastify/session') 5 | var grant = require('../../').fastify() 6 | 7 | 8 | fastify() 9 | .decorateRequest('grant', null) 10 | .addHook('preHandler', async (req, res) => { 11 | if (/^\/connect\/google/.test(req.url)) { 12 | req.grant = {dynamic: {scope: ['openid']}} 13 | } 14 | else if (/^\/connect\/twitter/.test(req.url)) { 15 | req.grant = {dynamic: {key: 'CONSUMER_KEY', 'secret': 'CONSUMER_SECRET'}} 16 | } 17 | }) 18 | .register(cookie) 19 | .register(session, {secret: '01234567890123456789012345678912', cookie: {secure: false}}) 20 | .register(grant(require('./config'))) 21 | .route({method: 'GET', path: '/hello', handler: async (req, res) => { 22 | res.send(JSON.stringify(req.session.grant.response, null, 2)) 23 | }}) 24 | .route({method: 'GET', path: '/hi', handler: async (req, res) => { 25 | res.send(JSON.stringify(req.session.grant.response, null, 2)) 26 | }}) 27 | .listen({port: 3000}) 28 | -------------------------------------------------------------------------------- /examples/dynamic-state/hapi.js: -------------------------------------------------------------------------------- 1 | 2 | var Hapi = require('@hapi/hapi') 3 | var yar = require('@hapi/yar') 4 | var grant = require('../../').hapi() 5 | 6 | 7 | var server = new Hapi.Server({host: 'localhost', port: 3000}) 8 | 9 | server.ext('onPreHandler', (req, res) => { 10 | if (/^\/connect\/google/.test(req.path)) { 11 | req.plugins.grant = {dynamic: {scope: ['openid']}} 12 | } 13 | else if (/^\/connect\/twitter/.test(req.path)) { 14 | req.plugins.grant = {dynamic: {key: 'CONSUMER_KEY', 'secret': 'CONSUMER_SECRET'}} 15 | } 16 | return res.continue 17 | }) 18 | server.route({method: 'GET', path: '/hello', handler: (req, res) => { 19 | return res 20 | .response(JSON.stringify(req.yar.get('grant').response, null, 2)) 21 | .header('content-type', 'text/plain') 22 | }}) 23 | server.route({method: 'GET', path: '/hi', handler: (req, res) => { 24 | return res 25 | .response(JSON.stringify(req.yar.get('grant').response, null, 2)) 26 | .header('content-type', 'text/plain') 27 | }}) 28 | 29 | ;(async () => { 30 | await server.register([ 31 | {plugin: yar, options: { 32 | name: 'grant', 33 | cookieOptions: {password: '01234567890123456789012345678901', isSecure: false}}}, 34 | {plugin: grant(require('./config.json'))} 35 | ]) 36 | await server.start() 37 | })() 38 | -------------------------------------------------------------------------------- /examples/dynamic-state/koa.js: -------------------------------------------------------------------------------- 1 | 2 | var Koa = require('koa') 3 | var session = require('koa-session') 4 | var Router = require('koa-router') 5 | var grant = require('../../').koa() 6 | 7 | 8 | var app = new Koa() 9 | app.keys = ['grant'] 10 | 11 | app 12 | .use(session(app)) 13 | .use(new Router() 14 | .all('/connect/google', async (ctx, next) => { 15 | ctx.state.grant = {dynamic: {scope: ['openid']}} 16 | await next() 17 | }) 18 | .all('/connect/twitter', async (ctx, next) => { 19 | ctx.state.grant = {dynamic: {key: 'CONSUMER_KEY', secret: 'CONSUMER_SECRET'}} 20 | await next() 21 | }) 22 | .routes()) 23 | .use(grant(require('./config.json'))) 24 | .use(new Router() 25 | .get('/hello', (ctx) => { 26 | ctx.body = JSON.stringify(ctx.session.grant.response, null, 2) 27 | }) 28 | .get('/hi', (ctx) => { 29 | ctx.body = JSON.stringify(ctx.session.grant.response, null, 2) 30 | }) 31 | .routes()) 32 | .listen(3000) 33 | -------------------------------------------------------------------------------- /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 | "@fastify/cookie": "^9.1.0", 15 | "@fastify/session": "^10.5.0", 16 | "@hapi/hapi": "^21.3.2", 17 | "@hapi/yar": "^11.0.1", 18 | "express": "^4.18.2", 19 | "express-session": "^1.17.3", 20 | "fastify": "^4.23.2", 21 | "koa": "^2.14.2", 22 | "koa-router": "^9.4.0", 23 | "koa-session": "^6.4.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/request-options/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaults": { 3 | "origin": "http://localhost:3000", 4 | "transport": "session", 5 | "response": ["tokens", "profile"] 6 | }, 7 | "google": { 8 | "key": "APP_ID", 9 | "secret": "APP_SECRET", 10 | "callback": "/hello", 11 | "scope": [ 12 | "openid" 13 | ] 14 | }, 15 | "twitter": { 16 | "key": "CONSUMER_KEY", 17 | "secret": "CONSUMER_SECRET", 18 | "callback": "/hi" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/request-options/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 | "express": "^4.18.2", 15 | "express-session": "^1.17.3", 16 | "proxy-agent": "^6.3.1", 17 | "tunnel": "0.0.6" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/request-options/proxy-agent.js: -------------------------------------------------------------------------------- 1 | 2 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0 3 | 4 | var proxy = require('proxy-agent').ProxyAgent 5 | var agent = new proxy('http://localhost:8009') 6 | 7 | var express = require('express') 8 | var session = require('express-session') 9 | var grant = require('../../').express({ 10 | config: require('./config.json'), 11 | request: {agent} 12 | }) 13 | 14 | 15 | express() 16 | .use(session({secret: 'grant', saveUninitialized: true, resave: false})) 17 | .use(grant) 18 | .get('/hello', (req, res) => { 19 | res.end(JSON.stringify(req.session.grant.response, null, 2)) 20 | }) 21 | .get('/hi', (req, res) => { 22 | res.end(JSON.stringify(req.session.grant.response, null, 2)) 23 | }) 24 | .listen(3000) 25 | -------------------------------------------------------------------------------- /examples/request-options/tunnel-agent.js: -------------------------------------------------------------------------------- 1 | 2 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0 3 | 4 | var tunnel = require('tunnel') 5 | var agent = tunnel.httpsOverHttp({ 6 | proxy: {host: 'localhost', port: 8009} 7 | }) 8 | 9 | var express = require('express') 10 | var session = require('express-session') 11 | var grant = require('../../').express({ 12 | config: require('./config.json'), 13 | request: {agent} 14 | }) 15 | 16 | 17 | express() 18 | .use(session({secret: 'grant', saveUninitialized: true, resave: false})) 19 | .use(grant) 20 | .get('/hello', (req, res) => { 21 | res.end(JSON.stringify(req.session.grant.response, null, 2)) 22 | }) 23 | .get('/hi', (req, res) => { 24 | res.end(JSON.stringify(req.session.grant.response, null, 2)) 25 | }) 26 | .listen(3000) 27 | -------------------------------------------------------------------------------- /examples/response-profile/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaults": { 3 | "origin": "http://localhost:3000", 4 | "transport": "session" 5 | }, 6 | "google": { 7 | "key": "APP_ID", 8 | "secret": "APP_SECRET", 9 | "callback": "/hello", 10 | "response": ["tokens", "raw", "jwt", "profile"], 11 | "scope": [ 12 | "openid", 13 | "email", 14 | "profile" 15 | ], 16 | "overrides": { 17 | "gender": { 18 | "profile_url": "https://people.googleapis.com/v1/people/me?personFields=names,genders", 19 | "scope": [ 20 | "https://www.googleapis.com/auth/userinfo.profile", 21 | "https://www.googleapis.com/auth/user.gender.read" 22 | ] 23 | } 24 | } 25 | }, 26 | "twitter": { 27 | "key": "CONSUMER_KEY", 28 | "secret": "CONSUMER_SECRET", 29 | "callback": "/hi", 30 | "response": ["tokens", "profile"] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/response-profile/express.js: -------------------------------------------------------------------------------- 1 | 2 | var express = require('express') 3 | var session = require('express-session') 4 | var grant = require('../../').express() 5 | 6 | 7 | express() 8 | .use(session({secret: 'grant', saveUninitialized: true, resave: false})) 9 | .use(grant(require('./config.json'))) 10 | .get('/hello', (req, res) => { 11 | res.end(JSON.stringify(req.session.grant.response, null, 2)) 12 | }) 13 | .get('/hi', (req, res) => { 14 | res.end(JSON.stringify(req.session.grant.response, null, 2)) 15 | }) 16 | .listen(3000) 17 | -------------------------------------------------------------------------------- /examples/response-profile/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 | "express": "^4.18.2", 15 | "express-session": "^1.17.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/session-store/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaults": { 3 | "origin": "http://localhost:3000", 4 | "transport": "session" 5 | }, 6 | "google": { 7 | "key": "APP_ID", 8 | "secret": "APP_SECRET", 9 | "callback": "/hello", 10 | "scope": [ 11 | "openid" 12 | ] 13 | }, 14 | "twitter": { 15 | "key": "CONSUMER_KEY", 16 | "secret": "CONSUMER_SECRET", 17 | "callback": "/hi" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/session-store/express-cookie.js: -------------------------------------------------------------------------------- 1 | 2 | var express = require('express') 3 | var session = require('cookie-session') 4 | var grant = require('../../').express() 5 | 6 | 7 | express() 8 | .use(session({signed: true, secret: 'grant', maxAge: 30 * 60 * 1000})) 9 | .use(grant(require('./config.json'))) 10 | .get('/hello', (req, res) => { 11 | res.end(JSON.stringify(req.session.grant.response, null, 2)) 12 | }) 13 | .get('/hi', (req, res) => { 14 | res.end(JSON.stringify(req.session.grant.response, null, 2)) 15 | }) 16 | .listen(3000) 17 | -------------------------------------------------------------------------------- /examples/session-store/express-mongo.js: -------------------------------------------------------------------------------- 1 | 2 | var express = require('express') 3 | var session = require('express-session') 4 | var grant = require('../../').express() 5 | var MongoStore = require('connect-mongo')(session) 6 | 7 | 8 | express() 9 | .use(session({ 10 | store: new MongoStore({db: 'grant', url: 'mongodb://localhost:27017/grant'}), 11 | secret: 'grant', saveUninitialized: true, resave: false 12 | })) 13 | .use(grant(require('./config.json'))) 14 | .get('/hello', (req, res) => { 15 | res.end(JSON.stringify(req.session.grant.response, null, 2)) 16 | }) 17 | .get('/hi', (req, res) => { 18 | res.end(JSON.stringify(req.session.grant.response, null, 2)) 19 | }) 20 | .listen(3000) 21 | -------------------------------------------------------------------------------- /examples/session-store/express-redis.js: -------------------------------------------------------------------------------- 1 | 2 | var express = require('express') 3 | var session = require('express-session') 4 | var grant = require('../../').express() 5 | var redis = require('redis') 6 | var RedisStore = require('connect-redis')(session) 7 | 8 | 9 | express() 10 | .use(session({ 11 | store: new RedisStore({client: redis.createClient()}), 12 | secret: 'grant', saveUninitialized: true, resave: false 13 | })) 14 | .use(grant(require('./config.json'))) 15 | .get('/hello', (req, res) => { 16 | res.end(JSON.stringify(req.session.grant.response, null, 2)) 17 | }) 18 | .get('/hi', (req, res) => { 19 | res.end(JSON.stringify(req.session.grant.response, null, 2)) 20 | }) 21 | .listen(3000) 22 | -------------------------------------------------------------------------------- /examples/session-store/express-session.js: -------------------------------------------------------------------------------- 1 | 2 | var express = require('express') 3 | var session = require('express-session') 4 | var grant = require('../../').express() 5 | 6 | 7 | express() 8 | .use(session({secret: 'grant', saveUninitialized: true, resave: false})) 9 | .use(grant(require('./config.json'))) 10 | .get('/hello', (req, res) => { 11 | res.end(JSON.stringify(req.session.grant.response, null, 2)) 12 | }) 13 | .get('/hi', (req, res) => { 14 | res.end(JSON.stringify(req.session.grant.response, null, 2)) 15 | }) 16 | .listen(3000) 17 | -------------------------------------------------------------------------------- /examples/session-store/fastify-secure-session.js: -------------------------------------------------------------------------------- 1 | 2 | var fastify = require('fastify') 3 | var session = require('@fastify/secure-session') 4 | var grant = require('../../').fastify() 5 | 6 | 7 | fastify() 8 | .register(session, { 9 | secret: '01234567890123456789012345678912', 10 | salt: 'mOXbPOPAcEb8hMDH', 11 | cookie: { path: '/', httpOnly: true } 12 | }) 13 | .addHook('onRequest', (req, res, next) => { 14 | Object.defineProperty(req.session, 'grant', { 15 | get: () => req.session.get('grant'), 16 | set: (value) => req.session.set('grant', value) 17 | }) 18 | next() 19 | }) 20 | .register(grant(require('./config'))) 21 | .route({method: 'GET', path: '/hello', handler: async (req, res) => { 22 | res.send(JSON.stringify(req.session.grant.response, null, 2)) 23 | }}) 24 | .route({method: 'GET', path: '/hi', handler: async (req, res) => { 25 | res.send(JSON.stringify(req.session.grant.response, null, 2)) 26 | }}) 27 | .listen({port: 3000}) 28 | -------------------------------------------------------------------------------- /examples/session-store/fastify-session.js: -------------------------------------------------------------------------------- 1 | 2 | var fastify = require('fastify') 3 | var cookie = require('@fastify/cookie') 4 | var session = require('@fastify/session') 5 | var grant = require('../../').fastify() 6 | 7 | 8 | fastify() 9 | .register(cookie) 10 | .register(session, {secret: '01234567890123456789012345678912', cookie: {secure: false}}) 11 | .register(grant(require('./config'))) 12 | .route({method: 'GET', path: '/hello', handler: async (req, res) => { 13 | res.send(JSON.stringify(req.session.grant.response, null, 2)) 14 | }}) 15 | .route({method: 'GET', path: '/hi', handler: async (req, res) => { 16 | res.send(JSON.stringify(req.session.grant.response, null, 2)) 17 | }}) 18 | .listen({port: 3000}) 19 | -------------------------------------------------------------------------------- /examples/session-store/hapi-redis.js: -------------------------------------------------------------------------------- 1 | 2 | var Hapi = require('@hapi/hapi') 3 | var yar = require('@hapi/yar') 4 | var redis = require('@hapi/catbox-redis') 5 | var grant = require('../../').hapi() 6 | 7 | 8 | var server = new Hapi.Server({ 9 | host: 'localhost', port: 3000, 10 | cache : [{provider: {constructor: redis}}] 11 | }) 12 | 13 | server.route({method: 'GET', path: '/hello', handler: (req, res) => { 14 | return res 15 | .response(JSON.stringify(req.yar.get('grant').response, null, 2)) 16 | .header('content-type', 'text/plain') 17 | }}) 18 | server.route({method: 'GET', path: '/hi', handler: (req, res) => { 19 | return res 20 | .response(JSON.stringify(req.yar.get('grant').response, null, 2)) 21 | .header('content-type', 'text/plain') 22 | }}) 23 | 24 | ;(async () => { 25 | await server.register([ 26 | {plugin: yar, options: { 27 | name: 'grant', 28 | maxCookieSize: 0, // forces all session data to be written to redis 29 | cookieOptions: {password: '01234567890123456789012345678901', isSecure: false}}}, 30 | {plugin: grant(require('./config.json'))} 31 | ]) 32 | await server.start() 33 | })() 34 | -------------------------------------------------------------------------------- /examples/session-store/hapi-session.js: -------------------------------------------------------------------------------- 1 | 2 | var Hapi = require('@hapi/hapi') 3 | var yar = require('@hapi/yar') 4 | var grant = require('../../').hapi() 5 | 6 | 7 | var server = new Hapi.Server({host: 'localhost', port: 3000}) 8 | 9 | server.route({method: 'GET', path: '/hello', handler: (req, res) => { 10 | return res 11 | .response(JSON.stringify(req.yar.get('grant').response, null, 2)) 12 | .header('content-type', 'text/plain') 13 | }}) 14 | server.route({method: 'GET', path: '/hi', handler: (req, res) => { 15 | return res 16 | .response(JSON.stringify(req.yar.get('grant').response, null, 2)) 17 | .header('content-type', 'text/plain') 18 | }}) 19 | 20 | ;(async () => { 21 | await server.register([ 22 | {plugin: yar, options: { 23 | name: 'grant', 24 | cookieOptions: {password: '01234567890123456789012345678901', isSecure: false}}}, 25 | {plugin: grant(require('./config.json'))} 26 | ]) 27 | await server.start() 28 | })() 29 | -------------------------------------------------------------------------------- /examples/session-store/koa-redis.js: -------------------------------------------------------------------------------- 1 | 2 | var Koa = require('koa') 3 | var session = require('koa-generic-session') 4 | var Redis = require('koa-redis') 5 | var Router = require('koa-router') 6 | var grant = require('../../').koa() 7 | 8 | 9 | var app = new Koa() 10 | app.keys = ['grant'] 11 | 12 | app 13 | .use(session({store: Redis()})) 14 | .use(grant(require('./config.json'))) 15 | .use(new Router() 16 | .get('/hello', (ctx) => { 17 | ctx.body = JSON.stringify(ctx.session.grant.response, null, 2) 18 | }) 19 | .get('/hi', (ctx) => { 20 | ctx.body = JSON.stringify(ctx.session.grant.response, null, 2) 21 | }) 22 | .routes()) 23 | .listen(3000) 24 | -------------------------------------------------------------------------------- /examples/session-store/koa-session.js: -------------------------------------------------------------------------------- 1 | 2 | var Koa = require('koa') 3 | var session = require('koa-session') 4 | var Router = require('koa-router') 5 | var grant = require('../../').koa() 6 | 7 | 8 | var app = new Koa() 9 | app.keys = ['grant'] 10 | 11 | app 12 | .use(session(app)) 13 | .use(grant(require('./config.json'))) 14 | .use(new Router() 15 | .get('/hello', (ctx) => { 16 | ctx.body = JSON.stringify(ctx.session.grant.response, null, 2) 17 | }) 18 | .get('/hi', (ctx) => { 19 | ctx.body = JSON.stringify(ctx.session.grant.response, null, 2) 20 | }) 21 | .routes()) 22 | .listen(3000) 23 | -------------------------------------------------------------------------------- /examples/session-store/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 | "@fastify/cookie": "^9.1.0", 15 | "@fastify/secure-session": "^7.1.0", 16 | "@fastify/session": "^10.5.0", 17 | "@hapi/catbox-redis": "^5.0.5", 18 | "@hapi/hapi": "^21.3.2", 19 | "@hapi/yar": "^11.0.1", 20 | "connect-mongo": "^3.2.0", 21 | "connect-redis": "^4.0.4", 22 | "cookie-session": "^1.4.0", 23 | "express": "^4.18.2", 24 | "express-session": "^1.17.3", 25 | "fastify": "^4.23.2", 26 | "koa": "^2.14.2", 27 | "koa-generic-session": "^2.0.4", 28 | "koa-redis": "^4.0.1", 29 | "koa-router": "^9.4.0", 30 | "koa-session": "^6.4.0", 31 | "redis": "^3.0.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/static-overrides/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaults": { 3 | "origin": "http://localhost:3000", 4 | "transport": "session" 5 | }, 6 | "google": { 7 | "key": "APP_ID", 8 | "secret": "APP_SECRET", 9 | "callback": "/hello", 10 | "response": ["tokens", "raw", "jwt"], 11 | "scope": [ 12 | "openid" 13 | ], 14 | "overrides": { 15 | "profile": { 16 | "scope": ["openid", "profile", "email"] 17 | }, 18 | "gmail": { 19 | "response": ["tokens"], 20 | "scope": [ 21 | "https://www.googleapis.com/auth/gmail.send" 22 | ], 23 | "custom_params": { 24 | "access_type": "offline" 25 | } 26 | } 27 | } 28 | }, 29 | "twitter": { 30 | "key": "CONSUMER_KEY", 31 | "secret": "CONSUMER_SECRET", 32 | "callback": "/hi", 33 | "overrides": { 34 | "write": { 35 | "key": "CONSUMER_KEY", 36 | "secret": "CONSUMER_SECRET" 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/static-overrides/express.js: -------------------------------------------------------------------------------- 1 | 2 | var express = require('express') 3 | var session = require('express-session') 4 | var grant = require('../../').express() 5 | 6 | 7 | express() 8 | .use(session({secret: 'grant', saveUninitialized: true, resave: false})) 9 | .use(grant(require('./config.json'))) 10 | .get('/hello', (req, res) => { 11 | res.end(JSON.stringify(req.session.grant.response, null, 2)) 12 | }) 13 | .get('/hi', (req, res) => { 14 | res.end(JSON.stringify(req.session.grant.response, null, 2)) 15 | }) 16 | .listen(3000) 17 | -------------------------------------------------------------------------------- /examples/static-overrides/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 | "express": "^4.18.2", 15 | "express-session": "^1.17.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/token-endpoint/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaults": { 3 | "origin": "http://localhost:3000", 4 | "transport": "session" 5 | }, 6 | "microsoft": { 7 | "key": "APP_ID", 8 | "token_endpoint_auth_method": "private_key_jwt", 9 | "public_key": "-----BEGIN CERTIFICATE-----\n...actual key here...\n-----END CERTIFICATE-----\n\n", 10 | "private_key": "-----BEGIN RSA PRIVATE KEY-----\n...actual key here...\n-----END RSA PRIVATE KEY-----\n\n", 11 | "scope": [ 12 | "openid" 13 | ], 14 | "callback": "/hello" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/token-endpoint/express.js: -------------------------------------------------------------------------------- 1 | 2 | var express = require('express') 3 | var session = require('express-session') 4 | var grant = require('../../').express() 5 | 6 | 7 | express() 8 | .use(session({secret: 'grant', saveUninitialized: true, resave: false})) 9 | .use(grant(require('./config.json'))) 10 | .get('/hello', (req, res) => { 11 | res.end(JSON.stringify(req.session.grant.response, null, 2)) 12 | }) 13 | .listen(3000) 14 | -------------------------------------------------------------------------------- /examples/token-endpoint/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 | "express": "^4.18.2", 15 | "express-session": "^1.17.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/transport-querystring/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaults": { 3 | "origin": "http://localhost:3000", 4 | "transport": "querystring" 5 | }, 6 | "google": { 7 | "key": "APP_ID", 8 | "secret": "APP_SECRET", 9 | "callback": "/hello", 10 | "scope": [ 11 | "openid" 12 | ] 13 | }, 14 | "twitter": { 15 | "key": "CONSUMER_KEY", 16 | "secret": "CONSUMER_SECRET", 17 | "callback": "/hi" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/transport-querystring/express.js: -------------------------------------------------------------------------------- 1 | 2 | var express = require('express') 3 | var session = require('express-session') 4 | var grant = require('../../').express() 5 | 6 | 7 | express() 8 | .use(session({secret: 'grant', saveUninitialized: true, resave: false})) 9 | .use(grant(require('./config.json'))) 10 | .get('/hello', (req, res) => { 11 | res.end(JSON.stringify(req.query, null, 2)) 12 | }) 13 | .get('/hi', (req, res) => { 14 | res.end(JSON.stringify(req.query, null, 2)) 15 | }) 16 | .listen(3000) 17 | -------------------------------------------------------------------------------- /examples/transport-querystring/fastify.js: -------------------------------------------------------------------------------- 1 | 2 | var fastify = require('fastify') 3 | var cookie = require('@fastify/cookie') 4 | var session = require('@fastify/session') 5 | var qs = require('fastify-qs') 6 | var grant = require('../../').fastify() 7 | 8 | 9 | fastify() 10 | .register(qs) 11 | .register(cookie) 12 | .register(session, {secret: '01234567890123456789012345678912', cookie: {secure: false}}) 13 | .register(grant(require('./config'))) 14 | .route({method: 'GET', path: '/hello', handler: async (req, res) => { 15 | res.send(JSON.stringify(req.query, null, 2)) 16 | }}) 17 | .route({method: 'GET', path: '/hi', handler: async (req, res) => { 18 | res.send(JSON.stringify(req.query, null, 2)) 19 | }}) 20 | .listen({port: 3000}) 21 | -------------------------------------------------------------------------------- /examples/transport-querystring/hapi.js: -------------------------------------------------------------------------------- 1 | 2 | var Hapi = require('@hapi/hapi') 3 | var yar = require('@hapi/yar') 4 | var qs = require('qs') 5 | var grant = require('../../').hapi() 6 | 7 | 8 | var server = new Hapi.Server({host: 'localhost', port: 3000}) 9 | 10 | server.route({method: 'GET', path: '/hello', handler: (req, res) => { 11 | return res 12 | .response(JSON.stringify(qs.parse(req.query), null, 2)) 13 | .header('content-type', 'text/plain') 14 | }}) 15 | server.route({method: 'GET', path: '/hi', handler: (req, res) => { 16 | return res 17 | .response(JSON.stringify(qs.parse(req.query), null, 2)) 18 | .header('content-type', 'text/plain') 19 | }}) 20 | 21 | ;(async () => { 22 | await server.register([ 23 | {plugin: yar, options: { 24 | name: 'grant', 25 | cookieOptions: {password: '01234567890123456789012345678901', isSecure: false}}}, 26 | {plugin: grant(require('./config.json'))} 27 | ]) 28 | await server.start() 29 | })() 30 | -------------------------------------------------------------------------------- /examples/transport-querystring/koa.js: -------------------------------------------------------------------------------- 1 | 2 | var Koa = require('koa') 3 | var session = require('koa-session') 4 | var Router = require('koa-router') 5 | var qs = require('koa-qs') 6 | var grant = require('../../').koa() 7 | 8 | 9 | var app = new Koa() 10 | app.keys = ['grant'] 11 | 12 | qs(app) 13 | .use(session(app)) 14 | .use(grant(require('./config.json'))) 15 | .use(new Router() 16 | .get('/hello', (ctx) => { 17 | ctx.body = JSON.stringify(ctx.query, null, 2) 18 | }) 19 | .get('/hi', (ctx) => { 20 | ctx.body = JSON.stringify(ctx.query, null, 2) 21 | }) 22 | .routes()) 23 | .listen(3000) 24 | -------------------------------------------------------------------------------- /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 | "@fastify/cookie": "^9.1.0", 15 | "@fastify/session": "^10.5.0", 16 | "@hapi/hapi": "^21.3.2", 17 | "@hapi/yar": "^11.0.1", 18 | "express": "^4.18.2", 19 | "express-session": "^1.17.3", 20 | "fastify": "^4.23.2", 21 | "fastify-qs": "^4.0.2", 22 | "koa": "^2.14.2", 23 | "koa-qs": "^3.0.0", 24 | "koa-router": "^9.4.0", 25 | "koa-session": "^6.4.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/transport-session/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaults": { 3 | "origin": "http://localhost:3000", 4 | "transport": "session" 5 | }, 6 | "google": { 7 | "key": "APP_ID", 8 | "secret": "APP_SECRET", 9 | "callback": "/hello", 10 | "scope": [ 11 | "openid" 12 | ] 13 | }, 14 | "twitter": { 15 | "key": "CONSUMER_KEY", 16 | "secret": "CONSUMER_SECRET", 17 | "callback": "/hi" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/transport-session/express.js: -------------------------------------------------------------------------------- 1 | 2 | var express = require('express') 3 | var session = require('express-session') 4 | var grant = require('../../').express() 5 | 6 | 7 | express() 8 | .use(session({secret: 'grant', saveUninitialized: true, resave: false})) 9 | .use(grant(require('./config.json'))) 10 | .get('/hello', (req, res) => { 11 | res.end(JSON.stringify(req.session.grant.response, null, 2)) 12 | }) 13 | .get('/hi', (req, res) => { 14 | res.end(JSON.stringify(req.session.grant.response, null, 2)) 15 | }) 16 | .listen(3000) 17 | -------------------------------------------------------------------------------- /examples/transport-session/fastify.js: -------------------------------------------------------------------------------- 1 | 2 | var fastify = require('fastify') 3 | var cookie = require('@fastify/cookie') 4 | var session = require('@fastify/session') 5 | var grant = require('../../').fastify() 6 | 7 | 8 | fastify() 9 | .register(cookie) 10 | .register(session, {secret: '01234567890123456789012345678912', cookie: {secure: false}}) 11 | .register(grant(require('./config'))) 12 | .route({method: 'GET', path: '/hello', handler: async (req, res) => { 13 | res.send(JSON.stringify(req.session.grant.response, null, 2)) 14 | }}) 15 | .route({method: 'GET', path: '/hi', handler: async (req, res) => { 16 | res.send(JSON.stringify(req.session.grant.response, null, 2)) 17 | }}) 18 | .listen({port: 3000}) 19 | -------------------------------------------------------------------------------- /examples/transport-session/hapi.js: -------------------------------------------------------------------------------- 1 | 2 | var Hapi = require('@hapi/hapi') 3 | var yar = require('@hapi/yar') 4 | var grant = require('../../').hapi() 5 | 6 | 7 | var server = new Hapi.Server({host: 'localhost', port: 3000}) 8 | 9 | server.route({method: 'GET', path: '/hello', handler: (req, res) => { 10 | return res 11 | .response(JSON.stringify(req.yar.get('grant').response, null, 2)) 12 | .header('content-type', 'text/plain') 13 | }}) 14 | server.route({method: 'GET', path: '/hi', handler: (req, res) => { 15 | return res 16 | .response(JSON.stringify(req.yar.get('grant').response, null, 2)) 17 | .header('content-type', 'text/plain') 18 | }}) 19 | 20 | ;(async () => { 21 | await server.register([ 22 | {plugin: yar, options: { 23 | name: 'grant', 24 | cookieOptions: {password: '01234567890123456789012345678901', isSecure: false}}}, 25 | {plugin: grant(require('./config.json'))} 26 | ]) 27 | await server.start() 28 | })() 29 | -------------------------------------------------------------------------------- /examples/transport-session/koa.js: -------------------------------------------------------------------------------- 1 | 2 | var Koa = require('koa') 3 | var session = require('koa-session') 4 | var Router = require('koa-router') 5 | var grant = require('../../').koa() 6 | 7 | 8 | var app = new Koa() 9 | app.keys = ['grant'] 10 | 11 | app 12 | .use(session(app)) 13 | .use(grant(require('./config.json'))) 14 | .use(new Router() 15 | .get('/hello', (ctx) => { 16 | ctx.body = JSON.stringify(ctx.session.grant.response, null, 2) 17 | }) 18 | .get('/hi', (ctx) => { 19 | ctx.body = JSON.stringify(ctx.session.grant.response, null, 2) 20 | }) 21 | .routes()) 22 | .listen(3000) 23 | -------------------------------------------------------------------------------- /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 | "@fastify/cookie": "^9.1.0", 15 | "@fastify/session": "^10.5.0", 16 | "@hapi/hapi": "^21.3.2", 17 | "@hapi/yar": "^11.0.1", 18 | "express": "^4.18.2", 19 | "express-session": "^1.17.3", 20 | "fastify": "^4.23.2", 21 | "koa": "^2.14.2", 22 | "koa-router": "^9.4.0", 23 | "koa-session": "^6.4.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/transport-state/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaults": { 3 | "origin": "http://localhost:3000", 4 | "transport": "state" 5 | }, 6 | "google": { 7 | "key": "APP_ID", 8 | "secret": "APP_SECRET", 9 | "scope": [ 10 | "openid" 11 | ] 12 | }, 13 | "twitter": { 14 | "key": "CONSUMER_KEY", 15 | "secret": "CONSUMER_SECRET" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/transport-state/express.js: -------------------------------------------------------------------------------- 1 | 2 | var express = require('express') 3 | var session = require('express-session') 4 | var grant = require('../../').express() 5 | 6 | 7 | express() 8 | .use(session({secret: 'grant', saveUninitialized: true, resave: false})) 9 | .use('/connect/:provider/:override?', express() 10 | .use(grant(require('./config.json'))) 11 | .use((req, res) => { 12 | res.end(JSON.stringify(res.locals.grant.response, null, 2)) 13 | }) 14 | ) 15 | .listen(3000) 16 | -------------------------------------------------------------------------------- /examples/transport-state/fastify.js: -------------------------------------------------------------------------------- 1 | 2 | var fastify = require('fastify') 3 | var cookie = require('@fastify/cookie') 4 | var session = require('@fastify/session') 5 | var grant = require('../../').fastify() 6 | 7 | 8 | fastify() 9 | .decorateReply('grant', null) 10 | .addHook('onSend', async (req, res, payload) => { 11 | if (/^\/connect\/.*?\/callback/.test(req.url)) { 12 | res.header('content-type', 'text/plain') 13 | payload = JSON.stringify(res.grant.response, null, 2) 14 | return payload 15 | } 16 | return payload 17 | }) 18 | .register(cookie) 19 | .register(session, {secret: '01234567890123456789012345678912', cookie: {secure: false}}) 20 | .register(grant(require('./config'))) 21 | .listen({port: 3000}) 22 | -------------------------------------------------------------------------------- /examples/transport-state/hapi.js: -------------------------------------------------------------------------------- 1 | 2 | var Hapi = require('@hapi/hapi') 3 | var yar = require('@hapi/yar') 4 | var grant = require('../../').hapi() 5 | 6 | 7 | var server = new Hapi.Server({host: 'localhost', port: 3000}) 8 | 9 | server.ext('onPostHandler', (req, res) => { 10 | if (/^\/connect\/.*?\/callback$/.test(req.path)) { 11 | return res 12 | .response(JSON.stringify(req.plugins.grant.response, null, 2)) 13 | .header('content-type', 'text/plain') 14 | } 15 | return res.continue 16 | }) 17 | 18 | ;(async () => { 19 | await server.register([ 20 | {plugin: yar, options: { 21 | name: 'grant', 22 | cookieOptions: {password: '01234567890123456789012345678901', isSecure: false}}}, 23 | {plugin: grant(require('./config.json'))} 24 | ]) 25 | await server.start() 26 | })() 27 | -------------------------------------------------------------------------------- /examples/transport-state/koa.js: -------------------------------------------------------------------------------- 1 | 2 | var Koa = require('koa') 3 | var session = require('koa-session') 4 | var Router = require('koa-router') 5 | var grant = require('../../').koa() 6 | 7 | 8 | var app = new Koa() 9 | app.keys = ['grant'] 10 | 11 | app 12 | .use(session(app)) 13 | .use(new Router() 14 | .all('/connect/:provider/:override?', async (ctx, next) => { 15 | await next() 16 | ctx.body = JSON.stringify(ctx.state.grant.response, null, 2) 17 | }) 18 | .routes()) 19 | .use(grant(require('./config.json'))) 20 | .listen(3000) 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 | "@fastify/cookie": "^9.1.0", 15 | "@fastify/session": "^10.5.0", 16 | "@hapi/hapi": "^21.3.2", 17 | "@hapi/yar": "^11.0.1", 18 | "express": "^4.18.2", 19 | "express-session": "^1.17.3", 20 | "fastify": "^4.23.2", 21 | "koa": "^2.14.2", 22 | "koa-router": "^9.4.0", 23 | "koa-session": "^6.4.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /grant.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RequestOptions as RequestComposeOptions, 3 | } from 'request-compose' 4 | 5 | // ---------------------------------------------------------------------------- 6 | 7 | /** 8 | * Grant options 9 | */ 10 | export interface GrantOptions { 11 | /** 12 | * Handler name 13 | */ 14 | handler?: 'express' | 'koa' | 'hapi' | 'fastify' | 'curveball' | 15 | 'node' | 'aws' | 'azure' | 'gcloud' | 'vercel' 16 | /** 17 | * Grant configuration 18 | */ 19 | config?: GrantConfig 20 | /** 21 | * HTTP client options 22 | */ 23 | request?: RequestComposeOptions 24 | /** 25 | * Grant session options 26 | */ 27 | session?: GrantSessionConfig 28 | // exclude 29 | defaults?: never 30 | } 31 | 32 | /** 33 | * Grant config 34 | */ 35 | export interface GrantConfig { 36 | /** 37 | * Default configuration for all providers 38 | */ 39 | defaults?: GrantProvider 40 | /** 41 | * Provider configuration 42 | */ 43 | [provider: string]: GrantProvider | undefined 44 | // exclude 45 | handler?: never 46 | config?: never 47 | request?: never 48 | session?: never 49 | } 50 | 51 | /** 52 | * Grant provider 53 | */ 54 | export interface GrantProvider { 55 | // Authorization Server 56 | 57 | /** 58 | * OAuth 1.0a only, first step 59 | */ 60 | request_url?: string 61 | /** 62 | * OAuth 2.0 first step, OAuth 1.0a second step 63 | */ 64 | authorize_url?: string 65 | /** 66 | * OAuth 2.0 second step, OAuth 1.0a third step 67 | */ 68 | access_url?: string 69 | /** 70 | * OAuth version number 71 | */ 72 | oauth?: number 73 | /** 74 | * String delimiter used for concatenating multiple scopes 75 | */ 76 | scope_delimiter?: string 77 | /** 78 | * Authentication method for the token endpoint 79 | */ 80 | token_endpoint_auth_method?: string 81 | /** 82 | * Signing algorithm for the token endpoint 83 | */ 84 | token_endpoint_auth_signing_alg?: string 85 | 86 | // Client Server 87 | 88 | /** 89 | * Where your client server can be reached 90 | */ 91 | origin?: string 92 | /** 93 | * Path prefix for the Grant internal routes 94 | */ 95 | prefix?: string 96 | /** 97 | * Random state string for OAuth 2.0 98 | */ 99 | state?: boolean | string 100 | /** 101 | * Random nonce string for OpenID Connect 102 | */ 103 | nonce?: boolean | string 104 | /** 105 | * Toggle PKCE support 106 | */ 107 | pkce?: boolean 108 | /** 109 | * Response data to receive 110 | */ 111 | response?: string[] 112 | /** 113 | * Transport type to deliver the response data 114 | */ 115 | transport?: string 116 | /** 117 | * Relative or absolute URL to receive the response data 118 | */ 119 | callback?: string 120 | /** 121 | * Static configuration overrides for a provider 122 | */ 123 | overrides?: { 124 | [key: string]: Omit 125 | } 126 | /** 127 | * Configuration keys that can be overridden dynamically over HTTP 128 | */ 129 | dynamic?: boolean | string[] 130 | 131 | // Client App 132 | 133 | /** 134 | * The client_id or consumer_key of your OAuth app 135 | */ 136 | key?: string 137 | /** 138 | * The client_id or consumer_key of your OAuth app 139 | */ 140 | client_id?: string 141 | /** 142 | * The client_id or consumer_key of your OAuth app 143 | */ 144 | consumer_key?: string 145 | /** 146 | * The client_secret or consumer_secret of your OAuth app 147 | */ 148 | secret?: string 149 | /** 150 | * The client_secret or consumer_secret of your OAuth app 151 | */ 152 | client_secret?: string 153 | /** 154 | * The client_secret or consumer_secret of your OAuth app 155 | */ 156 | consumer_secret?: string 157 | /** 158 | * List of scopes to request 159 | */ 160 | scope?: string | string[] 161 | /** 162 | * Custom authorization parameters and their values 163 | */ 164 | custom_params?: any 165 | /** 166 | * String to embed into the authorization server URLs 167 | */ 168 | subdomain?: string 169 | /** 170 | * Public PEM or JWK 171 | */ 172 | public_key?: any 173 | /** 174 | * Private PEM or JWK 175 | */ 176 | private_key?: any 177 | /** 178 | * Absolute redirect URL of the OAuth app 179 | */ 180 | redirect_uri?: string 181 | /** 182 | * User profile URL 183 | */ 184 | profile_url?: string 185 | } 186 | 187 | /** 188 | * Grant session config 189 | */ 190 | export interface GrantSessionConfig { 191 | /** 192 | * Cookie name 193 | */ 194 | name?: string 195 | /** 196 | * Cookie secret 197 | */ 198 | secret: string 199 | /** 200 | * Cookie options 201 | */ 202 | cookie?: any 203 | /** 204 | * Session store 205 | */ 206 | store?: GrantSessionStore 207 | } 208 | 209 | /** 210 | * Grant session store 211 | */ 212 | export interface GrantSessionStore { 213 | /** 214 | * Get item from session store 215 | */ 216 | get: (sid: string) => any 217 | /** 218 | * Set item in session store 219 | */ 220 | set: (sid: string, json: any) => void 221 | /** 222 | * Remove item from session store 223 | */ 224 | remove?: (sid: string) => void 225 | } 226 | 227 | // ---------------------------------------------------------------------------- 228 | 229 | /** 230 | * Grant instance 231 | */ 232 | export interface GrantInstance { 233 | /** 234 | * Grant instance configuration 235 | */ 236 | config: any 237 | } 238 | 239 | /** 240 | * Grant handler 241 | */ 242 | export type GrantHandler = ( 243 | /** 244 | * Request object 245 | */ 246 | req: any, 247 | /** 248 | * Response object 249 | */ 250 | res?: any, 251 | /** 252 | * Grant dynamic state overrides 253 | */ 254 | state?: {dynamic: GrantProvider} 255 | ) => Promise 256 | 257 | /** 258 | * Grant handler result 259 | */ 260 | export interface GrantHandlerResult { 261 | /** 262 | * Grant session store instance 263 | */ 264 | session: GrantSessionStore 265 | /** 266 | * HTTP redirect 267 | */ 268 | redirect?: any 269 | /** 270 | * Grant response 271 | */ 272 | response?: GrantResponse 273 | } 274 | 275 | // ---------------------------------------------------------------------------- 276 | 277 | /** 278 | * Grant session 279 | */ 280 | export interface GrantSession { 281 | /** 282 | * The provider name used for this authorization 283 | */ 284 | provider: string 285 | /** 286 | * The static override name used for this authorization 287 | */ 288 | override?: string 289 | /** 290 | * The dynamic override configuration passed to this authorization 291 | */ 292 | dynamic?: any 293 | /** 294 | * OAuth 2.0 state string that was generated 295 | */ 296 | state?: string 297 | /** 298 | * OpenID Connect nonce string that was generated 299 | */ 300 | nonce?: string 301 | /** 302 | * The code verifier that was generated for PKCE 303 | */ 304 | code_verifier?: string 305 | /** 306 | * Data returned from the first request of the OAuth 1.0a flow 307 | */ 308 | request?: string 309 | /** 310 | * The final response data 311 | */ 312 | response?: GrantResponse 313 | } 314 | 315 | /** 316 | * Grant response 317 | */ 318 | export interface GrantResponse { 319 | /** 320 | * OAuth 2.0 and OAuth 1.0a access secret 321 | */ 322 | access_token?: string 323 | /** 324 | * OAuth 2.0 refresh token 325 | */ 326 | refresh_token?: string 327 | /** 328 | * OpenID Connect id token 329 | */ 330 | id_token?: string 331 | /** 332 | * OAuth 1.0a access secret 333 | */ 334 | access_secret?: string 335 | /** 336 | * Raw response data 337 | */ 338 | raw?: any 339 | /** 340 | * Parsed id_token JWT 341 | */ 342 | jwt?: { 343 | id_token?: {header: any, payload: any, signature: string} 344 | } 345 | /** 346 | * User profile response 347 | */ 348 | profile?: any 349 | /** 350 | * Error response 351 | */ 352 | error?: any 353 | } 354 | 355 | // ---------------------------------------------------------------------------- 356 | 357 | /** 358 | * Express middleware 359 | */ 360 | export type ExpressMiddleware = () => Promise 361 | /** 362 | * Koa middleware 363 | */ 364 | export type KoaMiddleware = (ctx: any, next?: () => Promise) => Promise 365 | /** 366 | * Hapi middleware 367 | */ 368 | export interface HapiMiddleware {register: (server: any, options?: any) => void, pkg: any} 369 | /** 370 | * Fastify middleware 371 | */ 372 | export type FastifyMiddleware = (server: any, options: any, next: () => void) => void 373 | /** 374 | * Curveball middleware 375 | */ 376 | export type CurveballMiddleware = (ctx: any, next?: () => Promise) => Promise 377 | 378 | // ---------------------------------------------------------------------------- 379 | 380 | /** 381 | * Grant OAuth Proxy 382 | */ 383 | declare function grant(): (config: GrantConfig | GrantOptions) => any 384 | declare function grant(config: GrantConfig | GrantOptions): any 385 | 386 | /** 387 | * Grant OAuth Proxy 388 | */ 389 | declare namespace grant { 390 | /** 391 | * Express handler 392 | */ 393 | function express(): (config: GrantConfig | GrantOptions) => ExpressMiddleware & GrantInstance 394 | function express(config: GrantConfig | GrantOptions): ExpressMiddleware & GrantInstance 395 | /** 396 | * Koa handler 397 | */ 398 | function koa(): (config: GrantConfig | GrantOptions) => KoaMiddleware & GrantInstance 399 | function koa(config: GrantConfig | GrantOptions): KoaMiddleware & GrantInstance 400 | /** 401 | * Hapi handler 402 | */ 403 | function hapi(): (config: GrantConfig | GrantOptions) => HapiMiddleware & GrantInstance 404 | function hapi(config: GrantConfig | GrantOptions): HapiMiddleware & GrantInstance 405 | /** 406 | * Fastify handler 407 | */ 408 | function fastify(): (config: GrantConfig | GrantOptions) => FastifyMiddleware & GrantInstance 409 | function fastify(config: GrantConfig | GrantOptions): FastifyMiddleware & GrantInstance 410 | /** 411 | * Curveball handler 412 | */ 413 | function curveball(): (config: GrantConfig | GrantOptions) => CurveballMiddleware & GrantInstance 414 | function curveball(config: GrantConfig | GrantOptions): CurveballMiddleware & GrantInstance 415 | /** 416 | * Node handler 417 | */ 418 | function node(): (config: GrantConfig | GrantOptions) => GrantHandler & GrantInstance 419 | function node(config: GrantConfig | GrantOptions): GrantHandler & GrantInstance 420 | /** 421 | * AWS Lambda handler 422 | */ 423 | function aws(): (config: GrantConfig | GrantOptions) => GrantHandler & GrantInstance 424 | function aws(config: GrantConfig | GrantOptions): GrantHandler & GrantInstance 425 | /** 426 | * Azure Function handler 427 | */ 428 | function azure(): (config: GrantConfig | GrantOptions) => GrantHandler & GrantInstance 429 | function azure(config: GrantConfig | GrantOptions): GrantHandler & GrantInstance 430 | /** 431 | * Google Cloud Function handler 432 | */ 433 | function gcloud(): (config: GrantConfig | GrantOptions) => GrantHandler & GrantInstance 434 | function gcloud(config: GrantConfig | GrantOptions): GrantHandler & GrantInstance 435 | /** 436 | * Vercel Function handler 437 | */ 438 | function vercel(): (config: GrantConfig | GrantOptions) => GrantHandler & GrantInstance 439 | function vercel(config: GrantConfig | GrantOptions): GrantHandler & GrantInstance 440 | } 441 | 442 | export default grant 443 | -------------------------------------------------------------------------------- /grant.js: -------------------------------------------------------------------------------- 1 | 2 | function grant ({handler, ...rest}) { 3 | if (handler === 'express') { 4 | return require('./lib/handler/express-4')(rest) 5 | } 6 | else if (handler === 'koa') { 7 | try { 8 | var pkg = require('koa/package.json') 9 | } 10 | catch (err) {} 11 | var version = pkg ? parseInt(pkg.version.split('.')[0]) : 2 12 | return version >= 2 13 | ? require('./lib/handler/koa-2')(rest) 14 | : require('./lib/handler/koa-1')(rest) 15 | } 16 | else if (handler === 'hapi') { 17 | try { 18 | var pkg = require('@hapi/hapi/package.json') 19 | } 20 | catch (err) { 21 | try { 22 | var pkg = require('hapi/package.json') 23 | } 24 | catch (err) {} 25 | } 26 | var version = pkg ? parseInt(pkg.version.split('.')[0]) : 17 27 | return version >= 17 28 | ? require('./lib/handler/hapi-17')(rest) 29 | : require('./lib/handler/hapi-16')(rest) 30 | } 31 | else if (handler === 'express-4') { 32 | return require('./lib/handler/express-4')(rest) 33 | } 34 | else if (handler === 'koa-2') { 35 | return require('./lib/handler/koa-2')(rest) 36 | } 37 | else if (handler === 'koa-1') { 38 | return require('./lib/handler/koa-1')(rest) 39 | } 40 | else if (handler === 'hapi-17') { 41 | return require('./lib/handler/hapi-17')(rest) 42 | } 43 | else if (handler === 'hapi-16') { 44 | return require('./lib/handler/hapi-16')(rest) 45 | } 46 | else if (handler === 'fastify') { 47 | return require('./lib/handler/fastify')(rest) 48 | } 49 | else if (handler === 'curveball') { 50 | return require('./lib/handler/curveball')(rest) 51 | } 52 | else if (handler === 'node') { 53 | return require('./lib/handler/node')(rest) 54 | } 55 | else if (handler === 'aws') { 56 | return require('./lib/handler/aws')(rest) 57 | } 58 | else if (handler === 'azure') { 59 | return require('./lib/handler/azure')(rest) 60 | } 61 | else if (handler === 'gcloud') { 62 | return require('./lib/handler/gcloud')(rest) 63 | } 64 | else if (handler === 'vercel') { 65 | return require('./lib/handler/vercel')(rest) 66 | } 67 | } 68 | 69 | grant.express = (options) => { 70 | var handler = require('./lib/handler/express-4') 71 | return options ? handler(options) : handler 72 | } 73 | 74 | grant.koa = (options) => { 75 | try { 76 | var pkg = require('koa/package.json') 77 | } 78 | catch (err) {} 79 | var version = pkg ? parseInt(pkg.version.split('.')[0]) : 2 80 | var handler = version >= 2 81 | ? require('./lib/handler/koa-2') 82 | : require('./lib/handler/koa-1') 83 | return options ? handler(options) : handler 84 | } 85 | 86 | grant.hapi = (options) => { 87 | try { 88 | var pkg = require('@hapi/hapi/package.json') 89 | } 90 | catch (err) { 91 | try { 92 | var pkg = require('hapi/package.json') 93 | } 94 | catch (err) {} 95 | } 96 | var version = pkg ? parseInt(pkg.version.split('.')[0]) : 17 97 | var handler = version >= 17 98 | ? require('./lib/handler/hapi-17') 99 | : require('./lib/handler/hapi-16') 100 | return options ? handler(options) : handler 101 | } 102 | 103 | grant.fastify = (options) => { 104 | var handler = require('./lib/handler/fastify') 105 | return options ? handler(options) : handler 106 | } 107 | 108 | grant.curveball = (options) => { 109 | var handler = require('./lib/handler/curveball') 110 | return options ? handler(options) : handler 111 | } 112 | 113 | grant.node = (options) => { 114 | var handler = require('./lib/handler/node') 115 | return options ? handler(options) : handler 116 | } 117 | 118 | grant.aws = (options) => { 119 | var handler = require('./lib/handler/aws') 120 | return options ? handler(options) : handler 121 | } 122 | 123 | grant.azure = (options) => { 124 | var handler = require('./lib/handler/azure') 125 | return options ? handler(options) : handler 126 | } 127 | 128 | grant.gcloud = (options) => { 129 | var handler = require('./lib/handler/gcloud') 130 | return options ? handler(options) : handler 131 | } 132 | 133 | grant.vercel = (options) => { 134 | var handler = require('./lib/handler/vercel') 135 | return options ? handler(options) : handler 136 | } 137 | 138 | grant.default = grant 139 | module.exports = grant 140 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | 2 | var compose = require('request-compose') 3 | var oauth = require('request-oauth') 4 | var qs = require('qs') 5 | var pkg = require('../package') 6 | 7 | 8 | var defaults = (args) => () => { 9 | var {options} = compose.Request.defaults(args)() 10 | options.headers['user-agent'] = `simov/grant/${pkg.version}` 11 | return {options} 12 | } 13 | 14 | var parse = () => ({options, res, res: {headers}, body, raw}) => { 15 | 16 | raw = body 17 | 18 | var header = Object.keys(headers) 19 | .find((name) => name.toLowerCase() === 'content-type') 20 | 21 | if (/json|javascript/.test(headers[header])) { 22 | try { 23 | body = JSON.parse(body) 24 | } 25 | catch (err) {} 26 | } 27 | 28 | else if (/application\/x-www-form-urlencoded/.test(headers[header])) { 29 | try { 30 | body = qs.parse(body) // use qs instead of querystring for nested objects 31 | } 32 | catch (err) {} 33 | } 34 | 35 | // some providers return incorrect content-type like text/html or text/plain 36 | else { 37 | try { 38 | body = JSON.parse(body) 39 | } 40 | catch (err) { 41 | body = qs.parse(body) // use qs instead of querystring for nested objects 42 | } 43 | } 44 | 45 | log({parse: {res, body}}) 46 | 47 | return {options, res, body, raw} 48 | } 49 | 50 | var log = (data) => { 51 | if (process.env.DEBUG) { 52 | try { 53 | require('request-logs')(data) 54 | } 55 | catch (err) {} 56 | } 57 | } 58 | 59 | module.exports = compose.extend({ 60 | Request: {defaults, oauth}, 61 | Response: {parse} 62 | }).client 63 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | 2 | var crypto = require('crypto') 3 | 4 | var oauth = require('../config/oauth.json') 5 | var reserved = require('../config/reserved.json') 6 | var profile = require('../config/profile.json') 7 | 8 | 9 | var compose = (...fns) => 10 | fns.reduce((x, y) => (...args) => y(x(...args))) 11 | 12 | var dcopy = (obj) => 13 | JSON.parse(JSON.stringify(obj)) 14 | 15 | var merge = (...args) => 16 | Object.assign(...args.filter(Boolean).map(dcopy)) 17 | 18 | var filter = (obj) => Object.keys(obj) 19 | .filter((key) => 20 | // empty string 21 | obj[key] !== '' && ( 22 | // provider name 23 | key === obj.name || 24 | // reserved key 25 | reserved.includes(key) 26 | )) 27 | .reduce((all, key) => (all[key] = obj[key], all), {}) 28 | 29 | var format = { 30 | 31 | oauth: ({oauth}) => 32 | parseInt(oauth) || undefined 33 | , 34 | 35 | key: ({oauth, key, consumer_key, client_id}) => 36 | oauth === 1 37 | ? key || consumer_key 38 | 39 | : oauth === 2 40 | ? key || client_id 41 | 42 | : undefined 43 | , 44 | 45 | secret: ({oauth, secret, consumer_secret, client_secret}) => 46 | oauth === 1 47 | ? secret || consumer_secret 48 | 49 | : oauth === 2 50 | ? secret || client_secret 51 | 52 | : undefined 53 | , 54 | 55 | scope: ({scope, scope_delimiter = ','}) => 56 | scope instanceof Array 57 | ? scope.filter(Boolean).join(scope_delimiter) || undefined 58 | 59 | : typeof scope === 'object' 60 | ? JSON.stringify(scope) 61 | 62 | : scope || undefined 63 | , 64 | 65 | state: ({state}) => 66 | state || undefined 67 | , 68 | 69 | nonce: ({nonce}) => 70 | nonce || undefined 71 | , 72 | 73 | redirect_uri: ({redirect_uri, origin, prefix, protocol, host, name}) => 74 | redirect_uri 75 | ? redirect_uri 76 | 77 | : origin 78 | ? `${origin}${prefix}/${name}/callback` 79 | 80 | : protocol && host 81 | ? `${protocol}://${host}${prefix}/${name}/callback` 82 | 83 | : undefined 84 | , 85 | 86 | custom_params: (provider) => { 87 | var params = provider.custom_params || {} 88 | 89 | // remove falsy 90 | params = Object.keys(params) 91 | .filter((key) => params[key]) 92 | .reduce((all, key) => (all[key] = params[key], all), {}) 93 | 94 | return Object.keys(params).length ? params : undefined 95 | }, 96 | 97 | overrides: (provider) => { 98 | var overrides = provider.overrides || {} 99 | delete provider.overrides 100 | 101 | // remove nested 102 | Object.keys(overrides).forEach((name) => { 103 | overrides[name] = Object.keys(overrides[name]) 104 | .filter((key) => key !== 'overrides') 105 | .reduce((all, key) => (all[key] = overrides[name][key], all), {}) 106 | }) 107 | 108 | overrides = Object.keys(overrides) 109 | .reduce((all, key) => (all[key] = init(provider, overrides[key]), all), {}) 110 | 111 | return Object.keys(overrides).length ? overrides : undefined 112 | }, 113 | 114 | } 115 | 116 | var state = (provider, key = 'state', value = provider[key]) => 117 | value === true || value === 'true' 118 | ? crypto.randomBytes(20).toString('hex') 119 | 120 | : value === 'false' 121 | ? undefined 122 | 123 | : /string|number/.test(typeof value) 124 | ? value.toString() 125 | 126 | : undefined 127 | 128 | var pkce = (code_verifier = crypto.randomBytes(40).toString('hex')) => ({ 129 | code_verifier, 130 | code_challenge: crypto.createHash('sha256') 131 | .update(code_verifier).digest().toString('base64') 132 | .replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_') 133 | }) 134 | 135 | var transform = (provider) => { 136 | 137 | Object.keys(format) 138 | .forEach((key) => provider[key] = format[key](provider)) 139 | 140 | // filter undefined 141 | return dcopy(provider) 142 | } 143 | 144 | var init = compose(merge, filter, transform) 145 | 146 | var compat = (config) => 147 | config.fitbit2 ? (Object.assign({}, config, {fitbit2: Object.assign({}, oauth.fitbit, profile.fitbit, config.fitbit2)})) : 148 | config.linkedin2 ? (Object.assign({}, config, {linkedin2: Object.assign({}, oauth.linkedin, profile.linkedin, config.linkedin2)})) : 149 | config.zeit ? (Object.assign({}, config, {zeit: Object.assign({}, oauth.vercel, profile.vercel, config.zeit)})) : 150 | config 151 | 152 | var defaults = ({path, prefix = '/connect', ...rest} = {}) => ({ 153 | ...rest, 154 | prefix: path ? `${path}${prefix}` : prefix 155 | }) 156 | 157 | // init all configured providers 158 | var ctor = ((_defaults) => (config = {}, defaults = _defaults(config.defaults)) => 159 | Object.keys(compat(config)) 160 | .filter((name) => !/defaults/.test(name)) 161 | .reduce((all, name) => ( 162 | all[name] = init(oauth[name], profile[name], defaults, config[name], {name, [name]: true}), 163 | all 164 | ), {defaults}) 165 | )(defaults) 166 | 167 | // get provider on connect 168 | var provider = (config, session, _state = {}) => { 169 | var name = session.provider 170 | var provider = config[name] 171 | 172 | if (!provider) { 173 | if ((config.defaults || {}).dynamic !== true) { 174 | return {} 175 | } 176 | provider = init(oauth[name], profile[name], config.defaults, {name, [name]: true}) 177 | } 178 | 179 | if (session.override && provider.overrides) { 180 | var override = provider.overrides[session.override] 181 | if (override) { 182 | provider = override 183 | } 184 | } 185 | 186 | if ((session.dynamic && provider.dynamic) || _state.dynamic) { 187 | var dynamic = Object.assign( 188 | {}, 189 | _state.dynamic, 190 | provider.dynamic === true 191 | ? session.dynamic 192 | : Object.keys(session.dynamic || {}) 193 | .filter((key) => provider.dynamic.includes(key)) 194 | .reduce((all, key) => (all[key] = session.dynamic[key], all), {}) 195 | ) 196 | provider = init(provider, dynamic) 197 | } 198 | 199 | if (provider.state) { 200 | provider = dcopy(provider) 201 | provider.state = state(provider) 202 | } 203 | if (provider.nonce) { 204 | provider = dcopy(provider) 205 | provider.nonce = state(provider, 'nonce') 206 | } 207 | if (provider.pkce) { 208 | provider = dcopy(provider) 209 | ;({ 210 | code_verifier: provider.code_verifier, 211 | code_challenge: provider.code_challenge 212 | } = pkce()) 213 | } 214 | 215 | return provider 216 | } 217 | 218 | module.exports = Object.assign(ctor, { 219 | compose, dcopy, merge, filter, format, state, pkce, transform, init, defaults, compat, provider 220 | }) 221 | -------------------------------------------------------------------------------- /lib/flow/oauth1.js: -------------------------------------------------------------------------------- 1 | 2 | var qs = require('qs') 3 | var request = require('../client') 4 | 5 | 6 | exports.request = ({request:client}) => async ({provider, input}) => { 7 | var options = { 8 | method: 'POST', 9 | url: provider.request_url, 10 | oauth: { 11 | callback: provider.redirect_uri, 12 | consumer_key: provider.key, 13 | consumer_secret: provider.secret 14 | } 15 | } 16 | if (provider.private_key) { 17 | options.oauth.signature_method = 'RSA-SHA1' 18 | options.oauth.private_key = provider.private_key 19 | delete options.oauth.consumer_secret 20 | } 21 | if (provider.etsy || provider.linkedin) { 22 | options.qs = {scope: provider.scope} 23 | } 24 | if (provider.getpocket) { 25 | delete options.oauth 26 | options.headers = { 27 | 'x-accept': 'application/x-www-form-urlencoded' 28 | } 29 | options.form = { 30 | consumer_key: provider.key, 31 | redirect_uri: provider.redirect_uri, 32 | state: provider.state 33 | } 34 | } 35 | if (provider.freshbooks) { 36 | options.oauth.signature_method = 'PLAINTEXT' 37 | } 38 | if (provider.twitter) { 39 | if (provider.scope) { 40 | options.qs = {x_auth_access_type: [].concat(provider.scope).join()} 41 | } 42 | if (provider.custom_params) { 43 | options.qs = {x_auth_access_type: provider.custom_params.x_auth_access_type} 44 | } 45 | } 46 | if (provider.subdomain) { 47 | options.url = options.url.replace('[subdomain]', provider.subdomain) 48 | } 49 | try { 50 | var {body:output} = await request({...client, ...options}) 51 | if (provider.sellsy) { 52 | output = qs.parse(output) 53 | } 54 | } 55 | catch (err) { 56 | var output = {error: err.body || err.message} 57 | } 58 | return {provider, input, output} 59 | } 60 | 61 | exports.authorize = async ({provider, input, output}) => { 62 | if (!output.oauth_token && !output.code) { 63 | output = Object.keys(output).length 64 | ? output : {error: 'Grant: OAuth1 missing oauth_token parameter'} 65 | return {provider, input, output} 66 | } 67 | var url = provider.authorize_url 68 | var params = { 69 | oauth_token: output.oauth_token 70 | } 71 | if (provider.custom_params) { 72 | for (var key in provider.custom_params) { 73 | params[key] = provider.custom_params[key] 74 | } 75 | } 76 | if (provider.flickr && provider.scope) { 77 | params.perms = provider.scope 78 | } 79 | if (provider.getpocket) { 80 | params = { 81 | request_token: output.code, 82 | redirect_uri: provider.redirect_uri 83 | } 84 | } 85 | if (provider.ravelry || provider.trello) { 86 | params.scope = provider.scope 87 | } 88 | if (provider.tripit) { 89 | params.oauth_callback = provider.redirect_uri 90 | } 91 | if (provider.subdomain) { 92 | url = url.replace('[subdomain]', provider.subdomain) 93 | } 94 | return {provider, input, output: `${url}?${qs.stringify(params)}`} 95 | } 96 | 97 | exports.access = ({request:client}) => async ({provider, input, input:{session, query}}) => { 98 | if (!query.oauth_token && !session.request.code) { 99 | var output = Object.keys(query).length 100 | ? query : {error: 'Grant: OAuth1 missing oauth_token parameter'} 101 | return {provider, input, output} 102 | } 103 | var options = { 104 | method: 'POST', 105 | url: provider.access_url, 106 | oauth: { 107 | consumer_key: provider.key, 108 | consumer_secret: provider.secret, 109 | token: query.oauth_token, 110 | token_secret: session.request.oauth_token_secret, 111 | verifier: query.oauth_verifier 112 | } 113 | } 114 | if (provider.private_key) { 115 | options.oauth.signature_method = 'RSA-SHA1' 116 | options.oauth.private_key = provider.private_key 117 | delete options.oauth.consumer_secret 118 | } 119 | if (provider.freshbooks) { 120 | options.oauth.signature_method = 'PLAINTEXT' 121 | } 122 | if (provider.getpocket) { 123 | delete options.oauth 124 | options.headers = { 125 | 'x-accept': 'application/x-www-form-urlencoded' 126 | } 127 | options.form = { 128 | consumer_key: provider.key, 129 | code: session.request.code 130 | } 131 | } 132 | if (provider.goodreads || provider.tripit) { 133 | delete options.oauth.verifier 134 | } 135 | if (provider.subdomain) { 136 | options.url = options.url.replace('[subdomain]', provider.subdomain) 137 | } 138 | try { 139 | var {body:output} = await request({...client, ...options}) 140 | } 141 | catch (err) { 142 | var output = {error: err.body || err.message} 143 | } 144 | return {provider, input, output} 145 | } 146 | -------------------------------------------------------------------------------- /lib/flow/oauth2.js: -------------------------------------------------------------------------------- 1 | 2 | var crypto = require('crypto') 3 | var qs = require('qs') 4 | var request = require('../client') 5 | 6 | 7 | exports.authorize = async ({provider, input}) => { 8 | var url = provider.authorize_url 9 | var params = { 10 | client_id: provider.key, 11 | response_type: 'code', 12 | redirect_uri: provider.redirect_uri, 13 | scope: provider.scope, 14 | state: provider.state, 15 | nonce: provider.nonce 16 | } 17 | if (provider.pkce) { 18 | params.code_challenge_method = 'S256' 19 | params.code_challenge = provider.code_challenge 20 | } 21 | if (provider.custom_params) { 22 | for (var key in provider.custom_params) { 23 | params[key] = provider.custom_params[key] 24 | } 25 | } 26 | if (provider.basecamp) { 27 | params.type = 'web_server' 28 | } 29 | if (provider.freelancer && params.scope) { 30 | params.advanced_scopes = params.scope 31 | delete params.scope 32 | } 33 | if (provider.instagram && /^\d+$/.test(provider.key)) { 34 | params.app_id = params.client_id 35 | delete params.client_id 36 | params.scope = (params.scope || '').replace(/ /g, ',') || undefined 37 | } 38 | if (provider.optimizely && params.scope) { 39 | params.scopes = params.scope 40 | delete params.scope 41 | } 42 | if (provider.tiktok) { 43 | params.client_key = params.client_id 44 | delete params.client_id 45 | } 46 | if (provider.visualstudio) { 47 | params.response_type = 'Assertion' 48 | } 49 | if (provider.wechat) { 50 | params.appid = params.client_id 51 | delete params.client_id 52 | } 53 | if (provider.subdomain) { 54 | url = url.replace('[subdomain]', provider.subdomain) 55 | } 56 | var querystring = qs.stringify(params) 57 | if (provider.unsplash && params.scope) { 58 | var scope = params.scope 59 | delete params.scope 60 | querystring = qs.stringify(params) + '&scope=' + scope 61 | } 62 | return {provider, input, output: `${url}?${querystring}`} 63 | } 64 | 65 | exports.access = ({request:client}) => async ({provider, input, input:{query, body, session}}) => { 66 | query = Object.keys(query).length ? query : body 67 | if (!query.code) { 68 | var output = Object.keys(query).length 69 | ? query : {error: 'Grant: OAuth2 missing code parameter'} 70 | return {provider, input, output} 71 | } 72 | else if (session.state && (query.state !== session.state)) { 73 | var output = {error: 'Grant: OAuth2 state mismatch'} 74 | return {provider, input, output} 75 | } 76 | var options = { 77 | method: 'POST', 78 | url: provider.access_url, 79 | form: { 80 | grant_type: 'authorization_code', 81 | code: query.code, 82 | client_id: provider.key, 83 | client_secret: provider.secret, 84 | redirect_uri: provider.redirect_uri 85 | } 86 | } 87 | if (provider.pkce) { 88 | options.form.code_verifier = session.code_verifier 89 | } 90 | if (provider.basecamp) { 91 | options.form.type = 'web_server' 92 | } 93 | if (provider.concur) { 94 | delete options.form 95 | options.qs = { 96 | code: query.code, 97 | client_id: provider.key, 98 | client_secret: provider.secret 99 | } 100 | } 101 | if (/autodesk|ebay|fitbit|homeaway|hootsuite|notion|reddit|trustpilot/.test(provider.name) 102 | || provider.token_endpoint_auth_method === 'client_secret_basic' 103 | ) { 104 | delete options.form.client_id 105 | delete options.form.client_secret 106 | options.auth = {user: provider.key, pass: provider.secret} 107 | } 108 | if (/twitter/.test(provider.name)) { 109 | options.form.client_id = provider.key 110 | delete options.form.client_secret 111 | options.auth = {user: provider.key, pass: provider.secret} 112 | } 113 | if (provider.token_endpoint_auth_method === 'private_key_jwt') { 114 | var jwt = ({kid, x5t, secret}) => ({ 115 | header: { 116 | typ: 'JWT', 117 | alg: provider.token_endpoint_auth_signing_alg || 'RS256', 118 | kid, 119 | x5t 120 | }, 121 | payload: { 122 | iss: provider.key, 123 | sub: provider.key, 124 | aud: provider.access_url, 125 | jti: crypto.randomBytes(20).toString('hex'), 126 | exp: Math.round(Date.now() / 1000) + 300, 127 | iat: Math.round(Date.now() / 1000) - 120, 128 | nbf: Math.round(Date.now() / 1000) - 120 129 | }, 130 | secret 131 | }) 132 | 133 | var assertion = (() => { 134 | var oidc = require('../oidc') 135 | var {public_key, private_key} = provider 136 | return oidc.sign(jwt({ 137 | kid: private_key.kty ? oidc.kid(private_key) : undefined, 138 | x5t: public_key ? public_key.kty ? public_key.x5t : oidc.x5t(public_key) : undefined, 139 | secret: private_key.kty ? oidc.pem(private_key) : private_key, 140 | })) 141 | })() 142 | 143 | options.form.client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer' 144 | options.form.client_assertion = assertion 145 | delete options.form.client_id 146 | delete options.form.client_secret 147 | } 148 | if (provider.instagram && /^\d+$/.test(provider.key)) { 149 | options.form.app_id = options.form.client_id 150 | delete options.form.client_id 151 | options.form.app_secret = options.form.client_secret 152 | delete options.form.client_secret 153 | } 154 | if (provider.notion) { 155 | options.json = options.form 156 | delete options.form 157 | } 158 | if (provider.tiktok) { 159 | options.form.client_key = options.form.client_id 160 | delete options.form.client_id 161 | } 162 | if (provider.qq) { 163 | options.method = 'GET' 164 | options.qs = options.form 165 | delete options.form 166 | } 167 | if (provider.untappd) { 168 | options.method = 'GET' 169 | options.qs = options.form 170 | delete options.qs.grant_type 171 | options.qs.response_type = 'code' 172 | delete options.form 173 | } 174 | if (provider.wechat) { 175 | options.method = 'GET' 176 | options.qs = options.form 177 | delete options.form 178 | options.qs.appid = options.qs.client_id 179 | options.qs.secret = options.qs.client_secret 180 | delete options.qs.client_id 181 | delete options.qs.client_secret 182 | } 183 | if (provider.smartsheet) { 184 | delete options.form.client_secret 185 | var hash = crypto.createHash('sha256') 186 | hash.update(provider.secret + '|' + query.code) 187 | options.form.hash = hash.digest('hex') 188 | } 189 | if (provider.surveymonkey) { 190 | options.qs = {api_key: provider.custom_params.api_key} 191 | } 192 | if (provider.visualstudio) { 193 | options.form = { 194 | client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', 195 | client_assertion: provider.secret, 196 | grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', 197 | assertion: query.code, 198 | redirect_uri: provider.redirect_uri 199 | } 200 | } 201 | if (provider.withings) { 202 | options.form.action = 'requesttoken' 203 | } 204 | if (provider.subdomain) { 205 | options.url = options.url.replace('[subdomain]', provider.subdomain) 206 | } 207 | try { 208 | var {body:output} = await request({...client, ...options}) 209 | if (provider.intuit) { 210 | output.realmId = query.realmId 211 | } 212 | if (provider.withings) { 213 | output = output.body 214 | } 215 | } 216 | catch (err) { 217 | var output = {error: err.body || err.message} 218 | } 219 | return {provider, input, output} 220 | } 221 | -------------------------------------------------------------------------------- /lib/grant.js: -------------------------------------------------------------------------------- 1 | 2 | var {compose} = require('./util') 3 | var {defaults, connect, callback} = require('./request') 4 | var {data, transport} = require('./response') 5 | var _config = require('./config') 6 | 7 | 8 | module.exports = ({config, request, state, extend}) => { 9 | config = _config(config) 10 | 11 | if (!extend) { 12 | extend = [require('./profile')] 13 | } 14 | 15 | var pipe = compose( 16 | defaults(config), 17 | 18 | ({provider, input, input:{params}}) => params.override !== 'callback' 19 | ? connect({request})({provider, input}) 20 | : compose( 21 | callback({request})({provider, input}), 22 | data, 23 | extend ? compose(...extend.map((fn) => fn({request, state}))) : (args) => ({...args}) 24 | )({provider, input}), 25 | 26 | transport, 27 | ) 28 | 29 | pipe.config = config 30 | return pipe 31 | } 32 | -------------------------------------------------------------------------------- /lib/handler/aws.js: -------------------------------------------------------------------------------- 1 | 2 | var qs = require('qs') 3 | var Grant = require('../grant') 4 | var Session = require('../session') 5 | 6 | 7 | module.exports = function (args = {}) { 8 | var grant = Grant(args.config ? args : {config: args}) 9 | app.config = grant.config 10 | 11 | var regex = new RegExp([ 12 | '^', 13 | app.config.defaults.prefix, 14 | /(?:\/([^\/\?]+?))/.source, // /:provider 15 | /(?:\/([^\/\?]+?))?/.source, // /:override? 16 | /(?:\/$|\/?\?+.*)?$/.source, // querystring 17 | ].join(''), 'i') 18 | 19 | var store = Session(args.session) 20 | 21 | async function app (event, state) { 22 | var req = params(event) 23 | var session = store(req) 24 | var match = regex.exec(req.path) 25 | if (!match) { 26 | return {session} 27 | } 28 | 29 | var {location, session:sess, state} = await grant({ 30 | method: req.method, 31 | params: {provider: match[1], override: match[2]}, 32 | query: req.query, 33 | body: req.body, 34 | state, 35 | session: (await session.get()).grant 36 | }) 37 | 38 | await session.set({grant: sess}) 39 | 40 | return location 41 | ? {session, redirect: redirect(event, location, session)} 42 | : {session, response: state.response || sess.response} 43 | } 44 | 45 | return app 46 | } 47 | 48 | var path = ({version, path, rawPath, requestContext:ctx} = event) => 49 | version === '2.0' ? rawPath : 50 | version === '1.0' ? path : ctx.path 51 | 52 | var body = ({body, isBase64Encoded} = event) => 53 | body 54 | ? isBase64Encoded ? Buffer.from(body, 'base64').toString() 55 | : body : {} 56 | 57 | var params = (event) => 58 | !event.version || event.version === '1.0' ? 59 | { 60 | method: event.httpMethod, 61 | path: path(event), 62 | query: qs.parse(event.queryStringParameters), 63 | headers: event.headers, 64 | body: qs.parse(body(event)), 65 | } 66 | : event.version === '2.0' ? 67 | { 68 | method: event.requestContext.http.method, 69 | path: path(event), 70 | query: qs.parse(event.rawQueryString), 71 | headers: {...event.headers, Cookie: (event.cookies || []).join('; ')}, 72 | body: qs.parse(body(event)), 73 | } 74 | : {} 75 | 76 | var redirect = (event, location, session) => 77 | !event.version || event.version === '1.0' ? 78 | { 79 | statusCode: 302, 80 | headers: {location}, 81 | multiValueHeaders: {'set-cookie': session.headers['set-cookie']} 82 | } 83 | : event.version === '2.0' ? 84 | { 85 | statusCode: 302, 86 | headers: {location}, 87 | cookies: session.headers['set-cookie'] 88 | } 89 | : {} 90 | -------------------------------------------------------------------------------- /lib/handler/azure.js: -------------------------------------------------------------------------------- 1 | 2 | var qs = require('qs') 3 | var Grant = require('../grant') 4 | var Session = require('../session') 5 | 6 | 7 | module.exports = function (args = {}) { 8 | var grant = Grant(args.config ? args : {config: args}) 9 | app.config = grant.config 10 | 11 | var regex = new RegExp([ 12 | /^https?:\/\/[^/]+/.source, 13 | app.config.defaults.prefix, 14 | /(?:\/([^\/\?]+?))/.source, // /:provider 15 | /(?:\/([^\/\?]+?))?/.source, // /:override? 16 | /(?:\/$|\/?\?+.*)?$/.source, // querystring 17 | ].join(''), 'i') 18 | 19 | var store = Session(args.session) 20 | 21 | async function app (req, state) { 22 | var session = store(req) 23 | var match = regex.exec(req.originalUrl) 24 | if (!match) { 25 | return {session} 26 | } 27 | 28 | var {location, session:sess, state} = await grant({ 29 | method: req.method, 30 | params: {provider: match[1], override: match[2]}, 31 | query: {...req.query, code: req.query.oauth_code}, 32 | body: req.method === 'POST' ? req.body : {}, 33 | state, 34 | session: (await session.get()).grant 35 | }) 36 | 37 | await session.set({grant: sess}) 38 | 39 | return location 40 | ? {session, redirect: redirect(location, session)} 41 | : {session, response: state.response || sess.response} 42 | } 43 | 44 | return app 45 | } 46 | 47 | var redirect = (location, session) => ({ 48 | status: 302, 49 | headers: { 50 | location, 51 | 'set-cookie': session.headers['set-cookie'] 52 | } 53 | }) 54 | -------------------------------------------------------------------------------- /lib/handler/curveball.js: -------------------------------------------------------------------------------- 1 | 2 | var qs = require('qs') 3 | var Grant = require('../grant') 4 | 5 | 6 | module.exports = function (args = {}) { 7 | var grant = Grant(args.config ? args : {config: args}) 8 | app.config = grant.config 9 | 10 | var regex = new RegExp([ 11 | '^', 12 | app.config.defaults.prefix, 13 | /(?:\/([^\/\?]+?))/.source, // /:provider 14 | /(?:\/([^\/\?]+?))?/.source, // /:override? 15 | /(?:\/$|\/?\?+.*)?$/.source, // querystring 16 | ].join(''), 'i') 17 | 18 | async function app (ctx, next) { 19 | var match = regex.exec(ctx.path) 20 | if (!match) { 21 | return next() 22 | } 23 | 24 | if (!ctx.state.session) { 25 | throw new Error('Grant: mount session middleware first') 26 | } 27 | if (ctx.method === 'POST' && !ctx.request.body) { 28 | throw new Error('Grant: mount body parser middleware first') 29 | } 30 | 31 | var {location, session, state} = await grant({ 32 | method: ctx.method, 33 | params: {provider: match[1], override: match[2]}, 34 | query: qs.parse(ctx.request.query), 35 | body: qs.parse(ctx.request.body), 36 | state: ctx.state.grant, 37 | session: ctx.state.session.grant, 38 | }) 39 | 40 | ctx.state.session.grant = session 41 | ctx.state.grant = state 42 | location ? ctx.response.redirect(302, location) : await next() 43 | } 44 | 45 | return app 46 | } 47 | -------------------------------------------------------------------------------- /lib/handler/express-4.js: -------------------------------------------------------------------------------- 1 | 2 | var Grant = require('../grant') 3 | 4 | 5 | module.exports = function (args = {}) { 6 | var grant = Grant(args.config ? args : {config: args}) 7 | app.config = grant.config 8 | 9 | var regex = new RegExp([ 10 | '^', 11 | app.config.defaults.prefix, 12 | /(?:\/([^\/\?]+?))/.source, // /:provider 13 | /(?:\/([^\/\?]+?))?/.source, // /:override? 14 | /(?:\/$|\/?\?+.*)?$/.source, // querystring 15 | ].join(''), 'i') 16 | 17 | async function app (req, res, next) { 18 | var match = regex.exec(req.originalUrl) 19 | if (!match) { 20 | return next() 21 | } 22 | 23 | if (!req.session) { 24 | next(new Error('Grant: mount session middleware first')) 25 | return 26 | } 27 | if (req.method === 'POST' && !req.body) { 28 | next(new Error('Grant: mount body parser middleware first')) 29 | return 30 | } 31 | 32 | var {location, session, state} = await grant({ 33 | method: req.method, 34 | params: {provider: match[1], override: match[2]}, 35 | query: req.query, 36 | body: req.body, 37 | state: res.locals.grant, 38 | session: req.session.grant, 39 | }) 40 | 41 | req.session.grant = session 42 | res.locals.grant = state 43 | location ? redirect(req, res, location) : next() 44 | } 45 | 46 | return app 47 | } 48 | 49 | var redirect = (req, res, location) => 50 | typeof req.session.save === 'function' && 51 | Object.getPrototypeOf(req.session).save.length 52 | ? req.session.save(() => res.redirect(location)) 53 | : res.redirect(location) 54 | -------------------------------------------------------------------------------- /lib/handler/fastify.js: -------------------------------------------------------------------------------- 1 | 2 | var qs = require('qs') 3 | var Grant = require('../grant') 4 | 5 | 6 | module.exports = function (args = {}) { 7 | 8 | function app (server, options, next) { 9 | args = args.config ? args : {config: args} 10 | 11 | var grant = Grant(args) 12 | app.config = grant.config 13 | 14 | var prefix = app.config.defaults.prefix.replace(options.prefix, '') 15 | 16 | server.route({ 17 | method: ['GET', 'POST'], 18 | path: `${prefix}/:provider`, 19 | handler 20 | }) 21 | server.route({ 22 | method: ['GET', 'POST'], 23 | path: `${prefix}/:provider/:override`, 24 | handler 25 | }) 26 | 27 | async function handler (req, res) { 28 | if (!req.session) { 29 | throw new Error('Grant: register session plugin first') 30 | } 31 | 32 | var {location, session, state} = await grant({ 33 | method: req.method, 34 | params: req.params, 35 | query: qs.parse(req.query), 36 | body: qs.parse(req.body), 37 | state: req.grant, 38 | session: req.session.grant, 39 | }) 40 | 41 | req.session.grant = session 42 | res.grant = state 43 | return location ? res.redirect(location) : res.send() 44 | } 45 | 46 | next() 47 | } 48 | 49 | return app 50 | } 51 | -------------------------------------------------------------------------------- /lib/handler/gcloud.js: -------------------------------------------------------------------------------- 1 | 2 | var qs = require('qs') 3 | var Grant = require('../grant') 4 | var Session = require('../session') 5 | 6 | 7 | module.exports = function (args = {}) { 8 | var grant = Grant(args.config ? args : {config: args}) 9 | app.config = grant.config 10 | 11 | var regex = new RegExp([ 12 | '^', 13 | app.config.defaults.prefix, 14 | /(?:\/([^\/\?]+?))/.source, // /:provider 15 | /(?:\/([^\/\?]+?))?/.source, // /:override? 16 | /(?:\/$|\/?\?+.*)?$/.source, // querystring 17 | ].join(''), 'i') 18 | 19 | var store = Session(args.session) 20 | 21 | async function app (req, res, state) { 22 | var session = store(req, res) 23 | var match = regex.exec(req.url) 24 | if (!match) { 25 | return {session} 26 | } 27 | 28 | var {location, session:sess, state} = await grant({ 29 | method: req.method, 30 | params: {provider: match[1], override: match[2]}, 31 | query: qs.parse(req.query), 32 | body: req.body, 33 | state, 34 | session: (await session.get()).grant 35 | }) 36 | 37 | await session.set({grant: sess}) 38 | 39 | return location 40 | ? (redirect(res, location, session), {session, redirect: true}) 41 | : {session, response: state.response || sess.response} 42 | } 43 | 44 | return app 45 | } 46 | 47 | var redirect = (res, location, session) => { 48 | res.setHeader('set-cookie', session.headers['set-cookie']) 49 | setImmediate(() => { 50 | if (!res.headersSent) { 51 | res.statusCode = 302 52 | res.setHeader('location', location) 53 | res.end() 54 | } 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /lib/handler/hapi-16.js: -------------------------------------------------------------------------------- 1 | 2 | var url = require('url') 3 | var qs = require('qs') 4 | var Grant = require('../grant') 5 | 6 | 7 | module.exports = function (args = {}) { 8 | var app = {} 9 | 10 | function register (server, options, next) { 11 | args = args.config ? args : {config: args} 12 | args.config = Object.keys(options).length ? options : args.config 13 | 14 | var grant = Grant(args) 15 | app.config = grant.config 16 | 17 | var prefix = app.config.defaults.prefix 18 | .replace(server.realm.modifiers.route.prefix, '') 19 | 20 | server.route({ 21 | method: ['GET', 'POST'], 22 | path: `${prefix}/{provider}/{override?}`, 23 | handler: (req, res) => { 24 | if (!(req.session || req.yar)) { 25 | throw new Error('Grant: register session plugin first') 26 | } 27 | 28 | var query = (parseInt(server.version.split('.')[0]) >= 12) 29 | ? qs.parse(url.parse(req.url, false).query) // #2985 30 | : req.query 31 | 32 | var body = (parseInt(server.version.split('.')[0]) >= 12) 33 | ? qs.parse(req.payload) // #2985 34 | : req.payload 35 | 36 | grant({ 37 | method: req.method, 38 | params: req.params, 39 | query: query, 40 | body: body, 41 | state: req.plugins.grant, 42 | session: (req.session || req.yar).get('grant'), 43 | }).then(({location, session, state}) => { 44 | ;(req.session || req.yar).set('grant', session) 45 | req.plugins.grant = state 46 | location ? res.redirect(location) : res.continue() 47 | }) 48 | } 49 | }) 50 | 51 | next() 52 | } 53 | 54 | register.attributes = { 55 | pkg: require('../../package.json') 56 | } 57 | 58 | app.register = register 59 | return app 60 | } 61 | -------------------------------------------------------------------------------- /lib/handler/hapi-17.js: -------------------------------------------------------------------------------- 1 | 2 | var qs = require('qs') 3 | var Grant = require('../grant') 4 | 5 | 6 | module.exports = function (args = {}) { 7 | var app = {} 8 | 9 | function register (server, options) { 10 | args = args.config ? args : {config: args} 11 | args.config = Object.keys(options).length ? options : args.config 12 | 13 | var grant = Grant(args) 14 | app.config = grant.config 15 | 16 | var prefix = app.config.defaults.prefix 17 | .replace(server.realm.modifiers.route.prefix, '') 18 | 19 | server.route({ 20 | method: ['GET', 'POST'], 21 | path: `${prefix}/{provider}/{override?}`, 22 | handler: async (req, res) => { 23 | if (!req.yar) { 24 | throw new Error('Grant: register session plugin first') 25 | } 26 | 27 | var {location, session, state} = await grant({ 28 | method: req.method, 29 | params: req.params, 30 | query: qs.parse(req.query), 31 | body: qs.parse(req.payload), // #2985 32 | state: req.plugins.grant, 33 | session: req.yar.get('grant'), 34 | }) 35 | 36 | req.yar.set('grant', session) 37 | req.plugins.grant = state 38 | return location ? res.redirect(location) : res.continue 39 | } 40 | }) 41 | } 42 | 43 | app.pkg = require('../../package.json') 44 | 45 | app.register = register 46 | return app 47 | } 48 | -------------------------------------------------------------------------------- /lib/handler/koa-1.js: -------------------------------------------------------------------------------- 1 | 2 | var qs = require('qs') 3 | var Grant = require('../grant') 4 | 5 | 6 | module.exports = function (args) { 7 | var grant = Grant((args || {}).config ? args : {config: args}) 8 | app.config = grant.config 9 | 10 | var regex = new RegExp([ 11 | '^', 12 | app.config.defaults.prefix, 13 | /(?:\/([^\/\?]+?))/.source, // /:provider 14 | /(?:\/([^\/\?]+?))?/.source, // /:override? 15 | /(?:\/$|\/?\?+.*)?$/.source, // querystring 16 | ].join(''), 'i') 17 | 18 | function* app (next) { 19 | var match = regex.exec(this.request.originalUrl) 20 | if (!match) { 21 | return yield next 22 | } 23 | 24 | if (!this.session) { 25 | throw new Error('Grant: mount session middleware first') 26 | } 27 | if (this.method === 'POST' && !this.request.body) { 28 | throw new Error('Grant: mount body parser middleware first') 29 | } 30 | 31 | var result = yield grant({ 32 | method: this.method, 33 | params: {provider: match[1], override: match[2]}, 34 | query: qs.parse(this.request.query), 35 | body: this.request.body, 36 | state: this.state.grant, 37 | session: this.session.grant, 38 | }) 39 | 40 | this.session.grant = result.session 41 | this.state.grant = result.state 42 | result.location ? this.response.redirect(result.location) : yield next 43 | } 44 | 45 | return app 46 | } 47 | -------------------------------------------------------------------------------- /lib/handler/koa-2.js: -------------------------------------------------------------------------------- 1 | 2 | var qs = require('qs') 3 | var Grant = require('../grant') 4 | 5 | 6 | module.exports = function (args = {}) { 7 | var grant = Grant(args.config ? args : {config: args}) 8 | app.config = grant.config 9 | 10 | var regex = new RegExp([ 11 | '^', 12 | app.config.defaults.prefix, 13 | /(?:\/([^\/\?]+?))/.source, // /:provider 14 | /(?:\/([^\/\?]+?))?/.source, // /:override? 15 | /(?:\/$|\/?\?+.*)?$/.source, // querystring 16 | ].join(''), 'i') 17 | 18 | async function app (ctx, next) { 19 | var match = regex.exec(ctx.originalUrl) 20 | if (!match) { 21 | return next() 22 | } 23 | 24 | if (!ctx.session) { 25 | ctx.throw(400, 'Grant: mount session middleware first') 26 | } 27 | if (ctx.method === 'POST' && !ctx.request.body) { 28 | ctx.throw(400, 'Grant: mount body parser middleware first') 29 | } 30 | 31 | var {location, session, state} = await grant({ 32 | method: ctx.method, 33 | params: {provider: match[1], override: match[2]}, 34 | query: qs.parse(ctx.request.query), 35 | body: ctx.request.body, 36 | state: ctx.state.grant, 37 | session: ctx.session.grant, 38 | }) 39 | 40 | ctx.session.grant = session 41 | ctx.state.grant = state 42 | location ? ctx.response.redirect(location) : await next() 43 | } 44 | 45 | return app 46 | } 47 | -------------------------------------------------------------------------------- /lib/handler/node.js: -------------------------------------------------------------------------------- 1 | 2 | var qs = require('qs') 3 | var Grant = require('../grant') 4 | var Session = require('../session') 5 | 6 | 7 | module.exports = function (args = {}) { 8 | var grant = Grant(args.config ? args : {config: args}) 9 | app.config = grant.config 10 | 11 | var regex = new RegExp([ 12 | '^', 13 | app.config.defaults.prefix, 14 | /(?:\/([^\/\?]+?))/.source, // /:provider 15 | /(?:\/([^\/\?]+?))?/.source, // /:override? 16 | /(?:\/$|\/?\?+(.*))?$/.source, // querystring 17 | ].join(''), 'i') 18 | 19 | var store = Session(args.session) 20 | 21 | async function app (req, res, state) { 22 | var session = store(req, res) 23 | var match = regex.exec(req.url) 24 | if (!match) { 25 | return {session} 26 | } 27 | 28 | var {location, session:sess, state} = await grant({ 29 | method: req.method, 30 | params: {provider: match[1], override: match[2]}, 31 | query: qs.parse(match[3]), 32 | body: req.method === 'POST' ? qs.parse(await buffer(req)) : {}, 33 | state, 34 | session: (await session.get()).grant 35 | }) 36 | 37 | await session.set({grant: sess}) 38 | 39 | return location 40 | ? (redirect(res, location, session), {session, redirect: true}) 41 | : {session, response: state.response || sess.response} 42 | } 43 | 44 | return app 45 | } 46 | 47 | var redirect = (res, location, session) => { 48 | res.setHeader('set-cookie', session.headers['set-cookie']) 49 | setImmediate(() => { 50 | if (!res.headersSent) { 51 | res.statusCode = 302 52 | res.setHeader('location', location) 53 | res.end() 54 | } 55 | }) 56 | } 57 | 58 | var buffer = (req, body = []) => new Promise((resolve, reject) => req 59 | .on('data', (chunk) => body.push(chunk)) 60 | .on('end', () => resolve(Buffer.concat(body).toString('utf8'))) 61 | .on('error', reject) 62 | ) 63 | -------------------------------------------------------------------------------- /lib/handler/vercel.js: -------------------------------------------------------------------------------- 1 | 2 | var qs = require('qs') 3 | var Grant = require('../grant') 4 | var Session = require('../session') 5 | 6 | 7 | module.exports = function (args = {}) { 8 | var grant = Grant(args.config ? args : {config: args}) 9 | app.config = grant.config 10 | 11 | var regex = new RegExp([ 12 | '^', 13 | app.config.defaults.prefix, 14 | /(?:\/([^\/\?]+?))/.source, // /:provider 15 | /(?:\/([^\/\?]+?))?/.source, // /:override? 16 | /(?:\/$|\/?\?+.*)?$/.source, // querystring 17 | ].join(''), 'i') 18 | 19 | var store = Session(args.session) 20 | 21 | async function app (req, res, state) { 22 | var session = store(req, res) 23 | var match = regex.exec(req.url) 24 | if (!match) { 25 | return {session} 26 | } 27 | 28 | var {location, session:sess, state} = await grant({ 29 | method: req.method, 30 | params: {provider: match[1], override: match[2]}, 31 | query: qs.parse(req.query), 32 | body: req.body, 33 | state, 34 | session: (await session.get()).grant 35 | }) 36 | 37 | await session.set({grant: sess}) 38 | 39 | return location 40 | ? (redirect(res, location, session), {session, redirect: true}) 41 | : {session, response: state.response || sess.response} 42 | } 43 | 44 | return app 45 | } 46 | 47 | var redirect = (res, location, session) => { 48 | res.setHeader('set-cookie', session.headers['set-cookie']) 49 | setImmediate(() => { 50 | if (!res.headersSent) { 51 | res.statusCode = 302 52 | res.setHeader('location', location) 53 | res.end() 54 | } 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /lib/oidc.js: -------------------------------------------------------------------------------- 1 | 2 | var crypto = require('crypto') 3 | 4 | 5 | var base64url = (str) => 6 | str.toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_') 7 | 8 | var kid = (jwk) => { 9 | if (jwk.kid) { 10 | return jwk.kid 11 | } 12 | var keys = 13 | jwk.kty === 'RSA' ? {e: jwk.e, kty: jwk.kty, n: jwk.n} : 14 | jwk.kty === 'EC' ? {crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y} : 15 | jwk.kty === 'oct' ? {k: jwk.k, kty: jwk.kty} : undefined 16 | return keys 17 | ? base64url(crypto.createHash('sha256').update(JSON.stringify(keys)).digest()) 18 | : undefined 19 | } 20 | 21 | var x5t = (cert) => { 22 | var s1 = cert.replace(/(?:-----(?:BEGIN|END) CERTIFICATE-----|\s)/g, '') 23 | var s2 = Buffer.from(s1, 'base64') 24 | var s3 = crypto.createHash('sha1').update(s2).digest('hex').toUpperCase() 25 | return base64url(Buffer.from(s3, 'hex')) 26 | } 27 | 28 | var pem = (jwk) => { 29 | var pem = require('jwk-to-pem') 30 | return pem(jwk, {private: true}) 31 | } 32 | 33 | var sign = (jwt) => { 34 | var jws = require('jws') 35 | return jws.sign(jwt) 36 | } 37 | 38 | var jwt = (str) => { 39 | var [header, payload, signature] = str.split('.') 40 | return { 41 | header: JSON.parse(Buffer.from(header, 'base64').toString('binary')), 42 | payload: JSON.parse(Buffer.from(payload, 'base64').toString('utf8')), 43 | signature, 44 | } 45 | } 46 | 47 | module.exports = {base64url, kid, x5t, pem, sign, jwt} 48 | -------------------------------------------------------------------------------- /lib/profile.js: -------------------------------------------------------------------------------- 1 | 2 | var request = require('./client') 3 | 4 | 5 | module.exports = ({request:client}) => async ({provider, input, output}) => { 6 | if (!provider.response || !provider.response.includes('profile')) { 7 | return {provider, input, output} 8 | } 9 | 10 | if (provider.apple && !provider.profile_url && input.body.user) { 11 | output.profile = input.body.user 12 | return {provider, input, output} 13 | } 14 | 15 | if (!provider.profile_url) { 16 | output.profile = {error: 'Grant: No profile URL found!'} 17 | return {provider, input, output} 18 | } 19 | 20 | var options = { 21 | method: 'GET', 22 | url: provider.profile_url, 23 | headers: {}, 24 | } 25 | 26 | if (provider.oauth === 2) { 27 | options.headers.authorization = `Bearer ${output.access_token}` 28 | } 29 | else if (provider.oauth === 1) { 30 | options.oauth = { 31 | consumer_key: provider.key, 32 | consumer_secret: provider.secret, 33 | token: output.access_token, 34 | token_secret: output.access_secret, 35 | } 36 | } 37 | 38 | if (custom[provider.name]) { 39 | Object.assign(options, custom[provider.name]({provider, output})) 40 | } 41 | 42 | if (provider.subdomain) { 43 | options.url = options.url.replace('[subdomain]', provider.subdomain) 44 | } 45 | 46 | try { 47 | var {body} = await request({...client, ...options}) 48 | // JSONP 49 | if (provider.flickr) { 50 | body = JSON.parse(/^.*\((.*)\)/.exec(body)[1]) 51 | } 52 | // JSONP + secondary request 53 | if (provider.qq) { 54 | body = JSON.parse(/^.*\((.*)\)/.exec(Object.keys(body)[0])[1]) 55 | body = {...body, ...(await request({...client, ...options, 56 | url: 'https://graph.qq.com/user/get_user_info', 57 | qs: { 58 | access_token: output.access_token, 59 | oauth_consumer_key: provider.key, 60 | openid: body.openid 61 | } 62 | })).body} 63 | 64 | } 65 | output.profile = body 66 | } 67 | catch (err) { 68 | output.profile = {error: err.body || err.message} 69 | } 70 | 71 | return {provider, input, output} 72 | } 73 | 74 | var custom = { 75 | arcgis: () => ({qs: {f: 'json'}}), 76 | baidu: ({output}) => ({qs: {access_token: output.access_token}}), 77 | constantcontact: ({provider}) => ({qs: {api_key: provider.key}}), 78 | deezer: ({output}) => ({qs: {access_token: output.access_token}}), 79 | disqus: ({provider}) => ({qs: {api_key: provider.key}}), 80 | dropbox: () => ({method: 'POST'}), 81 | echosign: ({output}) => ({headers: {'Access-Token': output.access_token}}), 82 | flickr: ({provider}) => ({qs: {method: 'flickr.urls.getUserProfile', api_key: provider.key, format: 'json'}}), 83 | foursquare: ({output}) => ({qs: {oauth_token: output.access_token}}), 84 | getpocket: ({provider, output}) => ({json: {consumer_key: provider.key, access_token: output.access_token}}), 85 | instagram: ({provider, output}) => /^\d+$/.test(provider.key) ? {qs: {fields: 'id,account_type,username'}} : {url: 'https://api.instagram.com/v1/users/self', qs: {access_token: output.access_token}}, 86 | mailchimp: ({output}) => ({qs: {apikey: output.access_token}}), 87 | meetup: ({output}) => ({qs: {member_id: 'self'}}), 88 | mixcloud: ({output}) => ({qs: {access_token: output.access_token}}), 89 | qq: ({output}) => ({qs: {access_token: output.access_token}}), 90 | shopify: ({output}) => ({headers: {'X-Shopify-Access-Token': output.access_token}}), 91 | slack: ({output}) => ({qs: {token: output.access_token}}), 92 | soundcloud: ({output}) => ({qs: {oauth_token: output.access_token}}), 93 | stackexchange: ({output}) => ({qs: {key: output.access_token}}), 94 | stocktwits: ({output}) => ({qs: {access_token: output.access_token}}), 95 | tiktok: ({output}) => ({method: 'POST', json: {access_token: output.access_token, open_id: output.raw.open_id, fields: ['open_id', 'union_id', 'avatar_url', 'display_name']}}), 96 | tumblr: ({output}) => ({qs: {api_key: output.access_token}}), 97 | vk: ({output}) => ({qs: {access_token: output.access_token, v: '5.103'}}), 98 | wechat: ({output}) => ({qs: {access_token: output.access_token, openid: output.raw.openid, lang: 'zh_CN'}}), 99 | weibo: ({output}) => ({qs: {access_token: output.access_token, uid: output.raw.uid}}), 100 | twitch: ({provider, output}) => ({headers: {'client-id': provider.key, authorization: `Bearer ${output.access_token}`}}), 101 | twitter: ({output}) => ({qs: {user_id: output.raw.user_id}}), 102 | } 103 | -------------------------------------------------------------------------------- /lib/request.js: -------------------------------------------------------------------------------- 1 | 2 | var {compose, dcopy} = require('./util') 3 | var _config = require('./config') 4 | var oauth1 = require('./flow/oauth1') 5 | var oauth2 = require('./flow/oauth2') 6 | 7 | 8 | var defaults = (config) => ({method, params, query, body, state, session}) => { 9 | method = method.toUpperCase() 10 | params = dcopy(params || {}) 11 | query = dcopy(query || {}) 12 | body = dcopy(body || {}) 13 | state = dcopy(state || {}) 14 | session = dcopy(params.override === 'callback' ? (session || {}) : {}) 15 | 16 | if (params.override !== 'callback') { 17 | session.provider = params.provider 18 | 19 | if (params.override) { 20 | session.override = params.override 21 | } 22 | if (method === 'GET' && Object.keys(query).length) { 23 | session.dynamic = query 24 | } 25 | else if (method === 'POST' && Object.keys(body).length) { 26 | session.dynamic = body 27 | } 28 | } 29 | 30 | var provider = _config.provider(config, session, state) 31 | return {provider, input: {method, params, query, body, state, session}} 32 | } 33 | 34 | var connect = ({request}) => ({provider, input, input:{session}, output}) => 35 | provider.oauth === 1 36 | ? compose( 37 | oauth1.request({request}), 38 | ({provider, input, input:{session}, output}) => ( 39 | session.request = output, 40 | oauth1.authorize({provider, input, output}) 41 | ) 42 | )({provider, input}) 43 | 44 | : provider.oauth === 2 45 | ? ( 46 | session.state = provider.state, 47 | session.nonce = provider.nonce, 48 | session.code_verifier = provider.code_verifier, 49 | oauth2.authorize({provider, input}) 50 | ) 51 | 52 | : ( 53 | output = {error: 'Grant: missing or misconfigured provider'}, 54 | {provider, input, output} 55 | ) 56 | 57 | var callback = ({request}) => ({provider, input, output}) => 58 | provider.oauth === 1 59 | ? oauth1.access({request}) 60 | 61 | : provider.oauth === 2 62 | ? oauth2.access({request}) 63 | 64 | : ({provider, input, output}) => ( 65 | output = {error: 'Grant: missing session or misconfigured provider'}, 66 | {provider, input, output} 67 | ) 68 | 69 | module.exports = {defaults, connect, callback} 70 | -------------------------------------------------------------------------------- /lib/response.js: -------------------------------------------------------------------------------- 1 | 2 | var qs = require('qs') 3 | 4 | 5 | var tokens = (provider, response) => { 6 | var data = {} 7 | 8 | if (provider.concur) { 9 | data.access_token = response.replace( 10 | /[\s\S]+([^<]+)<\/Token>[\s\S]+/, '$1') 11 | data.refresh_token = response.replace( 12 | /[\s\S]+([^<]+)<\/Refresh_Token>[\s\S]+/, '$1') 13 | } 14 | else if (provider.getpocket) { 15 | data.access_token = response.access_token 16 | } 17 | else if (provider.yammer) { 18 | data.access_token = response.access_token.token 19 | } 20 | 21 | else if (provider.oauth === 1) { 22 | if (response.oauth_token) { 23 | data.access_token = response.oauth_token 24 | } 25 | if (response.oauth_token_secret) { 26 | data.access_secret = response.oauth_token_secret 27 | } 28 | } 29 | else if (provider.oauth === 2) { 30 | if (response.id_token) { 31 | data.id_token = response.id_token 32 | } 33 | if (response.access_token) { 34 | data.access_token = response.access_token 35 | } 36 | if (response.refresh_token) { 37 | data.refresh_token = response.refresh_token 38 | } 39 | } 40 | 41 | return data 42 | } 43 | 44 | var oidc = (provider, session, response) => { 45 | if (!/^[a-zA-Z0-9\-_]+?\.[a-zA-Z0-9\-_]+?\.([a-zA-Z0-9\-_]+)?$/.test(response.id_token)) { 46 | return {error: 'Grant: OpenID Connect invalid id_token format'} 47 | } 48 | 49 | var [header, payload, signature] = response.id_token.split('.') 50 | 51 | try { 52 | header = JSON.parse(Buffer.from(header, 'base64').toString('binary')) 53 | payload = JSON.parse(Buffer.from(payload, 'base64').toString('utf8')) 54 | } 55 | catch (err) { 56 | return {error: 'Grant: OpenID Connect error decoding id_token'} 57 | } 58 | 59 | if (![].concat(payload.aud).includes(provider.key)) { 60 | return {error: 'Grant: OpenID Connect invalid id_token audience'} 61 | } 62 | else if (session.nonce && (payload.nonce !== session.nonce)) { 63 | return {error: 'Grant: OpenID Connect nonce mismatch'} 64 | } 65 | 66 | return {header, payload, signature} 67 | } 68 | 69 | var data = ({provider, input, input:{session}, output}) => { 70 | if (output.error) { 71 | return {provider, input, output} 72 | } 73 | 74 | if (output.id_token) { 75 | var jwt = oidc(provider, session, output) 76 | if (jwt.error) { 77 | return {provider, input, output: jwt} 78 | } 79 | } 80 | 81 | if (!provider.response) { 82 | var data = tokens(provider, output) 83 | data.raw = output 84 | } 85 | else { 86 | var data = {} 87 | var response = [].concat(provider.response) 88 | if (response.find((key) => /token/.test(key))) { 89 | data = tokens(provider, output) 90 | } 91 | if (response.includes('jwt') && jwt) { 92 | data.jwt = {id_token: jwt} 93 | } 94 | if (response.includes('raw')) { 95 | data.raw = output 96 | } 97 | } 98 | 99 | return {provider, input, output: data} 100 | } 101 | 102 | var transport = ({provider, input, input:{params, state, session}, output}) => ({ 103 | location: 104 | (params.override !== 'callback' && !output.error) 105 | ? output 106 | 107 | : (!provider.transport || provider.transport === 'querystring') 108 | ? `${provider.callback || '/'}?${qs.stringify(output)}` 109 | 110 | : provider.transport === 'session' 111 | ? provider.callback 112 | 113 | : undefined, 114 | session: ( 115 | provider.transport === 'session' ? session.response = output : null, 116 | session 117 | ), 118 | state: ( 119 | provider.transport === 'state' ? state.response = output : null, 120 | state 121 | ), 122 | }) 123 | 124 | module.exports = {data, transport} 125 | -------------------------------------------------------------------------------- /lib/session.js: -------------------------------------------------------------------------------- 1 | 2 | var crypto = require('crypto') 3 | var cookie = require('cookie') 4 | var signature = require('cookie-signature') 5 | 6 | 7 | module.exports = ({name, secret, cookie:options, store}) => { 8 | name = name || 'grant' 9 | options = options || {path: '/', httpOnly: true, secure: false, maxAge: null} 10 | 11 | if (!secret) { 12 | throw new Error('Grant: cookie secret is required') 13 | } 14 | 15 | var embed = !store 16 | 17 | return (req) => { 18 | var headers = Object.keys(req.headers) 19 | .filter((key) => /(?:set-)?cookie/i.test(key)) 20 | .reduce((all, key) => (all[key.toLowerCase()] = req.headers[key], all), {}) 21 | 22 | headers['set-cookie'] = 23 | headers['set-cookie'] || 24 | (req.multiValueHeaders && req.multiValueHeaders['Set-Cookie']) || 25 | [] 26 | 27 | var cookies = { 28 | input: 29 | // vercel - parsed object 30 | typeof req.cookies === 'object' && !(req.cookies instanceof Array) ? req.cookies : 31 | cookie.parse( 32 | headers.cookie ? headers.cookie : 33 | // aws v2 event - array of key=value pairs 34 | req.cookies ? req.cookies.join('; ') : 35 | '' 36 | ), 37 | output: headers['set-cookie'].reduce((all, str) => 38 | (all[str.split(';')[0].split('=')[0]] = str, all), {}) 39 | } 40 | 41 | var encode = (payload, opt = {}) => { 42 | var data = embed 43 | ? Buffer.from(JSON.stringify(payload)).toString('base64') 44 | : payload 45 | var value = signature.sign(data, secret) 46 | var output = cookie.serialize(name, value, {...options, ...opt}) 47 | cookies.output[name] = output 48 | headers['set-cookie'] = Object.keys(cookies.output) 49 | .map((name) => cookies.output[name]) 50 | } 51 | 52 | var cookieStore = () => { 53 | var session = (() => { 54 | var payload = signature.unsign(cookies.input[name] || '', secret) 55 | try { 56 | return JSON.parse(Buffer.from(payload, 'base64').toString()) 57 | } 58 | catch (err) { 59 | return {grant: {}} 60 | } 61 | })() 62 | var store = { 63 | get: async (sid) => session, 64 | set: async (sid, value) => session = value, 65 | remove: async (sid) => session = {} 66 | } 67 | return { 68 | get: async () => { 69 | return store.get() 70 | }, 71 | set: async (value) => { 72 | encode(value) 73 | return store.set(null, value) 74 | }, 75 | remove: async () => { 76 | encode('', {maxAge: 0}) 77 | await store.remove() 78 | }, 79 | cookies, 80 | headers, 81 | } 82 | } 83 | 84 | var sessionStore = () => { 85 | var sid = signature.unsign(cookies.input[name] || '', secret) 86 | || crypto.randomBytes(20).toString('hex') 87 | return { 88 | get: async () => { 89 | return await store.get(sid) || {grant: {}} 90 | }, 91 | set: async (value) => { 92 | encode(sid) 93 | return store.set(sid, value) 94 | }, 95 | remove: async () => { 96 | encode(sid, {maxAge: 0}) 97 | await store.remove(sid) 98 | }, 99 | cookies, 100 | headers, 101 | } 102 | } 103 | 104 | return embed ? cookieStore() : sessionStore() 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | 2 | var compose = (...fns) => (args) => 3 | fns.reduce((p, f) => p.then(f), Promise.resolve(args)) 4 | 5 | var dcopy = (obj) => 6 | JSON.parse(JSON.stringify(obj)) 7 | 8 | module.exports = {compose, dcopy} 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grant", 3 | "version": "5.4.24", 4 | "description": "OAuth Proxy", 5 | "keywords": [ 6 | "oauth", 7 | "oauth2", 8 | "openid", 9 | "openid-connect", 10 | "authentication", 11 | "authorization", 12 | "proxy", 13 | "middleware", 14 | "lambda", 15 | "express", 16 | "koa", 17 | "hapi", 18 | "fastify", 19 | "aws", 20 | "azure", 21 | "google-cloud", 22 | "vercel" 23 | ], 24 | "license": "MIT", 25 | "homepage": "https://github.com/simov/grant", 26 | "author": "Simeon Velichkov (https://simov.github.io)", 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/simov/grant.git" 30 | }, 31 | "dependencies": { 32 | "qs": "^6.14.0", 33 | "request-compose": "^2.1.7", 34 | "request-oauth": "^1.0.1" 35 | }, 36 | "optionalDependencies": { 37 | "cookie": "^0.7.2", 38 | "cookie-signature": "^1.2.2", 39 | "jwk-to-pem": "^2.0.7", 40 | "jws": "^4.0.0" 41 | }, 42 | "devDependencies": { 43 | "@curveball/bodyparser": "0.4.6", 44 | "@curveball/core": "0.14.2", 45 | "@curveball/router": "0.2.4", 46 | "@curveball/session": "0.5.0", 47 | "@fastify/cookie": "^9.4.0", 48 | "@fastify/formbody": "^7.4.0", 49 | "@fastify/session": "^10.9.0", 50 | "@hapi/hapi": "^21.3.10", 51 | "@hapi/yar": "^11.0.2", 52 | "body-parser": "^1.20.3", 53 | "cookie-session": "^2.1.0", 54 | "express": "^4.21.0", 55 | "express-session": "^1.18.0", 56 | "fastify": "^4.28.1", 57 | "grant-profile": "^1.0.2", 58 | "koa": "^2.15.3", 59 | "koa-bodyparser": "^4.4.1", 60 | "koa-mount": "^4.0.0", 61 | "koa-qs": "^3.0.0", 62 | "koa-session": "^6.4.0", 63 | "mocha": "^10.7.3", 64 | "nyc": "^17.0.0", 65 | "request-cookie": "^1.0.1", 66 | "request-logs": "^2.1.5" 67 | }, 68 | "main": "./grant.js", 69 | "files": [ 70 | "config/", 71 | "lib/", 72 | "grant.js", 73 | "grant.d.ts", 74 | "CHANGELOG.md", 75 | "LICENSE", 76 | "README.md", 77 | "package.json" 78 | ], 79 | "types": "grant.d.ts", 80 | "scripts": { 81 | "test": "npm run test:ci", 82 | "test:ci": "npx mocha --recursive", 83 | "test:cov": "npx nyc --reporter=lcov --reporter=text-summary mocha -- --recursive" 84 | }, 85 | "engines": { 86 | "node": ">=12.0.0" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /test/client.js: -------------------------------------------------------------------------------- 1 | 2 | var t = require('assert') 3 | var http = require('http') 4 | var qs = require('qs') 5 | var compose = require('request-compose') 6 | var request = require('../lib/client') 7 | 8 | 9 | describe('client', () => { 10 | describe('defaults', () => { 11 | var server 12 | 13 | before((done) => { 14 | server = http.createServer() 15 | server.on('request', (req, res) => { 16 | t.ok(/^simov\/grant/.test(req.headers['user-agent'])) 17 | res.end() 18 | }) 19 | server.listen(5000, done) 20 | }) 21 | 22 | after((done) => { 23 | server.close(done) 24 | }) 25 | 26 | it('user-agent', async () => { 27 | var {res} = await request({url: 'http://localhost:5000'}) 28 | t.equal(res.statusCode, 200) 29 | }) 30 | }) 31 | describe('parse', () => { 32 | var server 33 | 34 | before((done) => { 35 | server = http.createServer() 36 | server.on('request', (req, res) => { 37 | if (req.url === '/json') { 38 | res.writeHead(200, {'content-type': 'application/json'}) 39 | res.end(JSON.stringify({json: true})) 40 | } 41 | if (req.url === '/qs') { 42 | res.writeHead(200, {'content-type': 'application/x-www-form-urlencoded'}) 43 | res.end(qs.stringify({nested: {querystring: true}})) 44 | } 45 | if (req.url === '/jsontext') { 46 | res.writeHead(200, {'content-type': 'text/plain'}) 47 | res.end(JSON.stringify({json: true})) 48 | } 49 | if (req.url === '/qstext') { 50 | res.writeHead(200, {'content-type': 'text/html'}) 51 | res.end(qs.stringify({nested: {querystring: true}})) 52 | } 53 | }) 54 | server.listen(5000, done) 55 | }) 56 | 57 | after((done) => { 58 | server.close(done) 59 | }) 60 | 61 | it('json', async () => { 62 | var {body} = await request({url: 'http://localhost:5000/json'}) 63 | t.deepStrictEqual(body, {json: true}) 64 | }) 65 | 66 | it('querystring', async () => { 67 | var {body} = await request({url: 'http://localhost:5000/qs'}) 68 | t.deepStrictEqual(body, {nested: {querystring: 'true'}}) 69 | }) 70 | 71 | it('json as text', async () => { 72 | var {body} = await request({url: 'http://localhost:5000/jsontext'}) 73 | t.deepStrictEqual(body, {json: true}) 74 | }) 75 | 76 | it('querystring as text', async () => { 77 | var {body} = await request({url: 'http://localhost:5000/qstext'}) 78 | t.deepStrictEqual(body, {nested: {querystring: 'true'}}) 79 | }) 80 | 81 | it('extend', async () => { 82 | var {body} = await request({url: 'http://localhost:5000/qstext'}) 83 | t.deepStrictEqual(body, {nested: {querystring: 'true'}}) 84 | var {body} = await compose.client({url: 'http://localhost:5000/qstext'}) 85 | t.equal(body, 'nested%5Bquerystring%5D=true') 86 | }) 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /test/flow/oauth1.js: -------------------------------------------------------------------------------- 1 | 2 | var t = require('assert') 3 | 4 | var request = require('request-compose').extend({ 5 | Request: {cookie: require('request-cookie').Request}, 6 | Response: {cookie: require('request-cookie').Response}, 7 | }).client 8 | 9 | var oauth = require('../../config/oauth') 10 | var keys = require('../util/keys') 11 | 12 | var Provider = require('../util/provider'), provider 13 | var Client = require('../util/client'), client 14 | 15 | 16 | describe('oauth1', () => { 17 | before(async () => { 18 | provider = await Provider({flow: 'oauth1'}) 19 | client = await Client({ 20 | test: 'handlers', 21 | handler: 'express', 22 | config: { 23 | defaults: { 24 | origin: 'http://localhost:5001', 25 | callback: '/', 26 | }, 27 | ...Object.keys(oauth).reduce((all, name) => (all[name] = { 28 | request_url: provider.url(`/${name}/request_url`), 29 | authorize_url: provider.url(`/${name}/authorize_url`), 30 | access_url: provider.url(`/${name}/access_url`), 31 | dynamic: true, 32 | }, all), {}) 33 | } 34 | }) 35 | }) 36 | 37 | after(async () => { 38 | await client.close() 39 | await provider.close() 40 | }) 41 | 42 | afterEach(() => { 43 | provider.on.request = () => {} 44 | provider.on.authorize = () => {} 45 | provider.on.access = () => {} 46 | }) 47 | 48 | describe('success', () => { 49 | it('twitter', async () => { 50 | provider.on.request = ({url, headers, query, form, oauth}) => { 51 | t.equal(url, '/twitter/request_url') 52 | t.ok(/^simov\/grant/.test(headers['user-agent'])) 53 | t.equal(typeof query, 'object') 54 | t.equal(typeof form, 'object') 55 | t.equal(oauth.oauth_signature_method, 'HMAC-SHA1') 56 | t.equal(oauth.oauth_consumer_key, 'key') 57 | t.equal(oauth.oauth_callback, 'http://localhost:5001/connect/twitter/callback') 58 | } 59 | provider.on.authorize = ({url, headers, query}) => { 60 | t.equal(url, '/twitter/authorize_url?oauth_token=token') 61 | t.equal(typeof headers, 'object') 62 | t.deepEqual(query, {oauth_token: 'token'}) 63 | } 64 | provider.on.access = ({url, headers, query, form, oauth}) => { 65 | t.equal(url, '/twitter/access_url') 66 | t.ok(/^simov\/grant/.test(headers['user-agent'])) 67 | t.equal(typeof query, 'object') 68 | t.equal(typeof form, 'object') 69 | t.equal(oauth.oauth_signature_method, 'HMAC-SHA1') 70 | t.equal(oauth.oauth_consumer_key, 'key') 71 | t.equal(oauth.oauth_token, 'token') 72 | } 73 | var {body: {response}} = await request({ 74 | url: client.url('/connect/twitter'), 75 | qs: {key: 'key'}, 76 | cookie: {}, 77 | }) 78 | t.deepEqual(response, { 79 | access_token: 'token', 80 | access_secret: 'secret', 81 | raw: {oauth_token: 'token', oauth_token_secret: 'secret', user_id: 'id'} 82 | }) 83 | }) 84 | }) 85 | 86 | describe('subdomain', () => { 87 | it('freshbooks', async () => { 88 | provider.on.request = ({url, headers, query, form, oauth}) => { 89 | t.ok(url.startsWith('/freshbooks/request_url')) 90 | } 91 | provider.on.authorize = ({url, headers, query}) => { 92 | t.ok(url.startsWith('/freshbooks/authorize_url')) 93 | } 94 | provider.on.access = ({url, headers, query, form, oauth}) => { 95 | t.ok(url.startsWith('/freshbooks/access_url')) 96 | } 97 | var {body: {response}} = await request({ 98 | url: client.url('/connect/freshbooks'), 99 | qs: { 100 | request_url: provider.url('/[subdomain]/request_url'), 101 | authorize_url: provider.url('/[subdomain]/authorize_url'), 102 | access_url: provider.url('/[subdomain]/access_url'), 103 | subdomain: 'freshbooks', 104 | }, 105 | cookie: {}, 106 | }) 107 | t.deepEqual(response, { 108 | access_token: 'token', 109 | access_secret: 'secret', 110 | raw: {oauth_token: 'token', oauth_token_secret: 'secret'} 111 | }) 112 | }) 113 | }) 114 | 115 | describe('private_key', () => { 116 | it('freshbooks', async () => { 117 | provider.on.request = ({url, headers, query, form, oauth}) => { 118 | t.equal(oauth.oauth_signature_method, 'RSA-SHA1') 119 | t.equal(oauth.oauth_consumer_secret, undefined) 120 | } 121 | provider.on.access = ({url, headers, query, form, oauth}) => { 122 | t.equal(oauth.oauth_signature_method, 'RSA-SHA1') 123 | t.equal(oauth.oauth_consumer_secret, undefined) 124 | } 125 | var {body: {response}} = await request({ 126 | url: client.url('/connect/twitter'), 127 | qs: { 128 | private_key: keys['RSA-SHA1'].private_key, 129 | }, 130 | cookie: {}, 131 | }) 132 | t.deepEqual(response, { 133 | access_token: 'token', 134 | access_secret: 'secret', 135 | raw: {oauth_token: 'token', oauth_token_secret: 'secret', user_id: 'id'} 136 | }) 137 | }) 138 | }) 139 | 140 | describe('custom', () => { 141 | it('querystring scope - request - etsy', async () => { 142 | provider.on.request = ({query}) => { 143 | t.deepEqual(query, {scope: 'email_r profile_r'}) 144 | } 145 | var {body: {response}} = await request({ 146 | url: client.url('/connect/etsy'), 147 | qs: {scope: 'email_r profile_r'}, 148 | cookie: {}, 149 | }) 150 | t.deepEqual(response, { 151 | access_token: 'token', 152 | access_secret: 'secret', 153 | raw: {oauth_token: 'token', oauth_token_secret: 'secret'} 154 | }) 155 | }) 156 | 157 | it('incorrect reponse content type - request - sellsy', async () => { 158 | var {res, body} = await request({ 159 | url: provider.url('/sellsy/request_url') 160 | }) 161 | t.equal(res.headers['content-type'], 'application/json') 162 | t.equal(body, 'authentification_url=https://apifeed.sellsy.com/0/login.php&oauth_token=token&oauth_token_secret=secret&oauth_callback_confirmed=true') 163 | 164 | var {body: {response}} = await request({ 165 | url: client.url('/connect/sellsy'), 166 | cookie: {}, 167 | }) 168 | t.deepEqual(response, { 169 | access_token: 'token', 170 | access_secret: 'secret', 171 | raw: {oauth_token: 'token', oauth_token_secret: 'secret'} 172 | }) 173 | }) 174 | 175 | it('signature_method - request/access - freshbooks', async () => { 176 | provider.on.request = ({headers}) => { 177 | t.ok(/oauth_signature_method="PLAINTEXT"/.test(headers.authorization)) 178 | } 179 | provider.on.access = ({headers}) => { 180 | t.ok(/oauth_signature_method="PLAINTEXT"/.test(headers.authorization)) 181 | } 182 | var {body: {response}} = await request({ 183 | url: client.url('/connect/freshbooks'), 184 | cookie: {}, 185 | }) 186 | t.deepEqual(response, { 187 | access_token: 'token', 188 | access_secret: 'secret', 189 | raw: {oauth_token: 'token', oauth_token_secret: 'secret'} 190 | }) 191 | }) 192 | 193 | it('scope - request - twitter', async () => { 194 | provider.on.request = ({query}) => { 195 | t.deepEqual(query, {x_auth_access_type: 'read'}) 196 | } 197 | var {body: {response}} = await request({ 198 | url: client.url('/connect/twitter'), 199 | qs: {scope: ['read']}, 200 | cookie: {}, 201 | }) 202 | t.deepEqual(response, { 203 | access_token: 'token', 204 | access_secret: 'secret', 205 | raw: {oauth_token: 'token', oauth_token_secret: 'secret', user_id: 'id'} 206 | }) 207 | }) 208 | 209 | it('custom_params - request - twitter', async () => { 210 | provider.on.request = ({query}) => { 211 | t.deepEqual(query, {x_auth_access_type: 'read'}) 212 | } 213 | var {body: {response}} = await request({ 214 | url: client.url('/connect/twitter'), 215 | // request-compose:querystring can't handle nested objects 216 | qs: 'custom_params%5Bx_auth_access_type%5D=read', 217 | cookie: {}, 218 | }) 219 | t.deepEqual(response, { 220 | access_token: 'token', 221 | access_secret: 'secret', 222 | raw: {oauth_token: 'token', oauth_token_secret: 'secret', user_id: 'id'} 223 | }) 224 | }) 225 | 226 | it('scope - authorize - flickr', async () => { 227 | provider.on.authorize = ({query}) => { 228 | t.deepEqual(query, {perms: 'a,b', oauth_token: 'token'}) 229 | } 230 | var {body: {response}} = await request({ 231 | url: client.url('/connect/flickr'), 232 | qs: {scope: 'a,b'}, 233 | cookie: {}, 234 | }) 235 | t.deepEqual(response, { 236 | access_token: 'token', 237 | access_secret: 'secret', 238 | raw: {oauth_token: 'token', oauth_token_secret: 'secret'} 239 | }) 240 | }) 241 | 242 | it('scope - authorize - ravelry', async () => { 243 | provider.on.authorize = ({query}) => { 244 | t.deepEqual(query, {scope: 'a b', oauth_token: 'token'}) 245 | } 246 | var {body: {response}} = await request({ 247 | url: client.url('/connect/ravelry'), 248 | qs: {scope: 'a b'}, 249 | cookie: {}, 250 | }) 251 | t.deepEqual(response, { 252 | access_token: 'token', 253 | access_secret: 'secret', 254 | raw: {oauth_token: 'token', oauth_token_secret: 'secret'} 255 | }) 256 | }) 257 | 258 | it('scope - authorize - trello', async () => { 259 | provider.on.authorize = ({query}) => { 260 | t.deepEqual(query, {scope: 'a,b', oauth_token: 'token'}) 261 | } 262 | var {body: {response}} = await request({ 263 | url: client.url('/connect/trello'), 264 | qs: {scope: 'a,b'}, 265 | cookie: {}, 266 | }) 267 | t.deepEqual(response, { 268 | access_token: 'token', 269 | access_secret: 'secret', 270 | raw: {oauth_token: 'token', oauth_token_secret: 'secret'} 271 | }) 272 | }) 273 | 274 | it('custom_params - authorize - trello', async () => { 275 | provider.on.authorize = ({query}) => { 276 | t.deepEqual(query, {oauth_token: 'token', name: 'grant'}) 277 | } 278 | var {body: {response}} = await request({ 279 | url: client.url('/connect/trello'), 280 | // request-compose:querystring can't handle nested objects 281 | qs: 'custom_params%5Bname%5D=grant', 282 | cookie: {}, 283 | }) 284 | t.deepEqual(response, { 285 | access_token: 'token', 286 | access_secret: 'secret', 287 | raw: {oauth_token: 'token', oauth_token_secret: 'secret'} 288 | }) 289 | }) 290 | 291 | it('oauth_verifier - access - goodreads', async () => { 292 | provider.on.access = ({oauth}) => { 293 | t.equal(oauth.oauth_verifier, undefined) 294 | } 295 | var {body: {response}} = await request({ 296 | url: client.url('/connect/goodreads'), 297 | cookie: {}, 298 | }) 299 | t.deepEqual(response, { 300 | access_token: 'token', 301 | access_secret: 'secret', 302 | raw: {oauth_token: 'token', oauth_token_secret: 'secret'} 303 | }) 304 | }) 305 | 306 | it('oauth_callback - authorize, oauth_verifier - access - tripit', async () => { 307 | provider.on.authorize = ({query}) => { 308 | t.deepEqual(query, { 309 | oauth_callback: 'http://localhost:5001/connect/tripit/callback', 310 | oauth_token: 'token' 311 | }) 312 | } 313 | provider.on.access = ({oauth}) => { 314 | t.equal(oauth.oauth_verifier, undefined) 315 | } 316 | var {body: {response}} = await request({ 317 | url: client.url('/connect/tripit'), 318 | cookie: {}, 319 | }) 320 | t.deepEqual(response, { 321 | access_token: 'token', 322 | access_secret: 'secret', 323 | raw: {oauth_token: 'token', oauth_token_secret: 'secret'} 324 | }) 325 | }) 326 | 327 | it('custom - request/authorize/access - getpocket', async () => { 328 | provider.on.request = ({headers, form}) => { 329 | t.equal(headers['x-accept'], 'application/x-www-form-urlencoded') 330 | t.deepEqual(form, { 331 | consumer_key: 'key', 332 | state: 'state', 333 | redirect_uri: 'http://localhost:5001/connect/getpocket/callback', 334 | }) 335 | } 336 | provider.on.authorize = ({query}) => { 337 | t.deepEqual(query, { 338 | request_token: 'code', 339 | redirect_uri: 'http://localhost:5001/connect/getpocket/callback' 340 | }) 341 | } 342 | provider.on.access = ({headers, form}) => { 343 | t.equal(headers['x-accept'], 'application/x-www-form-urlencoded') 344 | t.deepEqual(form, { 345 | consumer_key: 'key', 346 | code: 'code' 347 | }) 348 | } 349 | var {body: {response}} = await request({ 350 | url: client.url('/connect/getpocket'), 351 | qs: {key: 'key', state: 'state'}, 352 | cookie: {}, 353 | }) 354 | t.deepEqual(response, { 355 | access_token: 'token', 356 | raw: {access_token: 'token'} 357 | }) 358 | }) 359 | }) 360 | 361 | describe('error', () => { 362 | it('request - missing oauth_token with response message', async () => { 363 | var {body: {response}} = await request({ 364 | url: client.url('/connect/twitter'), 365 | qs: {request_url: provider.url('/request_error_message')}, 366 | cookie: {}, 367 | }) 368 | t.deepEqual(response, {error: {message: 'invalid'}}) 369 | }) 370 | 371 | it('request - status code', async () => { 372 | var {body: {response}} = await request({ 373 | url: client.url('/connect/twitter'), 374 | qs: {request_url: provider.url('/request_error_status')}, 375 | cookie: {}, 376 | }) 377 | t.deepEqual(response, {error: {invalid: 'request_url'}}) 378 | }) 379 | 380 | it('request - missing oauth_token without response message', async () => { 381 | var {body: {response}} = await request({ 382 | url: client.url('/connect/twitter'), 383 | qs: {request_url: provider.url('/request_error_token')}, 384 | cookie: {}, 385 | }) 386 | t.deepEqual(response, {error: 'Grant: OAuth1 missing oauth_token parameter'}) 387 | }) 388 | 389 | it('authorize - mising oauth_token with response message', async () => { 390 | var {body: {response}} = await request({ 391 | url: client.url('/connect/twitter'), 392 | qs: {authorize_url: provider.url('/authorize_error_message')}, 393 | cookie: {}, 394 | }) 395 | t.deepEqual(response, {error: {message: 'invalid'}}) 396 | }) 397 | 398 | it('authorize - mising oauth_token without error message', async () => { 399 | var {body: {response}} = await request({ 400 | url: client.url('/connect/twitter'), 401 | qs: {authorize_url: provider.url('/authorize_error_token')}, 402 | cookie: {}, 403 | }) 404 | t.deepEqual(response, {error: 'Grant: OAuth1 missing oauth_token parameter'}) 405 | }) 406 | 407 | it('access - status code', async () => { 408 | var {body: {response}} = await request({ 409 | url: client.url('/connect/twitter'), 410 | qs: {access_url: provider.url('/access_error_status')}, 411 | cookie: {}, 412 | }) 413 | t.deepEqual(response, {error: {invalid: 'access_url'}}) 414 | }) 415 | }) 416 | }) 417 | -------------------------------------------------------------------------------- /test/handler.js: -------------------------------------------------------------------------------- 1 | 2 | var t = require('assert') 3 | 4 | 5 | describe('handler', () => { 6 | 7 | describe('handlers', () => { 8 | var grant = require('../') 9 | var config = {defaults: {dynamic: true}} 10 | var output = {defaults: {prefix: '/connect', dynamic: true}} 11 | 12 | it('express', () => { 13 | t.deepEqual(grant.express()(config).config, output) 14 | t.deepEqual(grant.express()({config}).config, output) 15 | t.deepEqual(grant.express(config).config, output) 16 | t.deepEqual(grant.express({config}).config, output) 17 | t.deepEqual(grant({config, handler: 'express'}).config, output) 18 | t.deepEqual(grant({config, handler: 'express-4'}).config, output) 19 | }) 20 | it('koa', () => { 21 | t.deepEqual(grant.koa()(config).config, output) 22 | t.deepEqual(grant.koa()({config}).config, output) 23 | t.deepEqual(grant.koa(config).config, output) 24 | t.deepEqual(grant.koa({config}).config, output) 25 | t.deepEqual(grant({config, handler: 'koa'}).config, output) 26 | t.deepEqual(grant({config, handler: 'koa-2'}).config, output) 27 | }) 28 | it('hapi', () => { 29 | t.ok(typeof grant.hapi()(config).register === 'function') 30 | t.ok(typeof grant.hapi()({config}).register === 'function') 31 | t.ok(typeof grant.hapi(config).register === 'function') 32 | t.ok(typeof grant.hapi({config}).register === 'function') 33 | t.ok(typeof grant({config, handler: 'hapi'}).register === 'function') 34 | t.ok(typeof grant({config, handler: 'hapi-17'}).register === 'function') 35 | }) 36 | }) 37 | 38 | describe('expose config', () => { 39 | it('express', () => { 40 | var Grant = require('../').express() 41 | var grant = Grant() 42 | t.ok(typeof grant.config === 'object') 43 | }) 44 | it('koa', () => { 45 | var Grant = require('../').koa() 46 | var grant = Grant() 47 | t.ok(typeof grant.config === 'object') 48 | }) 49 | }) 50 | 51 | describe('constructor', () => { 52 | it('using new', () => { 53 | var Grant = require('../').express() 54 | var grant1 = new Grant({grant1: {}}) 55 | var grant2 = new Grant({grant2: {}}) 56 | t.deepEqual(grant1.config, { 57 | defaults: {prefix: '/connect'}, 58 | grant1: {prefix: '/connect', grant1: true, name: 'grant1'} 59 | }) 60 | t.deepEqual(grant2.config, { 61 | defaults: {prefix: '/connect'}, 62 | grant2: {prefix: '/connect', grant2: true, name: 'grant2'} 63 | }) 64 | 65 | var Grant = require('../').koa() 66 | var grant1 = new Grant({grant1: {}}) 67 | var grant2 = new Grant({grant2: {}}) 68 | t.deepEqual(grant1.config, { 69 | defaults: {prefix: '/connect'}, 70 | grant1: {prefix: '/connect', grant1: true, name: 'grant1'} 71 | }) 72 | t.deepEqual(grant2.config, { 73 | defaults: {prefix: '/connect'}, 74 | grant2: {prefix: '/connect', grant2: true, name: 'grant2'} 75 | }) 76 | }) 77 | it('without using new', () => { 78 | var Grant = require('../').express() 79 | var grant1 = Grant({grant1: {}}) 80 | var grant2 = Grant({grant2: {}}) 81 | t.deepEqual(grant1.config, { 82 | defaults: {prefix: '/connect'}, 83 | grant1: {prefix: '/connect', grant1: true, name: 'grant1'} 84 | }) 85 | t.deepEqual(grant2.config, { 86 | defaults: {prefix: '/connect'}, 87 | grant2: {prefix: '/connect', grant2: true, name: 'grant2'} 88 | }) 89 | 90 | var Grant = require('../').koa() 91 | var grant1 = Grant({grant1: {}}) 92 | var grant2 = Grant({grant2: {}}) 93 | t.deepEqual(grant1.config, { 94 | defaults: {prefix: '/connect'}, 95 | grant1: {prefix: '/connect', grant1: true, name: 'grant1'} 96 | }) 97 | t.deepEqual(grant2.config, { 98 | defaults: {prefix: '/connect'}, 99 | grant2: {prefix: '/connect', grant2: true, name: 'grant2'} 100 | }) 101 | }) 102 | }) 103 | 104 | describe('hapi options', () => { 105 | var {Hapi, hapi} = (() => { 106 | var load = (prefix) => ({ 107 | Hapi: require(`${prefix}hapi`), 108 | hapi: parseInt(require(`${prefix}hapi/package.json`).version.split('.')[0]) 109 | }) 110 | try { 111 | return load('') 112 | } 113 | catch (err) { 114 | return load('@hapi/') 115 | } 116 | })() 117 | var Grant = require('../').hapi() 118 | 119 | if (hapi < 17) { 120 | it('passed in server.register', (done) => { 121 | var config = {grant: {}} 122 | var grant = new Grant() 123 | var server = new Hapi.Server() 124 | server.connection({host: 'localhost', port: 5000}) 125 | server.register([{register: grant, options: config}], () => { 126 | t.deepEqual( 127 | grant.config, 128 | { 129 | defaults: {prefix: '/connect'}, 130 | grant: { 131 | prefix: '/connect', grant: true, name: 'grant' 132 | } 133 | } 134 | ) 135 | done() 136 | }) 137 | }) 138 | it('passed in the constructor', (done) => { 139 | var config = {grant: {}} 140 | var grant = Grant(config) 141 | var server = new Hapi.Server() 142 | server.connection({host: 'localhost', port: 5000}) 143 | server.register([{register: grant}], () => { 144 | t.deepEqual( 145 | grant.config, 146 | { 147 | defaults: {prefix: '/connect'}, 148 | grant: { 149 | prefix: '/connect', grant: true, name: 'grant' 150 | } 151 | } 152 | ) 153 | done() 154 | }) 155 | }) 156 | } 157 | else { 158 | it('passed in server.register', (done) => { 159 | var config = {grant: {}} 160 | var grant = new Grant() 161 | var server = new Hapi.Server({host: 'localhost', port: 5000}) 162 | server.register([{plugin: grant, options: config}]).then(() => { 163 | t.deepEqual( 164 | grant.config, 165 | { 166 | defaults: {prefix: '/connect'}, 167 | grant: { 168 | prefix: '/connect', grant: true, name: 'grant' 169 | } 170 | } 171 | ) 172 | done() 173 | }) 174 | }) 175 | it('passed in the constructor', (done) => { 176 | var config = {grant: {}} 177 | var grant = Grant(config) 178 | var server = new Hapi.Server({host: 'localhost', port: 5000}) 179 | server.register([{plugin: grant}]).then(() => { 180 | t.deepEqual( 181 | grant.config, 182 | { 183 | defaults: {prefix: '/connect'}, 184 | grant: { 185 | prefix: '/connect', grant: true, name: 'grant' 186 | } 187 | } 188 | ) 189 | done() 190 | }) 191 | }) 192 | } 193 | }) 194 | }) 195 | -------------------------------------------------------------------------------- /test/handler/oidc.js: -------------------------------------------------------------------------------- 1 | 2 | var t = require('assert') 3 | 4 | var request = require('request-compose').extend({ 5 | Request: {cookie: require('request-cookie').Request}, 6 | Response: {cookie: require('request-cookie').Response}, 7 | }).client 8 | 9 | var Provider = require('../util/provider'), provider 10 | var Client = require('../util/client'), client 11 | 12 | var qs = require('qs') 13 | var jws = require('jws') 14 | var oidc = require('../../lib/oidc') 15 | var keys = require('../util/keys') 16 | 17 | 18 | describe('oidc', () => { 19 | before(async () => { 20 | provider = await Provider({flow: 'oauth2'}) 21 | var config = { 22 | defaults: { 23 | origin: 'http://localhost:5001', callback: '/', 24 | }, 25 | oauth2: { 26 | authorize_url: provider.url('/oauth2/authorize_url'), 27 | access_url: provider.url('/oauth2/access_url'), 28 | profile_url: provider.url('/oauth2/profile_url'), 29 | oauth: 2, 30 | dynamic: true, 31 | } 32 | } 33 | client = await Client({test: 'handlers', handler: 'express', config}) 34 | }) 35 | 36 | after(async () => { 37 | await provider.close() 38 | await client.close() 39 | provider.on.authorize = () => {} 40 | provider.on.access = () => {} 41 | }) 42 | 43 | var verify = ({form, alg}) => { 44 | t.equal(form.grant_type, 'authorization_code') 45 | t.equal(form.code, 'code') 46 | t.equal(form.redirect_uri, 'http://localhost:5001/connect/oauth2/callback') 47 | t.equal(form.client_assertion_type, 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer') 48 | 49 | var jwt = oidc.jwt(form.client_assertion) 50 | t.ok(jws.verify(form.client_assertion, jwt.header.alg, keys[alg].public_pem)) 51 | 52 | t.equal(jwt.header.typ, 'JWT') 53 | t.equal(jwt.header.alg, alg) 54 | t.equal(jwt.payload.iss, 'client_id') 55 | t.equal(jwt.payload.iss, jwt.payload.sub) 56 | t.equal(jwt.payload.aud, 'http://localhost:5000/oauth2/access_url') 57 | t.equal(jwt.payload.jti.length, 40) 58 | t.equal(jwt.payload.exp, jwt.payload.iat + 420) 59 | t.equal(jwt.payload.iat, jwt.payload.nbf) 60 | t.ok(typeof jwt.signature === 'string') 61 | 62 | return jwt 63 | } 64 | 65 | var success = ({response}) => { 66 | t.deepEqual(response, { 67 | access_token: 'token', 68 | refresh_token: 'refresh', 69 | raw: {access_token: 'token', refresh_token: 'refresh', expires_in: '3600'} 70 | }) 71 | } 72 | 73 | it('private pem - no kid', async () => { 74 | provider.on.access = ({form}) => { 75 | var jwt = verify({form, alg: 'RS256'}) 76 | t.ok(jwt.header.kid === undefined, 'no pem-to-jwk convertion yet') 77 | } 78 | var {body: {response}} = await request({ 79 | url: client.url('/connect/oauth2'), 80 | qs: { 81 | token_endpoint_auth_method: 'private_key_jwt', 82 | key: 'client_id', 83 | private_key: keys.RS256.private_pem, 84 | }, 85 | cookie: {}, 86 | }) 87 | success({response}) 88 | }) 89 | 90 | it('private jwk - set kid', async () => { 91 | provider.on.access = ({form}) => { 92 | var jwt = verify({form, alg: 'RS256'}) 93 | t.ok(typeof jwt.header.kid === 'string', 'should pick the kid from the jwk') 94 | } 95 | var {body: {response}} = await request({ 96 | url: client.url('/connect/oauth2'), 97 | qs: qs.stringify({ 98 | token_endpoint_auth_method: 'private_key_jwt', 99 | key: 'client_id', 100 | private_key: keys.RS256.private_jwk, 101 | }), 102 | cookie: {}, 103 | }) 104 | success({response}) 105 | }) 106 | 107 | it('private jwk - generate kid', async () => { 108 | delete keys.RS256.private_jwk.kid 109 | provider.on.access = ({form}) => { 110 | var jwt = verify({form, alg: 'RS256'}) 111 | t.ok(typeof jwt.header.kid === 'string', 'should generate the kid out of the jwk') 112 | } 113 | var {body: {response}} = await request({ 114 | url: client.url('/connect/oauth2'), 115 | qs: qs.stringify({ 116 | token_endpoint_auth_method: 'private_key_jwt', 117 | key: 'client_id', 118 | private_key: keys.RS256.private_jwk, 119 | }), 120 | cookie: {}, 121 | }) 122 | success({response}) 123 | }) 124 | 125 | it('public pem - generate x5t', async () => { 126 | provider.on.access = ({form}) => { 127 | var jwt = verify({form, alg: 'RS256'}) 128 | t.ok(jwt.header.kid === undefined, 'no pem-to-jwk convertion yet') 129 | t.ok(typeof jwt.header.x5t === 'string', 'should generate x5t out of the pem') 130 | } 131 | var {body: {response}} = await request({ 132 | url: client.url('/connect/oauth2'), 133 | qs: { 134 | token_endpoint_auth_method: 'private_key_jwt', 135 | key: 'client_id', 136 | public_key: keys.RS256.public_pem, 137 | private_key: keys.RS256.private_pem, 138 | }, 139 | cookie: {}, 140 | }) 141 | success({response}) 142 | }) 143 | 144 | it('public jwk - set x5t', async () => { 145 | provider.on.access = ({form}) => { 146 | var jwt = verify({form, alg: 'RS256'}) 147 | t.ok(jwt.header.kid === undefined, 'no pem-to-jwk convertion yet') 148 | t.ok(typeof jwt.header.x5t === 'string', 'should pick the x5t from the jwk') 149 | } 150 | var {body: {response}} = await request({ 151 | url: client.url('/connect/oauth2'), 152 | qs: qs.stringify({ 153 | token_endpoint_auth_method: 'private_key_jwt', 154 | key: 'client_id', 155 | public_key: keys.RS256.public_jwk, 156 | private_key: keys.RS256.private_pem, 157 | }), 158 | cookie: {}, 159 | }) 160 | success({response}) 161 | }) 162 | 163 | it('token alg - ES256', async () => { 164 | provider.on.access = ({form}) => { 165 | var jwt = verify({form, alg: 'ES256'}) 166 | t.ok(jwt.header.kid === undefined, 'no pem-to-jwk convertion yet') 167 | } 168 | var {body: {response}} = await request({ 169 | url: client.url('/connect/oauth2'), 170 | qs: { 171 | token_endpoint_auth_method: 'private_key_jwt', 172 | token_endpoint_auth_signing_alg: 'ES256', 173 | key: 'client_id', 174 | private_key: keys.ES256.private_pem, 175 | }, 176 | cookie: {}, 177 | }) 178 | success({response}) 179 | }) 180 | 181 | it('token alg - PS256', async () => { 182 | provider.on.access = ({form}) => { 183 | var jwt = verify({form, alg: 'PS256'}) 184 | t.ok(jwt.header.kid === undefined, 'no pem-to-jwk convertion yet') 185 | } 186 | var {body: {response}} = await request({ 187 | url: client.url('/connect/oauth2'), 188 | qs: { 189 | token_endpoint_auth_method: 'private_key_jwt', 190 | token_endpoint_auth_signing_alg: 'PS256', 191 | key: 'client_id', 192 | private_key: keys.PS256.private_pem, 193 | }, 194 | cookie: {}, 195 | }) 196 | success({response}) 197 | }) 198 | 199 | }) 200 | -------------------------------------------------------------------------------- /test/handler/profile.js: -------------------------------------------------------------------------------- 1 | 2 | var t = require('assert') 3 | 4 | var request = require('request-compose').extend({ 5 | Request: {cookie: require('request-cookie').Request}, 6 | Response: {cookie: require('request-cookie').Response}, 7 | }).client 8 | 9 | var Provider = require('../util/provider'), provider 10 | var Client = require('../util/client'), client 11 | 12 | var mw = require('../../lib/profile') 13 | var oauth = require('../../config/oauth') 14 | 15 | 16 | describe('profile', () => { 17 | before(async () => { 18 | provider = { 19 | oauth2: await Provider({flow: 'oauth2', port: 5000}), 20 | oauth1: await Provider({flow: 'oauth1', port: 5002}), 21 | } 22 | var config = { 23 | defaults: { 24 | origin: 'http://localhost:5001', callback: '/', 25 | response: ['tokens', 'profile'], 26 | key: 'key', secret: 'secret', 27 | dynamic: true, 28 | }, 29 | oauth2: { 30 | authorize_url: provider.oauth2.url('/oauth2/authorize_url'), 31 | access_url: provider.oauth2.url('/oauth2/access_url'), 32 | profile_url: provider.oauth2.url('/oauth2/profile_url'), 33 | oauth: 2, 34 | }, 35 | oauth1: { 36 | request_url: provider.oauth1.url('/oauth1/request_url'), 37 | authorize_url: provider.oauth1.url('/oauth1/authorize_url'), 38 | access_url: provider.oauth1.url('/oauth1/access_url'), 39 | profile_url: provider.oauth1.url('/oauth1/profile_url'), 40 | oauth: 1, 41 | } 42 | } 43 | client = await Client({test: 'handlers', handler: 'express', config}) 44 | }) 45 | 46 | after(async () => { 47 | await provider.oauth2.close() 48 | await provider.oauth1.close() 49 | await client.close() 50 | provider.oauth2.on.profile = () => {} 51 | provider.oauth1.on.profile = () => {} 52 | }) 53 | 54 | it('oauth2', async () => { 55 | var {body: {response}} = await request({ 56 | url: client.url('/connect/oauth2'), 57 | cookie: {}, 58 | }) 59 | t.deepEqual(response, { 60 | access_token: 'token', refresh_token: 'refresh', 61 | profile: {user: 'simov'} 62 | }) 63 | }) 64 | 65 | it('oauth1', async () => { 66 | var {body: {response}} = await request({ 67 | url: client.url('/connect/oauth1'), 68 | cookie: {}, 69 | }) 70 | t.deepEqual(response, { 71 | access_token: 'token', access_secret: 'secret', 72 | profile: {user: 'simov'} 73 | }) 74 | }) 75 | 76 | it('no profile_url', async () => { 77 | var {body: {response}} = await request({ 78 | url: client.url('/connect/oauth2'), 79 | qs: { 80 | profile_url: '', 81 | }, 82 | cookie: {}, 83 | }) 84 | t.deepEqual(response, { 85 | access_token: 'token', refresh_token: 'refresh', 86 | profile: {error: 'Grant: No profile URL found!'} 87 | }) 88 | }) 89 | 90 | it('subdomain', async () => { 91 | var {body: {response}} = await request({ 92 | url: client.url('/connect/oauth2'), 93 | qs: { 94 | profile_url: provider.oauth2.url('[subdomain]'), 95 | subdomain: '/oauth2/profile_url', 96 | }, 97 | cookie: {}, 98 | }) 99 | t.deepEqual(response, { 100 | access_token: 'token', refresh_token: 'refresh', 101 | profile: {user: 'simov'} 102 | }) 103 | }) 104 | 105 | it('error', async () => { 106 | var {body: {response}} = await request({ 107 | url: client.url('/connect/oauth2'), 108 | qs: { 109 | profile_url: provider.oauth2.url('/oauth2/profile_error'), 110 | }, 111 | cookie: {}, 112 | }) 113 | t.deepEqual(response, { 114 | access_token: 'token', refresh_token: 'refresh', 115 | profile: {error: {error: {message: 'Not Found'}}} 116 | }) 117 | }) 118 | 119 | it('custom', async () => { 120 | var providers = [ 121 | 'arcgis', 122 | 'constantcontact', 123 | 'baidu', 124 | 'deezer', 125 | 'disqus', 126 | 'dropbox', 127 | 'echosign', 128 | 'flickr', 129 | 'foursquare', 130 | // 'getpocket', 131 | // 'instagram', 132 | 'linkedin', 133 | 'mailchimp', 134 | 'meetup', 135 | 'mixcloud', 136 | 'shopify', 137 | 'slack', 138 | 'soundcloud', 139 | 'stackexchange', 140 | 'stocktwits', 141 | 'tiktok', 142 | 'tumblr', 143 | 'vk', 144 | 'wechat', 145 | 'weibo', 146 | 'twitter', 147 | ] 148 | for (var name of providers) { 149 | var version = oauth[name].oauth 150 | provider[`oauth${version}`].on.profile = ({method, query, headers, form}) => { 151 | 'arcgis' === name ? t.equal(query.f, 'json') : 152 | 'constantcontact' === name ? t.equal(query.api_key, 'key') : 153 | 'baidu' === name ? t.equal(query.access_token, 'token') : 154 | 'deezer' === name ? t.equal(query.access_token, 'token') : 155 | 'disqus' === name ? t.equal(query.api_key, 'key') : 156 | 'dropbox' === name ? t.equal(method, 'POST') : 157 | 'echosign' === name ? t.equal(headers['access-token'], 'token') : 158 | 'flickr' === name ? t.deepEqual(query, {method: 'flickr.urls.getUserProfile', api_key: 'key', format: 'json'}) : 159 | 'foursquare' === name ? t.equal(query.oauth_token, 'token') : 160 | 'getpocket' === name ? t.deepEqual(query, {consumer_key: 'key', access_token: 'token'}) : 161 | 'instagram' === name ? t.equal(query.access_token, 'token') : 162 | 'mailchimp' === name ? t.equal(query.apikey, 'token') : 163 | 'meetup' === name ? t.equal(query.member_id, 'self') : 164 | 'mixcloud' === name ? t.equal(query.access_token, 'token') : 165 | 'shopify' === name ? t.equal(headers['x-shopify-access-token'], 'token') : 166 | 'slack' === name ? t.equal(query.token, 'token') : 167 | 'soundcloud' === name ? t.equal(query.oauth_token, 'token') : 168 | 'stackexchange' === name ? t.equal(query.key, 'token') : 169 | 'stocktwits' === name ? t.equal(query.access_token, 'token') : 170 | 'tiktok' === name ? (t.equal(method, 'POST'), t.deepEqual(form, {access_token: 'token', open_id: 'id', fields: ['open_id', 'union_id', 'avatar_url', 'display_name']})) : 171 | 'tumblr' === name ? t.equal(query.api_key, 'token') : 172 | 'vk' === name ? t.deepEqual(query, {access_token: 'token', v: '5.103'}) : 173 | 'wechat' === name ? t.deepEqual(query, {access_token: 'token', openid: 'openid', lang: 'zh_CN'}) : 174 | 'weibo' === name ? t.deepEqual(query, {access_token: 'token', uid: 'id'}) : 175 | 'twitter' === name ? t.equal(query.user_id, 'id') : 176 | undefined 177 | } 178 | var {body: {response}} = await request({ 179 | url: client.url(`/connect/${name}`), 180 | qs: { 181 | request_url: provider[`oauth${version}`].url(`/${name}/request_url`), 182 | authorize_url: provider[`oauth${version}`].url(`/${name}/authorize_url`), 183 | access_url: provider[`oauth${version}`].url(`/${name}/access_url`), 184 | profile_url: provider[`oauth${version}`].url(`/${name}/profile_url`), 185 | response: ['tokens', 'raw', 'profile'] 186 | }, 187 | cookie: {}, 188 | }) 189 | delete response.raw 190 | if (version === 2) { 191 | t.deepEqual(response, { 192 | access_token: 'token', refresh_token: 'refresh', 193 | profile: {user: 'simov'} 194 | }) 195 | } 196 | else if (version === 1) { 197 | t.deepEqual(response, { 198 | access_token: 'token', access_secret: 'secret', 199 | profile: {user: 'simov'} 200 | }) 201 | } 202 | } 203 | }) 204 | 205 | }) 206 | -------------------------------------------------------------------------------- /test/handler/session.js: -------------------------------------------------------------------------------- 1 | 2 | var t = require('assert') 3 | var qs = require('qs') 4 | 5 | var request = require('request-compose').extend({ 6 | Request: {cookie: require('request-cookie').Request}, 7 | Response: {cookie: require('request-cookie').Response}, 8 | }).client 9 | 10 | var Provider = require('../util/provider'), provider, oauth1 11 | var Client = require('../util/client'), client 12 | 13 | 14 | describe('session', () => { 15 | var config 16 | 17 | before(async () => { 18 | provider = await Provider({flow: 'oauth2'}) 19 | oauth1 = await Provider({flow: 'oauth1', port: 5002}) 20 | config = { 21 | defaults: { 22 | origin: 'http://localhost:5001', callback: '/', 23 | dynamic: true 24 | }, 25 | oauth1: { 26 | request_url: oauth1.url('/request_url'), 27 | authorize_url: oauth1.url('/authorize_url'), 28 | access_url: oauth1.url('/access_url'), 29 | oauth: 1, 30 | }, 31 | oauth2: { 32 | authorize_url: provider.url('/authorize_url'), 33 | access_url: provider.url('/access_url'), 34 | oauth: 2, 35 | } 36 | } 37 | }) 38 | 39 | after(async () => { 40 | await provider.close() 41 | await oauth1.close() 42 | }) 43 | 44 | ;['express', 'koa', 'hapi', 'fastify', 'curveball', 'node', 'aws', 'azure', 'gcloud', 'vercel'].forEach((handler) => { 45 | describe(handler, () => { 46 | before(async () => { 47 | client = await Client({test: 'handlers', handler, config}) 48 | }) 49 | 50 | after(async () => { 51 | await client.close() 52 | }) 53 | 54 | afterEach(() => { 55 | provider.oauth2.authorize = () => {} 56 | provider.oauth2.access = () => {} 57 | }) 58 | 59 | it('provider', async () => { 60 | var {body: {session}} = await request({ 61 | url: client.url('/connect/oauth2'), 62 | cookie: {}, 63 | }) 64 | t.deepEqual(session, {provider: 'oauth2'}) 65 | }) 66 | 67 | it('override', async () => { 68 | var {body: {session}} = await request({ 69 | url: client.url('/connect/oauth2/contacts'), 70 | cookie: {}, 71 | }) 72 | t.deepEqual(session, {provider: 'oauth2', override: 'contacts'}) 73 | }) 74 | 75 | it('dynamic - POST', async () => { 76 | var {body: {session}} = await request({ 77 | method: 'POST', 78 | url: client.url('/connect/oauth2/contacts'), 79 | form: qs.stringify({ 80 | scope: ['scope1', 'scope2'], state: 'Grant', nonce: 'simov', 81 | custom_params: {access_type: 'offline'} 82 | }, {arrayFormat: 'repeat'}), 83 | cookie: {}, 84 | redirect: {all: true, method: false}, 85 | }) 86 | t.deepEqual(session, {provider: 'oauth2', override: 'contacts', 87 | dynamic: {scope: ['scope1', 'scope2'], state: 'Grant', nonce: 'simov', 88 | custom_params: {access_type: 'offline'}}, 89 | state: 'Grant', nonce: 'simov' 90 | }) 91 | }) 92 | 93 | it('dynamic - GET', async () => { 94 | var {body: {session}} = await request({ 95 | url: client.url('/connect/oauth2/contacts'), 96 | qs: qs.stringify({ 97 | scope: ['scope1', 'scope2'], state: 'Grant', nonce: 'simov', 98 | custom_params: {access_type: 'offline'} 99 | }, {arrayFormat: 'repeat'}), 100 | cookie: {}, 101 | }) 102 | t.deepEqual(session, {provider: 'oauth2', override: 'contacts', 103 | dynamic: {scope: ['scope1', 'scope2'], state: 'Grant', nonce: 'simov', 104 | custom_params: {access_type: 'offline'}}, 105 | state: 'Grant', nonce: 'simov' 106 | }) 107 | }) 108 | 109 | it('dynamic - non configured provider', async () => { 110 | t.equal(client.grant.config.google, undefined) 111 | 112 | var {body: {session}} = await request({ 113 | url: client.url('/connect/google'), 114 | qs: { 115 | authorize_url: provider.url('/authorize_url'), 116 | access_url: provider.url('/access_url'), 117 | scope: ['scope1', 'scope2'], state: 'Grant', nonce: 'simov', 118 | }, 119 | cookie: {}, 120 | }) 121 | t.deepEqual(session, { 122 | provider: 'google', 123 | dynamic: { 124 | authorize_url: 'http://localhost:5000/authorize_url', 125 | access_url: 'http://localhost:5000/access_url', 126 | scope: ['scope1', 'scope2'], state: 'Grant', nonce: 'simov', 127 | }, 128 | state: 'Grant', nonce: 'simov' 129 | }) 130 | }) 131 | 132 | it('dynamic - non existing provider', async () => { 133 | t.equal(client.grant.config.grant, undefined) 134 | 135 | var {body: {session}} = await request({ 136 | url: client.url('/connect/grant'), 137 | qs: { 138 | authorize_url: provider.url('/authorize_url'), 139 | access_url: provider.url('/access_url'), 140 | oauth: 2, 141 | }, 142 | cookie: {}, 143 | }) 144 | t.equal(client.grant.config.grant, undefined) 145 | t.deepEqual(session, { 146 | provider: 'grant', 147 | dynamic: { 148 | authorize_url: 'http://localhost:5000/authorize_url', 149 | access_url: 'http://localhost:5000/access_url', 150 | oauth: '2', 151 | } 152 | }) 153 | }) 154 | 155 | it('state and nonce', async () => { 156 | provider.oauth2.authorize = ({query}) => { 157 | t.ok(/[\d\w]{20}/.test(query.state)) 158 | t.ok(/[\d\w]{20}/.test(query.nonce)) 159 | } 160 | var {body: {session}} = await request({ 161 | url: client.url('/connect/oauth2'), 162 | qs: {state: true, nonce: true}, 163 | cookie: {}, 164 | }) 165 | t.deepEqual(session.dynamic, {state: 'true', nonce: 'true'}) 166 | t.ok(/[\d\w]{20}/.test(session.state)) 167 | t.ok(/[\d\w]{20}/.test(session.nonce)) 168 | }) 169 | 170 | it('pkce', async () => { 171 | provider.oauth2.authorize = ({query}) => { 172 | t.equal(query.code_challenge_method, 'S256') 173 | t.ok(typeof query.code_challenge === 'string') 174 | } 175 | provider.oauth2.access = ({form}) => { 176 | t.ok(typeof form.code_verifier === 'string') 177 | t.ok(/[a-z0-9]{80}/.test(form.code_verifier)) 178 | } 179 | var {body: {session}} = await request({ 180 | url: client.url('/connect/oauth2'), 181 | qs: {pkce: true}, 182 | cookie: {}, 183 | }) 184 | t.deepEqual(session.dynamic, {pkce: 'true'}) 185 | t.ok(/[a-z0-9]{80}/.test(session.code_verifier)) 186 | }) 187 | 188 | it('oauth1', async () => { 189 | var {body: {session}} = await request({ 190 | url: client.url('/connect/oauth1'), 191 | cookie: {}, 192 | }) 193 | t.deepEqual(session, { 194 | provider: 'oauth1', 195 | request: {oauth_token: 'token', oauth_token_secret: 'secret'} 196 | }) 197 | }) 198 | 199 | it('fresh session on connect', async () => { 200 | var cookie = {} 201 | var {body: {session}} = await request({ 202 | url: client.url('/connect/oauth2/grant'), 203 | cookie, 204 | }) 205 | t.deepEqual(session, {provider: 'oauth2', override: 'grant'}) 206 | var {body: {session}} = await request({ 207 | url: client.url('/connect/oauth2'), 208 | cookie, 209 | }) 210 | t.deepEqual(session, {provider: 'oauth2'}) 211 | }) 212 | }) 213 | }) 214 | 215 | }) 216 | -------------------------------------------------------------------------------- /test/response.js: -------------------------------------------------------------------------------- 1 | 2 | var t = require('assert') 3 | var response = require('../lib/response') 4 | 5 | var sign = (...args) => args.map((arg, index) => index < 2 6 | ? Buffer.from(JSON.stringify(arg)).toString('base64') 7 | .replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_') 8 | : arg).join('.') 9 | 10 | 11 | describe('response', () => { 12 | 13 | it('concur', () => { 14 | var provider = {concur: true} 15 | var input = {} 16 | var output = 17 | '\r\n' + 18 | ' https://www.concursolutions.com/\r\n' + 19 | ' q962LLopjMgTOeTn3fRN+5uABCg=\r\n' + 20 | ' 9/25/2016 1:36:50 PM\r\n' + 21 | ' AXvRqWeb77Lq9F2WK6TXLCSTuxpwZO6\r\n' + 22 | '' 23 | t.deepEqual(response.data({provider, input, output}).output, { 24 | access_token: 'q962LLopjMgTOeTn3fRN+5uABCg=', 25 | refresh_token: 'AXvRqWeb77Lq9F2WK6TXLCSTuxpwZO6', 26 | raw: output 27 | }) 28 | }) 29 | 30 | it('getpocket', () => { 31 | var provider = {getpocket: true} 32 | var input = {} 33 | var output = {access_token: 'token'} 34 | t.deepEqual(response.data({provider, input, output}).output, 35 | {access_token: 'token', raw: {access_token: 'token'}} 36 | ) 37 | }) 38 | 39 | it('yammer', () => { 40 | var provider = {yammer: true} 41 | var input = {} 42 | var output = {access_token: {token: 'token'}} 43 | t.deepEqual(response.data({provider, input, output}).output, 44 | {access_token: 'token', raw: {access_token: {token: 'token'}}} 45 | ) 46 | }) 47 | 48 | it('oauth1', () => { 49 | var provider = {oauth: 1} 50 | var input = {} 51 | var output = {oauth_token: 'token', oauth_token_secret: 'secret'} 52 | t.deepEqual(response.data({provider, input, output}).output, { 53 | access_token: 'token', access_secret: 'secret', 54 | raw: {oauth_token: 'token', oauth_token_secret: 'secret'} 55 | }) 56 | }) 57 | 58 | it('oauth2', () => { 59 | var provider = {oauth: 2} 60 | var input = {session: {}} 61 | var output = { 62 | id_token: sign({typ: 'JWT'}, {hey: 'hi'}, 'signature'), 63 | access_token: 'token', refresh_token: 'refresh' 64 | } 65 | t.deepEqual(response.data({provider, input, output}).output, { 66 | id_token: 'eyJ0eXAiOiJKV1QifQ.eyJoZXkiOiJoaSJ9.signature', 67 | access_token: 'token', 68 | refresh_token: 'refresh', 69 | raw: { 70 | id_token: 'eyJ0eXAiOiJKV1QifQ.eyJoZXkiOiJoaSJ9.signature', 71 | access_token: 'token', 72 | refresh_token: 'refresh' 73 | } 74 | }) 75 | }) 76 | 77 | describe('id_token', () => { 78 | 79 | it('invalid format', () => { 80 | var provider = {oauth: 2, response: ['jwt']} 81 | var input = {} 82 | var output = {id_token: sign('a', 'b')} 83 | t.deepEqual(response.data({provider, input, output}).output, { 84 | error: 'Grant: OpenID Connect invalid id_token format' 85 | }) 86 | }) 87 | 88 | it('error decoding', () => { 89 | var provider = {oauth: 2, response: ['jwt']} 90 | var input = {} 91 | var output = {id_token: 'a.b.c'} 92 | t.deepEqual(response.data({provider, input, output}).output, { 93 | error: 'Grant: OpenID Connect error decoding id_token' 94 | }) 95 | }) 96 | 97 | it('invalid audience - string', () => { 98 | var provider = {oauth: 2, key: 'simov', response: ['jwt']} 99 | var input = {} 100 | var output = {id_token: sign({}, {aud: 'grant'}, 'c')} 101 | t.deepEqual(response.data({provider, input, output}).output, { 102 | error: 'Grant: OpenID Connect invalid id_token audience' 103 | }) 104 | }) 105 | 106 | it('invalid audience - array', () => { 107 | var provider = {oauth: 2, key: 'simov', response: ['jwt']} 108 | var input = {} 109 | var output = {id_token: sign({}, {aud: ['grant']}, 'c')} 110 | t.deepEqual(response.data({provider, input, output}).output, { 111 | error: 'Grant: OpenID Connect invalid id_token audience' 112 | }) 113 | }) 114 | 115 | it('nonce mismatch', () => { 116 | var provider = {oauth: 2, key: 'grant', response: ['jwt']} 117 | var input = {session: {nonce: 'bar'}} 118 | var output = {id_token: sign({}, {aud: 'grant', nonce: 'foo'}, 'c')} 119 | t.deepEqual(response.data({provider, input, output}).output, { 120 | error: 'Grant: OpenID Connect nonce mismatch' 121 | }) 122 | }) 123 | 124 | it('valid jwt', () => { 125 | var provider = {oauth: 2, key: 'grant', response: ['tokens', 'jwt']} 126 | var input = {session: {nonce: 'foo'}} 127 | var output = {id_token: sign({typ: 'JWT'}, {aud: 'grant', nonce: 'foo'}, 'signature')} 128 | t.deepEqual(response.data({provider, input, output}).output, { 129 | id_token: 'eyJ0eXAiOiJKV1QifQ.eyJhdWQiOiJncmFudCIsIm5vbmNlIjoiZm9vIn0.signature', 130 | jwt: { 131 | id_token: { 132 | header: {typ: 'JWT'}, 133 | payload: {aud: 'grant', nonce: 'foo'}, 134 | signature: 'signature' 135 | } 136 | } 137 | }) 138 | }) 139 | 140 | it('valid jwt - audience array', () => { 141 | var provider = {oauth: 2, key: 'grant', response: ['tokens', 'jwt']} 142 | var input = {session: {nonce: 'foo'}} 143 | var output = {id_token: sign({typ: 'JWT'}, {aud: ['grant'], nonce: 'foo'}, 'signature')} 144 | t.deepEqual(response.data({provider, input, output}).output, { 145 | id_token: 'eyJ0eXAiOiJKV1QifQ.eyJhdWQiOlsiZ3JhbnQiXSwibm9uY2UiOiJmb28ifQ.signature', 146 | jwt: { 147 | id_token: { 148 | header: {typ: 'JWT'}, 149 | payload: {aud: ['grant'], nonce: 'foo'}, 150 | signature: 'signature' 151 | } 152 | } 153 | }) 154 | }) 155 | }) 156 | 157 | }) 158 | -------------------------------------------------------------------------------- /test/session.js: -------------------------------------------------------------------------------- 1 | var t = require('assert') 2 | var signature = require('cookie-signature') 3 | var session = require('../lib/session') 4 | 5 | 6 | describe('session', () => { 7 | 8 | it('throw on missing cookie secret', () => { 9 | try { 10 | session({}) 11 | } 12 | catch (err) { 13 | t.equal(err.message, 'Grant: cookie secret is required') 14 | } 15 | }) 16 | 17 | it('cookie store - set, get, remove - no input headers', async () => { 18 | var config = {secret: 'grant'} 19 | var request = {headers: {}} 20 | var store = session(config)(request) 21 | 22 | var input = {provider: 'google'} 23 | await store.set(input) 24 | t.deepEqual(await store.get(), input) 25 | 26 | var output = 'eyJwcm92aWRlciI6Imdvb2dsZSJ9.5Zguv22ColWMBAH4A8w7ymszwQ8yXkxcjcHzSB1NoRw' 27 | t.deepEqual(store.headers, {'set-cookie': [`grant=${output}; Path=/; HttpOnly`]}) 28 | t.deepEqual( 29 | input, 30 | JSON.parse(Buffer.from(signature.unsign(output, config.secret), 'base64')) 31 | ) 32 | 33 | await store.remove() 34 | t.deepEqual(await store.get(), {}) 35 | var output = 'IiI%3D.0IZwUDQpopV3fCMGLJec49pTIr1nf4OjyzB%2FbxsqD%2FE' 36 | t.deepEqual(store.headers, {'set-cookie': [`grant=${output}; Max-Age=0; Path=/; HttpOnly`]}) 37 | t.equal( 38 | '', 39 | JSON.parse(Buffer.from(signature.unsign(decodeURIComponent(output), config.secret), 'base64')) 40 | ) 41 | }) 42 | 43 | it('session store - set, get, remove - no input headers', async () => { 44 | var store = ((session) => ({ 45 | get: async (sid) => session, 46 | set: async (sid, value) => session = value, 47 | remove: async (sid) => session = {} 48 | }))() 49 | var config = {secret: 'grant', store} 50 | var request = {headers: {}} 51 | var store = session(config)(request) 52 | 53 | var input = {provider: 'google'} 54 | await store.set(input) 55 | t.deepEqual(await store.get(), input) 56 | 57 | var output = store.headers['set-cookie'][0].split(';')[0].split('=')[1] 58 | var sid = signature.unsign(decodeURIComponent(output), config.secret) 59 | output = `${sid}.${output.split('.')[1]}` 60 | t.deepEqual(store.headers, {'set-cookie': [`grant=${output}; Path=/; HttpOnly`]}) 61 | 62 | await store.remove() 63 | t.deepEqual(await store.get(), {}) 64 | var output = store.headers['set-cookie'][0].split(';')[0].split('=')[1] 65 | var sid = signature.unsign(decodeURIComponent(output), config.secret) 66 | output = `${sid}.${output.split('.')[1]}` 67 | t.deepEqual(store.headers, {'set-cookie': [`grant=${output}; Max-Age=0; Path=/; HttpOnly`]}) 68 | }) 69 | 70 | it('existing set-cookie headers', async () => { 71 | var config = {secret: 'grant'} 72 | var request = {headers: {'set-cookie': ['foo=bar; Path=/; HttpOnly']}} 73 | var store = session(config)(request) 74 | 75 | var input = {provider: 'google'} 76 | await store.set(input) 77 | t.deepEqual(await store.get(), input) 78 | 79 | var output = 'eyJwcm92aWRlciI6Imdvb2dsZSJ9.5Zguv22ColWMBAH4A8w7ymszwQ8yXkxcjcHzSB1NoRw' 80 | t.deepEqual(store.headers, { 81 | 'set-cookie': ['foo=bar; Path=/; HttpOnly', `grant=${output}; Path=/; HttpOnly`] 82 | }) 83 | t.deepEqual( 84 | input, 85 | JSON.parse(Buffer.from(signature.unsign(output, config.secret), 'base64')) 86 | ) 87 | }) 88 | 89 | it('cookie store - get session from cookie header', async () => { 90 | var input = {provider: 'google'} 91 | var output = 'eyJwcm92aWRlciI6Imdvb2dsZSJ9.5Zguv22ColWMBAH4A8w7ymszwQ8yXkxcjcHzSB1NoRw' 92 | 93 | var config = {secret: 'grant'} 94 | var request = {headers: {cookie: `grant=${output}; foo=bar`}} 95 | var store = session(config)(request) 96 | 97 | t.deepEqual(await store.get(), input) 98 | 99 | t.deepEqual(store.headers, { 100 | cookie: `grant=${output}; foo=bar`, 101 | 'set-cookie': [] 102 | }) 103 | t.deepEqual( 104 | input, 105 | JSON.parse(Buffer.from(signature.unsign(output, config.secret), 'base64')) 106 | ) 107 | }) 108 | 109 | it('session store - get session from cookie header', async () => { 110 | var store = ((session) => ({ 111 | get: async (sid) => session, 112 | set: async (sid, value) => session = value, 113 | remove: async (sid) => session = {} 114 | }))() 115 | 116 | var input = {provider: 'google'} 117 | var output = '4b3c6de9d57d653c16615aef3062fc418483a2aa.n5nMvp5FH5ewOwDOnx%2Beqd6m8XlbtVHIX19YyG81HvQ' 118 | 119 | var config = {secret: 'grant', store} 120 | var request = {headers: {cookie: `grant=${output}; foo=bar`}} 121 | var store = session(config)(request) 122 | 123 | t.deepEqual(await store.get(), {grant: {}}) 124 | 125 | t.deepEqual( 126 | store.headers, { 127 | cookie: `grant=${output}; foo=bar`, 128 | 'set-cookie': [] 129 | }) 130 | }) 131 | 132 | }) 133 | -------------------------------------------------------------------------------- /test/util/keys.json: -------------------------------------------------------------------------------- 1 | { 2 | "RS256": { 3 | "public_jwk": { 4 | "e": "AQAB", 5 | "n": "5wJNQcReFYnsSwzevghL1CtFC4HFOxJwWDPvUnsW5g9_L5z-O49rebF-YaoCdvRr1fkWMxwXPvrYbP9RxryO1KEe0MbrB36UbR9JgjXfRxtn5sjOWzD_7YypMOO-BB7zbRCdgd7tNFSSwKFOCO43y-DjBDLPVn5BvqR_COMmoi5AtW7f6SifYkbKclAV1mdwmsTKdXcAcO5cEKwyRIlMFWpHsIAa-QJC2HE7kqzIsL3BkjXArko_L7gqtLFLyK5NMn3l_5SM6CuyKHeWycrqubDcCYfP1yR4gqFcwRuBy2CxvrOHDlItQXnCBFxY80U2mOZsGiluCfzSB5mEALq3Bw", 6 | "kty": "RSA", 7 | "x5t": "foobar", 8 | "kid": "TuL_-Djwmx8nBnAHhojD5YJaYGsvdFpVI8R6izzeOr8", 9 | "use": "sig" 10 | }, 11 | "private_jwk": { 12 | "e": "AQAB", 13 | "n": "5wJNQcReFYnsSwzevghL1CtFC4HFOxJwWDPvUnsW5g9_L5z-O49rebF-YaoCdvRr1fkWMxwXPvrYbP9RxryO1KEe0MbrB36UbR9JgjXfRxtn5sjOWzD_7YypMOO-BB7zbRCdgd7tNFSSwKFOCO43y-DjBDLPVn5BvqR_COMmoi5AtW7f6SifYkbKclAV1mdwmsTKdXcAcO5cEKwyRIlMFWpHsIAa-QJC2HE7kqzIsL3BkjXArko_L7gqtLFLyK5NMn3l_5SM6CuyKHeWycrqubDcCYfP1yR4gqFcwRuBy2CxvrOHDlItQXnCBFxY80U2mOZsGiluCfzSB5mEALq3Bw", 14 | "d": "PXS8TuHJ0dsWdMTgwRd97NzyCmSkrtlx79UpNv0uE1hOEsGmVPwLsJ1KrPrImxLdWVhh0okHmiaryxuFiuSA7wpKI2q3_g9rtzgamzxvwQAGfTwwwcvgWjcQj8QWugvt4LcJ4BIJcuGJBRwkoXWWfHPAkU5fIHiITwp-DUQ48-3up6h6grd262e-SpWgP6r32rGg8eITxBXYlvuP1yH3Q1p2KAUvpvzNsGys97OxnGEwwQhEmYfmg4dLxBpy-HR3o0Y6WqzUGYgPfuSNl_kdUjwbgtCxrxYsLrg_MLu6YTysI3ihaxreLBPtrrLHmjjqcp-21hyHRzVNyqPNhc8sAQ", 15 | "p": "_HJ7NcQMk68oLCf2PEtMqgWCrxPGYZqDzHtugpeg_1HgKVn82Xr_xYO_Ym9FhY7s3bGMB6mkgkaBr0bpxzsG4aPW0hDz8fTUKAdCvnhZwvqKs-ZbWzOoENIltUQ2NaVHdjZi9peiOAs24GmUiwJR635QsHZoeySGMlM5kOIFPQc", 16 | "q": "6kKVMOdWjG2sZyLH_X98sJA2_0VHHN_-zmodTEK1FFE4i93qUkXAherHtKFrNRgN_A6xtzrSHBDf67-tt-5SNnPFFLY2UUt3tbl79nv0MptncQ4KqZbRCNaMzjFd8mf7HKqeKomQy9X0CVezhc2V-O3jGy9qASDNz-j5xH36NgE", 17 | "dp": "C8p5EMDQaZFzyeQv7w8BTKunN_Avgt2JrGJTfgwA_Avh8Wx_j9Tb32jQ5pMV5zAOxigFx1HYGjGa2wnv31tVbfKOFQ-vpSxvQFNefbD3WFEFa3Ol7rOR9P8rvbSq54SJuu69XpEkhYOYk5C63GdGVj53HFbbeGzzS6Rxet_jqL8", 18 | "dq": "lnBR7QjzfNVFvSoJ8tK3WZATsZVk2LdEpHxi-kFwlm6eeLv3qgQfYiDnRSnQdlVPTcqF-Fxu6BUyJ-x9fDwxUcTBEM7_TO_BHdPTNvRdW06PfHv-_u_ap-lYnWX4ph5-ledAEaMseKqawJEwucHQCP-ENEYbtkPByD4egEBCugE", 19 | "qi": "tnOxkZdJ45uUPv0H7A8jLz8cpN1nfVtD8eNnZcUZsy4zz6-NdeeP5deRbFdra_wPfAeWWERK7dGRCuWWjLVMxXIxugkxqiPrGDMdIyQg7Khyl4b_nu1VIj0QbljwVC5ZMHxrSz-kkIVQ1M1ugkXbkpthgY7VZW0fKzpZdSrOeRI", 20 | "kty": "RSA", 21 | "kid": "TuL_-Djwmx8nBnAHhojD5YJaYGsvdFpVI8R6izzeOr8", 22 | "use": "sig" 23 | }, 24 | "public_pem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5wJNQcReFYnsSwzevghL\n1CtFC4HFOxJwWDPvUnsW5g9/L5z+O49rebF+YaoCdvRr1fkWMxwXPvrYbP9RxryO\n1KEe0MbrB36UbR9JgjXfRxtn5sjOWzD/7YypMOO+BB7zbRCdgd7tNFSSwKFOCO43\ny+DjBDLPVn5BvqR/COMmoi5AtW7f6SifYkbKclAV1mdwmsTKdXcAcO5cEKwyRIlM\nFWpHsIAa+QJC2HE7kqzIsL3BkjXArko/L7gqtLFLyK5NMn3l/5SM6CuyKHeWycrq\nubDcCYfP1yR4gqFcwRuBy2CxvrOHDlItQXnCBFxY80U2mOZsGiluCfzSB5mEALq3\nBwIDAQAB\n-----END PUBLIC KEY-----\n\n\n", 25 | "private_pem": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDnAk1BxF4ViexL\nDN6+CEvUK0ULgcU7EnBYM+9SexbmD38vnP47j2t5sX5hqgJ29GvV+RYzHBc++ths\n/1HGvI7UoR7QxusHfpRtH0mCNd9HG2fmyM5bMP/tjKkw474EHvNtEJ2B3u00VJLA\noU4I7jfL4OMEMs9WfkG+pH8I4yaiLkC1bt/pKJ9iRspyUBXWZ3CaxMp1dwBw7lwQ\nrDJEiUwVakewgBr5AkLYcTuSrMiwvcGSNcCuSj8vuCq0sUvIrk0yfeX/lIzoK7Io\nd5bJyuq5sNwJh8/XJHiCoVzBG4HLYLG+s4cOUi1BecIEXFjzRTaY5mwaKW4J/NIH\nmYQAurcHAgMBAAECggEAPXS8TuHJ0dsWdMTgwRd97NzyCmSkrtlx79UpNv0uE1hO\nEsGmVPwLsJ1KrPrImxLdWVhh0okHmiaryxuFiuSA7wpKI2q3/g9rtzgamzxvwQAG\nfTwwwcvgWjcQj8QWugvt4LcJ4BIJcuGJBRwkoXWWfHPAkU5fIHiITwp+DUQ48+3u\np6h6grd262e+SpWgP6r32rGg8eITxBXYlvuP1yH3Q1p2KAUvpvzNsGys97OxnGEw\nwQhEmYfmg4dLxBpy+HR3o0Y6WqzUGYgPfuSNl/kdUjwbgtCxrxYsLrg/MLu6YTys\nI3ihaxreLBPtrrLHmjjqcp+21hyHRzVNyqPNhc8sAQKBgQD8cns1xAyTrygsJ/Y8\nS0yqBYKvE8ZhmoPMe26Cl6D/UeApWfzZev/Fg79ib0WFjuzdsYwHqaSCRoGvRunH\nOwbho9bSEPPx9NQoB0K+eFnC+oqz5ltbM6gQ0iW1RDY1pUd2NmL2l6I4CzbgaZSL\nAlHrflCwdmh7JIYyUzmQ4gU9BwKBgQDqQpUw51aMbaxnIsf9f3ywkDb/RUcc3/7O\nah1MQrUUUTiL3epSRcCF6se0oWs1GA38DrG3OtIcEN/rv6237lI2c8UUtjZRS3e1\nuXv2e/Qym2dxDgqpltEI1ozOMV3yZ/scqp4qiZDL1fQJV7OFzZX47eMbL2oBIM3P\n6PnEffo2AQKBgAvKeRDA0GmRc8nkL+8PAUyrpzfwL4LdiaxiU34MAPwL4fFsf4/U\n299o0OaTFecwDsYoBcdR2BoxmtsJ799bVW3yjhUPr6Usb0BTXn2w91hRBWtzpe6z\nkfT/K720queEibruvV6RJIWDmJOQutxnRlY+dxxW23hs80ukcXrf46i/AoGBAJZw\nUe0I83zVRb0qCfLSt1mQE7GVZNi3RKR8YvpBcJZunni796oEH2Ig50Up0HZVT03K\nhfhcbugVMifsfXw8MVHEwRDO/0zvwR3T0zb0XVtOj3x7/v7v2qfpWJ1l+KYefpXn\nQBGjLHiqmsCRMLnB0Aj/hDRGG7ZDwcg+HoBAQroBAoGBALZzsZGXSeOblD79B+wP\nIy8/HKTdZ31bQ/HjZ2XFGbMuM8+vjXXnj+XXkWxXa2v8D3wHllhESu3RkQrlloy1\nTMVyMboJMaoj6xgzHSMkIOyocpeG/57tVSI9EG5Y8FQuWTB8a0s/pJCFUNTNboJF\n25KbYYGO1WVtHys6WXUqznkS\n-----END PRIVATE KEY-----\n\n\n" 26 | }, 27 | "PS256": { 28 | "public_jwk": { 29 | "e": "AQAB", 30 | "n": "zHm1LsdFzC1lwiI8sDQyi-rvI5oTQTUwr7kUO0W2bvEH6Z0C8NyhxBd6E2BPBdg1GmD5AvRJTyrKPUNTSRdrd34zSydXoUDZcJKATOWpTev2IZoqB_LORGHs_MPdUkj6-p6tpzMJfAoa2lJmpoHpYkPn9FMchygTrQaLIKSU7jJNBSrzallN9167XDG9gve27BHVVSbwoX0DoGI4B3zQJy0pbRwmXwnwTGsD04BR7wFNOtOYaPZTqGkLR9ZSLUrONujEe-_EzQa-Uo-tvuqvDY_YKh-ZH9pX98BE0WUAw2oZx_3ko0nUzGyliCyff8M2VhncvKu0pz43XlrSixoK4Q", 31 | "kty": "RSA", 32 | "kid": "xYocjEyF_4lMM7A4dUFLBbxbExE_vl2B0Z8hW32b9t8", 33 | "alg": "PS256", 34 | "use": "sig" 35 | }, 36 | "private_jwk": { 37 | "e": "AQAB", 38 | "n": "zHm1LsdFzC1lwiI8sDQyi-rvI5oTQTUwr7kUO0W2bvEH6Z0C8NyhxBd6E2BPBdg1GmD5AvRJTyrKPUNTSRdrd34zSydXoUDZcJKATOWpTev2IZoqB_LORGHs_MPdUkj6-p6tpzMJfAoa2lJmpoHpYkPn9FMchygTrQaLIKSU7jJNBSrzallN9167XDG9gve27BHVVSbwoX0DoGI4B3zQJy0pbRwmXwnwTGsD04BR7wFNOtOYaPZTqGkLR9ZSLUrONujEe-_EzQa-Uo-tvuqvDY_YKh-ZH9pX98BE0WUAw2oZx_3ko0nUzGyliCyff8M2VhncvKu0pz43XlrSixoK4Q", 39 | "d": "oYNVmYCwYmpNob4Xf_uLHbhScyXGJdDVB0jDcVpMk29yl5Z9dzJf2RvOQrXlVbGFqLOGnk3GBnG_VAr1I5wLOFUIQUnvTyGBYAlorNUQGcvUHJDAuIyTX7KWVsaD8PZSwVpLwvFugXsd4OwLx2SArC0FcJmhxCTk_dxYU02NkmPggZiqMDkBZhNmBlbHFa_xwfKSb6BiQAnWafaiP37S19nzMDEPVUMhcVgexqTDkzIna8rJ3cLXU0O9IiaH9cNA43F3HMTGQrhRN0IbXRw7QujvlDeXrpvS9vLpEioBaOCjXN2iAZgpx4p3uPiFT9Mb3V4_O9x9M8IaY7IrjZPnTQ", 40 | "p": "-Ic8Z_quqcinugHZGz65Z_vs91z0oaYd3FLeDffI_R5GgndDRrskpY6oOrydTf72oFHI3mR5fCnENgAt_L4QQZJ1rkVFlbQ__sqdGm5LmncfzmMYMIhpI7g3HEsggnn2kTs4c3NrDuYkDNsrSWM4EuscqOQ9bFhyRr2js43th9M", 41 | "q": "0p9sz_uPtiIez2rPVQ3nrjLbyhf4dbN2bG7oAKtIvRHUAiokTPkLnnY0mr0nHr20mwJC9elYPyGohnaatYTaaSls2Rvqo9itpX8ZVAPMlu73XflqQ6oc5591iRb4Pq1LnUDK7VI4DrMnUEuQJ0N5qxpZ_KvAgGpLq3Hn6NW-Rfs", 42 | "dp": "kBho76irCInspa5YoLFXcnDgzfM5a1gTTCFH2jVmdUvOeeqIOURcVRlHdPbTBdvkRsPkgP0katcUinLENxxD9KDkVmyXkdr2l9YGDMMSVrbm3BUce1c8DpfKbD8q1Du-uCnr6xRqaDMLh-CzUlOSuXVUIqBi9KS7bUSa24pYxD0", 43 | "dq": "Bcm9ysVmAKJVaGvOAM9eA4qAQcCA4nMpGPe_Rm1ulUuNIPYZg0gAyr0C2xHBpnWeJfhc8LcV5r49DFzsCXr6KZOq2xiKTTBiLT8d6hIkqC0u_RUil3NwUeku6LKJ5ecLQeoK6ZSt17GSrgE1l-6hxFL4EBqEMsM5CyAZOGvPS8s", 44 | "qi": "F41EaGbqqz00rzK-Xg8qp6BGCnnAcCPB43x7ODKkhSuzfk-urbDvvwFV9APmNJaiuVn6escpHWteOfEB0Xiz3Vtu2sCs48a0k84N1wtOos6fLbTcg2dORqWziU6NchSL_25q84CUM_HEfMSWV-IIftkSRVaEQ4GnjEYDwO6qDUg", 45 | "kty": "RSA", 46 | "kid": "xYocjEyF_4lMM7A4dUFLBbxbExE_vl2B0Z8hW32b9t8", 47 | "alg": "PS256", 48 | "use": "sig" 49 | }, 50 | "public_pem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzHm1LsdFzC1lwiI8sDQy\ni+rvI5oTQTUwr7kUO0W2bvEH6Z0C8NyhxBd6E2BPBdg1GmD5AvRJTyrKPUNTSRdr\nd34zSydXoUDZcJKATOWpTev2IZoqB/LORGHs/MPdUkj6+p6tpzMJfAoa2lJmpoHp\nYkPn9FMchygTrQaLIKSU7jJNBSrzallN9167XDG9gve27BHVVSbwoX0DoGI4B3zQ\nJy0pbRwmXwnwTGsD04BR7wFNOtOYaPZTqGkLR9ZSLUrONujEe+/EzQa+Uo+tvuqv\nDY/YKh+ZH9pX98BE0WUAw2oZx/3ko0nUzGyliCyff8M2VhncvKu0pz43XlrSixoK\n4QIDAQAB\n-----END PUBLIC KEY-----\n\n\n", 51 | "private_pem": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDMebUux0XMLWXC\nIjywNDKL6u8jmhNBNTCvuRQ7RbZu8QfpnQLw3KHEF3oTYE8F2DUaYPkC9ElPKso9\nQ1NJF2t3fjNLJ1ehQNlwkoBM5alN6/YhmioH8s5EYez8w91SSPr6nq2nMwl8Chra\nUmamgeliQ+f0UxyHKBOtBosgpJTuMk0FKvNqWU33XrtcMb2C97bsEdVVJvChfQOg\nYjgHfNAnLSltHCZfCfBMawPTgFHvAU0605ho9lOoaQtH1lItSs426MR778TNBr5S\nj62+6q8Nj9gqH5kf2lf3wETRZQDDahnH/eSjSdTMbKWILJ9/wzZWGdy8q7SnPjde\nWtKLGgrhAgMBAAECggEBAKGDVZmAsGJqTaG+F3/7ix24UnMlxiXQ1QdIw3FaTJNv\ncpeWfXcyX9kbzkK15VWxhaizhp5NxgZxv1QK9SOcCzhVCEFJ708hgWAJaKzVEBnL\n1ByQwLiMk1+yllbGg/D2UsFaS8LxboF7HeDsC8dkgKwtBXCZocQk5P3cWFNNjZJj\n4IGYqjA5AWYTZgZWxxWv8cHykm+gYkAJ1mn2oj9+0tfZ8zAxD1VDIXFYHsakw5My\nJ2vKyd3C11NDvSImh/XDQONxdxzExkK4UTdCG10cO0Lo75Q3l66b0vby6RIqAWjg\no1zdogGYKceKd7j4hU/TG91ePzvcfTPCGmOyK42T500CgYEA+Ic8Z/quqcinugHZ\nGz65Z/vs91z0oaYd3FLeDffI/R5GgndDRrskpY6oOrydTf72oFHI3mR5fCnENgAt\n/L4QQZJ1rkVFlbQ//sqdGm5LmncfzmMYMIhpI7g3HEsggnn2kTs4c3NrDuYkDNsr\nSWM4EuscqOQ9bFhyRr2js43th9MCgYEA0p9sz/uPtiIez2rPVQ3nrjLbyhf4dbN2\nbG7oAKtIvRHUAiokTPkLnnY0mr0nHr20mwJC9elYPyGohnaatYTaaSls2Rvqo9it\npX8ZVAPMlu73XflqQ6oc5591iRb4Pq1LnUDK7VI4DrMnUEuQJ0N5qxpZ/KvAgGpL\nq3Hn6NW+RfsCgYEAkBho76irCInspa5YoLFXcnDgzfM5a1gTTCFH2jVmdUvOeeqI\nOURcVRlHdPbTBdvkRsPkgP0katcUinLENxxD9KDkVmyXkdr2l9YGDMMSVrbm3BUc\ne1c8DpfKbD8q1Du+uCnr6xRqaDMLh+CzUlOSuXVUIqBi9KS7bUSa24pYxD0CgYAF\nyb3KxWYAolVoa84Az14DioBBwIDicykY979GbW6VS40g9hmDSADKvQLbEcGmdZ4l\n+FzwtxXmvj0MXOwJevopk6rbGIpNMGItPx3qEiSoLS79FSKXc3BR6S7osonl5wtB\n6grplK3XsZKuATWX7qHEUvgQGoQywzkLIBk4a89LywKBgBeNRGhm6qs9NK8yvl4P\nKqegRgp5wHAjweN8ezgypIUrs35Prq2w778BVfQD5jSWorlZ+nrHKR1rXjnxAdF4\ns91bbtrArOPGtJPODdcLTqLOny203INnTkals4lOjXIUi/9uavOAlDPxxHzEllfi\nCH7ZEkVWhEOBp4xGA8Duqg1I\n-----END PRIVATE KEY-----\n\n\n" 52 | }, 53 | "ES256": { 54 | "public_jwk": { 55 | "crv": "P-256", 56 | "x": "iXWOTZ1dKYX0UoNRNZ6ZnU2XzgRi7iRWjNEztmT6fNM", 57 | "y": "kDR93WcyoNaG5uWWDt4nZQNAD0cuLrSQuZzGz5pVhtg", 58 | "kty": "EC", 59 | "kid": "GtI1ZQYYPrCvhtRyYcAlGT-gBUEkEpfw98wl1-m1nrM", 60 | "use": "sig" 61 | }, 62 | "private_jwk": { 63 | "crv": "P-256", 64 | "x": "iXWOTZ1dKYX0UoNRNZ6ZnU2XzgRi7iRWjNEztmT6fNM", 65 | "y": "kDR93WcyoNaG5uWWDt4nZQNAD0cuLrSQuZzGz5pVhtg", 66 | "d": "Cmy7ymkqGsHwP7yIdlAJ2EPXeWVaOOWxeabFMKocxvA", 67 | "kty": "EC", 68 | "kid": "GtI1ZQYYPrCvhtRyYcAlGT-gBUEkEpfw98wl1-m1nrM", 69 | "use": "sig" 70 | }, 71 | "public_pem": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiXWOTZ1dKYX0UoNRNZ6ZnU2XzgRi\n7iRWjNEztmT6fNOQNH3dZzKg1obm5ZYO3idlA0APRy4utJC5nMbPmlWG2A==\n-----END PUBLIC KEY-----\n\n\n", 72 | "private_pem": "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgCmy7ymkqGsHwP7yI\ndlAJ2EPXeWVaOOWxeabFMKocxvChRANCAASJdY5NnV0phfRSg1E1npmdTZfOBGLu\nJFaM0TO2ZPp805A0fd1nMqDWhubllg7eJ2UDQA9HLi60kLmcxs+aVYbY\n-----END PRIVATE KEY-----\n\n\n" 73 | }, 74 | "EdDSA": { 75 | "public_jwk": { 76 | "crv": "Ed25519", 77 | "x": "Lj-xqjJ9GsoGM4FvN2UBIWrsJLMCB19oS1RbLuZKKRY", 78 | "kty": "OKP", 79 | "kid": "W-r_JrLsLcisj09GYTHf1D02yQ2tA7772TEXtjEqQZU", 80 | "use": "sig" 81 | }, 82 | "private_jwk": { 83 | "crv": "Ed25519", 84 | "x": "Lj-xqjJ9GsoGM4FvN2UBIWrsJLMCB19oS1RbLuZKKRY", 85 | "d": "cVBn4vcgEAmrfwliLgspa96ryCyr1RzvnaPKXuURdHc", 86 | "kty": "OKP", 87 | "kid": "W-r_JrLsLcisj09GYTHf1D02yQ2tA7772TEXtjEqQZU", 88 | "use": "sig" 89 | }, 90 | "public_pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEALj+xqjJ9GsoGM4FvN2UBIWrsJLMCB19oS1RbLuZKKRY=\n-----END PUBLIC KEY-----\n\n\n", 91 | "private_pem": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIHFQZ+L3IBAJq38JYi4LKWveq8gsq9Uc752jyl7lEXR3\n-----END PRIVATE KEY-----\n\n\n" 92 | }, 93 | "HS256": { 94 | "public_jwk": { 95 | "kty": "oct", 96 | "kid": "pqDyUxlr4XOLW3RwYQjwzSeY97t55itusThf5g9uNgg", 97 | "use": "sig" 98 | }, 99 | "private_jwk": { 100 | "k": "yPNP0rfS0lFfr0jKAbkmZ5e36_OefAmBIy5wG63-37k", 101 | "kty": "oct", 102 | "kid": "pqDyUxlr4XOLW3RwYQjwzSeY97t55itusThf5g9uNgg", 103 | "use": "sig" 104 | } 105 | }, 106 | "RSA-SHA1": { 107 | "private_key": "-----BEGIN PRIVATE KEY-----\nMIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBANEYHOlxym0mrt5S\n3f2vUjxEvmfb8XQ1vdFCO567mYpHgT3CEGedXycCkZJWG5fxwZ9I1Widem0rslHL\n7/vOXTx0YzbSc3MT3Sk6efolv5MejNZJkx4iCs9y/K1IfGRKSBNlxDBigHsmmewd\n8qhsGJS/jWlXLoZET0X+iTRgEk5/AgMBAAECgYADoxu9o9EVnPqlu2NJKmePzNJT\nFCxbDSREI5bN6A1/rka9QEbwxngFQbIujXjlZ7sqfiXYMAUVKRFtAtMl2i0c/Av5\ncz2gFYo5Jz1oqF/NCm57ZX6533mry0uZSEhvqlo8ik8oKJ8tUH6gIHWcwQf4lzaD\nWfaig2oI4l+e5utygQJBAO3CUSQVzu/Zee0LvEr1fSkitfxnNeh/QPWNGUCIJGNq\nmJgfZb8DRG9sWbfa9M2xFnX+48C+c+IesPgaUeVEk28CQQDhIs6Aw0tm3VqJ75yr\nFFKJu3S937K75KWgmpB281zq7eWSbXHDE68neOKMU5yRqn9eauoViumAm2gFa+dM\ndy3xAkEA4fFvuqMe7L/3JlWosnNoZdceqqZKjI+h47ga70BxlCiQqr/rqQIp3tlQ\nyW/ChFZtyeRX+6sB5TjVZHFeskng8QJAQicRGKLJ8CLQrME8fsSM8C2lwvkNMsqf\npE5mbp1Zyyo2D82a5OBO0kFiCCu1UNQRcvPlbokPzZtceGNZZo2KcQJAK2WmRoyp\njyxyLOsNuEc6Xh9M/AUJQTbwLVimQAo5rcBFT3oRagNIzGajQwYiWtunOJQa3415\nlKdNZ/SDTwH5cA==\n-----END PRIVATE KEY-----\n\n" 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /test/util/provider.js: -------------------------------------------------------------------------------- 1 | 2 | var http = require('http') 3 | var _url = require('url') 4 | var qs = require('qs') 5 | 6 | var buffer = (req, done) => { 7 | var data = '' 8 | req.on('data', (chunk) => data += chunk) 9 | req.on('end', () => done( 10 | /^{.*}$/.test(data) ? JSON.parse(data) : qs.parse(data))) 11 | } 12 | var _query = (req) => { 13 | var parsed = _url.parse(req.url, false) 14 | var query = qs.parse(parsed.query) 15 | return query 16 | } 17 | var _oauth = (req) => 18 | qs.parse((req.headers.authorization || '') 19 | .replace('OAuth ', '').replace(/"/g, '').replace(/,/g, '&')) 20 | 21 | var sign = (...args) => args.map((arg, index) => index < 2 22 | ? Buffer.from(JSON.stringify(arg)).toString('base64') 23 | .replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_') 24 | : arg).join('.') 25 | 26 | var provider = async ({flow, port = 5000}) => { 27 | var server = await (flow === 'oauth2' ? oauth2(port) : oauth1(port)) 28 | return { 29 | oauth1, 30 | oauth2, 31 | on, 32 | server, 33 | url: (path) => `http://localhost:${port}${path}`, 34 | close: () => new Promise((resolve) => server.close(resolve)) 35 | } 36 | } 37 | 38 | var oauth1 = (port) => new Promise((resolve) => { 39 | var callback 40 | var server = http.createServer() 41 | server.on('request', (req, res) => { 42 | var method = req.method 43 | var url = req.url 44 | var headers = req.headers 45 | var oauth = _oauth(req) 46 | var query = _query(req) 47 | var provider = /^\/(.*)\/.*/.exec(url) && /^\/(.*)\/.*/.exec(url)[1] 48 | 49 | if (/request_url/.test(url)) { 50 | callback = oauth.oauth_callback 51 | buffer(req, (form) => { 52 | if (provider === 'getpocket') { 53 | callback = form.redirect_uri 54 | } 55 | on.request({url, headers, query, form, oauth}) 56 | provider === 'sellsy' 57 | ? res.writeHead(200, {'content-type': 'application/json'}) 58 | : res.writeHead(200, {'content-type': 'application/x-www-form-urlencoded'}) 59 | provider === 'getpocket' 60 | ? res.end(qs.stringify({code: 'code'})) 61 | : provider === 'sellsy' 62 | ? res.end('authentification_url=https://apifeed.sellsy.com/0/login.php&oauth_token=token&oauth_token_secret=secret&oauth_callback_confirmed=true') 63 | : res.end(qs.stringify({oauth_token: 'token', oauth_token_secret: 'secret'})) 64 | }) 65 | } 66 | else if (/authorize_url/.test(url)) { 67 | var location = callback + '?' + 68 | qs.stringify({oauth_token: 'token', oauth_verifier: 'verifier'}) 69 | on.authorize({url, headers, query}) 70 | res.writeHead(302, {location}) 71 | res.end() 72 | } 73 | else if (/access_url/.test(url)) { 74 | buffer(req, (form) => { 75 | on.access({url, headers, query, form, oauth}) 76 | res.writeHead(200, {'content-type': 'application/json'}) 77 | provider === 'getpocket' 78 | ? res.end(JSON.stringify({access_token: 'token'})) 79 | : res.end(JSON.stringify({ 80 | oauth_token: 'token', 81 | oauth_token_secret: 'secret', 82 | user_id: provider === 'twitter' ? 'id' : undefined, 83 | })) 84 | }) 85 | } 86 | else if (/request_error_message/.test(url)) { 87 | callback = oauth.oauth_callback 88 | buffer(req, (form) => { 89 | on.request({url, headers, query, form, oauth}) 90 | res.writeHead(200, {'content-type': 'application/x-www-form-urlencoded'}) 91 | res.end(qs.stringify({error: {message: 'invalid'}})) 92 | }) 93 | } 94 | else if (/request_error_token/.test(url)) { 95 | callback = oauth.oauth_callback 96 | buffer(req, (form) => { 97 | on.request({url, headers, query, form, oauth}) 98 | res.writeHead(200, {'content-type': 'application/x-www-form-urlencoded'}) 99 | res.end() 100 | }) 101 | } 102 | else if (/request_error_status/.test(url)) { 103 | callback = oauth.oauth_callback 104 | buffer(req, (form) => { 105 | on.request({url, headers, query, form, oauth}) 106 | res.writeHead(500, {'content-type': 'application/x-www-form-urlencoded'}) 107 | res.end(qs.stringify({invalid: 'request_url'})) 108 | }) 109 | } 110 | else if (/authorize_error_message/.test(url)) { 111 | var location = callback + '?' + qs.stringify({error: {message: 'invalid'}}) 112 | on.authorize({url, headers, query}) 113 | res.writeHead(302, {location}) 114 | res.end() 115 | } 116 | else if (/authorize_error_token/.test(url)) { 117 | var location = callback 118 | on.authorize({url, headers, query}) 119 | res.writeHead(302, {location}) 120 | res.end() 121 | } 122 | else if (/access_error_status/.test(url)) { 123 | buffer(req, (form) => { 124 | on.access({url, headers, query, form, oauth}) 125 | res.writeHead(500, {'content-type': 'application/json'}) 126 | res.end(JSON.stringify({invalid: 'access_url'})) 127 | }) 128 | } 129 | else if (/profile_url/.test(req.url)) { 130 | on.profile({method, url, query, headers}) 131 | res.writeHead(200, {'content-type': 'application/json'}) 132 | provider === 'flickr' 133 | ? res.end('callback({"user": "simov"})') 134 | : res.end(JSON.stringify({user: 'simov'})) 135 | } 136 | }) 137 | server.listen(port, () => resolve(server)) 138 | }) 139 | 140 | var oauth2 = (port) => new Promise((resolve) => { 141 | var server = http.createServer() 142 | var openid 143 | server.on('request', (req, res) => { 144 | var method = req.method 145 | var url = req.url 146 | var headers = req.headers 147 | var query = _query(req) 148 | var provider = /^\/(.*)\/.*/.exec(url) && /^\/(.*)\/.*/.exec(url)[1] 149 | 150 | if (/authorize_url/.test(req.url)) { 151 | openid = (query.scope || []).includes('openid') 152 | on.authorize({provider, method, url, headers, query}) 153 | if (query.response_mode === 'form_post') { 154 | provider === 'apple' 155 | ? res.end(qs.stringify({ 156 | code: 'code', 157 | user: {name: {firstName: 'jon', lastName: 'doe'}, email: 'jon@doe.com'} 158 | })) 159 | : res.end('code') 160 | return 161 | } 162 | var location = query.redirect_uri + '?' + ( 163 | provider === 'intuit' 164 | ? qs.stringify({code: 'code', realmId: '123'}) 165 | : qs.stringify({code: 'code'}) 166 | ) 167 | res.writeHead(302, {location}) 168 | res.end() 169 | } 170 | else if (/access_url/.test(req.url)) { 171 | buffer(req, (form) => { 172 | on.access({provider, method, url, headers, query, form}) 173 | res.writeHead(200, {'content-type': 'application/json'}) 174 | provider === 'concur' 175 | ? res.end(' token refresh ') 176 | : provider === 'withings' 177 | ? res.end(JSON.stringify({body: { 178 | access_token: 'token', refresh_token: 'refresh', expires_in: 3600 179 | }})) 180 | : res.end(JSON.stringify({ 181 | access_token: 'token', refresh_token: 'refresh', expires_in: 3600, 182 | id_token: openid ? sign({typ: 'JWT'}, {nonce: 'whatever'}, 'signature') : undefined, 183 | open_id: provider === 'tiktok' ? 'id' : undefined, 184 | uid: provider === 'weibo' ? 'id' : undefined, 185 | openid: provider === 'wechat' ? 'openid' : undefined, 186 | })) 187 | }) 188 | } 189 | else if (/authorize_error_message/.test(req.url)) { 190 | on.authorize({url, query, headers}) 191 | var location = query.redirect_uri + '?' + qs.stringify({error: {message: 'invalid'}}) 192 | res.writeHead(302, {location}) 193 | res.end() 194 | } 195 | else if (/authorize_error_code/.test(req.url)) { 196 | on.authorize({url, query, headers}) 197 | var location = query.redirect_uri 198 | res.writeHead(302, {location}) 199 | res.end() 200 | } 201 | else if (/authorize_error_state_mismatch/.test(req.url)) { 202 | on.authorize({url, query, headers}) 203 | var location = query.redirect_uri + '?' + qs.stringify({code: 'code', state: 'whatever'}) 204 | res.writeHead(302, {location}) 205 | res.end() 206 | } 207 | else if (/authorize_error_state_missing/.test(req.url)) { 208 | on.authorize({url, query, headers}) 209 | var location = query.redirect_uri + '?' + qs.stringify({code: 'code'}) 210 | res.writeHead(302, {location}) 211 | res.end() 212 | } 213 | else if (/access_error_nonce_mismatch/.test(req.url)) { 214 | buffer(req, (form) => { 215 | on.access({method, url, query, headers, form}) 216 | res.writeHead(200, {'content-type': 'application/json'}) 217 | res.end(JSON.stringify({ 218 | id_token: sign({typ: 'JWT'}, {nonce: 'whatever'}, 'signature') 219 | })) 220 | }) 221 | } 222 | else if (/access_error_nonce_missing/.test(req.url)) { 223 | buffer(req, (form) => { 224 | on.access({method, url, query, headers, form}) 225 | res.writeHead(200, {'content-type': 'application/json'}) 226 | res.end(JSON.stringify({ 227 | id_token: sign({typ: 'JWT'}, {}, 'signature') 228 | })) 229 | }) 230 | } 231 | else if (/access_error_message/.test(req.url)) { 232 | buffer(req, (form) => { 233 | on.access({method, url, query, headers, form}) 234 | res.writeHead(200, {'content-type': 'application/json'}) 235 | res.end(JSON.stringify({error: {message: 'invalid'}})) 236 | }) 237 | } 238 | else if (/access_error_status/.test(req.url)) { 239 | buffer(req, (form) => { 240 | on.access({method, url, query, headers, form}) 241 | res.writeHead(500, {'content-type': 'application/json'}) 242 | res.end(JSON.stringify({invalid: 'access_url'})) 243 | }) 244 | } 245 | else if (/profile_url/.test(req.url)) { 246 | if (method === 'POST') { 247 | buffer(req, (form) => { 248 | on.profile({method, url, query, headers, form}) 249 | res.writeHead(200, {'content-type': 'application/json'}) 250 | res.end(JSON.stringify({user: 'simov'})) 251 | }) 252 | } 253 | else { 254 | on.profile({method, url, query, headers}) 255 | res.writeHead(200, {'content-type': 'application/json'}) 256 | res.end(JSON.stringify({user: 'simov'})) 257 | } 258 | } 259 | else if (/profile_error/.test(req.url)) { 260 | on.profile({method, url, query, headers}) 261 | res.writeHead(400, {'content-type': 'application/json'}) 262 | res.end(JSON.stringify({error: {message: 'Not Found'}})) 263 | } 264 | }) 265 | server.listen(port, () => resolve(server)) 266 | }) 267 | 268 | var on = { 269 | request: () => {}, 270 | authorize: () => {}, 271 | access: () => {}, 272 | profile: () => {}, 273 | } 274 | 275 | module.exports = provider 276 | --------------------------------------------------------------------------------