Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Passport.js / Google OAuth2 strategy - How to use token on login for API access

I am logging users in via their domain Google accounts using passport.js. This works great, but now I need to give this application access to a few Google API's (drive, sheets, etc).

When a user logs in, a message appears in the logs, that makes it seem like passport has all the required info:

info: [06/Jun/2019:21:24:37 +0000] "302 GET /auth/callback?code=** USER ACCESS TOKEN HERE **&scope=email%20profile%20https://www.googleapis.com/auth/drive.file%20https://www.googleapis.com/auth/spreadsheets%20https://www.googleapis.com/auth/userinfo.email%20https://www.googleapis.com/auth/userinfo.profile%20https://www.googleapis.com/auth/drive HTTP/1.1" [46]

This is achieved by passing the appended scopes via passport.authenticate(), which presents the user with the "Grant access to these things on your Google account to this app?" screen :

//Initial auth call to Google
router.get('/',
  passport.authenticate('google', {
    hd: 'edmonds.wednet.edu',
    scope: [
      'email',
      'profile',
      'https://www.googleapis.com/auth/drive',
      'https://www.googleapis.com/auth/drive.file',
      'https://www.googleapis.com/auth/spreadsheets'
    ],
    prompt: 'select_account'
  })
);

However, when I go and try to call an API with something like:

const {google} = require('googleapis');
const sheets = google.sheets({version: 'v4', auth});

router.post('/gsCreate', function(req,res,next){

  sheets.spreadsheets.create({
    // Details here.....
  });

});

I get nothing but errors (the current one is debug: authClient.request is not a function)

My question is: Is it possible for me to use a setup like this, asking the user to log in and grant permissions once, and then somehow save that to their user session via passport?

like image 298
Jensen010 Avatar asked Jun 06 '19 21:06

Jensen010


2 Answers

I had the same question, but I was able to access Google Gmail API functionalities along with Passport.js user authentication by specifying 'scopes' using the following process. First, create a file to setup the passport-google-strategy in nodejs as follows.

passport_setup.js

const passport = require('passport')
const GoogleStrategy = require('passport-google-oauth20')
const fs = require("fs");
const path = require('path');
//make OAuth2 Credentials file using Google Developer console and download it(credentials.json)
//replace the 'web' using 'installed' in the file downloaded
var pathToJson = path.resolve(__dirname, './credentials.json');
const config = JSON.parse(fs.readFileSync(pathToJson));


passport.serializeUser((user, done) => {
    done(null, user.id)
})

passport.deserializeUser((id, done) => {

    const query = { _id: id }
    Users.findOne(query, (err, user) => {
        if (err) {
            res.status(500).json(err);
        } else {
            done(null, user)
        }
    })
})

//create a google startergy including following details
passport.use(
    new GoogleStrategy({
        clientID: config.installed.client_id,
        clientSecret: config.installed.client_secret,
        callbackURL: config.installed.redirect_uris[0]
    }, (accessToken, refreshToken,otherTokenDetails, user, done) => {
        
        //in here you can access all token details to given API scope
        //and i have created file from that details
        let tokens = {
            access_token: accessToken,
            refresh_token: refreshToken,
            scope: otherTokenDetails.scope,
            token_type: otherTokenDetails.token_type,
            expiry_date:otherTokenDetails.expires_in
        }
        let data = JSON.stringify(tokens);
        fs.writeFileSync('./tokens.json', data);


        //you will get a "user" object which will include the google id, name details, 
        //email etc, using that details you can do persist user data in your DB or can check 
        //whether the user already exists

        //after persisting user data to a DB call done
        //better to use your DB user objects in the done method

        done(null, user)
  
    })
)

Then create your index.js file in nodejs for API route management and to call send method of Gmail API. Also, run the following command to install "google-apis"

npm install googleapis@39 --save

index.js

const express = require("express")
//import passport_setup.js
const passportSetup = require('./passport_setup')
const cookieSeesion = require('cookie-session');
const passport = require("passport");
//import google api
const { google } = require('googleapis');
//read credentials file you obtained from google developer console
const fs = require("fs");
const path = require('path');
var pathToJson_1 = path.resolve(__dirname, './credentials.json');
const credentials = JSON.parse(fs.readFileSync(pathToJson_1));


//get Express functionalities to app
const app = express();

// **Middleware Operations**//

//cookie encryption
app.use(cookieSeesion({
    name:'Reserve It',
    maxAge: 1*60*60*1000,
    keys: ['ranmalc6h12o6dewage']
}))

//initialize passort session handling
app.use(passport.initialize())
app.use(passport.session())

app.use(express.json());    

//**API urls**//

//route to authenticate users using google by calling google stratergy in passport_setup.js 
//mention access levels of API you want in the scope
app.get("/google", passport.authenticate('google', {
scope: ['profile',
    'email',
    'https://mail.google.com/'
],
accessType: 'offline',
prompt: 'consent'
}))

//redirected route after obtaining 'code' from user authentication with API scopes
app.get("/google/redirect", passport.authenticate('google'), (req, res) => {

    try {
        //read token file you saved earlier in passport_setup.js
        var pathToJson_2 = path.resolve(__dirname, './tokens.json');
        //get tokens to details to object
        const tokens = JSON.parse(fs.readFileSync(pathToJson_2));
        //extract credential details
        const { client_secret, client_id, redirect_uris } = credentials.installed

        //make OAuth2 object
        const oAuth2Client = new google.auth.OAuth2(client_id,
        client_secret,
        redirect_uris[0])

        // set token details to OAuth2 object
        oAuth2Client.setCredentials(tokens)
     
       //create gmail object to call APIs
       const gmail = google.gmail({ version: 'v1', auth: oAuth2Client })

       //call gmail APIs message send method
       gmail.users.messages.send({
             userId: 'me',//'me' indicate current logged in user id
             resource: {
                raw: //<email content>
               }
        }, (err, res) => {
            if (err) {
              console.log('The API returned an error: ' + err)
              throw err
            }
            console.log('Email Status : ' + res.status)
            console.log('Email Status Text : ' + res.statusText)
         })

        res.status(200).json({ status:true })
        
    } catch (err) {
        res.status(500).json(err)
    }

})

app.listen(3000, () => { console.log('Server Satrted at port 3000') })

You can separate the routes in the index.js file to different files for clarity using express.Router()

If you want to call another Google API service just change this code segment and code below that;

   const gmail = google.gmail({ version: 'v1', auth: oAuth2Client })
   gmail.users.messages.send(....Send Method internal implementation given above....)

For Google Drive:

const drive = google.drive({version: 'v3', auth: oAuth2Client});
drive.files.list(...Refer "Google Drive API" documentation for more details....)
like image 80
Ranmal Dewage Avatar answered Oct 12 '22 11:10

Ranmal Dewage


I believe you can't use passport.js for three-legged oauth for APIs like Sheets or Drive.

Have a look at the Using OAuth for web servers documentation instead.

like image 29
user835611 Avatar answered Oct 12 '22 11:10

user835611