Coming from an AWS Cloud background - when I hear authentication, I expected to use a service like Amazon Cognito, (or worse salting and hashing!) to authenticate and authorize permissions for users. Building your own would be a pain, and due to my natural aversion of Not Invented Here syndrome — I decided to see if I could change the problem.
Google’s OAuth
Google does two things that help us identify who a user is, and asking that user for a certain set of permissions, called scopes. Scopes can be:
Sending an Email on your behalf
Accessing contacts
Reading files from your google drive
After a google sign in, google will share a authentication code with you. This code can be transformed into a set of tokens that control access to the authorized scopes:
{
"access_token": "ya29.a0AfH6SMA_example_access_token",
"expires_in": 3599,
"refresh_token": "1//0gX9_example_refresh_token",
"scope": "openid email profile",
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE2a2_example_id_token"
}
Please take a look at the id_token. This field actually is a Javascript Web Token JWT payload that can be decoded into critical information:
{
"iss": "accounts.google.com",
"sub": "1234567890",
"aud": "your-app-client-id.apps.googleusercontent.com",
"exp": 1697654400,
"iat": 1697650800,
"email": "[email protected]",
"email_verified": true,
"name": "John Doe",
"picture": "https://lh3.googleusercontent.com/a-/...",
"given_name": "John",
"family_name": "Doe",
"locale": "en",
"hd": "example.com"
}If we were to ask google to verify the token, we now can use Google to Authorize these requests to have confidence that the requestor is who they say they are.
A Quick Overview of Google Sign In
To this day Google Sign in has given me trouble, but the flow we can use:
A Customer will click “Sign in With Google”
They will be brought through some authentication/authorization process
Google will redirect to some URL with a one-time credential
Your redirect page will communicate with some backend that transforms the onetime credential into a set of tokens.
You can keep track of the customer’s tokens to allow them to have a session, and create new sessions.
Note: Keeping track of sessions, requires the client side code to decode the JWT, and be aware of the expiration date.
So What?
Now with this knowledge, we can ask Google to verify messages coming to our backend via middleware. We will just need to keep the id_token on the client side and pass it in as a header when we send requests.
The following code is written for express.js, and it takes a request with an authorization header, gets the Id from google via a google verify. Finally the code will place the id into the response.locals (a good place to put information about the request that may be used later). You can then use this for access control:
import { NextFunction, Request, Response } from "express";
export async function verifyViaGoogle(
req: Request,
res: Response,
next: NextFunction
) {
if (!req.headers["authorization"]) {
next();
return;
}
try {
const client = new OAuth2Client();
const ticket = await client.verifyIdToken({
idToken: req.headers["authorization"] as string,
audience: process.env.GOOGLE_CLIENT_ID as string,
});
const payload = ticket.getPayload();
if (!payload) {
next();
return;
}
const userid = payload["sub"] as string;
res.locals.username = userid;
} catch (error) {
console.log(error);
res.status(401).json({ message: "Invalid Token" });
return;
}
next();
}Refreshing the Token
Some people keep refresh tokens in the client side, or in the server side. While there are pros and cons to both, I’m going to share an example that is for server side, that can easily be adapted for client side.
When a token expires, we need to allow our customer to refresh it (preferable by not re-logging in). You can write a quick little handler that leverages a google verify, and brokers a new set of tokens:
const refreshAuthToken = async (refreshToken: string) => {
const response = await axios.post(
"https://oauth2.googleapis.com/token",
null,
{
params: {
client_id: this.googleClientId,
client_secret: this.googleSecret,
refresh_token: refreshToken,
grant_type: "refresh_token",
},
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
}
);
return { ...response.data, refresh_token: refreshToken };
}Serve the id_token back to the client, and have them store it. I want to be very clear that the set of data you get back when using the authorization code has information like your google photo and a few other fields, when refreshing, you get a subset of these fields. This has burned me before.
Client Side
I recommend storing your tokens in the browser storage. (I am not a security engineer) It seems safe there. You can then use interceptors on your HTTP call libraries to automatically refresh tokens when your verification fails.
Take a look at the interceptor below:
const intercept = (axiosInstance: AxiosInstance) => {
axiosInstance.interceptors.response.use(
(response) => response, // Let successful responses pass through
async (error: AxiosError) => {
if (error.response?.status === 401) {
try {
// Call the AuthService to refresh the token
const newTokens = await requestTokenFromBackend();
// Get the Headers from the Previous Request and Retry.
if (config) {
config.headers = {
...config.headers,
Authorization: newTokens.idToken, // Update token
} as any;
return axiosInstance.request(config); // Retry the request
}
} catch (refreshError) {
// If the refresh fails, reject the promise
return Promise.reject(refreshError);
}
}
return Promise.reject(error); // Reject other errors
}
);
};At a high level, we are sniffing the axios transport responses for 401s, and if we find one, we will get a new token from the backend and instantly retry. We’re all wired up now!
Conclusion
A lot of time when I see a problem that is tedious or obnoxiuous to solve, I am solving the wrong problem. Instead of building out my own solution, I take a step back and ask how can I solve this another way, or even perhaps minimally.
This has saved me many headaches, and has helped me deliver software that places customer experience first.
Next time you want to build a SAAS app, ask yourself really if you need to create your own authentication and authorization.