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:
- Google OAuth Authentication React.js, MongoDB and Golang
- GitHub OAuth Authentication Vuejs, MongoDB and Golang
Related Articles:
- Google OAuth Authentication Vue.js and Node.js (No Passport)
- How to Implement Google OAuth2 in React.js
- How to Implement GitHub OAuth in React.js
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.
On the Developer settings page select OAuth Apps and click on the “New OAuth App” button.
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.
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.
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)}
.
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.
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.
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.
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: