├── .npmignore ├── .babelrc ├── test ├── mocha.opts ├── bootstrap.js ├── fixtures │ └── profile.js └── unit │ └── index.test.js ├── .editorconfig ├── .travis.yml ├── .gitignore ├── LICENSE ├── src ├── profile │ └── openid.js └── index.js ├── lib ├── profile │ └── openid.js └── index.js ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | src 3 | test 4 | .idea 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": ["add-module-exports"] 4 | } 5 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | ./test/unit/**/*.test.js 2 | --require ./test/bootstrap.js 3 | --reporter nyan 4 | --recursive 5 | -------------------------------------------------------------------------------- /test/bootstrap.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import passport from 'chai-passport-strategy'; 3 | 4 | chai.use(passport); 5 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /test/fixtures/profile.js: -------------------------------------------------------------------------------- 1 | module.exports = JSON.stringify({ 2 | "sub": "106322344677401150228", 3 | "name": "Luke Skywalker", 4 | "given_name": "Luke", 5 | "family_name": "Skywalker", 6 | "profile": "https://plus.google.com/106322344677401150228", 7 | "picture": "https://lh5.googleusercontent.com/-maynzk6pE7A/AAAAAAAAAAI/AAAAAAAAAAc/xBkkM5n-gds/photo.jpg", 8 | "email": "luke.skywalker@rebellion.com", 9 | "email_verified": true, 10 | "locale": "en", 11 | "hd": "rebellion.com" 12 | }); 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | notifications: 7 | email: true 8 | node_js: 9 | - stable 10 | - 10 11 | - 8 12 | - 6 13 | before_install: 14 | - npm install -g npm@latest 15 | before_script: 16 | - npm prune 17 | after_success: 18 | - 'curl -Lo travis_after_all.py https://git.io/travis_after_all' 19 | - python travis_after_all.py 20 | - 'export $(cat .to_export_back) &> /dev/null' 21 | - npm run coveralls 22 | - npm run semantic-release 23 | branches: 24 | except: 25 | - "/^v\\d+\\.\\d+\\.\\d+$/" 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | 30 | # IDE 31 | .idea 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Eugene Obrezkov 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 | -------------------------------------------------------------------------------- /src/profile/openid.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Parse profile. 3 | * 4 | * Parses user profiles as fetched from Google's OpenID Connect-compatible user 5 | * info endpoint. 6 | * 7 | * The amount of detail in the profile varies based on the scopes granted by the 8 | * user. The following scope values add additional data: 9 | * 10 | * `profile` - basic profile information 11 | * `email` - email address 12 | * 13 | * References: 14 | * - https://developers.google.com/identity/protocols/OpenIDConnect 15 | * 16 | * @param {object|string} json 17 | * @return {object} 18 | * @access public 19 | */ 20 | const parse = function(json) { 21 | if ('string' == typeof json) { 22 | json = JSON.parse(json); 23 | } 24 | 25 | var profile = {}; 26 | profile.provider = 'google'; 27 | profile.id = json.sub || json.id; 28 | profile.displayName = json.name; 29 | if (json.family_name || json.given_name) { 30 | profile.name = { familyName: json.family_name, 31 | givenName: json.given_name }; 32 | } 33 | if (json.email) { 34 | profile.emails = [ { value: json.email, verified: json.email_verified || json.verified_email } ]; 35 | } 36 | if (json.picture) { 37 | profile.photos = [{ value: json.picture }]; 38 | } 39 | 40 | return profile; 41 | }; 42 | 43 | export default parse; 44 | -------------------------------------------------------------------------------- /lib/profile/openid.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | /** 7 | * Parse profile. 8 | * 9 | * Parses user profiles as fetched from Google's OpenID Connect-compatible user 10 | * info endpoint. 11 | * 12 | * The amount of detail in the profile varies based on the scopes granted by the 13 | * user. The following scope values add additional data: 14 | * 15 | * `profile` - basic profile information 16 | * `email` - email address 17 | * 18 | * References: 19 | * - https://developers.google.com/identity/protocols/OpenIDConnect 20 | * 21 | * @param {object|string} json 22 | * @return {object} 23 | * @access public 24 | */ 25 | var parse = function parse(json) { 26 | if ('string' == typeof json) { 27 | json = JSON.parse(json); 28 | } 29 | 30 | var profile = {}; 31 | profile.provider = 'google'; 32 | profile.id = json.sub || json.id; 33 | profile.displayName = json.name; 34 | if (json.family_name || json.given_name) { 35 | profile.name = { familyName: json.family_name, 36 | givenName: json.given_name }; 37 | } 38 | if (json.email) { 39 | profile.emails = [{ value: json.email, verified: json.email_verified || json.verified_email }]; 40 | } 41 | if (json.picture) { 42 | profile.photos = [{ value: json.picture }]; 43 | } 44 | 45 | return profile; 46 | }; 47 | 48 | exports.default = parse; 49 | module.exports = exports['default']; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "passport-google-plus-token", 3 | "version": "0.0.0-semantic-release", 4 | "description": "Passport strategy for authenticating with Google Plus via OAuth2 access tokens (deprecating Google Plus account)", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "compile": "babel src --out-dir lib", 8 | "coveralls": "cat coverage/lcov.info | coveralls", 9 | "prepublish": "npm run compile", 10 | "semantic-release": "semantic-release pre && npm publish && semantic-release post", 11 | "test": "babel-node ./node_modules/.bin/isparta cover _mocha" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/balintsera/passport-google-plus-token.git" 16 | }, 17 | "keywords": [ 18 | "passport", 19 | "google", 20 | "token", 21 | "auth", 22 | "authentication" 23 | ], 24 | "author": { 25 | "name": "Eugene Obrezkov", 26 | "email": "ghaiklor@gmail.com", 27 | "url": "https://github.com/ghaiklor" 28 | }, 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/balintsera/passport-google-plus-token/issues" 32 | }, 33 | "homepage": "https://github.com/balintsera/passport-google-plus-token", 34 | "engines" : { "node" : ">=6.0" }, 35 | "dependencies": { 36 | "passport-oauth": "1.0.0" 37 | }, 38 | "devDependencies": { 39 | "babel-cli": "6.24.1", 40 | "babel-plugin-add-module-exports": "1.0.1", 41 | "babel-preset-es2015": "6.24.0", 42 | "chai": "3.5.0", 43 | "chai-passport-strategy": "1.0.1", 44 | "coveralls": "3.0.9", 45 | "cz-conventional-changelog": "3.1.0", 46 | "isparta": "4.1.1", 47 | "mocha": "7.0.1", 48 | "semantic-release": "17.0.3", 49 | "sinon": "7.4.0" 50 | }, 51 | "config": { 52 | "commitizen": { 53 | "path": "./node_modules/cz-conventional-changelog" 54 | } 55 | }, 56 | "publishConfig": { 57 | "tag": "latest" 58 | }, 59 | "release": { 60 | "branch": "master" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { OAuth2Strategy, InternalOAuthError } from 'passport-oauth'; 2 | import parse from './profile/openid' 3 | 4 | /** 5 | * `Strategy` constructor. 6 | * The Google Plus authentication strategy authenticates requests by delegating to Google Plus using OAuth2 access tokens. 7 | * Applications must supply a `verify` callback which accepts a accessToken, refreshToken, profile and callback. 8 | * Callback supplying a `user`, which should be set to `false` if the credentials are not valid. 9 | * If an exception occurs, `error` should be set. 10 | * 11 | * Options: 12 | * - clientID Identifies client to Google App 13 | * - clientSecret Secret used to establish ownership of the consumer key 14 | * - passReqToCallback If need, pass req to verify callback 15 | * 16 | * @param {Object} _options 17 | * @param {Function} _verify 18 | * @constructor 19 | * @example 20 | * passport.use(new GoogleTokenStrategy({ 21 | * clientID: '123456789', 22 | * clientSecret: 'shhh-its-a-secret' 23 | * }), function(req, accessToken, refreshToken, profile, next) { 24 | * User.findOrCreate({googleId: profile.id}, function(error, user) { 25 | * next(error, user); 26 | * }); 27 | * }); 28 | */ 29 | export default class GoogleTokenStrategy extends OAuth2Strategy { 30 | constructor(_options, _verify) { 31 | let options = _options || {}; 32 | let verify = _verify; 33 | 34 | options.authorizationURL = options.authorizationURL || 'https://accounts.google.com/o/oauth2/v2/auth'; 35 | options.tokenURL = options.tokenURL || 'https://www.googleapis.com/oauth2/v4/token'; 36 | 37 | super(options, verify); 38 | 39 | this.name = 'google-plus-token'; 40 | this._accessTokenField = options.accessTokenField || 'access_token'; 41 | this._refreshTokenField = options.refreshTokenField || 'refresh_token'; 42 | this._profileURL = options.profileURL || 'https://www.googleapis.com/oauth2/v3/userinfo'; 43 | this._passReqToCallback = options.passReqToCallback; 44 | 45 | this._oauth2.useAuthorizationHeaderforGET(true); 46 | } 47 | 48 | /** 49 | * Authenticate method 50 | * @param {Object} req 51 | * @param {Object} options 52 | * @returns {*} 53 | */ 54 | authenticate(req, options) { 55 | let accessToken = (req.body && req.body[this._accessTokenField]) || (req.query && req.query[this._accessTokenField]); 56 | let refreshToken = (req.body && req.body[this._refreshTokenField]) || (req.query && req.query[this._refreshTokenField]); 57 | 58 | if (!accessToken) return this.fail({message: `You should provide ${this._accessTokenField}`}); 59 | 60 | this._loadUserProfile(accessToken, (error, profile) => { 61 | if (error) return this.error(error); 62 | 63 | const verified = (error, user, info) => { 64 | if (error) return this.error(error); 65 | if (!user) return this.fail(info); 66 | 67 | return this.success(user, info); 68 | }; 69 | 70 | 71 | if (this._passReqToCallback) { 72 | this._verify(req, accessToken, refreshToken, profile, verified); 73 | } else { 74 | this._verify(accessToken, refreshToken, profile, verified); 75 | } 76 | }); 77 | } 78 | 79 | /** 80 | * Parse user profile 81 | * @param {String} accessToken Google OAuth2 access token 82 | * @param {Function} done 83 | */ 84 | userProfile(accessToken, done) { 85 | this._oauth2.get(this._profileURL, accessToken, (error, body, res) => { 86 | if (error) { 87 | try { 88 | let errorJSON = JSON.parse(error.data); 89 | return done(new InternalOAuthError(errorJSON.error.message, errorJSON.error.code)); 90 | } catch (_) { 91 | return done(new InternalOAuthError('Failed to fetch user profile', error)); 92 | } 93 | } 94 | let json; 95 | try { 96 | json = JSON.parse(body); 97 | } catch (ex) { 98 | return done(new Error('Failed to parse user profile')); 99 | } 100 | 101 | try { 102 | const profile = parse(json) 103 | profile._raw = body 104 | profile._json = json 105 | 106 | return done(null, profile); 107 | } catch (e) { 108 | return done(e); 109 | } 110 | }); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # passport-google-plus-token 2 | 3 | ![Build Status](https://img.shields.io/travis/ghaiklor/passport-google-plus-token.svg) 4 | ![Coverage](https://img.shields.io/coveralls/ghaiklor/passport-google-plus-token.svg) 5 | 6 | ![Downloads](https://img.shields.io/npm/dm/passport-google-plus-token.svg) 7 | ![Downloads](https://img.shields.io/npm/dt/passport-google-plus-token.svg) 8 | ![npm version](https://img.shields.io/npm/v/passport-google-plus-token.svg) 9 | ![License](https://img.shields.io/npm/l/passport-google-plus-token.svg) 10 | 11 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 12 | [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) 13 | ![dependencies](https://img.shields.io/david/ghaiklor/passport-google-plus-token.svg) 14 | ![dev dependencies](https://img.shields.io/david/dev/ghaiklor/passport-google-plus-token.svg) 15 | 16 | [Passport](http://passportjs.org/) strategy for authenticating with [Google Plus](https://plus.google.com/) access tokens using the OAuth 2.0 API. 17 | 18 | This module lets you authenticate using Google Plus in your Node.js applications. 19 | By plugging into Passport, Google Plus authentication can be easily and unobtrusively integrated into any application or framework that supports [Connect](http://www.senchalabs.org/connect/)-style middleware, including [Express](http://expressjs.com/). 20 | 21 | ## Installation 22 | 23 | ```shell 24 | npm install passport-google-plus-token 25 | ``` 26 | 27 | ## Usage 28 | 29 | ### Configure Strategy 30 | 31 | The Google Plus authentication strategy authenticates users using a Google Plus account and OAuth 2.0 tokens. 32 | The strategy requires a `verify` callback, which accepts these credentials and calls `next` providing a user, as well as `options` specifying a app ID and app secret. 33 | 34 | ```javascript 35 | var GooglePlusTokenStrategy = require('passport-google-plus-token'); 36 | 37 | passport.use(new GooglePlusTokenStrategy({ 38 | clientID: GOOGLE_CLIENT_ID, 39 | clientSecret: GOOGLE_CLIENT_SECRET, 40 | passReqToCallback: true 41 | }, function(req, accessToken, refreshToken, profile, next) { 42 | User.findOrCreate({'google.id': profile.id}, function(error, user) { 43 | return next(error, user); 44 | }); 45 | })); 46 | ``` 47 | 48 | ### Authenticate Requests 49 | 50 | Use `passport.authenticate()`, specifying the `google-plus-token` strategy, to authenticate requests. 51 | 52 | For example, as route middleware in an [Express](http://expressjs.com/) application: 53 | 54 | ```javascript 55 | app.get('/auth/google', passport.authenticate('google-plus-token')); 56 | ``` 57 | 58 | Or if you are using Sails framework: 59 | 60 | ```javascript 61 | // AuthController.js 62 | module.exports = { 63 | google: function(req, res) { 64 | passport.authenticate('google-plus-token', function(error, user, info) { 65 | if (error) return res.serverError(error); 66 | if (info) return res.unauthorized(info); 67 | return res.ok(user); 68 | })(req, res); 69 | } 70 | }; 71 | ``` 72 | 73 | The request to this route should include GET or POST data with the keys `access_token` and optionally, `refresh_token` set to the credentials you receive from Google Plus. 74 | 75 | ``` 76 | GET /auth/google?access_token= 77 | ``` 78 | 79 | ## Issues 80 | 81 | If you receive a `401 Unauthorized` error, it is most likely because you have wrong access token or not yet specified any application permissions. 82 | Once you refresh access token with new permissions, try to send this access token again. 83 | 84 | ## License 85 | 86 | The MIT License (MIT) 87 | 88 | Copyright (c) 2015 Eugene Obrezkov 89 | 90 | Permission is hereby granted, free of charge, to any person obtaining a copy 91 | of this software and associated documentation files (the "Software"), to deal 92 | in the Software without restriction, including without limitation the rights 93 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 94 | copies of the Software, and to permit persons to whom the Software is 95 | furnished to do so, subject to the following conditions: 96 | 97 | The above copyright notice and this permission notice shall be included in all 98 | copies or substantial portions of the Software. 99 | 100 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 101 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 102 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 103 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 104 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 105 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 106 | SOFTWARE. 107 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _passportOauth = require('passport-oauth'); 10 | 11 | var _openid = require('./profile/openid'); 12 | 13 | var _openid2 = _interopRequireDefault(_openid); 14 | 15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 16 | 17 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 18 | 19 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 20 | 21 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 22 | 23 | /** 24 | * `Strategy` constructor. 25 | * The Google Plus authentication strategy authenticates requests by delegating to Google Plus using OAuth2 access tokens. 26 | * Applications must supply a `verify` callback which accepts a accessToken, refreshToken, profile and callback. 27 | * Callback supplying a `user`, which should be set to `false` if the credentials are not valid. 28 | * If an exception occurs, `error` should be set. 29 | * 30 | * Options: 31 | * - clientID Identifies client to Google App 32 | * - clientSecret Secret used to establish ownership of the consumer key 33 | * - passReqToCallback If need, pass req to verify callback 34 | * 35 | * @param {Object} _options 36 | * @param {Function} _verify 37 | * @constructor 38 | * @example 39 | * passport.use(new GoogleTokenStrategy({ 40 | * clientID: '123456789', 41 | * clientSecret: 'shhh-its-a-secret' 42 | * }), function(req, accessToken, refreshToken, profile, next) { 43 | * User.findOrCreate({googleId: profile.id}, function(error, user) { 44 | * next(error, user); 45 | * }); 46 | * }); 47 | */ 48 | var GoogleTokenStrategy = function (_OAuth2Strategy) { 49 | _inherits(GoogleTokenStrategy, _OAuth2Strategy); 50 | 51 | function GoogleTokenStrategy(_options, _verify) { 52 | _classCallCheck(this, GoogleTokenStrategy); 53 | 54 | var options = _options || {}; 55 | var verify = _verify; 56 | 57 | options.authorizationURL = options.authorizationURL || 'https://accounts.google.com/o/oauth2/v2/auth'; 58 | options.tokenURL = options.tokenURL || 'https://www.googleapis.com/oauth2/v4/token'; 59 | 60 | var _this = _possibleConstructorReturn(this, (GoogleTokenStrategy.__proto__ || Object.getPrototypeOf(GoogleTokenStrategy)).call(this, options, verify)); 61 | 62 | _this.name = 'google-plus-token'; 63 | _this._accessTokenField = options.accessTokenField || 'access_token'; 64 | _this._refreshTokenField = options.refreshTokenField || 'refresh_token'; 65 | _this._profileURL = options.profileURL || 'https://www.googleapis.com/oauth2/v3/userinfo'; 66 | _this._passReqToCallback = options.passReqToCallback; 67 | 68 | _this._oauth2.useAuthorizationHeaderforGET(true); 69 | return _this; 70 | } 71 | 72 | /** 73 | * Authenticate method 74 | * @param {Object} req 75 | * @param {Object} options 76 | * @returns {*} 77 | */ 78 | 79 | 80 | _createClass(GoogleTokenStrategy, [{ 81 | key: 'authenticate', 82 | value: function authenticate(req, options) { 83 | var _this2 = this; 84 | 85 | var accessToken = req.body && req.body[this._accessTokenField] || req.query && req.query[this._accessTokenField]; 86 | var refreshToken = req.body && req.body[this._refreshTokenField] || req.query && req.query[this._refreshTokenField]; 87 | 88 | if (!accessToken) return this.fail({ message: 'You should provide ' + this._accessTokenField }); 89 | 90 | this._loadUserProfile(accessToken, function (error, profile) { 91 | if (error) return _this2.error(error); 92 | 93 | var verified = function verified(error, user, info) { 94 | if (error) return _this2.error(error); 95 | if (!user) return _this2.fail(info); 96 | 97 | return _this2.success(user, info); 98 | }; 99 | 100 | if (_this2._passReqToCallback) { 101 | _this2._verify(req, accessToken, refreshToken, profile, verified); 102 | } else { 103 | _this2._verify(accessToken, refreshToken, profile, verified); 104 | } 105 | }); 106 | } 107 | 108 | /** 109 | * Parse user profile 110 | * @param {String} accessToken Google OAuth2 access token 111 | * @param {Function} done 112 | */ 113 | 114 | }, { 115 | key: 'userProfile', 116 | value: function userProfile(accessToken, done) { 117 | this._oauth2.get(this._profileURL, accessToken, function (error, body, res) { 118 | if (error) { 119 | try { 120 | var errorJSON = JSON.parse(error.data); 121 | return done(new _passportOauth.InternalOAuthError(errorJSON.error.message, errorJSON.error.code)); 122 | } catch (_) { 123 | return done(new _passportOauth.InternalOAuthError('Failed to fetch user profile', error)); 124 | } 125 | } 126 | var json = void 0; 127 | try { 128 | json = JSON.parse(body); 129 | } catch (ex) { 130 | return done(new Error('Failed to parse user profile')); 131 | } 132 | 133 | try { 134 | var profile = (0, _openid2.default)(json); 135 | profile._raw = body; 136 | profile._json = json; 137 | 138 | return done(null, profile); 139 | } catch (e) { 140 | return done(e); 141 | } 142 | }); 143 | } 144 | }]); 145 | 146 | return GoogleTokenStrategy; 147 | }(_passportOauth.OAuth2Strategy); 148 | 149 | exports.default = GoogleTokenStrategy; 150 | module.exports = exports['default']; -------------------------------------------------------------------------------- /test/unit/index.test.js: -------------------------------------------------------------------------------- 1 | import chai, { assert } from 'chai'; 2 | import sinon from 'sinon'; 3 | import GooglePlusTokenStrategy from '../../src/index'; 4 | import fakeProfile from '../fixtures/profile'; 5 | 6 | const STRATEGY_CONFIG = { 7 | clientID: '123', 8 | clientSecret: '123' 9 | }; 10 | 11 | const BLANK_FUNCTION = () => { 12 | }; 13 | 14 | describe('GooglePlusTokenStrategy:init', () => { 15 | it('Should properly export Strategy constructor', () => { 16 | assert.isFunction(GooglePlusTokenStrategy); 17 | }); 18 | 19 | it('Should properly initialize', () => { 20 | let strategy = new GooglePlusTokenStrategy(STRATEGY_CONFIG, BLANK_FUNCTION); 21 | 22 | assert.equal(strategy.name, 'google-plus-token'); 23 | assert(strategy._oauth2._useAuthorizationHeaderForGET); 24 | }); 25 | 26 | it('Should properly throw error on empty options', () => { 27 | assert.throws(() => new GooglePlusTokenStrategy(), Error); 28 | }); 29 | }); 30 | 31 | describe('GooglePlusTokenStrategy:authenticate', () => { 32 | describe('Authenticate without passReqToCallback', () => { 33 | let strategy; 34 | 35 | before(() => { 36 | strategy = new GooglePlusTokenStrategy(STRATEGY_CONFIG, (accessToken, refreshToken, profile, next) => { 37 | assert.equal(accessToken, 'access_token'); 38 | assert.equal(refreshToken, 'refresh_token'); 39 | assert.typeOf(profile, 'object'); 40 | assert.typeOf(next, 'function'); 41 | return next(null, profile, {info: 'foo'}); 42 | }); 43 | 44 | sinon.stub(strategy._oauth2, 'get', (url, accessToken, next) => next(null, fakeProfile, null)); 45 | }); 46 | 47 | it('Should properly parse token from body', done => { 48 | chai.passport.use(strategy) 49 | .success((user, info) => { 50 | assert.typeOf(user, 'object'); 51 | assert.typeOf(info, 'object'); 52 | assert.deepEqual(info, {info: 'foo'}); 53 | done(); 54 | }) 55 | .req(req => { 56 | req.body = { 57 | access_token: 'access_token', 58 | refresh_token: 'refresh_token' 59 | } 60 | }) 61 | .authenticate(); 62 | }); 63 | 64 | it('Should properly parse token from query', done => { 65 | chai.passport.use(strategy) 66 | .success((user, info) => { 67 | assert.typeOf(user, 'object'); 68 | assert.typeOf(info, 'object'); 69 | assert.deepEqual(info, {info: 'foo'}); 70 | done(); 71 | }) 72 | .req(req => { 73 | req.query = { 74 | access_token: 'access_token', 75 | refresh_token: 'refresh_token' 76 | } 77 | }) 78 | .authenticate(); 79 | }); 80 | 81 | it('Should properly call fail if access_token is not provided', done => { 82 | chai.passport.use(strategy) 83 | .fail(error => { 84 | assert.typeOf(error, 'object'); 85 | assert.typeOf(error.message, 'string'); 86 | assert.equal(error.message, 'You should provide access_token'); 87 | done(); 88 | }) 89 | .authenticate(); 90 | }); 91 | }); 92 | 93 | describe('Authenticate with passReqToCallback', () => { 94 | let strategy; 95 | 96 | before(() => { 97 | strategy = new GooglePlusTokenStrategy(Object.assign(STRATEGY_CONFIG, {passReqToCallback: true}), (req, accessToken, refreshToken, profile, next) => { 98 | assert.typeOf(req, 'object'); 99 | assert.equal(accessToken, 'access_token'); 100 | assert.equal(refreshToken, 'refresh_token'); 101 | assert.typeOf(profile, 'object'); 102 | assert.typeOf(next, 'function'); 103 | return next(null, profile, {info: 'foo'}); 104 | }); 105 | 106 | sinon.stub(strategy._oauth2, 'get', (url, accessToken, next) => next(null, fakeProfile, null)); 107 | }); 108 | 109 | it('Should properly call _verify with req', done => { 110 | chai.passport.use(strategy) 111 | .success((user, info) => { 112 | assert.typeOf(user, 'object'); 113 | assert.typeOf(info, 'object'); 114 | assert.deepEqual(info, {info: 'foo'}); 115 | done(); 116 | }) 117 | .req(req => { 118 | req.body = { 119 | access_token: 'access_token', 120 | refresh_token: 'refresh_token' 121 | } 122 | }) 123 | .authenticate({}); 124 | }); 125 | }); 126 | }); 127 | 128 | describe('GooglePlusTokenStrategy:userProfile', () => { 129 | it('Should properly fetch profile', done => { 130 | let strategy = new GooglePlusTokenStrategy(STRATEGY_CONFIG, BLANK_FUNCTION); 131 | 132 | sinon.stub(strategy._oauth2, 'get', (url, accessToken, next) => next(null, fakeProfile, null)); 133 | 134 | strategy.userProfile('accessToken', (error, profile) => { 135 | if (error) return done(error); 136 | 137 | assert.equal(profile.provider, 'google'); 138 | assert.equal(profile.id, '106322344677401150228'); 139 | assert.equal(profile.displayName, 'Luke Skywalker'); 140 | assert.equal(profile.name.familyName, 'Skywalker'); 141 | assert.equal(profile.name.givenName, 'Luke'); 142 | assert.deepEqual(profile.emails, [{ value: 'luke.skywalker@rebellion.com', verified: true }]); 143 | assert.equal(profile.photos[0].value, 'https://lh5.googleusercontent.com/-maynzk6pE7A/AAAAAAAAAAI/AAAAAAAAAAc/xBkkM5n-gds/photo.jpg'); 144 | assert.equal(typeof profile._raw, 'string'); 145 | assert.equal(typeof profile._json, 'object'); 146 | 147 | done(); 148 | }); 149 | }); 150 | 151 | it('Should properly handle exception on fetching profile', done => { 152 | let strategy = new GooglePlusTokenStrategy(STRATEGY_CONFIG, BLANK_FUNCTION); 153 | 154 | sinon.stub(strategy._oauth2, 'get', (url, accessToken, done) => done(null, 'not a JSON', null)); 155 | 156 | strategy.userProfile('accessToken', (error, profile) => { 157 | assert(error.message === 'Failed to parse user profile'); 158 | assert.equal(typeof profile, 'undefined'); 159 | done(); 160 | }); 161 | }); 162 | 163 | it('Should properly handle wrong JSON on fetching profile', done => { 164 | let strategy = new GooglePlusTokenStrategy(STRATEGY_CONFIG, BLANK_FUNCTION); 165 | 166 | sinon.stub(strategy._oauth2, 'get', (url, accessToken, done) => done(new Error('ERROR'), 'not a JSON', null)); 167 | 168 | strategy.userProfile('accessToken', (error, profile) => { 169 | assert.instanceOf(error, Error); 170 | assert.equal(typeof profile, 'undefined'); 171 | done(); 172 | }); 173 | }); 174 | 175 | it('Should properly handle wrong JSON on fetching profile', done => { 176 | let strategy = new GooglePlusTokenStrategy(STRATEGY_CONFIG, BLANK_FUNCTION); 177 | 178 | sinon.stub(strategy._oauth2, 'get', (url, accessToken, done) => done({ 179 | data: JSON.stringify({ 180 | error: { 181 | message: 'MESSAGE', 182 | code: 'CODE' 183 | } 184 | }) 185 | }, 'not a JSON', null)); 186 | 187 | strategy.userProfile('accessToken', (error, profile) => { 188 | assert.equal(error.message, 'MESSAGE'); 189 | assert.equal(error.oauthError, 'CODE'); 190 | assert.equal(typeof profile, 'undefined'); 191 | done(); 192 | }); 193 | }); 194 | 195 | }); 196 | --------------------------------------------------------------------------------