├── .prettierrc ├── .eslintrc.json ├── .github ├── workflows │ └── nodejs.yml └── dependabot.yml ├── LICENSE ├── package.json ├── .gitignore ├── CHANGELOG.md ├── lib └── refresh.js ├── README.md └── test └── refresh.spec.js /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es6": true, 5 | "node": true, 6 | "mocha": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "globals": { 10 | "Atomics": "readonly", 11 | "SharedArrayBuffer": "readonly" 12 | }, 13 | "parserOptions": { 14 | "ecmaVersion": 2018 15 | }, 16 | "rules": { 17 | "eqeqeq": ["error", "smart"], 18 | "no-var": ["error"], 19 | "prefer-const": ["error"] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [10.x, 12.x, 14.x, 16.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: npm ci 24 | - run: npm test 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: '/' 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: eslint 10 | versions: 11 | - 7.18.0 12 | - 7.19.0 13 | - 7.20.0 14 | - 7.21.0 15 | - 7.22.0 16 | - 7.23.0 17 | - 7.24.0 18 | - dependency-name: mocha 19 | versions: 20 | - 8.3.0 21 | - 8.3.1 22 | - dependency-name: chai 23 | versions: 24 | - 4.3.0 25 | - 4.3.1 26 | - 4.3.3 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 - 2020 Tom Spencer 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "passport-oauth2-refresh", 3 | "version": "2.1.0", 4 | "description": "A passport.js add-on to provide automatic OAuth 2.0 token refreshing.", 5 | "main": "lib/refresh.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "lint": "eslint \"**/*.js\" --fix", 11 | "prettier": "prettier --write .", 12 | "test": "prettier --check . && mocha test/refresh.spec.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/fiznool/passport-oauth2-refresh.git" 17 | }, 18 | "keywords": [ 19 | "passport", 20 | "oauth", 21 | "oauth2", 22 | "auth", 23 | "authentication" 24 | ], 25 | "author": { 26 | "name": "Tom Spencer", 27 | "email": "fiznool@gmail.com", 28 | "url": "https://github.com/fiznool" 29 | }, 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/fiznool/passport-oauth2-refresh/issues" 33 | }, 34 | "homepage": "https://github.com/fiznool/passport-oauth2-refresh", 35 | "devDependencies": { 36 | "chai": "^4.3.4", 37 | "eslint": "^7.29.0", 38 | "mocha": "^9.0.1", 39 | "prettier": "^2.3.2", 40 | "sinon": "^11.1.1", 41 | "sinon-chai": "^3.7.0" 42 | }, 43 | "engines": { 44 | "node": ">=10" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node 2 | # Edit at https://www.gitignore.io/?templates=node 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Optional REPL history 61 | .node_repl_history 62 | 63 | # Output of 'npm pack' 64 | *.tgz 65 | 66 | # Yarn Integrity file 67 | .yarn-integrity 68 | 69 | # dotenv environment variables file 70 | .env 71 | .env.test 72 | 73 | # parcel-bundler cache (https://parceljs.org/) 74 | .cache 75 | 76 | # next.js build output 77 | .next 78 | 79 | # nuxt.js build output 80 | .nuxt 81 | 82 | # rollup.js default build output 83 | dist/ 84 | 85 | # Uncomment the public line if your project uses Gatsby 86 | # https://nextjs.org/blog/next-9-1#public-directory-support 87 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav 88 | # public 89 | 90 | # Storybook build outputs 91 | .out 92 | .storybook-out 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # Temporary folders 107 | tmp/ 108 | temp/ 109 | 110 | # End of https://www.gitignore.io/api/node 111 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## Unreleased 7 | 8 | ### Added 9 | 10 | - Added a new named export `AuthTokenRefresh`. This is a 11 | constructor that can be invoked to create a distinct instance, for 12 | applications that require more than one Passport instance. 13 | 14 | ## [2.1.0] - 2021-06-29 15 | 16 | ### Added 17 | 18 | - `setRefreshOAuth2` option to customise the OAuth2 adapter that is used to refresh the access token. 19 | 20 | ## [2.0.2] - 2021-05-11 21 | 22 | ### Updated 23 | 24 | - Update dependencies and test against node 16. 25 | 26 | ## [2.0.1] - 2020-12-02 27 | 28 | ### Updated 29 | 30 | - Update dependencies and test against node 14. 31 | 32 | ## [2.0.0] - 2020-03-25 33 | 34 | ### Breaking 35 | 36 | - Drop support for node < 10. 37 | 38 | ## [1.1.0] - 2018-06-06 39 | 40 | ### Added 41 | 42 | - Support using a strategy which overrides the `getOAuthAccessToken` function, for example the Reddit or Spotify strategy. #10 43 | 44 | ## [1.0.0] - 2015-12-17 45 | 46 | ### Added 47 | 48 | - Allow extra params to be sent when requesting access token. 49 | - Use embedded `_oauth2` constructor to create new OAuth2 instance, to support instances where the `_oauth2` object is using a custom implementation. 50 | 51 | ### Removed 52 | 53 | - Dropped peerDependency on `oauth2` library, in favour of using the `_oauth2` object exposed by passport. 54 | - Dropped support for node.js 0.6 and 0.8, lowest supported version is now 0.10. _If you still need support for 0.6 or 0.8, please continue to use v0.4.0 of this module._ 55 | 56 | ### Upgrading from 0.4 57 | 58 | The move from 0.4 to 1.0 is non-breaking, _unless_ you are using a version of node.js lower than 0.10. In this case, you should stick to using 0.4. Otherwise, you can safely upgrade with no code changes required. 59 | 60 | ## [0.4.0] - 2015-04-01 61 | 62 | ### Added 63 | 64 | - Allow strategy to be added with an explicit name: `refresh.use(name, strategy)`. 65 | 66 | ## [0.3.1] - 2015-03-06 67 | 68 | ### Changed 69 | 70 | - Removed peer dependency on passport-oauth2, to fix npm 3 warning. 71 | 72 | ## [0.3.0] - 2015-01-27 73 | 74 | ### Added 75 | 76 | - Support strategies which use separate URLs for generating and refreshing tokens (e.g. `passport-echosign`). 77 | 78 | ## [0.2.1] - 2014-11-16 79 | 80 | ### Fixed 81 | 82 | - Fixed passport-oauth2 peer dependency link. 83 | 84 | ## [0.2.0] - 2014-11-16 85 | 86 | ### Changed 87 | 88 | - Added passport-oauth2 as a peer dependency. 89 | 90 | ## [0.1.2] - 2014-11-16 91 | 92 | ### Changed 93 | 94 | - Fixed git url. 95 | 96 | ## [0.1.1] - 2014-11-16 97 | 98 | ### Changed 99 | 100 | - Fixed README typo. 101 | 102 | ## 0.1.0 - 2014-11-16 103 | 104 | ### Added 105 | 106 | - Initial release. 107 | 108 | [2.1.0]: https://github.com/fiznool/passport-oauth2-refresh/compare/v2.0.2...v2.1.0 109 | [2.0.2]: https://github.com/fiznool/passport-oauth2-refresh/compare/v2.0.1...v2.0.2 110 | [2.0.1]: https://github.com/fiznool/passport-oauth2-refresh/compare/v2.0.0...v2.0.1 111 | [2.0.0]: https://github.com/fiznool/passport-oauth2-refresh/compare/v1.1.0...v2.0.0 112 | [1.1.0]: https://github.com/fiznool/passport-oauth2-refresh/compare/v1.0.0...v1.1.0 113 | [1.0.0]: https://github.com/fiznool/passport-oauth2-refresh/compare/v0.4.0...v1.0.0 114 | [0.4.0]: https://github.com/fiznool/passport-oauth2-refresh/compare/v0.3.1...v0.4.0 115 | [0.3.1]: https://github.com/fiznool/passport-oauth2-refresh/compare/v0.3.0...v0.3.1 116 | [0.3.0]: https://github.com/fiznool/passport-oauth2-refresh/compare/v0.2.1...v0.3.0 117 | [0.2.1]: https://github.com/fiznool/passport-oauth2-refresh/compare/v0.2.0...v0.2.1 118 | [0.2.0]: https://github.com/fiznool/passport-oauth2-refresh/compare/v0.1.2...v0.2.0 119 | [0.1.2]: https://github.com/fiznool/passport-oauth2-refresh/compare/v0.1.1...v0.1.2 120 | [0.1.1]: https://github.com/fiznool/passport-oauth2-refresh/compare/v0.1.0...v0.1.1 121 | -------------------------------------------------------------------------------- /lib/refresh.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class AuthTokenRefresh { 4 | constructor() { 5 | this._strategies = {}; 6 | } 7 | /** 8 | * Register a passport strategy so it can refresh an access token, 9 | * with optional `name`, overridding the strategy's default name. 10 | * 11 | * A third optional options parameter is available, which can be 12 | * used to create a custom OAuth2 adapter, or modify the one 13 | * which is automatically created. This is useful if the 14 | * strategy does not expose its internal OAuth2 adapter, or 15 | * customizes the adapter in some way that needs to be replicated. 16 | * 17 | * Examples: 18 | * 19 | * refresh.use(strategy); 20 | * refresh.use('facebook', strategy); 21 | * refresh.use('activedirectory', strategy, { 22 | * setRefreshOAuth2() { 23 | * return new OAuth2(...); 24 | * } 25 | * }); 26 | * 27 | * @param {String|Strategy} name 28 | * @param {Strategy} passport strategy 29 | * @param {Object} options 30 | * @param {OAuth2} options.setRefreshOAuth2 a callback to modify the oauth2 adapter. Should return the oauth2 adapter to use when refreshing the token. 31 | */ 32 | use(name, strategy, options) { 33 | if (typeof name !== 'string') { 34 | // Infer name from strategy 35 | options = strategy; 36 | strategy = name; 37 | name = strategy && strategy.name; 38 | } 39 | 40 | if (strategy == null) { 41 | throw new Error('Cannot register: strategy is null'); 42 | } 43 | 44 | if (!name) { 45 | throw new Error( 46 | 'Cannot register: name must be specified, or strategy must include name', 47 | ); 48 | } 49 | 50 | options = options || {}; 51 | 52 | let refreshOAuth2 = undefined; 53 | 54 | if (strategy._oauth2) { 55 | // Try to use the internal oauth2 adapter, setting some sane defaults. 56 | // Use the strategy's OAuth2 object, since it might have been overwritten. 57 | // https://github.com/fiznool/passport-oauth2-refresh/issues/3 58 | const OAuth2 = strategy._oauth2.constructor; 59 | 60 | // Generate our own oauth2 object for use later. 61 | // Use the strategy's _refreshURL, if defined, 62 | // otherwise use the regular accessTokenUrl. 63 | refreshOAuth2 = new OAuth2( 64 | strategy._oauth2._clientId, 65 | strategy._oauth2._clientSecret, 66 | strategy._oauth2._baseSite, 67 | strategy._oauth2._authorizeUrl, 68 | strategy._refreshURL || strategy._oauth2._accessTokenUrl, 69 | strategy._oauth2._customHeaders, 70 | ); 71 | 72 | // Some strategies overwrite the getOAuthAccessToken function to set headers 73 | // https://github.com/fiznool/passport-oauth2-refresh/issues/10 74 | refreshOAuth2.getOAuthAccessToken = strategy._oauth2.getOAuthAccessToken; 75 | } 76 | 77 | // See if we need to customise the OAuth2 object any further 78 | if (typeof options.setRefreshOAuth2 === 'function') { 79 | refreshOAuth2 = options.setRefreshOAuth2({ 80 | strategyOAuth2: strategy._oauth2, 81 | refreshOAuth2, 82 | }); 83 | } 84 | 85 | if (!refreshOAuth2) { 86 | throw new Error( 87 | 'The OAuth2 adapter used to refresh the token is not configured correctly. Use the setRefreshOAuth2 option to return a OAuth 2.0 adapter.', 88 | ); 89 | } 90 | 91 | // Set the strategy and oauth2 adapter for use later 92 | this._strategies[name] = { 93 | strategy, 94 | refreshOAuth2, 95 | }; 96 | } 97 | 98 | /** 99 | * Check if a strategy is registered for refreshing. 100 | * @param {String} name Strategy name 101 | * @return {Boolean} 102 | */ 103 | has(name) { 104 | return !!this._strategies[name]; 105 | } 106 | 107 | /** 108 | * Request a new access token, using the passed refreshToken, 109 | * for the given strategy. 110 | * @param {String} name Strategy name. Must have already 111 | * been registered. 112 | * @param {String} refreshToken Refresh token to be sent to request 113 | * a new access token. 114 | * @param {Object} params (optional) an object containing additional 115 | * params to use when requesting the token. 116 | * @param {Function} done Callback when all is done. 117 | */ 118 | requestNewAccessToken(name, refreshToken, params, done) { 119 | if (arguments.length === 3) { 120 | done = params; 121 | params = {}; 122 | } 123 | 124 | // Send a request to refresh an access token, and call the passed 125 | // callback with the result. 126 | const strategy = this._strategies[name]; 127 | if (!strategy) { 128 | return done(new Error('Strategy was not registered to refresh a token')); 129 | } 130 | 131 | params = params || {}; 132 | params.grant_type = 'refresh_token'; 133 | 134 | strategy.refreshOAuth2.getOAuthAccessToken(refreshToken, params, done); 135 | } 136 | } 137 | 138 | // Default export, for typical case & backwards compatibility 139 | module.exports = new AuthTokenRefresh(); 140 | 141 | // Export a constructor, just like Passport does 142 | module.exports.AuthTokenRefresh = AuthTokenRefresh; 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Passport OAuth 2.0 Refresh 2 | 3 | An add-on to the [Passport](http://passportjs.org) authentication library to provide a simple way to refresh your OAuth 2.0 access tokens. 4 | 5 | [![Build Status](https://github.com/fiznool/passport-oauth2-refresh/workflows/Node.js%20CI/badge.svg)](https://github.com/fiznool/passport-oauth2-refresh/workflows/Node.js%20CI/badge.svg) 6 | [![npm version](https://img.shields.io/npm/v/passport-oauth2-refresh)](https://img.shields.io/npm/v/passport-oauth2-refresh) 7 | [![npm downloads per week](https://img.shields.io/npm/dw/passport-oauth2-refresh?color=blue)](https://img.shields.io/npm/dw/passport-oauth2-refresh?color=blue) 8 | [![Dependency Status](https://david-dm.org/fiznool/passport-oauth2-refresh.svg)](https://david-dm.org/fiznool/passport-oauth2-refresh) 9 | [![devDependency Status](https://david-dm.org/fiznool/passport-oauth2-refresh/dev-status.svg)](https://david-dm.org/fiznool/passport-oauth2-refresh#info=devDependencies) 10 | 11 | ## Installation 12 | 13 | ``` 14 | npm install passport-oauth2-refresh 15 | ``` 16 | 17 | ## Usage 18 | 19 | When setting up your passport strategies, add a call to `refresh.use()` after `passport.use()`. 20 | 21 | An example, using the Facebook strategy: 22 | 23 | ```js 24 | const passport = require('passport'); 25 | const refresh = require('passport-oauth2-refresh'); 26 | const FacebookStrategy = require('passport-facebook').Strategy; 27 | 28 | const strategy = new FacebookStrategy({ 29 | clientID: FACEBOOK_APP_ID, 30 | clientSecret: FACEBOOK_APP_SECRET, 31 | callbackURL: "http://www.example.com/auth/facebook/callback" 32 | }, 33 | function(accessToken, refreshToken, profile, done) { 34 | // Make sure you store the refreshToken somewhere! 35 | User.findOrCreate(..., function(err, user) { 36 | if (err) { return done(err); } 37 | done(null, user); 38 | }); 39 | }); 40 | 41 | passport.use(strategy); 42 | refresh.use(strategy); 43 | ``` 44 | 45 | When you need to refresh the access token, call `requestNewAccessToken()`: 46 | 47 | ```js 48 | const refresh = require('passport-oauth2-refresh'); 49 | refresh.requestNewAccessToken( 50 | 'facebook', 51 | 'some_refresh_token', 52 | function (err, accessToken, refreshToken) { 53 | // You have a new access token, store it in the user object, 54 | // or use it to make a new request. 55 | // `refreshToken` may or may not exist, depending on the strategy you are using. 56 | // You probably don't need it anyway, as according to the OAuth 2.0 spec, 57 | // it should be the same as the initial refresh token. 58 | }, 59 | ); 60 | ``` 61 | 62 | ### Specific name 63 | 64 | Instead of using the default `strategy.name`, you can setup `passport-oauth2-refresh` to use an specific name instead. 65 | 66 | ```js 67 | // Setup 68 | passport.use('gmail', googleStrategy); 69 | 70 | // To refresh 71 | refresh.requestNewAccessToken('gmail', 'some_refresh_token', done); 72 | ``` 73 | 74 | This can be useful if you'd like to reuse strategy objects but under a different name. 75 | 76 | ### Custom OAuth2 behaviour 77 | 78 | Most passport strategies that use OAuth 2.0 should work without any additional configuration. Some strategies, however require custom OAuth configuration, or do not expose an oauth2 adapter for internal use. In these cases, a callback can be specified by calling the `use` function with an extra `options` parameter: 79 | 80 | ```js 81 | const { OAuth2 } = require('oauth'); 82 | 83 | refresh.use(strategy, { 84 | setRefreshOAuth2() { 85 | return new OAuth2(/* custom oauth config */); 86 | }, 87 | }); 88 | ``` 89 | 90 | The `setRefreshOAuth2` callback should return an instance of [the node-oauth OAuth2 class](https://github.com/ciaranj/node-oauth#oauth20). 91 | 92 | The callback is called with two named parameters, which can be used to further customise the OAuth2 adapter: 93 | 94 | ```js 95 | refresh.use(strategy, { 96 | setRefreshOAuth2({ strategyOAuth2, refreshOAuth2 }) { 97 | // These named parameters are set for most strategies. 98 | // The `refreshOAuth2` instance is a clone of the one supplied by the strategy, inheriting most of its config. 99 | // Customise it here and return if necessary. 100 | // For example, to set a proxy: 101 | refreshOAuth2.setAgent(new HttpsProxyAgent(agentUrl)); 102 | return refreshOAuth2; 103 | }, 104 | }); 105 | ``` 106 | 107 | ### Additional parameters 108 | 109 | Some endpoints require additional parameters to be sent when requesting a new access token. To send these parameters, specify the parameters when calling `requestNewAccessToken` as follows: 110 | 111 | ```js 112 | const extraParams = { some: 'extra_param' }; 113 | refresh.requestNewAccessToken('gmail', 'some_refresh_token', extraParams, done); 114 | ``` 115 | 116 | ### Multiple instances 117 | 118 | Projects that need multiple instances of Passport can construct them using the `Passport` 119 | constructor available on the `passport` module. Similarly, this module provides 120 | an `AuthTokenRefresh` constructor that can be used instead of the single instance provided 121 | by default. 122 | 123 | ```javascript 124 | const { Passport } = require('passport'); 125 | const { AuthTokenRefresh } = require('passport-oauth2-refresh'); 126 | 127 | const passport = new Passport(); 128 | const refresh = new AuthTokenRefresh(); 129 | 130 | // Additional, distinct instances of these modules can also be created 131 | ``` 132 | 133 | ## Examples 134 | 135 | - See [issue #1](https://github.com/fiznool/passport-oauth2-refresh/issues/1) for an example of how to refresh a token when requesting data from the Google APIs. 136 | 137 | ## Why? 138 | 139 | Passport is a library which doesn't deal in implementation-specific details. From the author: 140 | 141 | > Passport is a library for authenticating requests, and only that. It is not going to get involved in anything that is specific to OAuth, or any other authorization protocol. 142 | 143 | Fair enough. Hence, this add-on was born as a way to help deal with refreshing OAuth 2.0 tokens. 144 | 145 | It is particularly useful when dealing with Google's OAuth 2.0 implementation, which expires access tokens after 1 hour. 146 | 147 | ## License 148 | 149 | MIT 150 | -------------------------------------------------------------------------------- /test/refresh.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | 5 | const chai = require('chai'); 6 | const sinon = require('sinon'); 7 | const expect = chai.expect; 8 | const refresh = require('../lib/refresh.js'); 9 | // Constructor, for additional distinct instances 10 | const AuthTokenRefresh = refresh.AuthTokenRefresh; 11 | 12 | chai.use(require('sinon-chai')); 13 | 14 | // Dummy OAuth2 object 15 | function OAuth2( 16 | clientId, 17 | clientSecret, 18 | baseSite, 19 | authorizeUrl, 20 | accessTokenUrl, 21 | ) { 22 | this._accessTokenUrl = accessTokenUrl; 23 | } 24 | 25 | // Add dummy method 26 | OAuth2.prototype.getOAuthAccessToken = new Function(); 27 | 28 | // Makes it easy to invocate in the specs 29 | const newOAuth2 = function (accessTokenUrl) { 30 | return new OAuth2(null, null, null, null, accessTokenUrl); 31 | }; 32 | 33 | describe('Auth token refresh', function () { 34 | beforeEach(function () { 35 | refresh._strategies = {}; 36 | }); 37 | 38 | describe('use', function () { 39 | it('should add a strategy with an explicitly defined name', function () { 40 | const strategy = { 41 | name: 'internal_name', 42 | _oauth2: newOAuth2(), 43 | }; 44 | 45 | refresh.use('explicit_name', strategy); 46 | 47 | expect(refresh._strategies.explicit_name.strategy).to.equal(strategy); 48 | expect(refresh._strategies.strategy).to.be.undefined; 49 | }); 50 | 51 | it('should add a strategy without an explicitly defined name', function () { 52 | const strategy = { 53 | name: 'internal_name', 54 | _oauth2: newOAuth2(), 55 | }; 56 | 57 | refresh.use(strategy); 58 | 59 | expect(refresh._strategies.internal_name.strategy).to.equal(strategy); 60 | }); 61 | 62 | it('should add a strategy with a refreshURL', function () { 63 | const strategy = { 64 | name: 'test_strategy', 65 | _refreshURL: 'refreshURL', 66 | _oauth2: newOAuth2('accessTokenUrl'), 67 | }; 68 | 69 | refresh.use(strategy); 70 | expect(refresh._strategies.test_strategy.strategy).to.equal(strategy); 71 | expect( 72 | refresh._strategies.test_strategy.refreshOAuth2._accessTokenUrl, 73 | ).to.equal('refreshURL'); 74 | }); 75 | 76 | it('should add a strategy without a refreshURL', function () { 77 | const strategy = { 78 | name: 'test_strategy', 79 | _oauth2: newOAuth2('accessTokenUrl'), 80 | }; 81 | 82 | refresh.use(strategy); 83 | expect(refresh._strategies.test_strategy.strategy).to.equal(strategy); 84 | expect( 85 | refresh._strategies.test_strategy.refreshOAuth2._accessTokenUrl, 86 | ).to.equal('accessTokenUrl'); 87 | }); 88 | 89 | it("should create a new oauth2 object with the same prototype as the strategy's _oauth2 object", function () { 90 | const strategyOAuth2 = newOAuth2(); 91 | const strategy = { 92 | name: 'test_strategy', 93 | _oauth2: strategyOAuth2, 94 | }; 95 | 96 | refresh.use(strategy); 97 | expect(refresh._strategies.test_strategy.refreshOAuth2).to.not.equal( 98 | strategyOAuth2, 99 | ); 100 | expect(refresh._strategies.test_strategy.refreshOAuth2).to.be.instanceof( 101 | OAuth2, 102 | ); 103 | }); 104 | 105 | it('should set the oauth2 adapter with the options object', function () { 106 | const strategyOAuth2 = newOAuth2(); 107 | const customOAuth2 = newOAuth2(); 108 | const strategy = { 109 | name: 'test_strategy', 110 | _oauth2: strategyOAuth2, 111 | }; 112 | const setRefreshOAuth2 = sinon.fake.returns(customOAuth2); 113 | 114 | refresh.use(strategy, { 115 | setRefreshOAuth2, 116 | }); 117 | 118 | expect(setRefreshOAuth2).to.have.been.calledWith({ 119 | strategyOAuth2, 120 | refreshOAuth2: sinon.match.instanceOf(OAuth2), 121 | }); 122 | expect(refresh._strategies.test_strategy.refreshOAuth2).to.equal( 123 | customOAuth2, 124 | ); 125 | }); 126 | 127 | it('should throw if the strategy does not supply an oauth2 instance and the setRefreshOAuth2 function is not specified', function () { 128 | const strategy = { 129 | name: 'test_strategy', 130 | }; 131 | 132 | const fn = function () { 133 | refresh.use(strategy); 134 | }; 135 | 136 | expect(fn).to.throw( 137 | Error, 138 | 'The OAuth2 adapter used to refresh the token is not configured correctly. Use the setRefreshOAuth2 option to return a OAuth 2.0 adapter.', 139 | ); 140 | }); 141 | 142 | it('should throw if the strategy does not supply an oauth2 instance and the setRefreshOAuth2 function does not return an oauth2 adapter', function () { 143 | const strategy = { 144 | name: 'test_strategy', 145 | }; 146 | const modifyOAuth2 = sinon.fake.returns(undefined); 147 | 148 | const fn = function () { 149 | refresh.use(strategy, { 150 | modifyOAuth2, 151 | }); 152 | }; 153 | 154 | expect(fn).to.throw( 155 | Error, 156 | 'The OAuth2 adapter used to refresh the token is not configured correctly. Use the setRefreshOAuth2 option to return a OAuth 2.0 adapter.', 157 | ); 158 | }); 159 | 160 | it('should not add a null strategy', function () { 161 | const strategy = null; 162 | const fn = function () { 163 | refresh.use(strategy); 164 | }; 165 | 166 | expect(fn).to.throw(Error, 'Cannot register: strategy is null'); 167 | }); 168 | 169 | it('should not add a strategy with no name', function () { 170 | const strategy = { 171 | name: '', 172 | _oauth2: newOAuth2(), 173 | }; 174 | 175 | const fn = function () { 176 | refresh.use(strategy); 177 | }; 178 | 179 | expect(fn).to.throw( 180 | Error, 181 | 'Cannot register: name must be specified, or strategy must include name', 182 | ); 183 | }); 184 | 185 | it('should use the default getOAuthAccessToken function if not overwritten by strategy', function () { 186 | const strategy = { 187 | name: 'test_strategy', 188 | _oauth2: newOAuth2(), 189 | }; 190 | 191 | refresh.use(strategy); 192 | expect( 193 | refresh._strategies.test_strategy.refreshOAuth2.getOAuthAccessToken, 194 | ).to.equal(OAuth2.prototype.getOAuthAccessToken); 195 | }); 196 | 197 | it('should use the overwritten getOAuthAccessToken function if overwritten by strategy', function () { 198 | const strategy = { 199 | name: 'test_strategy', 200 | _oauth2: newOAuth2(), 201 | }; 202 | 203 | strategy._oauth2.getOAuthAccessToken = new Function(); 204 | 205 | refresh.use(strategy); 206 | expect( 207 | refresh._strategies.test_strategy.refreshOAuth2.getOAuthAccessToken, 208 | ).to.equal(strategy._oauth2.getOAuthAccessToken); 209 | expect( 210 | refresh._strategies.test_strategy.refreshOAuth2.getOAuthAccessToken, 211 | ).not.equal(OAuth2.prototype.getOAuthAccessToken); 212 | }); 213 | }); 214 | 215 | describe('has', function () { 216 | it('should return true if a strategy has been added', function () { 217 | const strategy = { 218 | name: 'test_strategy', 219 | _oauth2: newOAuth2(), 220 | }; 221 | 222 | refresh.use(strategy); 223 | expect(refresh.has('test_strategy')).to.be.true; 224 | }); 225 | 226 | it('should return false if a strategy has not been added', function () { 227 | expect(refresh.has('test_strategy')).to.be.false; 228 | }); 229 | }); 230 | 231 | describe('request new access token', function () { 232 | it('should refresh an access token', function () { 233 | const getOAuthAccessTokenSpy = sinon.spy(); 234 | const done = sinon.spy(); 235 | 236 | refresh._strategies = { 237 | test_strategy: { 238 | refreshOAuth2: { 239 | getOAuthAccessToken: getOAuthAccessTokenSpy, 240 | }, 241 | }, 242 | }; 243 | 244 | refresh.requestNewAccessToken('test_strategy', 'refresh_token', done); 245 | 246 | expect(getOAuthAccessTokenSpy).to.have.been.calledWith( 247 | 'refresh_token', 248 | { grant_type: 'refresh_token' }, 249 | done, 250 | ); 251 | }); 252 | 253 | it('should refresh a new access token with extra params', function () { 254 | const getOAuthAccessTokenSpy = sinon.spy(); 255 | const done = sinon.spy(); 256 | 257 | refresh._strategies = { 258 | test_strategy: { 259 | refreshOAuth2: { 260 | getOAuthAccessToken: getOAuthAccessTokenSpy, 261 | }, 262 | }, 263 | }; 264 | 265 | refresh.requestNewAccessToken( 266 | 'test_strategy', 267 | 'refresh_token', 268 | { some: 'extra_param' }, 269 | done, 270 | ); 271 | 272 | expect(getOAuthAccessTokenSpy).to.have.been.calledWith( 273 | 'refresh_token', 274 | { grant_type: 'refresh_token', some: 'extra_param' }, 275 | done, 276 | ); 277 | }); 278 | 279 | it('should not refresh if the strategy was not previously registered', function () { 280 | const done = sinon.spy(); 281 | const expected = sinon.match 282 | .instanceOf(Error) 283 | .and( 284 | sinon.match.has( 285 | 'message', 286 | 'Strategy was not registered to refresh a token', 287 | ), 288 | ); 289 | 290 | refresh.requestNewAccessToken('test_strategy', 'refresh_token', done); 291 | 292 | expect(done).to.have.been.calledWith(expected); 293 | }); 294 | }); 295 | 296 | describe('multiple instances', function () { 297 | it('should support creating a second, independent instance', function () { 298 | const refresh2 = new AuthTokenRefresh(); 299 | expect(refresh).to.be.instanceof(AuthTokenRefresh); 300 | expect(refresh2).to.be.instanceof(AuthTokenRefresh); 301 | expect(refresh).to.not.equal(refresh2); 302 | }); 303 | }); 304 | }); 305 | --------------------------------------------------------------------------------