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 Vue.js and Node.js (No Passport)
- Google OAuth Authentication React.js and Node.js(No Passport)
- GitHub OAuth Authentication React.js and Node.js(No Passport)
- How to Implement GitHub OAuth in React.js
- How to Implement Google OAuth2 in React.js
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.
On the Developer settings page, select OAuth Apps and click on the “New OAuth App” button on the right side.
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.
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.
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.
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.
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.
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.
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 GitHubgetGithubUser()
– 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