Set up OAuth in your React app with Go and Gin
Using OAuth in web applications offloads managing passwords and credentials for your users, here's a guide to set it up with React and Gin.

Introduction
So this blog post exists mostly as a reminder of what I need to do if I ever need to setup OAuth again with an application since the discovery, trial and error, endless googling, stack overflow articles and countless other blogposts which didn’t really answer the questions I needed answers to get the solution to work. 😅
Here’s a cut of the relevant stack:
React frontend in Typescript
Go backend with Gin web framework, though, it’s not strictly necessary. You can probably get away with std lib if you know your way around
Google OAuth Identity (using the new version, which may have contributed to the difficulty in finding answers)
The approach I’ve taken is as follows:
The user is browsing the site and wants to sign in, so they click on the “Sign in with Google” button.
The user is provided with a popout prompt to sign in with Google.
At the completion of the sign in process with Google, a callback function is fired with the response auth token.
The callback calls our web server, where we validate the request.
Once validated, we mint our own JWT and set it as a cookie which is sent back to the user.
The user then provides that cookie on subsequent requests to access protected routes.
Setup your Google OAuth credentials
This is a pretty straight forward process and just involves a bunch of clicking through various screens to register your app. I’ll leave this part up to the reader, but at the end you’ll get a “client id” or “audience” (from a JWT perspective) that looks something like this:
<number>-<randomstring>.apps.googleusercontent.com
Take a note of this value. You can start the process from here.
Setup the “Sign in with Google button
In your React app, make a component, and then using the sample code Google gives you, add it to your app. It looks something like this:
<div
id="g_id_onload"
data-client_id="YOUR_CLIENT_ID"
data-context="signin"
data-ux_mode="popup"
data-callback="validateToken"
data-auto_prompt="false"
></div>
<div
className="g_id_signin"
data-type="standard"
data-shape="rectangular"
data-theme="outline"
data-text="signin_with"
data-size="large"
data-logo_alignment="left"
></div>
Note the keys in the first div block, the client_id
and the callback
values.
Paste your client id from the previous step into that field, and the callback field is the name of the function that will be fired when the response from the sign in process completes.
Setup your callback function
This part had me stumped for a very long time. After hours of pretty much fruitlessly googling "[GSI_LOGGER] The value of 'callback' is not a function. Configuration ignored."
, I finally worked out that the function that the Google Sign In (GSI) script is trying to call is not in the global scope. So in order to access that function callback that I had to attach it to the window.
In order to that in React, I hacked this together:
window.validateToken = response => {
console.log("Callback fired! Response:", response);
};
This attaches the validateToken
function which I defined as my data-callback
value in the previous step to the global scope of the application, so that the function is available to be called.
It’s worth noting at this stage that you can choose to get a POST callback to your server after a successful auth flow instead of firing a callback function, but this causes a url change which causes the react application to be redirected to your backend, which then relies on your API sending the user back to react which does work, but is a bit clunky and not a great user experience in my opinion, so instead I opted for the in-app callback function.
The response value from the callback function above will be the signed JWT from Google which contains things like the user’s name, email address and profile picture, etc. So now that we have that, we’ll need to send it to our backend.
Inside the validateToken function, let’s make an API call to our backend with the token.
window.validateToken = response => {
void fetch("http://localhost:8080/auth/validate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(response),
});
};
Setup your backend
I’m assuming you’re here because you’ve been building with Gin as your Go backend server, so to get started, you’ll just need to setup a route that your server can serve with a handler.
r.POST("/auth/validate", handler.LoginHandler())
And now the handler:
package handler
import (
"fmt"
"your-backend/server"
"net/http"
"github.com/gin-gonic/gin"
"google.golang.org/api/idtoken"
)
const audience string = "YOUR_CLIENT_ID"
type loginInfo struct {
Credential string `json:"credential"`
}
func LoginHandler() gin.HandlerFunc {
return func(c *gin.Context) {
var loginInfo loginInfo
if err := c.ShouldBindJSON(&loginInfo); err != nil {
server.ErrorLogger("Couldn't convert token to valid struct", err, c)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid token"})
return
}
payload, err := idtoken.Validate(c, loginInfo.Credential, audience)
if err != nil {
server.ErrorLogger("Could not validate sign in token", err, c)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid JWT."})
return
}
// create a JWT for the app and send it back to the client for future requests
tokenString, err := server.MakeJWT(payload.Subject, "secret")
if err != nil {
server.ErrorLogger("Failed to create JWT", err, c)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Something went wrong completing your sign in."})
return
}
c.SetCookie("app_session", tokenString, 86400, "/", "", true, true)
c.Status(http.StatusOK)
}
}
There’s bit going on here, so let me explain:
We’re setting up the audience to be our client, if you’re not familiar with
aud
in JSON Web Tokens (JWTs) then I would recommend reading up on that first.Then we’re setting up a credential value in our login json payload received from the callback function we discussed in the previous step.
Assuming the payload is all good, then we validate the signature using Google’s library, which under the hood makes a call to a Google server gets a signing key and validates the JWT sent to our server from the client is a valid one, that was issued by Google, and not fabricated.
If that’s good, then part of the validation function offered by Google’s library is that the token is also decoded for us.
Then we create a token of our own using the
sub
orSubject
from the token provided to us in the payload variable. The subject in this case is the unique Google user id (not to be confused with an email address).Using the
"github.com/golang-jwt/jwt/v4"
package, we can mint our own JWT which has what we need for our app, and include relevant information from the providers step. In this case we’re just taking the Subject.Finally at the end, we set a cookie response to be used in the response header and return a 200.
Configure middleware to protect your routes
Let’s take a look at how we can protect our routes where we need to validate the user’s cookie to enable only registered users to access our server.
Consider the following AuthenticationRequired middleware function. It takes nothing as an argument, and returns a gin.Handler function. This function takes in the context as c
and assigns a variable token
as the cookie from the context.
package server
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
func AuthenticationRequired() gin.HandlerFunc {
return func(c *gin.Context) {
token, err := c.Cookie("app_session")
if err != nil {
fmt.Println("TOKEN ERROR", err)
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authentication required."})
return
}
err = DecodeJWT(token, "secret")
if err != nil {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Invalid token"})
return
}
c.Next()
}
}
It first checks that the cookie is present, and if not halts further action on that handler by aborting the request with a 401 response. Then if that’s fine, it decodes the JWT, which at this point is just used to validate the token is not forged. If that’s fine, then the middleware allows the request to proceed with a call to Next()
.
But hold up, if you’ve got this far, and you’ve found that the frontend is connecting to the backend and not presenting a cookie when doing so, then I have the answer! I probably lost the most amount of time on this, at least 6 hours I would say. When calling the endpoint to get a cookie, you need to include a very important value, which I had missed somehow.
Here’s what you need to do if you’re running into this problem. Add the credentials: "include",
key/value to the object that get’s POST’ed to the backend when validating the token.
void fetch("http://localhost:8080/auth/validate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include", // Add this line to your request.
body: JSON.stringify(response),
});
Let’s take a look at that DecodeJWT function.
func DecodeJWT(tokenStr string, secretStr string) error {
mySigningKey := []byte(secretStr)
token, err := jwt.ParseWithClaims(tokenStr, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) {
return mySigningKey, nil
})
if err != nil {
fmt.Println("ERROR parsing token", err)
fmt.Println("Token provided", tokenStr)
return err
}
claims := token.Claims.(*jwt.RegisteredClaims)
if claims.Valid() != nil {
return claims.Valid()
}
return nil
}
Two args are required to call the function, the first is the cookie from the request, second is the secret string which was used initially to sign the token when it was created. This is to make sure that the signature is valid and to ensure the cookie that the user is presenting came from us!
If the token is valid we then check if it hasn’t expired with the call to .Valid()
. Finally if all is well, we return nil
to the caller which is the authentication middleware in this case.
Now we’ve protected routes in our app, logged our user in with OAuth via Sign In With Google.
What’s next!?
For me, the next things I need to look at are:
Updating the react application to recognise the user is signed in, and respond. For example, maybe showing a successful sign in toast, or displaying their name.
Setup custom claims on the JWT in our app, so for example, we might have a user that is an admin user, so we’ll need to provide additional claims in the JWT which says that they’re allowed to access certain types of routes.