In this article, you’ll learn how to implement GitHub OAuth Authentication with React.js and Node.js without using Passport. Also, you’ll learn how to send JWT access and refresh token cookies after the user has been authenticated.

Node.js, TypeScript, and MongoDB Tutorial Series:

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

Prerequisites

Create an OAuth Application on GitHub

The first step is to log into your GitHub account. At the top-right corner, click on the Profile dropdown and select “Settings”.

On your profile page, scroll down to the bottom and click “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 on the right side.

GitHub OAuth with react and node developer settings

Next, provide the required information and click on the “Register application” button.

The authorization callback URL should point to your server address. It’s similar to the Google OAuth implementation in the previous tutorial.

GitHub OAuth with react and node register application

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

You may be redirected to provide your password again so don’t be alarmed.

GitHub OAuth with react and node create credentials

Update your .env file on the server with the client ID, client secret, 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, update the .env.local file in the React application.


REACT_APP_GITHUB_OAUTH_CLIENT_ID=your client Id here
REACT_APP_GITHUB_OAUTH_CLIENT_SECRET=your client secret here
REACT_APP_GITHUB_OAUTH_REDIRECT_URL=http://localhost:8000/api/sessions/oauth/github

Build the Consent Screen URL

Now, we are ready to implement GitHub OAuth Authentication in our React.js application.

To begin, we need to build the OAuth consent screen link based on the client ID and the authorization callback URL.

Also, we need to provide scopes to have read access to certain resources on the GitHub API. In this tutorial, am only interested in the user’s email.

You can add more scopes depending on the type of application you are building.

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()}`;
}


Build the GitHub OAuth Login Page with React and MUI

Here, I created a simple login page having the GitHub login button with React and Material UI for new visitors.

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 came from the previous tutorial in this series, 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;


Once you click on the GitHub button, you should be taken to the consent screen.

GitHub OAuth with react and node consent screen

On the consent screen, click on the green Authorize button or sign in with your email and password if you haven’t done that.

You should get a 404 error assuming the NodeJs server is running. The most interesting part of the authorization callback URL is the code in the query string.

GitHub OAuth with react and node error route not implemented

The reason why you got the 404 error was that we haven’t added the OAuth implementation to the server yet.

Implement the OAuth Authentication on the Server

To make the HTTP requests to the GitHub API, we’ll use Axios. Run the following command in the terminal to install the Axios package.


yarn add axios

Edit the config/custom-environment-variables.ts file to have the OAuth client Id, client secret, and the authorization callback URL.

config/custom-environment-variables.ts


export default {
  dbName: 'MONGODB_USERNAME',
  dbPass: 'MONGODB_PASSWORD',
  
  accessTokenPrivateKey: 'ACCESS_TOKEN_PRIVATE_KEY',
  accessTokenPublicKey: 'ACCESS_TOKEN_PUBLIC_KEY',
  refreshTokenPrivateKey: 'REFRESH_TOKEN_PRIVATE_KEY',
  refreshTokenPublicKey: 'REFRESH_TOKEN_PUBLIC_KEY',

  googleClientId: 'GOOGLE_OAUTH_CLIENT_ID',
  googleClientSecret: 'GOOGLE_OAUTH_CLIENT_SECRET',
  googleOauthRedirect: 'GOOGLE_OAUTH_REDIRECT_URL',

  githubClientId: 'GITHUB_OAUTH_CLIENT_ID',
  githubClientSecret: 'GITHUB_OAUTH_CLIENT_SECRET',
  githubOauthRedirect: 'GITHUB_OAUTH_REDIRECT_URL',
};

Get GitHub OAuth Access Token and User’s Profile

In the session.service.ts file in the services folder and add these two functions:

  • getGithubOathToken() – To get the OAuth Access Token from GitHub
  • getGithubUser() – Get the user’s profile information with the Access Token

Functions to get the GitHub access token and the user’s profile information:

src/services/session.service.ts


// ? GitHub OAuth

type GitHubOauthToken = {
  access_token: string;
};

interface GitHubUser {
  login: string;
  id: number;
  node_id: string;
  avatar_url: string;
  gravatar_id: string;
  url: string;
  html_url: string;
  followers_url: string;
  following_url: string;
  gists_url: string;
  starred_url: string;
  subscriptions_url: string;
  organizations_url: string;
  repos_url: string;
  events_url: string;
  received_events_url: string;
  type: string;
  site_admin: boolean;
  name: string;
  company: string;
  blog: string;
  location: null;
  email: string;
  hireable: boolean;
  bio: string;
  twitter_username: string;
  public_repos: number;
  public_gists: number;
  followers: number;
  following: number;
  created_at: Date;
  updated_at: Date;
}

export const getGithubOathToken = async ({
  code,
}: {
  code: string;
}): Promise<GitHubOauthToken> => {
  const rootUrl = 'https://github.com/login/oauth/access_token';
  const options = {
    client_id: config.get<string>('githubClientId'),
    client_secret: config.get<string>('githubClientSecret'),
    code,
  };

  const queryString = qs.stringify(options);

  try {
    const { data } = await axios.post(`${rootUrl}?${queryString}`, {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
    });

    const decoded = qs.parse(data) as GitHubOauthToken;

    return decoded;
  } catch (err: any) {
    throw Error(err);
  }
};

export const getGithubUser = async ({
  access_token,
}: {
  access_token: string;
}): Promise<GitHubUser> => {
  try {
    const { data } = await axios.get<GitHubUser>(
      'https://api.github.com/user',
      {
        headers: {
          Authorization: `Bearer ${access_token}`,
        },
      }
    );

    return data;
  } catch (err: any) {
    throw Error(err);
  }
};

Complete code comprising Google and GitHub OAuth implementation:

src/services/session.service.ts


import config from 'config';
import axios from 'axios';
import qs from 'qs';

interface GoogleOauthToken {
  access_token: string;
  id_token: string;
  expires_in: number;
  refresh_token: string;
  token_type: string;
  scope: string;
}

export const getGoogleOauthToken = async ({
  code,
}: {
  code: string;
}): Promise<GoogleOauthToken> => {
  const rootURl = 'https://oauth2.googleapis.com/token';

  const options = {
    code,
    client_id: config.get<string>('googleClientId'),
    client_secret: config.get<string>('googleClientSecret'),
    redirect_uri: config.get<string>('googleOauthRedirect'),
    grant_type: 'authorization_code',
  };
  try {
    const { data } = await axios.post<GoogleOauthToken>(
      rootURl,
      qs.stringify(options),
      {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
      }
    );

    return data;
  } catch (err: any) {
    console.log('Failed to fetch Google Oauth Tokens');
    throw new Error(err);
  }
};

interface GoogleUserResult {
  id: string;
  email: string;
  verified_email: boolean;
  name: string;
  given_name: string;
  family_name: string;
  picture: string;
  locale: string;
}

export async function getGoogleUser({
  id_token,
  access_token,
}: {
  id_token: string;
  access_token: string;
}): Promise<GoogleUserResult> {
  try {
    const { data } = await axios.get<GoogleUserResult>(
      `https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=${access_token}`,
      {
        headers: {
          Authorization: `Bearer ${id_token}`,
        },
      }
    );

    return data;
  } catch (err: any) {
    console.log(err);
    throw Error(err);
  }
}

// ? GitHub OAuth

type GitHubOauthToken = {
  access_token: string;
};

interface GitHubUser {
  login: string;
  id: number;
  node_id: string;
  avatar_url: string;
  gravatar_id: string;
  url: string;
  html_url: string;
  followers_url: string;
  following_url: string;
  gists_url: string;
  starred_url: string;
  subscriptions_url: string;
  organizations_url: string;
  repos_url: string;
  events_url: string;
  received_events_url: string;
  type: string;
  site_admin: boolean;
  name: string;
  company: string;
  blog: string;
  location: null;
  email: string;
  hireable: boolean;
  bio: string;
  twitter_username: string;
  public_repos: number;
  public_gists: number;
  followers: number;
  following: number;
  created_at: Date;
  updated_at: Date;
}

export const getGithubOathToken = async ({
  code,
}: {
  code: string;
}): Promise<GitHubOauthToken> => {
  const rootUrl = 'https://github.com/login/oauth/access_token';
  const options = {
    client_id: config.get<string>('githubClientId'),
    client_secret: config.get<string>('githubClientSecret'),
    code,
  };

  const queryString = qs.stringify(options);

  try {
    const { data } = await axios.post(`${rootUrl}?${queryString}`, {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
    });

    const decoded = qs.parse(data) as GitHubOauthToken;

    return decoded;
  } catch (err: any) {
    throw Error(err);
  }
};

export const getGithubUser = async ({
  access_token,
}: {
  access_token: string;
}): Promise<GitHubUser> => {
  try {
    const { data } = await axios.get<GitHubUser>(
      'https://api.github.com/user',
      {
        headers: {
          Authorization: `Bearer ${access_token}`,
        },
      }
    );

    return data;
  } catch (err: any) {
    throw Error(err);
  }
};


User Model

src/models/user.model.ts


import {
  DocumentType,
  getModelForClass,
  index,
  modelOptions,
  pre,
  prop,
} from '@typegoose/typegoose';
import bcrypt from 'bcryptjs';

@index({ email: 1 })
@pre<User>('save', async function () {
  // Hash password if the password is new or was updated
  if (!this.isModified('password')) return;

  // Hash password with costFactor of 12
  this.password = await bcrypt.hash(this.password, 12);
})
@modelOptions({
  schemaOptions: {
    // Add createdAt and updatedAt fields
    timestamps: true,
  },
})

// Export the User class to be used as TypeScript type
export class User {
  @prop()
  name: string;

  @prop({ unique: true, required: true })
  email: string;

  @prop({ required: true, minlength: 8, maxLength: 32, select: false })
  password: string;

  @prop({ default: 'user' })
  role: string;

  @prop({ default: 'default.png' })
  photo: string;

  @prop({ default: false })
  verified: boolean;

  @prop({ default: 'local' })
  provider: string;

  // Instance method to check if passwords match
  async comparePasswords(hashedPassword: string, candidatePassword: string) {
    return await bcrypt.compare(candidatePassword, hashedPassword);
  }
}

// Create the user model from the User class
const userModel = getModelForClass(User);
export default userModel;

Service to Update the User

Below is the service function we’ll use to update the user’s information in the MongoDB database.

src/services/user.service.ts


export const findAndUpdateUser = async (
  query: FilterQuery<User>,
  update: UpdateQuery<User>,
  options: QueryOptions
) => {
  return await userModel.findOneAndUpdate(query, update, options);
};

Create the GitHub OAuth Controller

Add the githubOauthHandler to the auth.controller.ts . This handler will be called when GitHub redirects the user to the server.

src/controllers/auth.controller.ts


export const githubOauthHandler = async (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  try {
    // Get the code from the query
    const code = req.query.code as string;
    const pathUrl = (req.query.state as string) ?? '/';

    if (req.query.error) {
      return res.redirect(`${config.get<string>('origin')}/login`);
    }

    if (!code) {
      return next(new AppError('Authorization code not provided!', 401));
    }

    // Get the user the access_token with the code
    const { access_token } = await getGithubOathToken({ code });

    // Get the user with the access_token
    const { email, avatar_url, login } = await getGithubUser({ access_token });

    // Create new user or update user if user already exist
    const user = await findAndUpdateUser(
      { email },
      {
        email,
        photo: avatar_url,
        name: login,
        provider: 'GitHub',
        verified: true,
      },
      { runValidators: false, new: true, upsert: true }
    );

    if (!user) {
      return res.redirect(`${config.get<string>('origin')}/oauth/error`);
    }

    // Create access and refresh tokens
    const { access_token: accessToken, refresh_token } = await signToken(user);

    res.cookie('access_token', accessToken, accessTokenCookieOptions);
    res.cookie('refresh_token', refresh_token, refreshTokenCookieOptions);
    res.cookie('logged_in', true, {
      ...accessTokenCookieOptions,
      httpOnly: false,
    });

    res.redirect(`${config.get<string>('origin')}${pathUrl}`);
  } catch (err: any) {
    return res.redirect(`${config.get<string>('origin')}/oauth/error`);
  }
};

Create the Route

Update the session.routes.ts file to include the GitHub OAuth handler.

src/routes/session.routes.ts


import express from 'express';
import { googleOauthHandler,githubOauthHandler } from '../controllers/auth.controller';

const router = express.Router();

router.get('/oauth/google', googleOauthHandler);
router.get('/oauth/github', githubOauthHandler);

export default router;

Conclusion

Congrats for reaching the end. In this article, you learned how to add GitHub OAuth to React.js, Node.js, and MongoDB applications without using Passport.js.

You can find the complete code used in this tutorial on GitHub