This article will teach you how to integrate GitHub OAuth Authentication into your Vue.js, MongoDB, and Golang applications.
Also, after the user is authenticated, we will generate and return JSON Web Tokens to the user’s browser or client as HTTPOnly cookies.
Vue.js, MongoDB, and Golang Series:
- GitHub OAuth Authentication Vuejs, MongoDB and Golang
- Google OAuth Authentication Vue.js and Node.js (No Passport)
- GitHub OAuth Authentication Vue.js and Node.js (No Passport)
Related Articles:
- Google OAuth Authentication React.js and Node.js(No Passport)
- GitHub OAuth Authentication React.js and Node.js(No Passport)
- How to Implement GitHub OAuth in React.js
- How to Implement Google OAuth2 in React.js
Prerequisites
- Fundamental knowledge of HTML, CSS, Vue.js, and Golang is required.
- Must have Golang installed on your computer.
Create a New OAuth App on GitHub
To begin, sign in to your GitHub account. At the top-right corner, click on your profile and select “Settings” from the dropdown menu.
On the profile settings page, scroll down and click on “Developer settings” from the left sidebar.
Next, on the Developer settings screen select OAuth Apps and click on the “New OAuth App” button on the right side.
Now, provide the required data needed for the OAuth app and click on the “Register application” button.
The authorization callback URL should be a route on the Golang server. It’s similar to the Google OAuth integration in the previous article.
After GitHub has registered the OAuth app, click on the “Generate a new client secret” button to generate the client secret key.
For security reasons, GitHub might redirect you to provide your password again so don’t freak out.
Open the .env
file in your server folder and add the generated client secret, client ID, and the authorized callback URL.
.env
GITHUB_OAUTH_CLIENT_ID=your client Id here
GITHUB_OAUTH_CLIENT_SECRET=your client secret here
GITHUB_OAUTH_REDIRECT_URL=http://localhost:8000/api/sessions/oauth/github
Also, don’t forget to add the credentials to the .env.local
file in the Vue.js app.
VITE_GITHUB_OAUTH_CLIENT_ID=your client Id here
VITE_GITHUB_OAUTH_CLIENT_SECRET=your client secret here
VITE_GITHUB_OAUTH_REDIRECT_URL=http://localhost:8000/api/sessions/oauth/github
Note: use
Vue_App_
prefix if you created the Vue.js app with the@vue/cli
.
Generate the Consent Screen URI in Vue.js
Now that we have acquired the necessary credentials, we are now ready to implement the GitHub OAuth in our Vue.js application.
To begin, we need to create a helper function to generate the GitHub OAuth consent screen URL using the client ID, the selected scopes, and the authorization callback URL.
Adding the scopes to the consent screen URL will instruct GitHub to grant us read access to the resources listed in the scopes.
You can read more about the various scopes you can add on the GitHub OAuth official documentation.
src/utils/getGithubUrl.js
export function getGitHubUrl(from) {
const rootURl = 'https://github.com/login/oauth/authorize';
const options = {
client_id: import.meta.env.VITE_GITHUB_OAUTH_CLIENT_ID,
redirect_uri: import.meta.env.VITE_GITHUB_OAUTH_REDIRECT_URL,
scope: 'user:email',
state: from,
};
const qs = new URLSearchParams(options);
return `${rootURl}?${qs.toString()}`;
}
Create a Simple GitHub OAuth Button with Vue.js
Now, let’s create a simple GitHub OAuth button with Vue.js and bind getGitHubUrl(from)
to the href
.
src/App.vue
<script setup>
import GoogleLogo from './assets/google.svg';
import GitHubLogo from './assets/github.svg';
import { getGoogleUrl } from './utils/getGoogleUrl';
import { getGitHubUrl } from './utils/getGitHubUrl';
const from = '/';
</script>
<template>
<div class="container">
<div class="social-auth">
<!-- Google OAuth -->
<a :href="getGoogleUrl(from)" class="auth-btn google-auth">
<img :src="GoogleLogo" alt="Google Logo" />
<span>Google</span>
</a>
<!-- GitHub OAuth -->
<a :href="getGitHubUrl(from)" class="auth-btn github-auth">
<img :src="GitHubLogo" alt="GitHub Logo" />
<span>Google</span>
</a>
</div>
</div>
</template>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
a {
text-decoration: none;
color: inherit;
}
html {
font-size: 62.5%;
}
body {
font-family: Roboto, sans-serif;
color: #222;
font-size: 1.6rem;
}
.container {
background-color: #2363eb;
height: 100vh;
width: 100vw;
display: flex;
align-items: center;
justify-content: center;
}
.social-auth {
max-width: 27rem;
width: 100%;
display: flex;
align-items: center;
flex-direction: column;
}
.auth-btn {
background-color: #fff;
border-radius: 5px;
padding: 0.6rem 0;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease-in-out;
}
.auth-btn img {
height: 4rem;
margin-right: 1rem;
}
.auth-btn span {
font-size: 1.8rem;
}
.auth-btn:hover {
box-shadow: 0 1px 13px 0 rgb(0 0 0 / 15%);
}
.auth-btn.google-auth {
margin-bottom: 1.5rem;
}
</style>
Now when you click on the GitHub OAuth button, you will be redirected to the consent screen where you can click on the green authorize button assuming you’ve already logged into your GitHub account. Otherwise, you will need to provide your email and password.
After signing in with your email and password or when you click on the authorize button, a GET request will be made to the Golang server. The server should return a 404 error assuming it is running.
The most exciting part of the authorized callback URL is the code in the query string. Later, we’ll make a POST request with the code on the server to obtain an access token.
Implement the GitHub OAuth in Golang and MongoDB
Now open the config/default.go
file and add the generated GitHub OAuth client ID, client secret, the Vue.js origin URL, and the authorized callback URL to the Config struct.
Including the environment variables in the Config struct will enable the Viper package to load and make them available in the project.
go get github.com/spf13/viper
config/default.go
type Config struct {
DBUri string `mapstructure:"MONGODB_LOCAL_URI"`
RedisUri string `mapstructure:"REDIS_URL"`
Port string `mapstructure:"PORT"`
ClientOrigin string `mapstructure:"CLIENT_ORIGIN"`
AccessTokenPrivateKey string `mapstructure:"ACCESS_TOKEN_PRIVATE_KEY"`
AccessTokenPublicKey string `mapstructure:"ACCESS_TOKEN_PUBLIC_KEY"`
RefreshTokenPrivateKey string `mapstructure:"REFRESH_TOKEN_PRIVATE_KEY"`
RefreshTokenPublicKey string `mapstructure:"REFRESH_TOKEN_PUBLIC_KEY"`
AccessTokenExpiresIn time.Duration `mapstructure:"ACCESS_TOKEN_EXPIRED_IN"`
RefreshTokenExpiresIn time.Duration `mapstructure:"REFRESH_TOKEN_EXPIRED_IN"`
AccessTokenMaxAge int `mapstructure:"ACCESS_TOKEN_MAXAGE"`
RefreshTokenMaxAge int `mapstructure:"REFRESH_TOKEN_MAXAGE"`
GoogleClientID string `mapstructure:"GOOGLE_OAUTH_CLIENT_ID"`
GoogleClientSecret string `mapstructure:"GOOGLE_OAUTH_CLIENT_SECRET"`
GoogleOAuthRedirectUrl string `mapstructure:"GOOGLE_OAUTH_REDIRECT_URL"`
GitHubClientID string `mapstructure:"GITHUB_OAUTH_CLIENT_ID"`
GitHubClientSecret string `mapstructure:"GITHUB_OAUTH_CLIENT_SECRET"`
GitHubOAuthRedirectUrl string `mapstructure:"GITHUB_OAUTH_REDIRECT_URL"`
}
func LoadConfig(path string) (config Config, err error) {
viper.AddConfigPath(path)
viper.SetConfigType("env")
viper.SetConfigName("app")
viper.AutomaticEnv()
err = viper.ReadInConfig()
if err != nil {
return
}
err = viper.Unmarshal(&config)
return
}
Get the GitHub OAuth Access Token and User’s Credentials
Now let’s create a utils/githubOAuth.go
file in the root directory and add these two functions:
GetGitHubOauthToken()
– Makes a POST request to retrieve the OAuth access token from GitHub.GetGitHubUser()
– Makes a GET request with the access token to obtain the user’s profile information.
utils/githubOAuth.go
type GitHubOauthToken struct {
Access_token string
}
type GitHubUserResult struct {
Name string
Photo string
Email string
}
func GetGitHubOauthToken(code string) (*GitHubOauthToken, error) {
const rootURl = "https://github.com/login/oauth/access_token"
config, _ := config.LoadConfig(".")
values := url.Values{}
values.Add("code", code)
values.Add("client_id", config.GitHubClientID)
values.Add("client_secret", config.GitHubClientSecret)
query := values.Encode()
queryString := fmt.Sprintf("%s?%s", rootURl, bytes.NewBufferString(query))
req, err := http.NewRequest("POST", queryString, nil)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
client := http.Client{
Timeout: time.Second * 30,
}
res, err := client.Do(req)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
return nil, errors.New("could not retrieve token")
}
resBody, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
parsedQuery, err := url.ParseQuery(string(resBody))
if err != nil {
return nil, err
}
tokenBody := &GitHubOauthToken{
Access_token: parsedQuery["access_token"][0],
}
return tokenBody, nil
}
func GetGitHubUser(access_token string) (*GitHubUserResult, error) {
rootUrl := "https://api.github.com/user"
req, err := http.NewRequest("GET", rootUrl, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", access_token))
client := http.Client{
Timeout: time.Second * 30,
}
res, err := client.Do(req)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
return nil, errors.New("could not retrieve user")
}
resBody, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
var GitHubUserRes map[string]interface{}
if err := json.Unmarshal(resBody, &GitHubUserRes); err != nil {
return nil, err
}
userBody := &GitHubUserResult{
Email: GitHubUserRes["email"].(string),
Name: GitHubUserRes["login"].(string),
Photo: GitHubUserRes["avatar_url"].(string),
}
return userBody, nil
}
Update the User Structs
If you are coming from a previous article in this series then update the user structs in the models/user.model.go
file to have the Photo
and Provider
fields.
The logic here is when a user creates an account using GitHub OAuth, the provider
field will be set to GitHub
. However, a user who registers the account with email and password will have the provider
field set to local
.
models/user.model.ts
type SignUpInput struct {
Name string `json:"name" bson:"name" binding:"required"`
Email string `json:"email" bson:"email" binding:"required"`
Password string `json:"password" bson:"password" binding:"required,min=8"`
PasswordConfirm string `json:"passwordConfirm" bson:"passwordConfirm,omitempty" binding:"required"`
Role string `json:"role" bson:"role"`
Provider string `json:"provider,omitempty" bson:"provider,omitempty"`
Photo string `json:"photo,omitempty" bson:"photo,omitempty"`
Verified bool `json:"verified" bson:"verified"`
CreatedAt time.Time `json:"created_at" bson:"created_at"`
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
}
type SignInInput struct {
Email string `json:"email" bson:"email" binding:"required"`
Password string `json:"password" bson:"password" binding:"required"`
}
type DBResponse struct {
ID primitive.ObjectID `json:"id" bson:"_id"`
Name string `json:"name" bson:"name"`
Email string `json:"email" bson:"email"`
Password string `json:"password" bson:"password"`
PasswordConfirm string `json:"passwordConfirm,omitempty" bson:"passwordConfirm,omitempty"`
Provider string `json:"provider" bson:"provider"`
Photo string `json:"photo,omitempty" bson:"photo,omitempty"`
Role string `json:"role" bson:"role"`
Verified bool `json:"verified" bson:"verified"`
CreatedAt time.Time `json:"created_at" bson:"created_at"`
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
}
type UserResponse struct {
ID primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"`
Name string `json:"name,omitempty" bson:"name,omitempty"`
Email string `json:"email,omitempty" bson:"email,omitempty"`
Role string `json:"role,omitempty" bson:"role,omitempty"`
Photo string `json:"photo,omitempty" bson:"photo,omitempty"`
Provider string `json:"provider" bson:"provider"`
CreatedAt time.Time `json:"created_at" bson:"created_at"`
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
}
type UpdateDBUser struct {
ID primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"`
Name string `json:"name,omitempty" bson:"name,omitempty"`
Email string `json:"email,omitempty" bson:"email,omitempty"`
Password string `json:"password,omitempty" bson:"password,omitempty"`
PasswordConfirm string `json:"passwordConfirm,omitempty" bson:"passwordConfirm,omitempty"`
Role string `json:"role,omitempty" bson:"role,omitempty"`
Provider string `json:"provider" bson:"provider"`
Photo string `json:"photo,omitempty" bson:"photo,omitempty"`
Verified bool `json:"verified,omitempty" bson:"verified,omitempty"`
CreatedAt time.Time `json:"created_at,omitempty" bson:"created_at,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty" bson:"updated_at,omitempty"`
}
func FilteredResponse(user *DBResponse) UserResponse {
return UserResponse{
ID: user.ID,
Email: user.Email,
Name: user.Name,
Role: user.Role,
Provider: user.Provider,
Photo: user.Photo,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}
}
Add a Service to Upsert the User Document
Before we start working on the service, let’s create a helper function to marshal and unmarshal the struct into a BSON document.
utils/helper.go
func ToDoc(v interface{}) (doc *bson.D, err error) {
data, err := bson.Marshal(v)
if err != nil {
return
}
err = bson.Unmarshal(data, &doc)
return
}
Now open the services/user.service.go
file and add an UpsertUser
method to the UserService
interface. The purpose of the UpsertUser
service is to access and mutate the MongoDB database.
services/user.service.go
type UserService interface {
FindUserById(string) (*models.DBResponse, error)
FindUserByEmail(string) (*models.DBResponse, error)
UpsertUser(string, *models.UpdateDBUser) (*models.DBResponse, error)
}
Next, add the following code to the services/user.service.impl.go
file. The UpsertUser
function receiver will contain the logic to upsert the user’s credentials in the MongoDB database.
services/user.service.impl.go
type UserServiceImpl struct {
collection *mongo.Collection
ctx context.Context
}
func NewUserServiceImpl(collection *mongo.Collection, ctx context.Context) UserService {
return &UserServiceImpl{collection, ctx}
}
// FindUserByID
// FindUserByEmail
// UpsertUser
func (uc *UserServiceImpl) UpsertUser(email string, data *models.UpdateDBUser) (*models.DBResponse, error) {
doc, err := utils.ToDoc(data)
if err != nil {
return nil, err
}
opts := options.FindOneAndUpdate().SetUpsert(true).SetReturnDocument(1)
query := bson.D{{Key: "email", Value: email}}
update := bson.D{{Key: "$set", Value: doc}}
res := uc.collection.FindOneAndUpdate(uc.ctx, query, update, opts)
var updatedPost *models.DBResponse
if err := res.Decode(&updatedPost); err != nil {
return nil, errors.New("no post with that Id exists")
}
return updatedPost, nil
}
If you carefully take a look at the code above you’ll notice that we used .SetUpsert(true)
on the FindOneAndUpdateOptions instance returned by calling options.FindOneAndUpdate()
.
Including .SetUpsert(true)
will tell MongoDB to create a new user document if the email specified in the BSON query does not exist in the database whereas MongoDB will only update the user document if that email already exists.
Create the GitHub OAuth Handler
To begin, we need to install the Golang JWT package to enable us to generate the JSON Web Tokens.
The current version of the Golang JWT package is v4 but this might change in the future so navigate to their GitHub page to install the recent version.
go get -u github.com/golang-jwt/jwt/v4
Now let’s create two utility functions to generate and verify the access and refresh tokens.
utils/token.go
func CreateToken(ttl time.Duration, payload interface{}, privateKey string) (string, error) {
decodedPrivateKey, err := base64.StdEncoding.DecodeString(privateKey)
if err != nil {
return "", fmt.Errorf("could not decode key: %w", err)
}
key, err := jwt.ParseRSAPrivateKeyFromPEM(decodedPrivateKey)
if err != nil {
return "", fmt.Errorf("create: parse key: %w", err)
}
now := time.Now().UTC()
claims := make(jwt.MapClaims)
claims["sub"] = payload
claims["exp"] = now.Add(ttl).Unix()
claims["iat"] = now.Unix()
claims["nbf"] = now.Unix()
token, err := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(key)
if err != nil {
return "", fmt.Errorf("create: sign token: %w", err)
}
return token, nil
}
func ValidateToken(token string, publicKey string) (interface{}, error) {
decodedPublicKey, err := base64.StdEncoding.DecodeString(publicKey)
if err != nil {
return nil, fmt.Errorf("could not decode: %w", err)
}
key, err := jwt.ParseRSAPublicKeyFromPEM(decodedPublicKey)
if err != nil {
return "", fmt.Errorf("validate: parse key: %w", err)
}
parsedToken, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected method: %s", t.Header["alg"])
}
return key, nil
})
if err != nil {
return nil, fmt.Errorf("validate: %w", err)
}
claims, ok := parsedToken.Claims.(jwt.MapClaims)
if !ok || !parsedToken.Valid {
return nil, fmt.Errorf("validate: invalid token")
}
return claims["sub"], nil
}
Next, let’s add the GitHubOAuth
controller to the controllers/auth.controller.go
file. This handler will be evoked to authenticate the user when GitHub redirects them to the server.
controllers/auth.controller.ts
type AuthController struct {
authService services.AuthService
userService services.UserService
}
func NewAuthController(authService services.AuthService, userService services.UserService) AuthController {
return AuthController{authService, userService}
}
// SignUp User
// SignIn User
// Refresh Access Token
//Google OAuth
// GitHub OAuth
func (ac *AuthController) GitHubOAuth(ctx *gin.Context) {
code := ctx.Query("code")
var pathUrl string = "/"
if ctx.Query("state") != "" {
pathUrl = ctx.Query("state")
}
if code == "" {
ctx.JSON(http.StatusUnauthorized, gin.H{"status": "fail", "message": "Authorization code not provided!"})
return
}
// Use the code to get the id and access tokens
tokenRes, err := utils.GetGitHubOauthToken(code)
if err != nil {
ctx.JSON(http.StatusBadGateway, gin.H{"status": "fail", "message": err.Error()})
}
user, err := utils.GetGitHubUser(tokenRes.Access_token)
if err != nil {
ctx.JSON(http.StatusBadGateway, gin.H{"status": "fail", "message": err.Error()})
}
createdAt := time.Now()
resBody := &models.UpdateDBUser{
Email: user.Email,
Name: user.Name,
Photo: user.Photo,
Provider: "github",
Role: "user",
Verified: true,
CreatedAt: createdAt,
UpdatedAt: createdAt,
}
updatedUser, err := ac.userService.UpsertUser(user.Email, resBody)
if err != nil {
ctx.JSON(http.StatusBadGateway, gin.H{"status": "fail", "message": err.Error()})
}
config, _ := config.LoadConfig(".")
// Generate Tokens
access_token, err := utils.CreateToken(config.AccessTokenExpiresIn, updatedUser.ID.Hex(), config.AccessTokenPrivateKey)
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": err.Error()})
return
}
refresh_token, err := utils.CreateToken(config.RefreshTokenExpiresIn, updatedUser.ID.Hex(), config.RefreshTokenPrivateKey)
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": err.Error()})
return
}
ctx.SetCookie("access_token", access_token, config.AccessTokenMaxAge*60, "/", "localhost", false, true)
ctx.SetCookie("refresh_token", refresh_token, config.RefreshTokenMaxAge*60, "/", "localhost", false, true)
ctx.SetCookie("logged_in", "true", config.AccessTokenMaxAge*60, "/", "localhost", false, false)
ctx.Redirect(http.StatusTemporaryRedirect, fmt.Sprint(config.ClientOrigin, pathUrl))
}
Define the GitHub OAuth Route
Next, let’s add the GitHub OAuth route to the routes/session.routes.go
file.
routes/session.routes.go
type SessionRouteController struct {
authController controllers.AuthController
}
func NewSessionRouteController(authController controllers.AuthController) SessionRouteController {
return SessionRouteController{authController}
}
func (rc *SessionRouteController) SessionRoute(rg *gin.RouterGroup) {
router := rg.Group("/sessions/oauth")
router.GET("/google", rc.authController.GoogleOAuth)
router.GET("/github", rc.authController.GitHubOAuth)
}
Register the Session Router
Now in the main.go
file, instantiate the router constructor function, and add it to the Gin Gonic middleware stack.
We also need to install the cors package and include it in the middleware stack to enable us to accept requests from the Vue.js app.
var (
server *gin.Engine
ctx context.Context
mongoclient *mongo.Client
redisclient *redis.Client
userService services.UserService
UserController controllers.UserController
UserRouteController routes.UserRouteController
authCollection *mongo.Collection
authService services.AuthService
AuthController controllers.AuthController
AuthRouteController routes.AuthRouteController
SessionRouteController routes.SessionRouteController
)
func init() {
config, err := config.LoadConfig(".")
if err != nil {
log.Fatal("Could not load environment variables", err)
}
ctx = context.TODO()
// Connect to MongoDB
mongoconn := options.Client().ApplyURI(config.DBUri)
mongoclient, err := mongo.Connect(ctx, mongoconn)
if err != nil {
panic(err)
}
if err := mongoclient.Ping(ctx, readpref.Primary()); err != nil {
panic(err)
}
fmt.Println("MongoDB successfully connected...")
// Connect to Redis
redisclient = redis.NewClient(&redis.Options{
Addr: config.RedisUri,
})
if _, err := redisclient.Ping(ctx).Result(); err != nil {
panic(err)
}
err = redisclient.Set(ctx, "test", "Welcome to Golang with Redis and MongoDB", 0).Err()
if err != nil {
panic(err)
}
fmt.Println("Redis client connected successfully...")
// Collections
authCollection = mongoclient.Database("golang_mongodb").Collection("users")
userService = services.NewUserServiceImpl(authCollection, ctx)
authService = services.NewAuthService(authCollection, ctx)
AuthController = controllers.NewAuthController(authService, userService)
AuthRouteController = routes.NewAuthRouteController(AuthController)
SessionRouteController = routes.NewSessionRouteController(AuthController)
UserController = controllers.NewUserController(userService)
UserRouteController = routes.NewRouteUserController(UserController)
server = gin.Default()
}
func main() {
config, err := config.LoadConfig(".")
if err != nil {
log.Fatal("Could not load config", err)
}
defer mongoclient.Disconnect(ctx)
value, err := redisclient.Get(ctx, "test").Result()
if err == redis.Nil {
fmt.Println("key: test does not exist")
} else if err != nil {
panic(err)
}
corsConfig := cors.DefaultConfig()
corsConfig.AllowOrigins = []string{"http://localhost:8000", "http://localhost:3000"}
corsConfig.AllowCredentials = true
server.Use(cors.New(corsConfig))
router := server.Group("/api")
router.GET("/healthchecker", func(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{"status": "success", "message": value})
})
AuthRouteController.AuthRoute(router)
UserRouteController.UserRoute(router, userService)
SessionRouteController.SessionRoute(router)
log.Fatal(server.Run(":" + config.Port))
}
Conclusion
Congratulations on reaching the end. In this article, you learned how to integrate GitHub OAuth Authentication in your Vue.js, Golang, Gin Gonic, and MongoDB applications.
Check out the source code here:
Thanks for the walkthrough on how you do OAuth2 with Golang and Vue. It’s been good to see a full, working example. A few things jumped out at me that I wanted to get clarification on.
1. Golang has a great `oauth2` library that can replace a lot of the code you wrote around the OAuth2 flow.
2. It does not appear you are doing anything with the `state` value other than setting it to `/`. This is leaving things vulnerable to CSRF attacks, at least per Auth0 (https://auth0.com/docs/secure/attack-protection/state-parameters).
3. You’re getting the an access token and refresh token back from GitHub and promptly disregarding it after you load the user via GitHub’s API. You’re then generating your own access and refresh tokens. This doesn’t seem correct to me, as you’ve now basically disconnected your user from the GitHub user, meaning if they revoke access to your application, it will have no effect. Should the access and refresh tokens from GitHub by put into the JWT token? If the front end then makes a request to the backend and its access token is expired, you make a call to GitHub’s API to refresh it?
By the way, thanks for the detailed analysis.
1. I knew there was an oauth2 library in Golang that I could have used to make my life easier but the implementation is not that complex that’s why I decided to write the logic myself. Also, the oauth2 library has a similar implementation under the hood.
2. The primary purpose of the state parameter is to mitigate CSRF attacks. My intention was to redirect the user to a protected page after the authentication finishes. Let’s say a user attempted to access his account details which is a protected page but since he wasn’t logged in, he was redirected to the login page. Before the redirect, we’ll store the path he was trying to access in the
const from = 'path_user_tried_to-access'
variable and add it to the state parameter so that the Golang server will redirect the user to the path stored in the state parameter after the authentication is successful.However, I redirected the user to the home page (/) because the application does not have protected pages. The location state is supposed to be obtained from the URL but I hard-coded it with a slash (/) and assigned it to the
from
variable. When I get time I will write a statement to retrieve the location state from the URL instead of using a hard-coded slash (/).3. The access and refresh tokens returned by the GitHub API are to be used with only GitHub. The access token is required to retrieve the GitHub user’s information. I understand the point you are trying to make but since our API is different from GitHub API, we also need to generate our own access and refresh tokens.