Movatterモバイル変換


[0]ホーム

URL:


CodevoWeb

PressESC to close

Google OAuth Authentication React.js and Golang

3Comments91

This article will teach you how to implement Google OAuth Authentication with React.js, MongoDB-Go-Driver, and Golang.

In addition, you will learn how to return JWT access and refresh token cookies to the user’s browser or client after being authenticated on the server.

Related Articles:

Google OAuth Authentication React.js, MongoDB and Golang

Prerequisites

  • Have some basic knowledge of HTML, CSS, React.js, MongoDB, and Golang
  • HaveGolang installed on your computer

Generate Google client ID and client secret

To begin, search forGoogle OAuth in your browser and click the first link that appears in the search results. You should be taken toOAuth 2.0 to Access Google APIs.

On this page, you will find step-by-step instructions on how to get the access keys. However, I will guide you on creating the client Id and secret the right way.

To retrieve the OAuth 2.0 client credentials, click on theGoogle API Console link.

Note: You will need a Google account to access theGoogle API Console.

Create a New Project

Now, click on theGoogle-Oauth dropdown and a modal should appear where you can choose an existing project or create a new one.

If you haven’t created any project yet, click on the“New Project” button then type the project name and click on the“CREATE” button.

google oauth new project

Now, wait for a few seconds for Google to set up the project. Once the project has been successfully created, you will see a success notification.

Next, click on the“SELECT PROJECT” button and tap anywhere on the browser to close the modal.

google oauth new project success notification

Create an OAuth Consent Screen

To begin, we first need to create a consent screen before we can generate the credentials. The consent screen is where the user will be redirected when they click on the Sign in with Google OAuth button.

Next, select theOAuth consent screen menu under“APIs & Services” on the left sidebar. On theOAuth consent screen, choose the External User Type if your application is for the public and click on the“CREATE” button.

google oauth consent screen setup

Next, provide the necessary information for the app consent screen.

google oauth consent screen registration

After filling the fields, click on the“SAVE AND CONTINUE” button.

google oauth consent screen registration 2

Click on the“ADD OR REMOVE SCOPES” button on thescopes page. The number of scopes you can add depends on the type of application you are working on but am only interested in the user’s email and profile information.

After choosing the scopes, scroll down and click on the“UPDATE” button. Next, click on the“SAVE AND CONTINUE” button.

google oauth consent screen select scopes

Now, click on theADD USERS button on theTest users tab to add a test user.

Note: The test user is the only account that will have permission to log into your application whilst in sandbox mode.

After adding the test user, click on“SAVE AND CONTINUE”.

On theSummary tab, carefully review your information and click on the“BACK TO DASHBOARD” button once you are convinced the data you provided is correct.

Generate the OAuth Credentials

On the sidebar under“APIs & Services” select Credentials. On theCredentials tab, click on“CREATE CREDENTIALS” and chooseOAuth client ID.

google oauth create credentials

Next, provide the necessary information to allow access to the user’s data. The authorized redirect URI should point to a route on your server. This will enable us to make theGET request directly from the consent screen to the server.

There are different ways you can implement this but I find this method much quicker.

google oauth credentials

After providing the required data, click on the“CREATE” button and you should get the generated client ID and client secret.

Edit your.env file in your server folder and add the client Id, client secret, and authorized redirect URI.

.env

GOOGLE_OAUTH_CLIENT_ID=your client Id hereGOOGLE_OAUTH_CLIENT_SECRET=your client secret hereGOOGLE_OAUTH_REDIRECT_URL=http://localhost:8000/api/sessions/oauth/google

In addition, update the.env.local file in the React.js application.

REACT_APP_GOOGLE_OAUTH_CLIENT_ID=your client Id hereREACT_APP_GOOGLE_OAUTH_CLIENT_SECRET=your client secret hereREACT_APP_GOOGLE_OAUTH_ENDPOINT=http://localhost:8000REACT_APP_GOOGLE_OAUTH_REDIRECT=http://localhost:8000/api/sessions/oauth/google

Generate the Consent Screen URL

After all the configurations above, we are now ready to implement Google OAuth Authentication in our React.js application.

Now we need to define a function to generate the OAuth consent screen URL based on the client ID, the scopes, and the authorized redirect URL.

src/utils/getGoogleUrl.ts

export const getGoogleUrl = (from: string) => {  const rootUrl = `https://accounts.google.com/o/oauth2/v2/auth`;  const options = {    redirect_uri: process.env.REACT_APP_GOOGLE_OAUTH_REDIRECT as string,    client_id: process.env.REACT_APP_GOOGLE_OAUTH_CLIENT_ID as string,    access_type: 'offline',    response_type: 'code',    prompt: 'consent',    scope: [      'https://www.googleapis.com/auth/userinfo.profile',      'https://www.googleapis.com/auth/userinfo.email',    ].join(' '),    state: from,  };  const qs = new URLSearchParams(options);  return `${rootUrl}?${qs.toString()}`;};

Build the Google OAuth Button with React and MUI

Next, create a login screen with a Google OAuth button havinghref={getGoogleUrl(from)}.

Let’s create a simple Google OAuth button with React and Material UI.

OAuth login page with react

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 { getGoogleUrl } from '../utils/getGoogleUrl';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>        </Box>      </Box>    </Container>  );};export default LoginPage;

If you came from a previous tutorial in this series, then update thelogin.page.tsx file to have the Google OAuth button.

Google OAuth login page with react

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 { getGoogleUrl } from '../utils/getGoogleUrl';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>        </Box>      </Box>    </Container>  );};export default LoginPage;

Click on the Google OAuth button and you should be taken to the consent screen where you’ll see your logged-in Google accounts.

On the consent screen, click on the test user account or sign in with the test account if you haven’t already.

vuejs google oauth authentication consent screen

The server should return a404 error only if it is running. The most exciting part of the authorized redirect URI is the code in the query string.

The server returned the404 error because we haven’t added the Google OAuth logic yet.

google OAuth 2 click on google account

Implement the Google OAuth in Golang and MongoDB

Update theconfig/default.go file to have the OAuth client Id, the React origin URL, the client secret, and the authorized redirect URL.

Adding the environment variables to the Config struct will enableViper 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"`}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 Google OAuth Access Token and User’s Profile

Now create autils/googleOAuth.go file and add these two functions:

  • GetGoogleOauthToken() – To get the OAuth Access Token from the Google API
  • GetGoogleUser() – Retrieves the user’s profile data with the OAuth Access Token

utils/googleOAuth.go

type GoogleOauthToken struct {Access_token stringId_token     string}type GoogleUserResult struct {Id             stringEmail          stringVerified_email boolName           stringGiven_name     stringFamily_name    stringPicture        stringLocale         string}func GetGoogleOauthToken(code string) (*GoogleOauthToken, error) {const rootURl = "https://oauth2.googleapis.com/token"config, _ := config.LoadConfig(".")values := url.Values{}values.Add("grant_type", "authorization_code")values.Add("code", code)values.Add("client_id", config.GoogleClientID)values.Add("client_secret", config.GoogleClientSecret)values.Add("redirect_uri", config.GoogleOAuthRedirectUrl)query := values.Encode()req, err := http.NewRequest("POST", rootURl, bytes.NewBufferString(query))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}var GoogleOauthTokenRes map[string]interface{}if err := json.Unmarshal(resBody, &GoogleOauthTokenRes); err != nil {return nil, err}tokenBody := &GoogleOauthToken{Access_token: GoogleOauthTokenRes["access_token"].(string),Id_token:     GoogleOauthTokenRes["id_token"].(string),}return tokenBody, nil}func GetGoogleUser(access_token string, id_token string) (*GoogleUserResult, error) {rootUrl := fmt.Sprintf("https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=%s", access_token)req, err := http.NewRequest("GET", rootUrl, nil)if err != nil {return nil, err}req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", id_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 GoogleUserRes map[string]interface{}if err := json.Unmarshal(resBody, &GoogleUserRes); err != nil {return nil, err}userBody := &GoogleUserResult{Id:             GoogleUserRes["id"].(string),Email:          GoogleUserRes["email"].(string),Verified_email: GoogleUserRes["verified_email"].(bool),Name:           GoogleUserRes["name"].(string),Given_name:     GoogleUserRes["given_name"].(string),Picture:        GoogleUserRes["picture"].(string),Locale:         GoogleUserRes["locale"].(string),}return userBody, nil}

Update the User Model

If you followed the previous tutorials in this series then update themodels/user.model.go file to have thePhoto andProvider fields.

When a user registers an account through Google OAuth, we’ll set theprovider field toGoogle whereas a normal user who registers with email and password will have theprovider to belocal .

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

First, 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}

Next, add theUpsertUser method to theUserService 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)}

Now add the code below to theservices/user.service.impl.go file to enable us to upsert the user’s information in the MongoDB database.

services/user.service.impl.go

type UserServiceImpl struct {collection *mongo.Collectionctx        context.Context}func NewUserServiceImpl(collection *mongo.Collection, ctx context.Context) UserService {return &UserServiceImpl{collection, ctx}}// FindUserByID// FindUserByEmail// UpsertUserfunc (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.DBResponseif err := res.Decode(&updatedPost); err != nil {return nil, errors.New("no post with that Id exists")}return updatedPost, nil}

In the above code, you’ll notice that we used.SetUpsert(true) on the FindOneAndUpdateOptions instance returned byoptions.FindOneAndUpdate() .

Adding.SetUpsert(true) will instruct the MongoDB server to create the user if the email used in the BSON query doesn’t exist. On the other hand, MongoDB will only update the user’s data if that email already exists in the database.

Create the Google OAuth Controller

To generate the JWT tokens, we will use theGolang JWT package. In the future, the version might change so visit theirGitHub page to see the current version.

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

Functions to sign 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"] = payloadclaims["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}

Now let’s add theGoogleOAuth controller to thecontrollers/auth.controller.go file. This controller will be evoked when the user is redirected to the server.

controllers/auth.controller.ts

type AuthController struct {authService services.AuthServiceuserService services.UserService}func NewAuthController(authService services.AuthService, userService services.UserService) AuthController {return AuthController{authService, userService}}// SignUp User// SignIn User// Refresh Access Tokenfunc (ac *AuthController) GoogleOAuth(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 tokenstokenRes, err := utils.GetGoogleOauthToken(code)if err != nil {ctx.JSON(http.StatusBadGateway, gin.H{"status": "fail", "message": err.Error()})}user, err := utils.GetGoogleUser(tokenRes.Access_token, tokenRes.Id_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.Picture,Provider:  "google",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 Tokensaccess_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))}

Create the Route

Now create aroutes/session.routes.go file and add the code below. We have to create a new route file because later we will be adding GitHub and Facebook OAuth and it makes a lot of sense to group them in a single route 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)}

Register the Session Router

Next, add the router defined above to the middleware stack in themain.go file. Also, remember to install thecors package to enable you to make requests from the React.js app.

var (server      *gin.Enginectx         context.Contextmongoclient *mongo.Clientredisclient *redis.ClientuserService         services.UserServiceUserController      controllers.UserControllerUserRouteController routes.UserRouteControllerauthCollection         *mongo.CollectionauthService            services.AuthServiceAuthController         controllers.AuthControllerAuthRouteController    routes.AuthRouteControllerSessionRouteController routes.SessionRouteController)func init() {config, err := config.LoadConfig(".")if err != nil {log.Fatal("Could not load environment variables", err)}ctx = context.TODO()// Connect to MongoDBmongoconn := 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 Redisredisclient = 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...")// CollectionsauthCollection = 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 = trueserver.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 for reaching the end. In this article, you learned how to add Google OAuth Authentication to your React.js, Golang, and MongoDB applications.

You can find the complete code used in thistutorial on GitHub

Share Article:

GitHub OAuth Authentication Vue.js and Node.js (No Passport)

Left Arrow

Google OAuth Authentication With Vue.js and Golang

Right Arrow

3 Comments

  1. Alexon March 5, 2023

    Great tutorial! I notice that the ‘state’ is never really explained, could you perhaps include something about it?

    Reply
    • Edemon March 5, 2023

      Thank you for taking the time to read my tutorial and leave a comment! I appreciate your feedback and am happy to explain the state parameter in more detail.

      In short, the state parameter is an important security feature in the Google OAuth flow. When we generate the consent screen URL, we include a random string of characters in the state parameter.

      This helps us ensure that the authorization code we receive later comes from the same place that requested it originally. By doing this, we can protect against cross-site request forgery (CSRF) attacks, where a malicious third party tries to trick the user into granting access to their data.

      Here’s how it works: When a user clicks “Allow” on the consent screen, Google sends an authorization code back to our server, along with the state parameter. We can then compare the value of the state parameter that we originally sent with the value of the state parameter that Google returned.

      If they match, then we know that the authorization code is legitimate and came from the same place that requested it. If they don’t match, we can reject the authorization code and prevent any unauthorized access to our user’s data.

      The state parameter also allows us to redirect the user to a protected page after they’ve been authenticated. For example, if a user was trying to access a page that requires authentication, we can store the protected page’s URL in the state parameter. After the user has been authenticated, we can redirect them to the protected page by extracting the URL from the state parameter.

      I hope this explanation helps clarify the importance of the state parameter in the Google OAuth flow. If you have any more questions or feedback, please don’t hesitate to let me know!

      Reply
  2. Alexon March 5, 2023

    Hi Edem, thanks for the explanation! I found ‘let from = ((location.state as any)?.from?.pathname as string) || ‘/’;’ in the login page code, it all makes sense!

    Reply

Leave a ReplyCancel reply

This site is protected by reCAPTCHA and the GooglePrivacy Policy andTerms of Service apply.

This site uses Akismet to reduce spam.Learn how your comment data is processed.

Support Me!

paypal donate button

Recent posts

Categories


[8]ページ先頭

©2009-2025 Movatter.jp