DEV Community

Cover image for My experience with Google apis and oauth2
Dennis kinuthia
Dennis kinuthia

Posted on • Updated on

My experience with Google apis and oauth2

In previous post we made an app that authenticates users using firebase auth and it was simple and straight-fotward.

I even included a gmail scope because the documentation promised the ability to work with other google apis after sign in because i planned to send out email alert on behalf of the person who made a change to all te app's users it may concern.

import { GoogleAuthProvider, signInWithRedirect } from "firebase/auth";
import { auth} from "../../firebase/firebaseConfig";

const provider = new GoogleAuthProvider();
provider.addScope('https://mail.google.com/');

export const loginUser= () => {

signInWithRedirect(auth, provider)
.then((result:any) => {
console.log("auth result === === ",result)
}).catch((error) => {
// Handle Errors here.
console.log("auth error  === ",error)

});

}


Enter fullscreen mode Exit fullscreen mode

then use getredirectresult when your page loads again, in my case it redirects to the home component when authenticated

import { User} from 'firebase/auth';
import React,{useEffect}  from 'react'
import { getRedirectResult, GoogleAuthProvider } from "firebase/auth";
import { auth } from '../../firebase/firebaseConfig';


interface HomeProps {
user?:User|null
}

export const Home: React.FC<HomeProps> = () => {

useEffect(() => {

getRedirectResult(auth)
.then((result:any) => {
  // This gives you a Google Access Token. You can use it to access Google APIs.
  const credential = GoogleAuthProvider.credentialFromResult(result);
  const token = credential?.accessToken;
  console.log("creds ==== ", credential)
  console.log("access token ==== ", token)
}).catch((error) => {
console.log("error getting access token  === ",error)
  const credential = GoogleAuthProvider.credentialFromError(error);
  console.log("error getting access token  === ",credential)
  // ...
});


}, [])



return (
 <div className='w-full min-h-full bg-slate-400 flex-center flex-col'>
<button
className='bg-slate-700 p-5 text-xl font-bold'
onClick={()=>{}}
>click</button>

 </div>
);


}

Enter fullscreen mode Exit fullscreen mode

Note: this is not necessary when using signInWithPopup as the
result is available in the then block
the official docs recommend using signinWithRedirect as it is
better on mobile

import { getAuth, signInWithPopup, GoogleAuthProvider } from "firebase/auth";

const auth = getAuth();
signInWithPopup(auth, provider)
  .then((result) => {
    // This gives you a Google Access Token. You can use it to access the Google API.
    const credential = GoogleAuthProvider.credentialFromResult(result);
    const token = credential.accessToken;
    // The signed-in user info.
    const user = result.user;
    // ...
  }).catch((error) => {
    // Handle Errors here.
    const errorCode = error.code;
    const errorMessage = error.message;
    // The email of the user's account used.
    const email = error.customData.email;
    // The AuthCredential type that was used.
    const credential = GoogleAuthProvider.credentialFromError(error);
    // ...
  });
Enter fullscreen mode Exit fullscreen mode

you'll also need to enable the gmail api in the google cloud
console under your firebase project name

that works fine and issues an access token returned worked

https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=accessToken
Enter fullscreen mode Exit fullscreen mode

paste the above into your address bar and replace accessToken with the access token from the response

accessToken is only available on initial signin when inside the
signin function returns object

if it's valid you'll get such a response

{
 "issued_to": "75010101072-jq0gaom2tpgk01t78ffjisvgsgggggg.apps.googleusercontent.com",
  "audience": "75069777777-jq0gaom2fsfsrv78ffjisvgshfafess.apps.googleusercontent.com",
  "user_id": "112901390458597sfstv",
  "scope": "openid https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email https://mail.google.com/",
  "expires_in": 2244,
  "email": "email@gmail.com",
  "verified_email": true,
  "access_type": "online"
}
Enter fullscreen mode Exit fullscreen mode

which is good enough and can be used to send the email pings,
but there's more to the story.
knowing the difference between firebase tokens will come in handy.
for the tl-dr there's 2 types of tokens in authentication

  • accessToken : actual token that authenticates you on the api, very short lived (upto 30 minutes ) for security purposes
  • refreshToken :long lived (upto 200 days) , used to generate another accessToken when one expires without the need for re authentication client side

normally to get a refresh token on authentication , you include

"access_type": "offline"
Enter fullscreen mode Exit fullscreen mode

in the authentication request ,but this is not available for firebase auth client SDK which is a bummer because it would have been perfect to just grab everything at once ant not have to make the user re-authenticate.

The existing alternatives are things like google-signin which which is about to be deprecated in favour of sign in with google provided by their new google identity system , tools like gapi are also wrapped around the former mentioned technology and not advisable since they'll soon be deprecated.

I found a solution to this by bringing in a nodejs server which has its own googleapi library that wraps around their node js client to get a refresh and access token for signed in user but the setup process is tedious
first you'll need to setup your cloud console

  • No need to create a new google cloud project if you already have a firebase project.
  • Set the redirect url to http://localhost:4000/creds ,you'll handle the req.query.code that'll be sent to that route once authenticated and save it to firestore for future use

youtube video explaining how to set up google console (up to 2:04-6:35)

npm install googleapis
Enter fullscreen mode Exit fullscreen mode

then in your auth route

const express = require('express')
const {google} = require('googleapis');
const path = require('path');
const nodemailer = require('nodemailer');


const router=express.Router()

//replace below with your creds, also note that i hard coded the  
//refresh and access token that i got from the response   
//ideally you'd save it somewhere and load it in as a variable  
 //and refetch if it's invalid   

const creds={
  client_email:"email1@gmail.com",
  client_id:"client_id",
  client_secret:"your client secret",
  serveruri: "http://localhost:4000",
  uirui: "http://localhost:3000",
  redirectURL: "http://localhost:4000/auth/creds",
  access_token: 'your access token',
 refresh_token: 'your refresh token',

}

const oauth2Client = new google.auth.OAuth2(
  creds.client_id,
  creds.client_secret,
   creds.redirectURL
);
const scopes = [
  'https://mail.google.com/'
];


const sendMail=async()=>{
  try{
    // Create the email envelope (transport)
    const transport = nodemailer.createTransport({
     service: 'gmail',
     auth: {
       type: 'OAuth2',
       user:creds.client_email,
       clientId: creds.client_id,
       clientSecret: creds.client_secret,
       accessToken: creds.access_tokenfb,

     },
   });

   // Create the email options and body 
   // ('email': user's email and 'name': is the e-book the user wants to receive)
   const mailOptions = {
     from: `FRONT <${creds.client_email}>`,
     to: "email2@gmail.com",
     subject: `[FRONT]- Here is your e-Book!`,
     html: `Enjoy learning!`,

   };

   // Set up the email options and delivering it
   const result = await transport.sendMail(mailOptions);
   console.log("success    === ",result) 
   return result;

   } catch (error) {
  console.log("error sendng mail    === ",error)   
   return error;
   }
}

//default auth route
router.get('/',async(req,res)=>{
 console.log("hit auth route")
res.send("auth route")

})



//route to handle api client authentication
router.get('/google',async(req,res)=>{

const url = oauth2Client.generateAuthUrl({
  // 'online' (default) or 'offline' (gets refresh_token)
  access_type: 'offline',
 // If you only need one scope you can pass it as a string
  scope: scopes
})
console.log("url returned ======= ",url)
//url returned by google to redirect us to the login consent page // page
if(url){
// render an ejs view with a button that redirects to the url
res.render('authorize',{url:url})
}
})


//redirect route that receives the authentication creds and swaps them for access and refresh token 

router.get('/creds',async(req,res)=>{

const code = req.query.code
console.log("query ==== ",code)
//returns access and refresh tokens
const {tokens} = await oauth2Client.getToken(code)
console.log("query token response==== ",tokens)
//perform save to firestore or your db of choice here

//authenticate oauthclient
oauth2Client.setCredentials(tokens);

//render a view to indicate completion
res.render('done')

})




router.get('/mail',async(req,res)=>{
let email=""
await sendMail().then((result)=>email=result).catch((err)=>email=err)
console.log("email sent or error    === ",email)

await res.json(email)  


})


module.exports=router
Enter fullscreen mode Exit fullscreen mode

Check the repo for the complete code

I hope this saves you time in figuring out what approach to take , and also since i already have firebase in place i might as well host this logic into a cloud function that will trigger to authenticate and save refresh token for a new user and another one to send the email.
there's easier options like using a firebase extension or just using nodemailer with an smtp mail client ,but google has numerous apis with generous limits which could supercharge any app you're working on.
If there someone with more experience on this topic i'd very uch like to hear from you

repo link

firebase client sdk google sign in
GCP console
npm googleapis

Discussion (0)