diff --git a/README.md b/README.md new file mode 100644 index 0000000..21603a5 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# How to Implement GitHub OAuth in Node.js + +In this tutorial, I'll walk you through the process of integrating GitHub OAuth into a Node.js application, including setting up the OAuth App on GitHub, retrieving the OAuth client ID and secret, and implementing the necessary code to handle the OAuth flow. + +![How to Implement GitHub OAuth in Node.js](https://codevoweb.com/wp-content/uploads/2023/01/How-to-Implement-GitHub-OAuth-in-Node.js.webp) + +## Topics Covered + +- Run the Node.js GitHub OAuth Project +- Run the Node.js API with a React App +- Setup the Node.js Project +- Get the GitHub OAuth Credentials +- Create the Database Model +- Create the Validation Schemas +- Obtain the GitHub OAuth Access Token and User's Info + - Retrieve the OAuth Access Token + - Retrieve the GitHub Account Information +- Implement GitHub OAuth in Node.js + - Account Registration Route Function + - Account Login Route Function + - Logout Route Function + - Authenticate with GitHub OAuth Route Function +- GetMe Route Function +- Create the Authentication Middleware + - Authentication Guard + - Require User Middleware +- Create the API Routes +- Register the API Routes and Setup CORS + +Read the entire article here: [https://codevoweb.com/how-to-implement-github-oauth-in-nodejs/](https://codevoweb.com/how-to-implement-github-oauth-in-nodejs/) diff --git a/example.env b/example.env index e6e8a47..14088b3 100644 --- a/example.env +++ b/example.env @@ -1,9 +1,5 @@ DATABASE_URL="file:./dev.db" -GOOGLE_OAUTH_CLIENT_ID= -GOOGLE_OAUTH_CLIENT_SECRET= -GOOGLE_OAUTH_REDIRECT=http://localhost:8000/api/sessions/oauth/google - GITHUB_OAUTH_CLIENT_ID= GITHUB_OAUTH_CLIENT_SECRET= GITHUB_OAUTH_REDIRECT_URL=http://localhost:8000/api/sessions/oauth/github diff --git a/src/app.ts b/src/app.ts index 0883063..80001d8 100644 --- a/src/app.ts +++ b/src/app.ts @@ -28,10 +28,10 @@ app.use("/api/users", userRouter); app.use("/api/auth", authRouter); app.use("/api/sessions", sessionRouter); -app.get("/api/healthChecker", (req: Request, res: Response) => { +app.get("/api/healthchecker", (req: Request, res: Response) => { res.status(200).json({ status: "success", - message: "Implement OAuth in Node.js", + message: "How to Implement GitHub OAuth in Node.js", }); }); diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 5775dc9..61127cc 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -1,11 +1,6 @@ import { NextFunction, Request, Response } from "express"; import { CreateUserInput, LoginUserInput } from "../schema/user.schema"; -import { - getGithubOathToken, - getGithubUser, - getGoogleOauthToken, - getGoogleUser, -} from "../services/session.service"; +import { getGithubOathToken, getGithubUser } from "../services/session.service"; import { prisma } from "../utils/prisma"; import jwt from "jsonwebtoken"; @@ -68,15 +63,10 @@ export const loginHandler = async ( }); } - if (user.provider === "Google") { + if (user.provider === "GitHub") { return res.status(401).json({ status: "fail", - message: "Use Google OAuth2 instead", - }); - } else if (user.provider === "GitHub") { - return res.status(401).json({ - status: "fail", - message: "Use GitHub OAuth instead", + message: `Use ${user.provider} OAuth2 instead`, }); } @@ -111,67 +101,6 @@ export const logoutHandler = async ( } }; -export const googleOauthHandler = async (req: Request, res: Response) => { - const FRONTEND_ORIGIN = process.env.FRONTEND_ORIGIN as unknown as string; - - try { - const code = req.query.code as string; - const pathUrl = (req.query.state as string) || "/"; - - if (!code) { - return res.status(401).json({ - status: "fail", - message: "Authorization code not provided!", - }); - } - - const { id_token, access_token } = await getGoogleOauthToken({ code }); - - const { name, verified_email, email, picture } = await getGoogleUser({ - id_token, - access_token, - }); - - if (!verified_email) { - return res.status(403).json({ - status: "fail", - message: "Google account not verified", - }); - } - - const user = await prisma.user.upsert({ - where: { email }, - create: { - createdAt: new Date(), - name, - email, - photo: picture, - password: "", - verified: true, - provider: "Google", - }, - update: { name, email, photo: picture, provider: "Google" }, - }); - - if (!user) return res.redirect(`${FRONTEND_ORIGIN}/oauth/error`); - - const TOKEN_EXPIRES_IN = process.env.TOKEN_EXPIRES_IN as unknown as number; - const TOKEN_SECRET = process.env.JWT_SECRET as unknown as string; - const token = jwt.sign({ sub: user.id }, TOKEN_SECRET, { - expiresIn: `${TOKEN_EXPIRES_IN}m`, - }); - - res.cookie("token", token, { - expires: new Date(Date.now() + TOKEN_EXPIRES_IN * 60 * 1000), - }); - - res.redirect(`${FRONTEND_ORIGIN}${pathUrl}`); - } catch (err: any) { - console.log("Failed to authorize Google User", err); - return res.redirect(`${FRONTEND_ORIGIN}/oauth/error`); - } -}; - export const githubOauthHandler = async (req: Request, res: Response) => { const FRONTEND_ORIGIN = process.env.FRONTEND_ORIGIN as unknown as string; diff --git a/src/routes/session.route.ts b/src/routes/session.route.ts index 2f1e0ca..2d3c9ba 100644 --- a/src/routes/session.route.ts +++ b/src/routes/session.route.ts @@ -1,12 +1,8 @@ -import express from 'express'; -import { - githubOauthHandler, - googleOauthHandler, -} from '../controllers/auth.controller'; +import express from "express"; +import { githubOauthHandler } from "../controllers/auth.controller"; const router = express.Router(); -router.get('/oauth/google', googleOauthHandler); -router.get('/oauth/github', githubOauthHandler); +router.get("/oauth/github", githubOauthHandler); export default router; diff --git a/src/services/session.service.ts b/src/services/session.service.ts index bfef8b0..b139d77 100644 --- a/src/services/session.service.ts +++ b/src/services/session.service.ts @@ -1,135 +1,15 @@ import axios from "axios"; import qs from "qs"; -const GOOGLE_OAUTH_CLIENT_ID = process.env - .GOOGLE_OAUTH_CLIENT_ID as unknown as string; -const GOOGLE_OAUTH_CLIENT_SECRET = process.env - .GOOGLE_OAUTH_CLIENT_SECRET as unknown as string; -const GOOGLE_OAUTH_REDIRECT = process.env - .GOOGLE_OAUTH_REDIRECT as unknown as string; - const GITHUB_OAUTH_CLIENT_ID = process.env .GITHUB_OAUTH_CLIENT_ID as unknown as string; const GITHUB_OAUTH_CLIENT_SECRET = process.env .GITHUB_OAUTH_CLIENT_SECRET as unknown as string; -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 => { - const rootURl = "https://oauth2.googleapis.com/token"; - - const options = { - code, - client_id: GOOGLE_OAUTH_CLIENT_ID, - client_secret: GOOGLE_OAUTH_CLIENT_SECRET, - redirect_uri: GOOGLE_OAUTH_REDIRECT, - grant_type: "authorization_code", - }; - try { - const { data } = await axios.post( - 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 { - try { - const { data } = await axios.get( - `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, }: { @@ -159,6 +39,13 @@ export const getGithubOathToken = async ({ } }; +interface GitHubUser { + login: string; + avatar_url: string; + name: string; + email: string; +} + export const getGithubUser = async ({ access_token, }: {