This article will teach you how to implement GitHub OAuth Authentication with Vue.js and Node.js without Passport.js.

Also, after the user is authenticated, we’ll return JWT access and refresh token cookies to the user’s browser or client.

Related Articles:

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

Prerequisites

  • Basic knowledge of HTML, CSS, Vue.js and Golang is need.
  • You should have Golang installed on your computer.

Create an OAuth App on GitHub

First, you need to log into your GitHub account, and at the top-right corner, click on the Profile icon and select “Settings”.

On the profile page, scroll to the bottom, and from the left-sidebar click “Developer settings”.

GitHub OAuth with react and node click on the developer settings

Now, select OAuth Apps on the Developer settings page and click on the “New OAuth App” button on the right side.

GitHub OAuth with react and node developer settings

Next, provide the necessary information about the OAuth app and click on the “Register application” button.

The authorization callback URL should point to a route on your server. It’s similar to the Google OAuth implementation in the previous article.

GitHub OAuth with react and node register application

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

Don’t be alarmed when GitHub redirects you to provide your password again.

GitHub OAuth with react and node create credentials

Edit the .env file in your server folder and add 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, remember to update the .env.local file in the Vue.js app.


VITE_GITHUB_OAUTH_CLIENT_ID=your client Id here
VITE_GITHUB_OAUTH_CLIENT_SECRET=your client secret here
VITE_GITHUB_OAUTH_REDIRECT_URL=http://localhost:8000/api/sessions/oauth/github

Note: use Vue_App_ prefix if you generated the app with the @vue/cli .

Generate the Consent Screen URL

Now, we are ready to add GitHub OAuth Authentication to our Vue.js application.

First, we need to generate the GitHub OAuth consent screen URL based on the client ID, scopes, and the authorization callback URL.

The scopes give us read access to specific resources on the GitHub API. In this mini-app, we are only interested the user’s email address.

Feel free to play around with the different scopes to better understand how to use them.

src/utils/getGithubUrl.js


export function getGitHubUrl(from) {
  const rootURl = 'https://github.com/login/oauth/authorize';

  const options = {
    client_id: import.meta.env.VITE_GITHUB_OAUTH_CLIENT_ID,
    redirect_uri: import.meta.env.VITE_GITHUB_OAUTH_REDIRECT_URL,
    scope: 'user:email',
    state: from,
  };

  const qs = new URLSearchParams(options);

  return `${rootURl}?${qs.toString()}`;
}

Build a Simple GitHub OAuth Button with Vue.js

Now let’s create a simple GitHub OAuth button with Vue.js.

github oauth with vuejs and nodejs

src/App.vue


<script setup>
import GoogleLogo from './assets/google.svg';
import GitHubLogo from './assets/github.svg';
import { getGoogleUrl } from './utils/getGoogleUrl';
import { getGitHubUrl } from './utils/getGitHubUrl';

const from = '/';
</script>

<template>
  <div class="container">
    <div class="social-auth">
      <!-- Google OAuth -->
      <a :href="getGoogleUrl(from)" class="auth-btn google-auth">
        <img :src="GoogleLogo" alt="Google Logo" />
        <span>Google</span>
      </a>
      <!-- GitHub OAuth -->
      <a :href="getGitHubUrl(from)" class="auth-btn github-auth">
        <img :src="GitHubLogo" alt="GitHub Logo" />
        <span>Google</span>
      </a>
    </div>
  </div>
</template>

<style>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
a {
  text-decoration: none;
  color: inherit;
}

html {
  font-size: 62.5%;
}

body {
  font-family: Roboto, sans-serif;
  color: #222;
  font-size: 1.6rem;
}

.container {
  background-color: #2363eb;
  height: 100vh;
  width: 100vw;
  display: flex;
  align-items: center;
  justify-content: center;
}

.social-auth {
  max-width: 27rem;
  width: 100%;
  display: flex;
  align-items: center;
  flex-direction: column;
}

.auth-btn {
  background-color: #fff;
  border-radius: 5px;
  padding: 0.6rem 0;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.3s ease-in-out;
}

.auth-btn img {
  height: 4rem;
  margin-right: 1rem;
}
.auth-btn span {
  font-size: 1.8rem;
}

.auth-btn:hover {
  box-shadow: 0 1px 13px 0 rgb(0 0 0 / 15%);
}

.auth-btn.google-auth {
  margin-bottom: 1.5rem;
}
</style>


Next, click on the GitHub Oauth button and you should be taken to the GitHub consent screen.

GitHub OAuth with react and node consent screen

Click on the green Authorize button on the GitHub consent screen or log in with your email and password if you haven’t done that.

A 404 error should be returned by your NodeJs server assuming the server is running.

The important part of the authorization callback URL is the code in the query string. We’ll later use the code to obtain the access token on the server.

GitHub OAuth with react and node error route not implemented

The server returned a 404 error because we haven’t implemented the GitHub OAuth logic on the server yet.

Implement GitHub OAuth on the Server

Now, run the following command in the terminal to install the Axios package. The Axios package will enable us to make HTTP requests to the GitHub API.


yarn add axios

Update the config/custom-environment-variables.ts file to have the GitHub 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',
};

Retrieve the GitHub OAuth Access Token and User’s Profile

Add these two functions to the session.service.ts file in the services folder.

  • getGithubOathToken() – To obtain the OAuth Access Token from GitHub
  • getGithubUser() – To retrieve the user’s profile data with the Access Token

Below are the functions to retrieve the GitHub access token and the user’s profile data:

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 consisting of Google and GitHub OAuth implementation logic:

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

If you came from a previous tutorial in this series then update the user.model.ts file to have the provider field.

When a user creates an account with GitHub OAuth, we’ll set the provider field to GitHub whereas a user who registers with email and password will have the provider to be local .

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 Upsert the User

Now, add this service function to enable us to upsert 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

Next, let’s create a githubOauthHandler function in the auth.controller.ts file. This controller will be evoked 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`);
  }
};

In the above code, you’ll notice that when we called the findAndUpdateUser service we defined above, we added upsert: true in the query options.

Adding upsert: true will instruct Mongoose to create the user if the email used in the query doesn’t exist in the database. On the other hand, Mongoose will only update the user’s credentials if that email already exists in the database.

Create the Route

Edit the session.routes.ts file and add 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;

Add the Router To the Middleware Stack

Finally, add the session router defined above to the Express middleware stack in the src/app.ts file.

src/app.ts


import sessionRouter from './routes/session.route';

// ? Register the session router
app.use('/api/sessions', sessionRouter);

Conclusion

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

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