tutorial // Aug 09, 2022
How to Implement OAuth2 for Google Accounts in Node.js
How to implement OAuth2 login via Google using authorization links and retrieving profile information from the Google User Info API.
For this tutorial, we're going to use CheatCode's full-stack JavaScript framework, Joystick. Joystick brings together a front-end UI framework with a Node.js back-end for building apps.
To begin, we'll want to install Joystick via NPM. Make sure you're using Node.js 16+ before installing to ensure compatibility (give this tutorial a read first if you need to learn how to install Node.js or run multiple versions on your computer):
Terminal
npm i -g @joystick.js/cli
This will install Joystick globally on your computer. Once installed, next, let's create a fresh project:
Terminal
joystick create app
After a few seconds, you will see a message logged out to cd
into your new project and run joystick start
. Before we do that, we need to install two additional packages, googleapis
and node-fetch
:
Terminal
cd app && npm i googleapis node-fetch
After those packages are installed, you can go ahead and start up your app:
Terminal
joystick start
After this, your app should be running and we're ready to get started.
Getting Google credentials
In order to complete this tutorial, first, we'll need to head over to the Google Developer Console and generate credentials for authenticating our app with Google. To do it, head over to the Google Developer Console and login using the account where your project lives (or will live).
Once you're logged in, from the hamburger icon in the top-left, open the flyout navigation menu and select "API & Services." Next, select the "Credentials" navigation link in the left-hand menu. From this page, if you already have existing credentials you'd like to use, locate them in the list under the "OAuth 2.0 Client IDs" heading and then continue to the next section.
If you don't have credentials yet, make sure you've selected your project in the drop-down menu to the right of the Google Cloud logo in the navigation bar. If you don't have a project yet, you will need to create one to continue.
With your project created and selected, from the "Credentials" page we loaded above, click the blue "+ Create Credentials" link near the top of the page. This will reveal a dropdown menu. We want to click the second option "OAuth client ID."
On the next page, if you haven't already configured it, you will be prompted to "configure your consent screen." This is the screen that users are immediately redirected to after clicking the "Login with Google" button in your app. If you haven't already configured this, click the button next to the warning message and complete the consent screen.
On the next screen, if you're just testing things out, you will want to use the "External" option for the "User Type." This will ensure that any Google account can be used for your login (here, in development, but also in production).
Once this is set, you will be redirected to a form to configure your consent screen. In the first step, we'll provide some basic information about our app. For this tutorial, under the "App domain" section, we're just entering http://localhost:2600
for the "Application Home Page" and http://localhost:2600/privacy
and http://localhost:2600/terms
for the privacy and terms URLs, respectively. Of note, we're skipping "Authorized domains" here.
On the next screen, "Scopes," we can skip this as we'll be passing the scopes we need directly to Google via our code. Finally, on the summary screen, verify that everything looks correct and then click "Back to Dashboard" at the bottom of the page.
From this screen, you will want to scroll down and locate the "Test users" section. Here, we want to add Google users that will be able to use our OAuth login flow in testing (required as we're currently in test mode).
Once you've added your test user, now we can go back to creating our OAuth credentials. Head back to the "Credentials" page under "APIs & Services" and click the blue "+ Create Credentials" link, again selecting the "OAuth client ID" option in the drop down menu.
On the next screen, for "Application type" we want to select "Web application," enter the name of our app under "Name," and under "Authorized redirect URIs" add the URL where Google will redirect the user after they approve our access to their account on the consent screen.
For this tutorial, we're using http://localhost:2600/oauth/google
where the /oauth/google
part will be the route we'll wire up later to call the handler function that will exchange the temporary token Google sends us for a permanent access token associated with the user's account.
After this is filled out, click the "Create" button at the bottom of the screen. This will reveal a popup with "Your Client ID" and "Your Secret." Note: it's recommended that you store these somewhere secure like a password manager before moving forward.
Once you have these, next, we want to copy these keys over to the settings for our app so we can get started with the code portion of the tutorial.
Adding your Google credentials to your app's settings
Before we dig into the code, first, we want to add the credentials we just got from Google to our app's settings (this will make them easily and securely accessible in our code). In the project we created via joystick create app
earlier, open up the settings.development.json
file:
/settings.development.json
{
"config": {
"databases": [
{
"provider": "mongodb",
"users": true,
"options": {}
}
],
"i18n": {
"defaultLanguage": "en-US"
},
"middleware": {},
"email": {
"from": "",
"smtp": {
"host": "",
"port": 587,
"username": "",
"password": ""
}
}
},
"global": {
"google": {
"clientId": "348181960606-aqmbd10e22qd1lru9nc41ehn4ranrq8e.apps.googleusercontent.com",
"redirectURI": "http://localhost:2600/oauth/google"
}
},
"public": {},
"private": {
"google": {
"secret": "<Paste your secret here>"
}
}
}
In this file, first, under the global
object, we want to add an object at the key google
which contains two properties: clientId
and redirectURI
. Here, clientId
is the value copied from the "Your Client ID" box above while redirectURI
is the URL we entered for the "Authorized redirect URI[s]" above.
We put this under global
here as we want this information accessible globally in our app (meaning, in the browser and on the server). Notice, though, that we've omitted the "Your Secret" value here.
We're adding that value down in the private
object, again creating a google
object and on that object, setting secret
as a key and assigning the value to the "Your Secret" we copied on the Google dashboard. As you might have guessed, private
here is isolated only to the server side of our application (not accessible to the public or anyone else but ourselves and our server-side code).
With all of that out of the way, now, we're finally ready to dig into the code.
Wiring up a getter for generating the OAuth login link
Unlike most OAuth2 implementations, Google is a bit different in how they handle the initial redirect for users. Where most APIs will offer a direct URL to redirect to (with some query params), Google prefers that you use their API to generate the redirect URL first and then send users to that URL.
To do this in our app, we'll need to wire up a way to generate that URL. We're going to use Joystick's getters feature to help us do this. Getters are a short-hand way to wire up a JSON-RPC API in your app (you write functions and Joystick automatically maps them to HTTP GET routes like /api/_getters/myGetterName
on your server).
In the /api
folder created for you at the root of your app, we want to add another folder oauth
and in there, a file called getters.js
:
/api/oauth/getters.js
import { google } from 'googleapis';
import joystick from '@joystick.js/node';
export default {
googleOAuthPermissionURL: {
get: (input = {}, context = {}) => {
const oauth2Client = new google.auth.OAuth2(
joystick?.settings?.global?.google?.clientId,
joystick?.settings?.private?.google?.secret,
joystick?.settings?.global?.google?.redirectURI,
);
return oauth2Client.generateAuthUrl({
// NOTE: Passing 'offline' retrieves a refresh_token but we shouldn't need this for logins.
access_type: 'online',
scope: [
'profile',
'email'
],
// NOTE: State is a generic "metadata" field that allows us to attach identifying
state: JSON.stringify({}),
});
},
},
};
Above, we've added all of the code we'll need to generate the initial OAuth consent URL that we'll redirect our users to. To do it, we define a getter called googleOAuthPermissionURL
. In Joystick, a getter is just an object assigned to a key that represents the name of the getter we want to define. On that object, we at the very least must assign a function get()
which, like the name implies, "gets" some data when our getter is called.
Behind the scenes, Joystick maps our getter name to a route at /api/_getters/googleOAuthPermissionURL
. As we'll see on the client, we'll use a special function in Joystick's UI framework @joystick.js/ui
called get()
which calls a getter. Behind the scenes, this just makes an HTTP GET request to this dynamically generated route. When that route is matched on the server, the get()
function that we're defining for our getter above is called.
To that function, we expect to pass any input
included when calling get()
in our UI as the first argument, and as the second, the context
for the request which includes the HTTP req
uest object, the currently logged in user (if they exist), and other metadata related to the request.
Here, inside of our get()
function for googleOAuthPermissionURL
, we begin by making a call to new google.auth.OAuth2()
and storing its return value in a variable oauth2Client
. To access this, we're importing the named export google
(denoted by the curly braces surrounding google
in our import statement) from the googleapis
package we installed at the start of the tutorial.
To that function—technically, a class constructor—we pass three arguments:
- Our application's Client ID.
- Our application's Secret.
- Our application's Redirect URI.
To access those values, we pull them from the settings file we added them to earlier via the joystick.settings
object accessible via the default joystick
export from the @joystick.js/node
package (the "server side" counterpart to @joystick.js/ui
, installed when we ran joystick create app
earlier).
Pay close attention to the paths here. Remember that our secret
was stored in the private
object while our clientId
and redirectURIwere stored in the
global` object.
Next, at the bottom of our get()
function, we return a call to oauth2Client.generateAuthUrl()
. To that, we pass an options object with three properties on it:
access_type
which is set toonline
. This tells Google that we want to generate a single use access token, not a long-lived one (that's all we need for account access). If we passoffline
here, Google will include a refresh token which allows us to update the access token when it expires after the allotted lifetime (useful if we're going to be connecting to a Google account to perform API functions on the user's behalf).scope
which is set to an array of strings containing API scopes (permissions about what we're allowed to access on the user's account). Fair warning: Google has a ton of scopes available.state
which is an optional, string value (here we're providing an example of stringifying an object of multiple values) that allows us to pass identifying information along with the request. Because the initial user request is disconnected from the token exchange, thestate
value gives us a way to identify which token exchange request belongs to which user (if necessary).
That's all we need to do. Now, when we call this getter, a URL to redirect our user to will be returned.
Real quick, to make sure this works, we need to import this getters file and attach it to our API's schema located in /api/index.js
:
/api/index.js
import oauthGetters from './oauth/getters';
export default {
getters: {
...oauthGetters,
},
setters: {},
};
Here, we're just using the JavaScript spread ...
operator to "spread out" or "unpack" the contents of the object exported as the default from /api/oauth/getters.js
onto the main getters
object of our API's schema. This schema object is handed to the startup function for our server in /index.server.js
as api
which ultimately registers all of our getters and setters as routes on our server.
Adding a route and handler function for the OAuth token exchange
Before we move to the client to put our getter to use—to save some time and confusion—we're going to wire up the route that Google will redirect the user to for the token exchange process along with the function that will handle that process (and get our user's profile data).
/index.server.js
import node from "@joystick.js/node";
import api from "./api";
import google from "./api/oauth/google";
node.app({
api,
routes: {
"/": (req, res) => {
res.render("ui/pages/index/index.js", {
layout: "ui/layouts/app/index.js",
});
},
"/oauth/google": (req, res) => {
google({ req, res });
},
"*": (req, res) => {
res.render("ui/pages/error/index.js", {
layout: "ui/layouts/app/index.js",
props: {
statusCode: 404,
},
});
},
},
});
Here, we've added a route /oauth/google
which will receive an HTTP GET request from Google if and when our user approves the authorization request at the URL we learned how to generate above.
When we receive that request from Google, like we hinted at above, we need to exchange a temporary token that they include in the query params of the request for a permanent access token. This is how the OAuth2 standard (used by many different companies to handle third-party user authentication) works.
- We redirect the user to the third-party provider with details about what permissions we'd like to be granted in relation to their account.
- If the user approves those permissions, the third-party provider sends a request to a URL we specify, including a temporary token that can be exchanged for a permanent token.
- We call another API endpoint, passing that temporary token along with the credentials we used when initiating the request (proof that we're the intended app receiving permission for the user) to get the permanent access token.
All providers are a bit different in how they handle the specifics, but generally speaking: this is the workflow that takes place. To handle the request from Google, above, inside of our route's handler function we've made a call to a hypothetical function google()
passing in an object that contains the req
uest and res
ponse objects from our route.
Next, let's wire up that function (pay attention to the hypothetical path we used when importing the function at the top of the file) and get this working.
/api/oauth/google.js
/* eslint-disable consistent-return */
import joystick from '@joystick.js/node';
import { google as googleAPI } from 'googleapis';
const oauth2Client = new googleAPI.auth.OAuth2(
joystick?.settings?.global?.google?.clientId,
joystick?.settings?.private?.google?.secret,
joystick?.settings?.global?.google?.redirectURI,
);
const getGoogleUser = (accessToken = '') => { ... };
const exchangeToken = async (code = '') => { ... };
export default async (options) => {
try {
const state = options?.req?.query?.state ? JSON.parse(options?.req?.query?.state) : null;
const token = await exchangeToken(options?.req?.query?.code);
const access_token = token?.access_token;
const googleUser = await getGoogleUser(access_token);
console.log({
state,
token,
access_token,
googleUser,
});
options.res.redirect('/');
} catch (exception) {
options.res.status(500).send(`[google] ${exception.message}`);
}
};
First, at the top of our file, notice that we're again importing the named google
export from the googleapis
package, however, this time we're renaming that named variable from google
to googleAPI
using the as
operator to avoid name-collisions in our file.
Next, identical to what we saw when setting up our getter earlier, we're calling to new googleAPI.auth.OAuth2()
at the top of our file, passing in the exact same credentials as before (in the exact same order, too). Just like before, this is giving us an instance of the Google OAuth2 API in our code.
Before we put it to use, down in the function exported as the default
from our file, we've mapped out the calls we're going to need to make to handle the token exchange and get our user's profile data. To handle any unexpected errors, we've wrapped the body of our function in a try/catch
statement. In the event that any of our code "catches," we call to the .status().send()
method on the options.res
value we anticipate being passed into the function at call time. Here, the 500
being passed to status()
is the HTTP status code for a generic "Internal Server Error." To send()
, we just pass a string containing any error message we may have received.
Inside of the try
, we begin by checking to see if any state
value was passed along with our request. Remember, when we generated our authorization request URL earlier, we included a stringified object as state
that we can use to identify the request.
Here, we check to see if state
is defined in the query
params of the req
uest object and if it is, assume it contains a stringified JSON object that we need to parse into a JavaScript object with JSON.parse()
. If it's not defined, we just want to set the const state
variable we're creating here to null
.
Next, we make a call to a function we'll define next exchangeToken()
, passing in the code
params from req?.query
(an object containing all of the query params from the request URL). Here, code
is the token that we need to exchange with Google to get back the permanent access token for our user.
Skipping ahead a bit, after we've completed this exchange process, and have an access_token
(we expect to get back an object with multiple parameters from Google that we're storing in the variable const token
here), next we'll want to take that access_token
and call to Google's API endpoint for retrieving a user's profile.
The idea here is that we do not expect the token exchange to do anything but give us an access token. In order to contextualize that token, we should (but don't have to) get the associated profile data for the user so we can use it in our app for identification purposes.
Finally, once we have our user's credentials and profile data, we log it out (we're not going to do anything special with the data for this tutorial, just showing how to retrieve it) and then call the res.redirect()
function, redirecting the user/browser back to the root of our app.
To make sense of this, let's build out those two functions: exchangeToken()
and getGoogleUser()
.
Handling token exchange
The good news about token exchange is that using the API wrapper provided in the googleapis
package, it's quit simple:
/api/oauth/google.js
/* eslint-disable consistent-return */
import joystick from '@joystick.js/node';
import { google as googleAPI } from 'googleapis';
const oauth2Client = new googleAPI.auth.OAuth2(...);
const getGoogleUser = (accessToken = '') => {...};
const exchangeToken = async (code = '') => {
try {
const { tokens } = await oauth2Client.getToken(code);
return tokens;
} catch (exception) {
throw new Error(`[google.exchangeToken] ${exception.message}`);
}
};
export default async (options) => {
try {
...
const token = await exchangeToken(options?.req?.query?.code);
...
options.res.redirect('/');
} catch (exception) {
options.res.status(500).send(`[google] ${exception.message}`);
}
};
Here, retrieving the permanent access token just requires us to call the .getToken()
method of the oauth2Client
object we initialized at the top of our file, passing in the code
we plucked from the query params of the request from Google.
In response to that function call, we expect to get back an object with multiple properties. Here, we care about the tokens
property, so we use JavaScript object destructuring to "pluck off" the property we want from that returned object as a variable tokens
which we then return from exchangeToken()
.
Next, with our access token, let's take a look at getting our user's profile data.
Retrieving user profile data
For this step, we're going to use the node-fetch
library that we installed earlier to talk directly to Google's /userinfo
API endpoint.
/api/oauth/google.js
/* eslint-disable consistent-return */
import fetch from "node-fetch";
import { URL, URLSearchParams } from 'url';
import joystick from '@joystick.js/node';
import { google as googleAPI } from 'googleapis';
const oauth2Client = new googleAPI.auth.OAuth2(...);
const getGoogleUser = (accessToken = '') => {
try {
const url = new URL(`https://www.googleapis.com/oauth2/v1/userinfo`);
const searchParams = new URLSearchParams({
alt: 'json',
access_token: accessToken,
});
url.search = searchParams;
return fetch(url, {
method: 'GET',
}).then(async (response) => {
const json = await response.json();
return json;
});
} catch (exception) {
throw new Error(`[google.getGoogleUser] ${exception.message}`);
}
};
const exchangeToken = async (code = '') => { ... };
export default async (options) => {
try {
const state = options?.req?.query?.state ? JSON.parse(options?.req?.query?.state) : null;
const token = await exchangeToken(options?.req?.query?.code);
const access_token = token?.access_token;
const googleUser = await getGoogleUser(access_token);
console.log({
state,
token,
access_token,
googleUser,
});
options.res.redirect('/');
} catch (exception) {
options.res.status(500).send(`[google] ${exception.message}`);
}
};
First, at the top of our file, we've added a few imports. First, we've imported the node-fetch
library we installed earlier as fetch
and from the built-in Node.js library url
, we've imported the named exports URL
and URLSearchParams
.
Down in getGoogleUser()
we put all of these to work. First, we create a new URL()
object, passing in the URL for Google's /userinfo
endpoint and storing this in the variable const url
. Next, we create another variable searchParams
which stores the value of a call to new URLSearchParams()
. That constructor function takes in an object of key/value pairs that we want to convert into URL parameters.
Here, we're specifying alt
as json
which is the type of data we want to receive back for the user's profile and access_token
which is set to the access_token
we just retrieved via exchangeToken()
.
Next, on the url
object we received from new URL()
, we assign a .search
property dynamically, assigning it to the value we just stored in searchParams
. This creates a complete URL object that we can next hand off to fetch()
to define the URL we want to get or "fetch."
To fetch()
, as the first argument we pass in that url
object and as the second, we pass in an options object with a single property method
set to GET
(technically unnecessary as the default request method for fetch()
is GET but this removes some obscurity in our code).
Because we expect fetch()
to return a JavaScript Promise, we chain on a call to .then()
to handle a successful response from the API. To .then()
, we pass a callback function, appending the async
keyword so that we can use await
inside without triggering a syntax error in JavaScript. That function receives the raw response object from fetch()
.
Because we told Google that we want a JSON response, we need to call to the .json()
method on the response
object (which itself returns a Promise). To keep our code clean, we use the await
keyword instead of chaining on another .then()
call. If all goes well, we store the response in our variable const json
and then return it from our function.
Based on how this is written, we expect the user profile object we just got back from Google in that json
variable to "bubble up" to the return fetch()
call which will then be returned from getGoogleUser
.
That should do it! Now, we have all of the data we need to fill out our console.log()
(and implement our custom logic for storing that data).
Next, to wrap up, we need to move down to the client and call our original googleOAuthPermissionURL
getter to kick-off the OAuth login process.
Calling OAuth login from the client/browser
The last part is easy. Now, we're going to wire up an example button in our UI to kick-off the OAuth login process and test this all out. Let's open up the existing page component already wired to our router at /ui/pages/index/index.js
and replace the contents with the following:
/ui/pages/index/index.js
import ui, { get } from '@joystick.js/ui';
const Index = ui.component({
events: {
'click button': () => {
get('googleOAuthPermissionURL').then((oauthLoginURL) => {
window.location = oauthLoginURL;
});
},
},
render: () => {
return `
<div>
<button>Login with Google</button>
</div>
`;
},
});
export default Index;
This is all we need. Up top, we import two things from the @joystick.js/ui
package automatically installed when we ran joystick create app
at the start of the tutorial:
- A default export
ui
which contains the main component API for Joystick. - A named export
get
which is theget()
function used to call getters on our API.
A component in Joystick is created by calling ui.component()
, passing in an options object with a property render
set to a function (bare minimum component). For our render()
function, all we're doing here is passing a string of HTML with a <div></div>
tag which contains a <button></button>
that we can click to fire our login request.
Above render()
, we've added another property events
where we can attach event listeners to the HTML rendered by our render()
function. Because we're just testing things out, we can get away with a simple event listener for a click
event on our button
element (event listeners are defined using the scheme <eventToListenFor> <SelectorToListenOn>
.
To that property click button
, we've assigned a function to call when a click event is detected on our button. Inside, we're calling to the get()
function we imported up top, passing in the name of the getter we defined earlier in the tutorial: 'googleOAuthPermissionURL'
. Remember: we don't need to pass anything up to this getter, we just expect it to return a URL that we can redirect our user to.
Because we expect the get()
function here on the client to return a JavaScript Promise, we chain on a call to .then()
and to it, pass a callback function to fire once our getter responds, receiving a single argument oauthLoginURL
. Because we expect this to just be a URL as a string that we want to redirect the user to, we can just set window.location
equal to that value and the browser will automatically redirect the user to that URL.
That's it! From here, if we've done all of our wiring correctly, when we click the button, we should be redirected to Google where we can approve access (remember to use the test account you listed in the developer console earlier to avoid any issues), and then, redirected back to our app. If everything worked as expected, we should see the user's credentials and profile logged out to the server console (your terminal window) and redirected back to the index page (http://localhost:2600/
) in the browser.
Wrapping up
In this tutorial, we learned how to wire up an OAuth login flow to Google. First, we learned how to generate an OAuth client ID and secret on the Google developer console, as well as how to configure the consent screen that users see when they initially request to login with Google.
Next, we learned how to wire up the getter endpoint that generates the redirect URL for Google and then, how to wire up the token exchange process to trade our temporary OAuth token for a permanent access token. We also learned how to get a user's data via the Google API using Fetch, passing the access token we retrieved from the login to get the user's profile data.
Finally, we learned how to wire up a simple component with a click event for our button, calling our getter and dynamically redirecting the user to the generated URL to complete the login request.