GitHub OAuth is an authentication API that can be integrated into any web application to make the signup/login process easier.

In this article, you will learn how to implement GitHub OAuth Authentication in your React.js, MongoDB-Go-Driver, Gin Gonic, and Golang applications.

Also, to authenticate the user, the server will return access and refresh tokens as HTTPOnly cookies to the user’s browser or client.

Storing the access and refresh tokens in HTTPOnly cookies will prevent hackers from using Javascript to access and manipulate them.

React.js, MongoDB-Go-Driver, Gin Gonic, and Golang Series:

Related Articles:

GitHub OAuth Authentication React.js, MongoDB and Golang

Prerequisites

  • Basic knowledge of HTML, CSS, React.js, MongoDB, and Golang is required.
  • You should have Golang installed on your computer.

Create a New OAuth App on GitHub

To start writing the code for the GitHub OAuth, we first need to generate the OAuth credentials on GitHub.

Now, log in to your GitHub account, and at the top-right corner, click on your profile icon and then select “Settings” from the dropdown menu.

Scroll down on the profile settings page and click on “Developer settings” from the left sidebar.

GitHub OAuth with react and node click on the developer settings

On the Developer settings page select OAuth Apps and click on the “New OAuth App” button.

GitHub OAuth with react and node developer settings

Now, provide the necessary information needed for the OAuth app and click on the “Register application” button.

The authorization callback URL should be a route on the Golang Gin server. It’s similar to the Google OAuth integration in one of the previous articles.

Also, the homepage URL should be the URL of your frontend application.

GitHub OAuth with react and node register application

After GitHub has created the OAuth app, click on the “Generate a new client secret” button to generate the client secret key.

For security reasons, GitHub might ask you for your password again so don’t be alarmed.

GitHub OAuth with react and node create credentials

After the client secret has been generated, open the .env file located in the server folder and add it along with the client ID, and the authorization 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, add the credentials along with the server endpoint to the .env.local file in the React.js folder.


REACT_APP_GITHUB_OAUTH_CLIENT_ID=your client Id here
REACT_APP_GITHUB_OAUTH_CLIENT_SECRET=your client secret here
REACT_APP_GITHUB_OAUTH_ENDPOINT=http://localhost:8000
REACT_APP_GITHUB_OAUTH_REDIRECT=http://localhost:8000/api/sessions/oauth/google

Generate the Consent Screen URL with React.js

Now that we have the required credentials ready, it’s time to create a UI for the OAuth application.

Instead of using a library to display the “Log in with GitHub” button, I think it’s not that difficult to implement this from scratch.

To begin, we need a utility function that will be evoked to generate the GitHub OAuth consent screen URL using the client ID, the scopes, and the authorization callback URL.

Including the scopes in the consent screen URL will instruct GitHub to grant us read access to those resources.

You can learn more about the various scopes on the GitHub OAuth official documentation.

src/utils/getGithubUrl.ts


export function getGitHubUrl(from: string) {
  const rootURl = 'https://github.com/login/oauth/authorize';

  const options = {
    client_id: process.env.REACT_APP_GITHUB_OAUTH_CLIENT_ID as string,
    redirect_uri: process.env.REACT_APP_GITHUB_OAUTH_REDIRECT_URL as string,
    scope: 'user:email',
    state: from,
  };

  const qs = new URLSearchParams(options);

  return `${rootURl}?${qs.toString()}`;
}


Creating a Simple GitHub OAuth Button with React.js

Now, let’s create a simple GitHub OAuth button to display the GitHub consent screen with React.js where the getGitHubUrl(from) helper function will be evoked in the href={getGitHubUrl(from)}.

GitHub OAuth with react and node create the login page

src/pages/login.page.tsx


import { Box, Container, Typography, Link as MuiLink } from '@mui/material';
import { useLocation } from 'react-router-dom';
import { ReactComponent as GoogleLogo } from '../assets/google.svg';
import { ReactComponent as GitHubLogo } from '../assets/github.svg';
import { getGoogleUrl } from '../utils/getGoogleUrl';
import { getGitHubUrl } from '../utils/getGithubUrl';

const LoginPage = () => {
  const location = useLocation();
  let from = ((location.state as any)?.from?.pathname as string) || '/';

  return (
    <Container
      maxWidth={false}
      sx={{
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
        height: '100vh',
        backgroundColor: '#2363eb',
      }}
    >
      <Box width='27rem'>
        <Typography
          variant='h6'
          component='p'
          sx={{
            my: '1.5rem',
            textAlign: 'center',
            color: 'white',
          }}
        >
          Log in with another provider:
        </Typography>
        <Box
          width='100%'
          sx={{
            backgroundColor: '#e5e7eb',
            p: { xs: '1rem', sm: '2rem' },
            borderRadius: 2,
          }}
        >
          <MuiLink
            href={getGoogleUrl(from)}
            sx={{
              backgroundColor: '#f5f6f7',
              borderRadius: 1,
              py: '0.6rem',
              columnGap: '1rem',
              textDecoration: 'none',
              color: '#393e45',
              cursor: 'pointer',
              fontWeight: 500,
              '&:hover': {
                backgroundColor: '#fff',
                boxShadow: '0 1px 13px 0 rgb(0 0 0 / 15%)',
              },
            }}
            display='flex'
            justifyContent='center'
            alignItems='center'
          >
            <GoogleLogo style={{ height: '2rem' }} />
            Google
          </MuiLink>
         <MuiLink
            href={getGitHubUrl(from)}
            sx={{
              backgroundColor: '#f5f6f7',
              borderRadius: 1,
              py: '0.6rem',
              mt: '1.5rem',
              columnGap: '1rem',
              textDecoration: 'none',
              color: '#393e45',
              cursor: 'pointer',
              fontWeight: 500,
              '&:hover': {
                backgroundColor: '#fff',
                boxShadow: '0 1px 13px 0 rgb(0 0 0 / 15%)',
              },
            }}
            display='flex'
            justifyContent='center'
            alignItems='center'
          >
            <GitHubLogo style={{ height: '2rem' }} />
            GitHub
          </MuiLink>
        </Box>
      </Box>
    </Container>
  );
};

export default LoginPage;

If you followed one of my JWT authentication tutorials with React.js, then update the login.page.tsx file to include the GitHub login button.

GitHub OAuth with react and node create advance login page

src/pages/login.page.tsx


import { Box, Container, Typography, Link as MuiLink } from '@mui/material';
import { styled } from '@mui/material/styles';
import { FormProvider, SubmitHandler, useForm } from 'react-hook-form';
import { object, string, TypeOf } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import FormInput from '../components/FormInput';
import { useEffect } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { LoadingButton as _LoadingButton } from '@mui/lab';
import { toast } from 'react-toastify';
import { useLoginUserMutation } from '../redux/api/authApi';
import { ReactComponent as GoogleLogo } from '../assets/google.svg';
import { ReactComponent as GitHubLogo } from '../assets/github.svg';

import { getGoogleUrl } from '../utils/getGoogleUrl';
import { getGitHubUrl } from '../utils/getGithubUrl';

const LoadingButton = styled(_LoadingButton)`
  padding: 0.6rem 0;
  background-color: #f9d13e;
  color: #2363eb;
  font-weight: 500;

  &:hover {
    background-color: #ebc22c;
    transform: translateY(-2px);
  }
`;

const LinkItem = styled(Link)`
  text-decoration: none;
  color: #2363eb;
  &:hover {
    text-decoration: underline;
  }
`;

const loginSchema = object({
  email: string()
    .nonempty('Email address is required')
    .email('Email Address is invalid'),
  password: string()
    .nonempty('Password is required')
    .min(8, 'Password must be more than 8 characters')
    .max(32, 'Password must be less than 32 characters'),
});

export type LoginInput = TypeOf<typeof loginSchema>;

const LoginPage = () => {
  const methods = useForm<LoginInput>({
    resolver: zodResolver(loginSchema),
  });

  // ? API Login Mutation
  const [loginUser, { isLoading, isError, error, isSuccess }] =
    useLoginUserMutation();

  const navigate = useNavigate();
  const location = useLocation();

  const from = ((location.state as any)?.from.pathname as string) || '/profile';

  const {
    reset,
    handleSubmit,
    formState: { isSubmitSuccessful },
  } = methods;

  useEffect(() => {
    if (isSuccess) {
      toast.success('You successfully logged in');
      navigate(from);
    }
    if (isError) {
      if (Array.isArray((error as any).data.error)) {
        (error as any).data.error.forEach((el: any) =>
          toast.error(el.message, {
            position: 'top-right',
          })
        );
      } else {
        toast.error((error as any).data.message, {
          position: 'top-right',
        });
      }
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isLoading]);

  useEffect(() => {
    if (isSubmitSuccessful) {
      reset();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isSubmitSuccessful]);

  const onSubmitHandler: SubmitHandler<LoginInput> = (values) => {
    // ? Executing the loginUser Mutation
    loginUser(values);
  };

  return (
    <Container
      maxWidth={false}
      sx={{
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
        height: '100vh',
        backgroundColor: '#2363eb',
      }}
    >
      <Box
        sx={{
          display: 'flex',
          justifyContent: 'center',
          alignItems: 'center',
          flexDirection: 'column',
        }}
      >
        <Typography
          textAlign='center'
          component='h1'
          sx={{
            color: '#f9d13e',
            fontWeight: 600,
            fontSize: { xs: '2rem', md: '3rem' },
            mb: 2,
            letterSpacing: 1,
          }}
        >
          Welcome Back!
        </Typography>
        <Typography
          variant='body1'
          component='h2'
          sx={{ color: '#e5e7eb', mb: 2 }}
        >
          Login to have access!
        </Typography>

        <FormProvider {...methods}>
          <Box
            component='form'
            onSubmit={handleSubmit(onSubmitHandler)}
            noValidate
            autoComplete='off'
            maxWidth='27rem'
            width='100%'
            sx={{
              backgroundColor: '#e5e7eb',
              p: { xs: '1rem', sm: '2rem' },
              borderRadius: 2,
            }}
          >
            <FormInput name='email' label='Email Address' type='email' />
            <FormInput name='password' label='Password' type='password' />

            <Typography
              sx={{ fontSize: '0.9rem', mb: '1rem', textAlign: 'right' }}
            >
              <LinkItem to='/forgotpassword' style={{ color: '#333' }}>
                Forgot Password?
              </LinkItem>
            </Typography>

            <LoadingButton
              variant='contained'
              sx={{ mt: 1 }}
              fullWidth
              disableElevation
              type='submit'
              loading={isLoading}
            >
              Login
            </LoadingButton>

            <Typography sx={{ fontSize: '0.9rem', mt: '1rem' }}>
              Need an account? <LinkItem to='/register'>Sign Up Here</LinkItem>
            </Typography>
          </Box>
        </FormProvider>
        <Typography
          variant='h6'
          component='p'
          sx={{
            my: '1.5rem',
            textAlign: 'center',
            color: 'white',
          }}
        >
          Log in with another provider:
        </Typography>
        <Box
          maxWidth='27rem'
          width='100%'
          sx={{
            backgroundColor: '#e5e7eb',
            p: { xs: '1rem', sm: '2rem' },
            borderRadius: 2,
          }}
        >
          <MuiLink
            href={getGoogleUrl(from)}
            sx={{
              backgroundColor: '#f5f6f7',
              borderRadius: 1,
              py: '0.6rem',
              columnGap: '1rem',
              textDecoration: 'none',
              color: '#393e45',
              cursor: 'pointer',
              fontWeight: 500,
              '&:hover': {
                backgroundColor: '#fff',
                boxShadow: '0 1px 13px 0 rgb(0 0 0 / 15%)',
              },
            }}
            display='flex'
            justifyContent='center'
            alignItems='center'
          >
            <GoogleLogo style={{ height: '2rem' }} />
            Google
          </MuiLink>
          <MuiLink
            href={getGitHubUrl(from)}
            sx={{
              backgroundColor: '#f5f6f7',
              borderRadius: 1,
              py: '0.6rem',
              mt: '1.5rem',
              columnGap: '1rem',
              textDecoration: 'none',
              color: '#393e45',
              cursor: 'pointer',
              fontWeight: 500,
              '&:hover': {
                backgroundColor: '#fff',
                boxShadow: '0 1px 13px 0 rgb(0 0 0 / 15%)',
              },
            }}
            display='flex'
            justifyContent='center'
            alignItems='center'
          >
            <GitHubLogo style={{ height: '2rem' }} />
            GitHub
          </MuiLink>
        </Box>
      </Box>
    </Container>
  );
};

export default LoginPage;


Now when you click on the GitHub OAuth button, the OAuth consent screen will be displayed where you can either click on the green authorize button assuming you’ve already signed in, or log in with your email and password.

GitHub OAuth with react and node consent screen

After you log in with your email and password or when you click on the green authorize button, a GET request will be made to the Golang server. Assuming the Golang Gin server is running, it should return a 404 error since we haven’t implemented the OAuth logic yet.

Nevertheless, if you take a careful look at the authorization callback URL, you will see a unique code included in the query string.

Later, we’ll use that code on the server to obtain an access token from GitHub.

GitHub OAuth with react and node error route not implemented

Implement the GitHub OAuth in Golang

Now let’s start with the backend implementation. Open the config/default.go file and add the generated GitHub OAuth client ID, secret key, the React.js origin URL, and the authorization callback URL to the Config struct.

Adding the environment variables to the Config struct will enable Viper 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
}

Retrieve the GitHub OAuth Access Token and User’s Profile Data

In the root directory, create a utils/githubOAuth.go file and add these two utility functions:

  • GetGitHubOauthToken() – To make a POST request in order to get the OAuth access token from GitHub.
  • GetGitHubUser() – Makes a GET request with the access token to retrieve the user’s credentials from GitHub.

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 followed one of the Golang JWT authentication tutorials then update the user structs in the models/user.model.go file to have the Photo and Provider fields.

When a user creates an account through GitHub OAuth, the provider field will be set to GitHub whereas, a user who registers an 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’s Credentials

Working with the MongoDB-Go-Driver is a little challenging so instead of manually adding the fields in the BSON struct, let’s define 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
}

Open the services/user.service.go file and add an UpsertUser method to the UserService interface.

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 an UpsertUser function receiver to the services/user.service.impl.go file. The UpsertUser method will contain the logic to upsert the user’s data 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
}

From the above code, you’ll notice that we used .SetUpsert(true) on the instance returned by evoking options.FindOneAndUpdate() .

Adding .SetUpsert(true) will inform the MongoDB server to create a new document if the email used in the BSON query does not exist in the database.

However, the MongoDB server will only update the document if that email already exists.

Creating the GitHub OAuth Controller

Now let’s install the Golang JWT package to help us generate the access and refresh tokens.

The recent version of the Golang JWT package is v4 but in the future, this might change so go to their GitHub page to install the most current version.


go get -u github.com/golang-jwt/jwt/v4

Next, let’s define two utility functions to generate and verify the JSON Web 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
}

With the configurations above, let’s define the GitHubOAuth controller that 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 create a routes/session.routes.go file to register the GitHubOAuth controller.

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

In the main.go file, instantiate the session router constructor function, and add it to the Gin middleware stack.

We also need to make the middleware use the cors package so that our Golang server can accept requests from the React.js application.


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

Congrats on reaching the end. In this article, you learned how to implement GitHub OAuth Authentication in your React.js, Golang, Gin Gonic, and MongoDB-Go-Driver applications.

Check out the source code here: