Movatterモバイル変換


[0]ホーム

URL:


CodevoWeb

PressESC to close

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

0Comments66

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.

Related articles:

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

Prerequisites

Create an OAuth Application on GitHub

The first step is to log into yourGitHub 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 theGoogle 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 hereGITHUB_OAUTH_CLIENT_SECRET=your client secret hereGITHUB_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 hereREACT_APP_GITHUB_OAUTH_CLIENT_SECRET=your client secret hereREACT_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 morescopes 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 thelogin.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 greenAuthorize button or sign in with your email and password if you haven’t done that.

You should get a404 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 the404 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 useAxios. Run the following command in the terminal to install theAxios package.

yarn add axios

Edit theconfig/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 thesession.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 OAuthtype 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 OAuthtype 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 typeexport 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 classconst 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 thegithubOauthHandler to theauth.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 thesession.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 thistutorial on GitHub

Share Article:

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

Left Arrow

Top 10 Best VS Code Extensions for Vue.js Developers

Right Arrow

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