├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── example ├── .gitignore ├── app.js ├── package-lock.json ├── package.json └── views │ ├── account.ejs │ ├── index.ejs │ ├── layout.ejs │ └── login.ejs ├── lib └── oauth2.js ├── package-lock.json ├── package.json └── test ├── index-test.js ├── oauth2-test.js └── profile.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OS X 2 | .DS_Store 3 | 4 | # Node.js 5 | node_modules/* 6 | npm-debug.log 7 | 8 | .idea 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | README.md 2 | Makefile 3 | doc/ 4 | examples/ 5 | test/ 6 | 7 | # Mac OS X 8 | .DS_Store 9 | 10 | # Node.js 11 | .npmignore 12 | node_modules/ 13 | npm-debug.log 14 | 15 | # Git 16 | .git* 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: "node_js" 2 | node_js: 3 | - 0.11 4 | - 0.10 5 | - 0.8 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2012-2013 Jared Hanson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SOURCES = lib/**/*.js 2 | 3 | # ============================================================================== 4 | # Node Tests 5 | # ============================================================================== 6 | 7 | VOWS = ./node_modules/.bin/vows 8 | TESTS ?= test/*-test.js 9 | 10 | test: 11 | @NODE_ENV=test NODE_PATH=lib $(VOWS) $(TESTS) 12 | 13 | # ============================================================================== 14 | # Static Analysis 15 | # ============================================================================== 16 | 17 | JSHINT = jshint 18 | 19 | hint: lint 20 | lint: 21 | $(JSHINT) $(SOURCES) 22 | 23 | 24 | .PHONY: test hint lint 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Passport strategy for Google OAuth 2.0 2 | 3 | [Passport](http://passportjs.org/) strategies for authenticating with [Google](http://www.google.com/) 4 | using ONLY OAuth 2.0. 5 | 6 | This module lets you authenticate using Google in your Node.js applications. 7 | By plugging into Passport, Google authentication can be easily and 8 | unobtrusively integrated into any application or framework that supports 9 | [Connect](http://www.senchalabs.org/connect/)-style middleware, including 10 | [Express](http://expressjs.com/). 11 | 12 | ## Install 13 | 14 | $ npm install passport-google-oauth2 15 | 16 | ## Usage of OAuth 2.0 17 | 18 | #### Configure Strategy 19 | 20 | The Google OAuth 2.0 authentication strategy authenticates users using a Google 21 | account and OAuth 2.0 tokens. The strategy requires a `verify` callback, which 22 | accepts these credentials and calls `done` providing a user, as well as 23 | `options` specifying a client ID, client secret, and callback URL. 24 | 25 | ```Javascript 26 | var GoogleStrategy = require( 'passport-google-oauth2' ).Strategy; 27 | 28 | passport.use(new GoogleStrategy({ 29 | clientID: GOOGLE_CLIENT_ID, 30 | clientSecret: GOOGLE_CLIENT_SECRET, 31 | callbackURL: "http://yourdomain:3000/auth/google/callback", 32 | passReqToCallback: true 33 | }, 34 | function(request, accessToken, refreshToken, profile, done) { 35 | User.findOrCreate({ googleId: profile.id }, function (err, user) { 36 | return done(err, user); 37 | }); 38 | } 39 | )); 40 | ``` 41 | 42 | #### Note about Local environment 43 | 44 | Avoid usage of Private IP, otherwise you will get the device_id device_name issue for Private IP during authentication. 45 | 46 | A workaround consists to set up through the google cloud console a fully qualified domain name such as http://mydomain:3000/ for the callback, 47 | then edit your /etc/hosts on your computer and/or vm to point on your private IP. 48 | 49 | Also, both sign-in button + callbackURL need to have the same url, otherwise two cookies will be created and you will lose your current session. 50 | 51 | #### Authenticate Requests 52 | 53 | Use `passport.authenticate()`, specifying the `'google'` strategy, to 54 | authenticate requests. 55 | 56 | For example, as route middleware in an [Express](http://expressjs.com/) 57 | application: 58 | 59 | ```Javascript 60 | app.get('/auth/google', 61 | passport.authenticate('google', { scope: 62 | [ 'email', 'profile' ] } 63 | )); 64 | 65 | app.get( '/auth/google/callback', 66 | passport.authenticate( 'google', { 67 | successRedirect: '/auth/google/success', 68 | failureRedirect: '/auth/google/failure' 69 | })); 70 | ``` 71 | 72 | #### What you will get in profile response ? 73 | 74 | ``` 75 | provider always set to `google` 76 | id 77 | name 78 | displayName 79 | birthday 80 | relationship 81 | isPerson 82 | isPlusUser 83 | placesLived 84 | language 85 | emails 86 | gender 87 | picture 88 | coverPhoto 89 | ``` 90 | 91 | ## Examples 92 | 93 | For a complete, working example, refer to the [OAuth 2.0 example](example). 94 | 95 | ## Credits 96 | 97 | - [Jared Hanson](http://github.com/jaredhanson) 98 | 99 | ## License 100 | 101 | [The MIT License](http://opensource.org/licenses/MIT) 102 | 103 | Copyright (c) 2012-2013 Jared Hanson <[http://jaredhanson.net/](http://jaredhanson.net/)> 104 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | var express = require( 'express' ) 2 | , app = express() 3 | , server = require( 'http' ).createServer( app ) 4 | , passport = require( 'passport' ) 5 | , util = require( 'util' ) 6 | , bodyParser = require( 'body-parser' ) 7 | , cookieParser = require( 'cookie-parser' ) 8 | , session = require( 'express-session' ) 9 | , RedisStore = require( 'connect-redis' )( session ) 10 | , GoogleStrategy = require( '../' ).Strategy; 11 | 12 | // API Access link for creating client ID and secret: 13 | // https://code.google.com/apis/console/ 14 | var GOOGLE_CLIENT_ID = "--insert-google-client-id-here--" 15 | , GOOGLE_CLIENT_SECRET = "--insert-google-client-secret-here--"; 16 | 17 | // Passport session setup. 18 | // To support persistent login sessions, Passport needs to be able to 19 | // serialize users into and deserialize users out of the session. Typically, 20 | // this will be as simple as storing the user ID when serializing, and finding 21 | // the user by ID when deserializing. However, since this example does not 22 | // have a database of user records, the complete Google profile is 23 | // serialized and deserialized. 24 | passport.serializeUser(function(user, done) { 25 | done(null, user); 26 | }); 27 | 28 | passport.deserializeUser(function(obj, done) { 29 | done(null, obj); 30 | }); 31 | 32 | 33 | // Use the GoogleStrategy within Passport. 34 | // Strategies in Passport require a `verify` function, which accept 35 | // credentials (in this case, an accessToken, refreshToken, and Google 36 | // profile), and invoke a callback with a user object. 37 | passport.use(new GoogleStrategy({ 38 | clientID: GOOGLE_CLIENT_ID, 39 | clientSecret: GOOGLE_CLIENT_SECRET, 40 | //NOTE : 41 | //Carefull ! and avoid usage of Private IP, otherwise you will get the device_id device_name issue for Private IP during authentication 42 | //The workaround is to set up thru the google cloud console a fully qualified domain name such as http://mydomain:3000/ 43 | //then edit your /etc/hosts local file to point on your private IP. 44 | //Also both sign-in button + callbackURL has to be share the same url, otherwise two cookies will be created and lead to lost your session 45 | //if you use it. 46 | callbackURL: "http://example.com:3000/auth/google/callback", 47 | passReqToCallback : true 48 | }, 49 | function(request, accessToken, refreshToken, profile, done) { 50 | // asynchronous verification, for effect... 51 | process.nextTick(function () { 52 | 53 | // To keep the example simple, the user's Google profile is returned to 54 | // represent the logged-in user. In a typical application, you would want 55 | // to associate the Google account with a user record in your database, 56 | // and return that user instead. 57 | return done(null, profile); 58 | }); 59 | } 60 | )); 61 | 62 | // configure Express 63 | app.set('views', __dirname + '/views'); 64 | app.set('view engine', 'ejs'); 65 | app.use( express.static(__dirname + '/public')); 66 | app.use( cookieParser()); 67 | app.use( bodyParser.json()); 68 | app.use( bodyParser.urlencoded({ 69 | extended: true 70 | })); 71 | app.use( session({ 72 | secret: 'cookie_secret', 73 | name: 'kaas', 74 | store: new RedisStore({ 75 | host: '127.0.0.1', 76 | port: 6379 77 | }), 78 | proxy: true, 79 | resave: true, 80 | saveUninitialized: true 81 | })); 82 | app.use( passport.initialize()); 83 | app.use( passport.session()); 84 | 85 | app.get('/', function(req, res){ 86 | res.render('index', { user: req.user }); 87 | }); 88 | 89 | app.get('/account', ensureAuthenticated, function(req, res){ 90 | res.render('account', { user: req.user }); 91 | }); 92 | 93 | app.get('/login', function(req, res){ 94 | res.render('login', { user: req.user }); 95 | }); 96 | 97 | // GET /auth/google 98 | // Use passport.authenticate() as route middleware to authenticate the 99 | // request. The first step in Google authentication will involve 100 | // redirecting the user to google.com. After authorization, Google 101 | // will redirect the user back to this application at /auth/google/callback 102 | app.get('/auth/google', passport.authenticate('google', { scope: [ 103 | 'email', 'profile'] 104 | })); 105 | 106 | // GET /auth/google/callback 107 | // Use passport.authenticate() as route middleware to authenticate the 108 | // request. If authentication fails, the user will be redirected back to the 109 | // login page. Otherwise, the primary route function function will be called, 110 | // which, in this example, will redirect the user to the home page. 111 | app.get( '/auth/google/callback', 112 | passport.authenticate( 'google', { 113 | successRedirect: '/', 114 | failureRedirect: '/login' 115 | })); 116 | 117 | app.get('/logout', function(req, res){ 118 | req.logout(); 119 | res.redirect('/'); 120 | }); 121 | 122 | server.listen( 3000 ); 123 | 124 | 125 | // Simple route middleware to ensure user is authenticated. 126 | // Use this route middleware on any resource that needs to be protected. If 127 | // the request is authenticated (typically via a persistent login session), 128 | // the request will proceed. Otherwise, the user will be redirected to the 129 | // login page. 130 | function ensureAuthenticated(req, res, next) { 131 | if (req.isAuthenticated()) { return next(); } 132 | res.redirect('/login'); 133 | } 134 | -------------------------------------------------------------------------------- /example/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "passport-google-oauth2-example", 3 | "version": "0.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.3.5", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", 10 | "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", 11 | "requires": { 12 | "mime-types": "~2.1.18", 13 | "negotiator": "0.6.1" 14 | } 15 | }, 16 | "array-flatten": { 17 | "version": "1.1.1", 18 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 19 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 20 | }, 21 | "body-parser": { 22 | "version": "1.18.3", 23 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", 24 | "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", 25 | "requires": { 26 | "bytes": "3.0.0", 27 | "content-type": "~1.0.4", 28 | "debug": "2.6.9", 29 | "depd": "~1.1.2", 30 | "http-errors": "~1.6.3", 31 | "iconv-lite": "0.4.23", 32 | "on-finished": "~2.3.0", 33 | "qs": "6.5.2", 34 | "raw-body": "2.3.3", 35 | "type-is": "~1.6.16" 36 | } 37 | }, 38 | "bytes": { 39 | "version": "3.0.0", 40 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", 41 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" 42 | }, 43 | "connect-redis": { 44 | "version": "3.4.0", 45 | "resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-3.4.0.tgz", 46 | "integrity": "sha512-YKPSO9tLwzUr8jzhsGMdSJUxevWrDt0ggXRcTMb+mtnJ/vWGlWV7RC4VUMgqvZv3uTGDFye8Bf7d6No0oSVkOQ==", 47 | "requires": { 48 | "debug": "^4.0.1", 49 | "redis": "^2.8.0" 50 | }, 51 | "dependencies": { 52 | "debug": { 53 | "version": "4.1.1", 54 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 55 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 56 | "requires": { 57 | "ms": "^2.1.1" 58 | } 59 | }, 60 | "ms": { 61 | "version": "2.1.1", 62 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 63 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 64 | } 65 | } 66 | }, 67 | "content-disposition": { 68 | "version": "0.5.2", 69 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", 70 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" 71 | }, 72 | "content-type": { 73 | "version": "1.0.4", 74 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 75 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 76 | }, 77 | "cookie": { 78 | "version": "0.3.1", 79 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 80 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 81 | }, 82 | "cookie-parser": { 83 | "version": "1.4.4", 84 | "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.4.tgz", 85 | "integrity": "sha512-lo13tqF3JEtFO7FyA49CqbhaFkskRJ0u/UAiINgrIXeRCY41c88/zxtrECl8AKH3B0hj9q10+h3Kt8I7KlW4tw==", 86 | "requires": { 87 | "cookie": "0.3.1", 88 | "cookie-signature": "1.0.6" 89 | } 90 | }, 91 | "cookie-signature": { 92 | "version": "1.0.6", 93 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 94 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 95 | }, 96 | "crc": { 97 | "version": "3.4.4", 98 | "resolved": "https://registry.npmjs.org/crc/-/crc-3.4.4.tgz", 99 | "integrity": "sha1-naHpgOO9RPxck79as9ozeNheRms=" 100 | }, 101 | "debug": { 102 | "version": "2.6.9", 103 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 104 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 105 | "requires": { 106 | "ms": "2.0.0" 107 | } 108 | }, 109 | "depd": { 110 | "version": "1.1.2", 111 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 112 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 113 | }, 114 | "destroy": { 115 | "version": "1.0.4", 116 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 117 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 118 | }, 119 | "double-ended-queue": { 120 | "version": "2.1.0-0", 121 | "resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz", 122 | "integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw=" 123 | }, 124 | "ee-first": { 125 | "version": "1.1.1", 126 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 127 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 128 | }, 129 | "ejs": { 130 | "version": "2.6.1", 131 | "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz", 132 | "integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==" 133 | }, 134 | "encodeurl": { 135 | "version": "1.0.2", 136 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 137 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 138 | }, 139 | "escape-html": { 140 | "version": "1.0.3", 141 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 142 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 143 | }, 144 | "etag": { 145 | "version": "1.8.1", 146 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 147 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 148 | }, 149 | "express": { 150 | "version": "4.16.4", 151 | "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", 152 | "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", 153 | "requires": { 154 | "accepts": "~1.3.5", 155 | "array-flatten": "1.1.1", 156 | "body-parser": "1.18.3", 157 | "content-disposition": "0.5.2", 158 | "content-type": "~1.0.4", 159 | "cookie": "0.3.1", 160 | "cookie-signature": "1.0.6", 161 | "debug": "2.6.9", 162 | "depd": "~1.1.2", 163 | "encodeurl": "~1.0.2", 164 | "escape-html": "~1.0.3", 165 | "etag": "~1.8.1", 166 | "finalhandler": "1.1.1", 167 | "fresh": "0.5.2", 168 | "merge-descriptors": "1.0.1", 169 | "methods": "~1.1.2", 170 | "on-finished": "~2.3.0", 171 | "parseurl": "~1.3.2", 172 | "path-to-regexp": "0.1.7", 173 | "proxy-addr": "~2.0.4", 174 | "qs": "6.5.2", 175 | "range-parser": "~1.2.0", 176 | "safe-buffer": "5.1.2", 177 | "send": "0.16.2", 178 | "serve-static": "1.13.2", 179 | "setprototypeof": "1.1.0", 180 | "statuses": "~1.4.0", 181 | "type-is": "~1.6.16", 182 | "utils-merge": "1.0.1", 183 | "vary": "~1.1.2" 184 | } 185 | }, 186 | "express-session": { 187 | "version": "1.15.6", 188 | "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.15.6.tgz", 189 | "integrity": "sha512-r0nrHTCYtAMrFwZ0kBzZEXa1vtPVrw0dKvGSrKP4dahwBQ1BJpF2/y1Pp4sCD/0kvxV4zZeclyvfmw0B4RMJQA==", 190 | "requires": { 191 | "cookie": "0.3.1", 192 | "cookie-signature": "1.0.6", 193 | "crc": "3.4.4", 194 | "debug": "2.6.9", 195 | "depd": "~1.1.1", 196 | "on-headers": "~1.0.1", 197 | "parseurl": "~1.3.2", 198 | "uid-safe": "~2.1.5", 199 | "utils-merge": "1.0.1" 200 | } 201 | }, 202 | "finalhandler": { 203 | "version": "1.1.1", 204 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", 205 | "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", 206 | "requires": { 207 | "debug": "2.6.9", 208 | "encodeurl": "~1.0.2", 209 | "escape-html": "~1.0.3", 210 | "on-finished": "~2.3.0", 211 | "parseurl": "~1.3.2", 212 | "statuses": "~1.4.0", 213 | "unpipe": "~1.0.0" 214 | } 215 | }, 216 | "forwarded": { 217 | "version": "0.1.2", 218 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 219 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 220 | }, 221 | "fresh": { 222 | "version": "0.5.2", 223 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 224 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 225 | }, 226 | "http-errors": { 227 | "version": "1.6.3", 228 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", 229 | "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", 230 | "requires": { 231 | "depd": "~1.1.2", 232 | "inherits": "2.0.3", 233 | "setprototypeof": "1.1.0", 234 | "statuses": ">= 1.4.0 < 2" 235 | } 236 | }, 237 | "iconv-lite": { 238 | "version": "0.4.23", 239 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", 240 | "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", 241 | "requires": { 242 | "safer-buffer": ">= 2.1.2 < 3" 243 | } 244 | }, 245 | "inherits": { 246 | "version": "2.0.3", 247 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 248 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 249 | }, 250 | "ipaddr.js": { 251 | "version": "1.8.0", 252 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", 253 | "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" 254 | }, 255 | "media-typer": { 256 | "version": "0.3.0", 257 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 258 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 259 | }, 260 | "merge-descriptors": { 261 | "version": "1.0.1", 262 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 263 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 264 | }, 265 | "methods": { 266 | "version": "1.1.2", 267 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 268 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 269 | }, 270 | "mime": { 271 | "version": "1.4.1", 272 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", 273 | "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" 274 | }, 275 | "mime-db": { 276 | "version": "1.38.0", 277 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", 278 | "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" 279 | }, 280 | "mime-types": { 281 | "version": "2.1.22", 282 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", 283 | "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", 284 | "requires": { 285 | "mime-db": "~1.38.0" 286 | } 287 | }, 288 | "ms": { 289 | "version": "2.0.0", 290 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 291 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 292 | }, 293 | "negotiator": { 294 | "version": "0.6.1", 295 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 296 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" 297 | }, 298 | "oauth": { 299 | "version": "0.9.15", 300 | "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", 301 | "integrity": "sha1-vR/vr2hslrdUda7VGWQS/2DPucE=" 302 | }, 303 | "on-finished": { 304 | "version": "2.3.0", 305 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 306 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 307 | "requires": { 308 | "ee-first": "1.1.1" 309 | } 310 | }, 311 | "on-headers": { 312 | "version": "1.0.2", 313 | "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", 314 | "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" 315 | }, 316 | "parseurl": { 317 | "version": "1.3.2", 318 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", 319 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" 320 | }, 321 | "passport": { 322 | "version": "0.4.0", 323 | "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.0.tgz", 324 | "integrity": "sha1-xQlWkTR71a07XhgCOMORTRbwWBE=", 325 | "requires": { 326 | "passport-strategy": "1.x.x", 327 | "pause": "0.0.1" 328 | } 329 | }, 330 | "passport-google-oauth2": { 331 | "version": "file:..", 332 | "requires": { 333 | "passport-oauth2": "^1.1.2" 334 | } 335 | }, 336 | "passport-oauth2": { 337 | "version": "1.4.0", 338 | "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.4.0.tgz", 339 | "integrity": "sha1-9i+BWDy+EmCb585vFguTlaJ7hq0=", 340 | "requires": { 341 | "oauth": "0.9.x", 342 | "passport-strategy": "1.x.x", 343 | "uid2": "0.0.x", 344 | "utils-merge": "1.x.x" 345 | } 346 | }, 347 | "passport-strategy": { 348 | "version": "1.0.0", 349 | "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", 350 | "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" 351 | }, 352 | "path-to-regexp": { 353 | "version": "0.1.7", 354 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 355 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 356 | }, 357 | "pause": { 358 | "version": "0.0.1", 359 | "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", 360 | "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" 361 | }, 362 | "proxy-addr": { 363 | "version": "2.0.4", 364 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", 365 | "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", 366 | "requires": { 367 | "forwarded": "~0.1.2", 368 | "ipaddr.js": "1.8.0" 369 | } 370 | }, 371 | "qs": { 372 | "version": "6.5.2", 373 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 374 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 375 | }, 376 | "random-bytes": { 377 | "version": "1.0.0", 378 | "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", 379 | "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" 380 | }, 381 | "range-parser": { 382 | "version": "1.2.0", 383 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", 384 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" 385 | }, 386 | "raw-body": { 387 | "version": "2.3.3", 388 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", 389 | "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", 390 | "requires": { 391 | "bytes": "3.0.0", 392 | "http-errors": "1.6.3", 393 | "iconv-lite": "0.4.23", 394 | "unpipe": "1.0.0" 395 | } 396 | }, 397 | "redis": { 398 | "version": "2.8.0", 399 | "resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz", 400 | "integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==", 401 | "requires": { 402 | "double-ended-queue": "^2.1.0-0", 403 | "redis-commands": "^1.2.0", 404 | "redis-parser": "^2.6.0" 405 | } 406 | }, 407 | "redis-commands": { 408 | "version": "1.4.0", 409 | "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.4.0.tgz", 410 | "integrity": "sha512-cu8EF+MtkwI4DLIT0x9P8qNTLFhQD4jLfxLR0cCNkeGzs87FN6879JOJwNQR/1zD7aSYNbU0hgsV9zGY71Itvw==" 411 | }, 412 | "redis-parser": { 413 | "version": "2.6.0", 414 | "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz", 415 | "integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs=" 416 | }, 417 | "safe-buffer": { 418 | "version": "5.1.2", 419 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 420 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 421 | }, 422 | "safer-buffer": { 423 | "version": "2.1.2", 424 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 425 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 426 | }, 427 | "send": { 428 | "version": "0.16.2", 429 | "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", 430 | "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", 431 | "requires": { 432 | "debug": "2.6.9", 433 | "depd": "~1.1.2", 434 | "destroy": "~1.0.4", 435 | "encodeurl": "~1.0.2", 436 | "escape-html": "~1.0.3", 437 | "etag": "~1.8.1", 438 | "fresh": "0.5.2", 439 | "http-errors": "~1.6.2", 440 | "mime": "1.4.1", 441 | "ms": "2.0.0", 442 | "on-finished": "~2.3.0", 443 | "range-parser": "~1.2.0", 444 | "statuses": "~1.4.0" 445 | } 446 | }, 447 | "serve-static": { 448 | "version": "1.13.2", 449 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", 450 | "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", 451 | "requires": { 452 | "encodeurl": "~1.0.2", 453 | "escape-html": "~1.0.3", 454 | "parseurl": "~1.3.2", 455 | "send": "0.16.2" 456 | } 457 | }, 458 | "setprototypeof": { 459 | "version": "1.1.0", 460 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", 461 | "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" 462 | }, 463 | "statuses": { 464 | "version": "1.4.0", 465 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", 466 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" 467 | }, 468 | "type-is": { 469 | "version": "1.6.16", 470 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", 471 | "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", 472 | "requires": { 473 | "media-typer": "0.3.0", 474 | "mime-types": "~2.1.18" 475 | } 476 | }, 477 | "uid-safe": { 478 | "version": "2.1.5", 479 | "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", 480 | "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", 481 | "requires": { 482 | "random-bytes": "~1.0.0" 483 | } 484 | }, 485 | "uid2": { 486 | "version": "0.0.3", 487 | "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", 488 | "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=" 489 | }, 490 | "unpipe": { 491 | "version": "1.0.0", 492 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 493 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 494 | }, 495 | "utils-merge": { 496 | "version": "1.0.1", 497 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 498 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 499 | }, 500 | "vary": { 501 | "version": "1.1.2", 502 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 503 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 504 | } 505 | } 506 | } 507 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "passport-google-oauth2-example", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "body-parser": "^1.18.3", 6 | "connect-redis": "^3.4.0", 7 | "cookie-parser": "^1.4.4", 8 | "ejs": "^2.6.1", 9 | "express": "^4.16.4", 10 | "express-session": "^1.15.6", 11 | "passport": "^0.4.0", 12 | "passport-google-oauth2": "file:.." 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example/views/account.ejs: -------------------------------------------------------------------------------- 1 |

ID: <%= user.id %>

2 |

Name: <%= user.displayName %>

3 | -------------------------------------------------------------------------------- /example/views/index.ejs: -------------------------------------------------------------------------------- 1 | <% if (!user) { %> 2 |

Welcome! Please log in.

3 | <% } else { %> 4 |

Hello, <%= user.displayName %>.

5 | <% } %> 6 | -------------------------------------------------------------------------------- /example/views/layout.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Passport-Google (OAuth 2.0) Example 5 | 6 | 7 | <% if (!user) { %> 8 |

9 | Home | 10 | Log In 11 |

12 | <% } else { %> 13 |

14 | Home | 15 | Account | 16 | Log Out 17 |

18 | <% } %> 19 | <%- body %> 20 | 21 | 22 | -------------------------------------------------------------------------------- /example/views/login.ejs: -------------------------------------------------------------------------------- 1 | Login with Google 2 | -------------------------------------------------------------------------------- /lib/oauth2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var util = require('util') 5 | , OAuth2Strategy = require('passport-oauth2') 6 | , InternalOAuthError = require('passport-oauth2').InternalOAuthError; 7 | 8 | 9 | /** 10 | * `Strategy` constructor. 11 | * 12 | * The Google authentication strategy authenticates requests by delegating to 13 | * Google using the OAuth 2.0 protocol. 14 | * 15 | * Applications must supply a `verify` callback which accepts an `accessToken`, 16 | * `refreshToken` and service-specific `profile`, and then calls the `done` 17 | * callback supplying a `user`, which should be set to `false` if the 18 | * credentials are not valid. If an exception occured, `err` should be set. 19 | * 20 | * Options: 21 | * - `clientID` your Google application's client id 22 | * - `clientSecret` your Google application's client secret 23 | * - `callbackURL` URL to which Google will redirect the user after granting authorization 24 | * 25 | * Examples: 26 | * 27 | * passport.use(new GoogleStrategy({ 28 | * clientID: '123-456-789', 29 | * clientSecret: 'shhh-its-a-secret' 30 | * callbackURL: 'https://www.example.net/auth/google/callback' 31 | * }, 32 | * function(accessToken, refreshToken, profile, done) { 33 | * User.findOrCreate(..., function (err, user) { 34 | * done(err, user); 35 | * }); 36 | * } 37 | * )); 38 | * 39 | * @param {Object} options 40 | * @param {Function} verify 41 | * @api public 42 | */ 43 | function Strategy(options, verify) { 44 | options = options || {}; 45 | options.authorizationURL = options.authorizationURL || 'https://accounts.google.com/o/oauth2/v2/auth'; 46 | options.tokenURL = options.tokenURL || 'https://www.googleapis.com/oauth2/v4/token'; 47 | 48 | OAuth2Strategy.call(this, options, verify); 49 | this.name = 'google'; 50 | } 51 | 52 | /** 53 | * Inherit from `OAuth2Strategy`. 54 | */ 55 | util.inherits(Strategy, OAuth2Strategy); 56 | 57 | Strategy.prototype.authenticate = function(req, options) { 58 | options || (options = {}) 59 | 60 | var oldHint = options.loginHint 61 | options.loginHint = req.query.login_hint 62 | OAuth2Strategy.prototype.authenticate.call(this, req, options) 63 | options.loginHint = oldHint 64 | } 65 | 66 | 67 | /** 68 | * Retrieve user profile from Google. 69 | * 70 | * This function constructs a normalized profile, with the following properties: 71 | * 72 | * - `provider` always set to `google` 73 | * - `id` 74 | * - `name` 75 | * - `displayName` 76 | * - `birthday` 77 | * - `relationship` 78 | * - `isPerson` 79 | * - `isPlusUser` 80 | * - `placesLived` 81 | * - `language` 82 | * - `emails` 83 | * - `gender` 84 | * - `picture` 85 | * 86 | * @param {String} accessToken 87 | * @param {Function} done 88 | * @api protected 89 | */ 90 | Strategy.prototype.userProfile = function(accessToken, done) { 91 | this._oauth2.get('https://www.googleapis.com/oauth2/v3/userinfo', accessToken, function (err, body, res) { 92 | if (err) { return done(new InternalOAuthError('failed to fetch user profile', err)); } 93 | 94 | try { 95 | var json = JSON.parse(body); 96 | 97 | var profile = { provider: 'google' }; 98 | profile.sub = json.sub; 99 | profile.id = json.id || json.sub; 100 | profile.displayName = json.name; 101 | profile.name = { 102 | givenName: json.given_name, 103 | familyName: json.family_name 104 | }; 105 | profile.given_name = json.given_name; 106 | profile.family_name = json.family_name; 107 | if (json.birthday) profile.birthday = json.birthday; 108 | if (json.relationshipStatus) profile.relationship = json.relationshipStatus; 109 | if (json.objectType && json.objectType == 'person') { 110 | profile.isPerson = true; 111 | } 112 | if (json.isPlusUser) profile.isPlusUser = json.isPlusUser; 113 | if (json.email_verified !== undefined) { 114 | profile.email_verified = json.email_verified; 115 | profile.verified = json.email_verified; 116 | } 117 | if (json.placesLived) profile.placesLived = json.placesLived; 118 | if (json.language) profile.language = json.language; 119 | if (!json.language && json.locale) { 120 | profile.language = json.locale; 121 | profile.locale = json.local; 122 | } 123 | if (json.emails) { 124 | profile.emails = json.emails; 125 | 126 | profile.emails.some(function(email) { 127 | if (email.type === 'account') { 128 | profile.email = email.value 129 | return true 130 | } 131 | }) 132 | } 133 | if (!profile.email && json.email) { 134 | profile.email = json.email; 135 | } 136 | if (!profile.emails && profile.email) { 137 | profile.emails = [{ 138 | value: profile.email, 139 | type: "account" 140 | }]; 141 | } 142 | if (json.gender) profile.gender = json.gender; 143 | if (!json.domain && json.hd) json.domain = json.hd; 144 | if (json.image && json.image.url) { 145 | var photo = { 146 | value: json.image.url 147 | }; 148 | if (json.image.isDefault) photo.type = 'default'; 149 | profile.photos = [photo]; 150 | } 151 | if (!json.image && json.picture) { 152 | var photo = { 153 | value: json.picture 154 | }; 155 | photo.type = 'default'; 156 | profile.photos = [photo]; 157 | profile.picture = json.picture; 158 | } 159 | if (json.cover && json.cover.coverPhoto && json.cover.coverPhoto.url) 160 | profile.coverPhoto = json.cover.coverPhoto.url; 161 | 162 | profile._raw = body; 163 | profile._json = json; 164 | 165 | done(null, profile); 166 | } catch(e) { 167 | done(e); 168 | } 169 | }); 170 | }; 171 | 172 | /** 173 | * Return extra Google-specific parameters to be included in the authorization 174 | * request. 175 | * 176 | * @param {Object} options 177 | * @return {Object} 178 | * @api protected 179 | */ 180 | Strategy.prototype.authorizationParams = function(options) { 181 | var params = {}; 182 | if (options.accessType) { 183 | params['access_type'] = options.accessType; 184 | } 185 | if (options.approvalPrompt) { 186 | params['approval_prompt'] = options.approvalPrompt; 187 | } 188 | if (options.prompt) { 189 | // This parameter is undocumented in Google's official documentation. 190 | // However, it was detailed by Breno de Medeiros (who works at Google) in 191 | // this Stack Overflow answer: 192 | // http://stackoverflow.com/questions/14384354/force-google-account-chooser/14393492#14393492 193 | params['prompt'] = options.prompt; 194 | } 195 | if (options.loginHint) { 196 | // This parameter is derived from OpenID Connect, and supported by Google's 197 | // OAuth 2.0 endpoint. 198 | // https://github.com/jaredhanson/passport-google-oauth/pull/8 199 | // https://bitbucket.org/openid/connect/commits/970a95b83add 200 | params['login_hint'] = options.loginHint; 201 | } 202 | if (options.userID) { 203 | // Undocumented, but supported by Google's OAuth 2.0 endpoint. Appears to 204 | // be equivalent to `login_hint`. 205 | params['user_id'] = options.userID; 206 | } 207 | if (options.hostedDomain || options.hd) { 208 | // This parameter is derived from Google's OAuth 1.0 endpoint, and (although 209 | // undocumented) is supported by Google's OAuth 2.0 endpoint was well. 210 | // https://developers.google.com/accounts/docs/OAuth_ref 211 | params['hd'] = options.hostedDomain || options.hd; 212 | } 213 | return params; 214 | }; 215 | 216 | 217 | /** 218 | * Expose `Strategy` directly from package. 219 | */ 220 | exports = module.exports = Strategy; 221 | 222 | /** 223 | * Export constructors. 224 | */ 225 | exports.Strategy = Strategy; 226 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "passport-google-oauth2", 3 | "version": "0.1.6", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "balanced-match": { 8 | "version": "1.0.0", 9 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 10 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 11 | "dev": true 12 | }, 13 | "brace-expansion": { 14 | "version": "1.1.11", 15 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 16 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 17 | "dev": true, 18 | "requires": { 19 | "balanced-match": "^1.0.0", 20 | "concat-map": "0.0.1" 21 | } 22 | }, 23 | "concat-map": { 24 | "version": "0.0.1", 25 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 26 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 27 | "dev": true 28 | }, 29 | "diff": { 30 | "version": "1.0.8", 31 | "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.8.tgz", 32 | "integrity": "sha1-NDJ2MI7Jkbe8giZ+1VvBQR+XFmY=", 33 | "dev": true 34 | }, 35 | "eyes": { 36 | "version": "0.1.8", 37 | "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", 38 | "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=", 39 | "dev": true 40 | }, 41 | "fs.realpath": { 42 | "version": "1.0.0", 43 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 44 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 45 | "dev": true 46 | }, 47 | "glob": { 48 | "version": "7.1.3", 49 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", 50 | "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", 51 | "dev": true, 52 | "requires": { 53 | "fs.realpath": "^1.0.0", 54 | "inflight": "^1.0.4", 55 | "inherits": "2", 56 | "minimatch": "^3.0.4", 57 | "once": "^1.3.0", 58 | "path-is-absolute": "^1.0.0" 59 | } 60 | }, 61 | "inflight": { 62 | "version": "1.0.6", 63 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 64 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 65 | "dev": true, 66 | "requires": { 67 | "once": "^1.3.0", 68 | "wrappy": "1" 69 | } 70 | }, 71 | "inherits": { 72 | "version": "2.0.3", 73 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 74 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 75 | "dev": true 76 | }, 77 | "minimatch": { 78 | "version": "3.0.4", 79 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 80 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 81 | "dev": true, 82 | "requires": { 83 | "brace-expansion": "^1.1.7" 84 | } 85 | }, 86 | "oauth": { 87 | "version": "0.9.15", 88 | "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", 89 | "integrity": "sha1-vR/vr2hslrdUda7VGWQS/2DPucE=" 90 | }, 91 | "once": { 92 | "version": "1.4.0", 93 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 94 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 95 | "dev": true, 96 | "requires": { 97 | "wrappy": "1" 98 | } 99 | }, 100 | "passport-oauth2": { 101 | "version": "1.4.0", 102 | "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.4.0.tgz", 103 | "integrity": "sha1-9i+BWDy+EmCb585vFguTlaJ7hq0=", 104 | "requires": { 105 | "oauth": "0.9.x", 106 | "passport-strategy": "1.x.x", 107 | "uid2": "0.0.x", 108 | "utils-merge": "1.x.x" 109 | } 110 | }, 111 | "passport-strategy": { 112 | "version": "1.0.0", 113 | "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", 114 | "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" 115 | }, 116 | "path-is-absolute": { 117 | "version": "1.0.1", 118 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 119 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 120 | "dev": true 121 | }, 122 | "uid2": { 123 | "version": "0.0.3", 124 | "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", 125 | "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=" 126 | }, 127 | "utils-merge": { 128 | "version": "1.0.1", 129 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 130 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 131 | }, 132 | "vows": { 133 | "version": "0.8.2", 134 | "resolved": "https://registry.npmjs.org/vows/-/vows-0.8.2.tgz", 135 | "integrity": "sha1-aR95qybM3oC6cm3en+yOc9a88us=", 136 | "dev": true, 137 | "requires": { 138 | "diff": "~1.0.8", 139 | "eyes": "~0.1.6", 140 | "glob": "^7.1.2" 141 | } 142 | }, 143 | "wrappy": { 144 | "version": "1.0.2", 145 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 146 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 147 | "dev": true 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "passport-google-oauth2", 3 | "version": "0.2.0", 4 | "description": "Passport strategy for Google OAuth 2.0", 5 | "main": "./lib/oauth2.js", 6 | "directories": { 7 | "example": "examples", 8 | "test": "test" 9 | }, 10 | "dependencies": { 11 | "passport-oauth2": "^1.1.2" 12 | }, 13 | "devDependencies": { 14 | "vows": "^0.8.0" 15 | }, 16 | "scripts": { 17 | "test": "make test" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git://github.com/mstade/passport-google-oauth2.git" 22 | }, 23 | "keywords": [ 24 | "auth", 25 | "google", 26 | "google+", 27 | "passportjs", 28 | "oauth2" 29 | ], 30 | "author": "Marcus Stade , Ilya Kantor , Alexis Gruet , Jared Hanson", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/mstade/passport-google-oauth2/issues" 34 | }, 35 | "homepage": "https://github.com/mstade/passport-google-oauth2" 36 | } 37 | -------------------------------------------------------------------------------- /test/index-test.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows'); 2 | var assert = require('assert'); 3 | var util = require('util'); 4 | var google = require('../'); 5 | 6 | 7 | vows.describe('passport-google-oauth2').addBatch({ 8 | 9 | 'module': { 10 | 'should export OAuth 2.0 strategy': function (x) { 11 | assert.isFunction(google); 12 | }, 13 | 'should make OAuth 2.0 strategy available at .Strategy': function (x) { 14 | assert.isFunction(google.Strategy); 15 | } 16 | }, 17 | 18 | }).export(module); 19 | -------------------------------------------------------------------------------- /test/oauth2-test.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows'); 2 | var assert = require('assert'); 3 | var util = require('util'); 4 | var GoogleStrategy = require('../'); 5 | 6 | 7 | vows.describe('GoogleStrategy').addBatch({ 8 | 9 | 'strategy': { 10 | topic: function() { 11 | return new GoogleStrategy({ 12 | clientID: 'ABC123', 13 | clientSecret: 'secret' 14 | }, 15 | function() {}); 16 | }, 17 | 18 | 'should be named google': function (strategy) { 19 | assert.equal(strategy.name, 'google'); 20 | }, 21 | }, 22 | 23 | 'strategy authorization params': { 24 | topic: function() { 25 | return new GoogleStrategy({ 26 | clientID: 'ABC123', 27 | clientSecret: 'secret' 28 | }, 29 | function() {}); 30 | }, 31 | 32 | 'should return empty object when parsing invalid options': function (strategy) { 33 | var params = strategy.authorizationParams({ foo: 'bar' }); 34 | assert.lengthOf(Object.keys(params), 0); 35 | }, 36 | 'should return access_type': function (strategy) { 37 | var params = strategy.authorizationParams({ accessType: 'offline' }); 38 | assert.equal(params.access_type, 'offline'); 39 | }, 40 | 'should return approval_prompt': function (strategy) { 41 | var params = strategy.authorizationParams({ approvalPrompt: 'force' }); 42 | assert.equal(params.approval_prompt, 'force'); 43 | }, 44 | 'should return prompt': function (strategy) { 45 | var params = strategy.authorizationParams({ prompt: 'consent' }); 46 | assert.equal(params.prompt, 'consent'); 47 | }, 48 | 'should return login_hint': function (strategy) { 49 | var params = strategy.authorizationParams({ loginHint: 'bob@gmail.com' }); 50 | assert.equal(params.login_hint, 'bob@gmail.com'); 51 | }, 52 | 'should return user_id': function (strategy) { 53 | var params = strategy.authorizationParams({ userID: 'bob@gmail.com' }); 54 | assert.equal(params.user_id, 'bob@gmail.com'); 55 | }, 56 | 'should return hd from hostedDomain option': function (strategy) { 57 | var params = strategy.authorizationParams({ hostedDomain: 'mycollege.edu' }); 58 | assert.equal(params.hd, 'mycollege.edu'); 59 | }, 60 | 'should return hd from hd option': function (strategy) { 61 | var params = strategy.authorizationParams({ hd: 'mycollege.edu' }); 62 | assert.equal(params.hd, 'mycollege.edu'); 63 | }, 64 | 'should return access_type and approval_prompt': function (strategy) { 65 | var params = strategy.authorizationParams({ accessType: 'offline', approvalPrompt: 'force' }); 66 | assert.equal(params.access_type, 'offline'); 67 | assert.equal(params.approval_prompt, 'force'); 68 | }, 69 | }, 70 | 71 | 'strategy when loading user profile': { 72 | topic: function() { 73 | var strategy = new GoogleStrategy({ 74 | clientID: 'ABC123', 75 | clientSecret: 'secret' 76 | }, 77 | function() {}); 78 | 79 | // mock 80 | strategy._oauth2.get = function(url, accessToken, callback) { 81 | var body = JSON.stringify(require('./profile.json')) 82 | callback(null, body, undefined); 83 | } 84 | 85 | return strategy; 86 | }, 87 | 88 | 'when told to load user profile': { 89 | topic: function(strategy) { 90 | var self = this; 91 | function done(err, profile) { 92 | self.callback(err, profile); 93 | } 94 | 95 | process.nextTick(function () { 96 | strategy.userProfile('access-token', done); 97 | }); 98 | }, 99 | 100 | 'should not error' : function(err, req) { 101 | assert.isNull(err); 102 | }, 103 | 'should load profile' : function(err, profile) { 104 | assert.equal(profile.provider, 'google'); 105 | assert.equal(profile.id, '000000000000000000000'); 106 | assert.equal(profile.displayName, 'Fred Example'); 107 | assert.equal(profile.name.familyName, 'Example'); 108 | assert.equal(profile.name.givenName, 'Fred'); 109 | assert.equal(profile.email, 'fred.example@gmail.com') 110 | assert.equal(profile.emails[0].value, 'fred.example@gmail.com'); 111 | }, 112 | 'should set raw property' : function(err, profile) { 113 | assert.isString(profile._raw); 114 | }, 115 | 'should set json property' : function(err, profile) { 116 | assert.isObject(profile._json); 117 | }, 118 | }, 119 | }, 120 | 121 | 'strategy when loading user profile and encountering an error': { 122 | topic: function() { 123 | var strategy = new GoogleStrategy({ 124 | clientID: 'ABC123', 125 | clientSecret: 'secret' 126 | }, 127 | function() {}); 128 | 129 | // mock 130 | strategy._oauth2.get = function(url, accessToken, callback) { 131 | callback(new Error('something-went-wrong')); 132 | } 133 | 134 | return strategy; 135 | }, 136 | 137 | 'when told to load user profile': { 138 | topic: function(strategy) { 139 | var self = this; 140 | function done(err, profile) { 141 | self.callback(err, profile); 142 | } 143 | 144 | process.nextTick(function () { 145 | strategy.userProfile('access-token', done); 146 | }); 147 | }, 148 | 149 | 'should error' : function(err, req) { 150 | assert.isNotNull(err); 151 | }, 152 | 'should wrap error in InternalOAuthError' : function(err, req) { 153 | assert.equal(err.constructor.name, 'InternalOAuthError'); 154 | }, 155 | 'should not load profile' : function(err, profile) { 156 | assert.isUndefined(profile); 157 | }, 158 | }, 159 | }, 160 | 161 | }).export(module); 162 | -------------------------------------------------------------------------------- /test/profile.json: -------------------------------------------------------------------------------- 1 | { "sub": "000000000000000000000", 2 | "name": "Fred Example", 3 | "given_name": "Fred", 4 | "family_name": "Example", 5 | "picture": 6 | "https://lh5.googleusercontent.com/000000000000000000000.jpg", 7 | "email": "fred.example@gmail.com", 8 | "email_verified": true, 9 | "locale": "en" } --------------------------------------------------------------------------------