DEV Community

Cover image for Implementing Authentication with Twitter OAuth 2.0 using Typescript, Express.js and Next.js
Rafid Hamid for Reinforz

Posted on • Updated on

Implementing Authentication with Twitter OAuth 2.0 using Typescript, Express.js and Next.js

Implementing Authentication with Twitter OAuth 2.0 using Typescript, Node.js, Express.js and Next.js in a Full Stack Application

Table of contents

What will we learn

Here, we will learn to implement authentication using Twitter OAuth 2.0 on a minimal working full-stack web application. We will not be using Passport.js or similar libraries to handle authentication for us. As a result, we will understand the OAuth 2.0 flow better. We will also learn about the following stacks:

  • express.js backend framework
  • prisma to create and login users, you can really use anything to communicate with any database.
  • next.js, a React.js framework, for the frontend
  • typescript (optional) type-safety for javascript

Requirements

Anyone with a basic knowledge of javascript can follow along with this blog.
If you already have a similar project setup, you can also jump straight to the Twitter OAuth2 Implementation section.

Project Setup

Firstly, let's add a package.json file at the root directory and add the following content:

{
  "private": true,
  "workspaces": [
    "server",
    "client"
  ],
  "scripts": {
    "client:dev": "yarn workspace client dev",
    "server:dev": "yarn workspace server dev",
    "client:add": "yarn workspace client add",
    "server:add": "yarn workspace server add",
    "migrate-db": "yarn workspace server prisma-migrate"
  }
}
Enter fullscreen mode Exit fullscreen mode

You can set up version control in this directory, but that is optional. Either way, we will now add a client and server for our web app.

Client setup

Make a Next.js app by running the following commands:

yarn create next-app --typescript client
Enter fullscreen mode Exit fullscreen mode

Skip the --typescript flag if you want to work with javascript.

This will create a client folder in the project directory. Navigate there and delete the files we don't need, i.e. client\styles\Home.module.css and client\pages\api. Also, let's replace all the code in client\pages\index.ts with the following:

import { NextPage } from "next";

const Home: NextPage = () => {
  return (
    <div className="column-container">
      <p>Hello!</p>
    </div>
  );
};

export default Home;
Enter fullscreen mode Exit fullscreen mode

Starting the client with our command yarn client:dev and going to the address at http://www.localhost:3000/ should display a webpage saying Hello!

Web page saying hello!

Now that the frontend is set up, let's move on to our backend.

Server setup

Make a directory called server and make a package.json file in the project directory with the following content:

{
  "name": "server",
  "version": "1.0.0",
  "scripts": {
    "start": "node dist/index.js",
    "build": "tsc --sourceMap false",
    "build:watch": "tsc -w",
    "start:watch": "nodemon dist/index.js",
    "dev": "concurrently \"yarn build:watch\" \"yarn start:watch\" --names \"tsc,node\" -c \"blue,green\"",
    "prisma-migrate": "prisma migrate dev",
    "prisma-gen": "prisma generate"
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, we added various scripts to help us in our development stage. We will mostly use the dev and the migrate-db scripts. They allow us to start the server in watch mode and let us migrate the database respectively. Now we can return to the workspace directory and use our yarn server:add to add packages. So its time to install the required dependencies using the following commands in the terminal:

yarn server:add @prisma/client argon2 axios cookie-parser cors dotenv express jsonwebtoken
Enter fullscreen mode Exit fullscreen mode
yarn server:add -D nodemon prisma typescript concurrently @types/cookie-parser @types/cors @types/express @types/jsonwebtoken @types/node
Enter fullscreen mode Exit fullscreen mode

After installing the dependencies we need, make a few files to have a minimal running express server:

  • server/tsconfig.json Edit according to preferences or skip if not using typescript
  {
    "compilerOptions": {
      "target": "ES2018",
      "module": "commonjs",
      "lib": [
        "esnext", "esnext.asynciterable"
      ],
      "strict": true,
      "skipLibCheck": true,
      "sourceMap": true,
      "declaration": true,
      "moduleResolution": "node",
      "noImplicitAny": true,
      "strictNullChecks": true,
      "strictFunctionTypes": true,
      "noImplicitThis": true,
      "noUnusedLocals": true,
      "noUnusedParameters": true,
      "noImplicitReturns": true,
      "noFallthroughCasesInSwitch": true,
      "allowSyntheticDefaultImports": true,
      "esModuleInterop": true,
      "emitDecoratorMetadata": true,
      "experimentalDecorators": true,
      "resolveJsonModule": true,
      "incremental": false,
      "baseUrl": "./src",
      "watch": false,
      "removeComments": true,
      "outDir": "./dist",
      "rootDir": "./src"
    },
    "types": ["node"],
    "include": ["./src/**/*.ts"],
  }
Enter fullscreen mode Exit fullscreen mode
  • server/src/index.ts to listen to the server.
  import { CLIENT_URL, SERVER_PORT } from "./config";
  import cookieParser from "cookie-parser";
  import cors from "cors";
  import express from "express";

  const app = express();

  const origin = [CLIENT_URL];

  app.use(cookieParser());
  app.use(cors({
    origin,
    credentials: true
  }))

  app.get("/ping", (_, res) => res.json("pong"));

  app.listen(SERVER_PORT, () => console.log(`Server listening on port ${SERVER_PORT}`))
Enter fullscreen mode Exit fullscreen mode
  • server/src/config.ts to export some constant configuration variables
  import { PrismaClient } from "@prisma/client"

  export const CLIENT_URL = process.env.CLIENT_URL!
  export const SERVER_PORT = process.env.SERVER_PORT!

  export const prisma = new PrismaClient()
Enter fullscreen mode Exit fullscreen mode
  • server/.env to setup the port, client URL and database URL for the server. Make sure to ignore this file if you are working with version control
  DATABASE_URL=postgres://postgres:postgres@localhost:5432/twitter-oauth2
  CLIENT_URL=http://www.localhost:3000
  SERVER_PORT=3001
Enter fullscreen mode Exit fullscreen mode
  • server/prisma/schema.prisma to let prisma handle the database structure
  generator client {
    provider = "prisma-client-js"
  }

  datasource db {
    provider = "postgresql"
    url      = env("DATABASE_URL")
  }

  enum UserType {
    local
    twitter
  }

  model User {
    id       String   @id @default(uuid())
    name     String
    username String   @unique
    type     UserType @default(local)
  }
Enter fullscreen mode Exit fullscreen mode

Now migrate the database using the yarn migrate-db command, and then we can run the server using yarn server:dev.

We should now be able to ping our server at http://localhost:3001/ping

Response of the request is "pong"

Twitter OAuth2 Implementation

We are ready to implement authentication via Twitter OAuth 2.0 into our app. We will follow this approach to do so.
Firstly, we have to make an app on Twitter.

Setup twitter user authentication settings

Head over to twitter's developer portal and make a project and a development app in the project with any name. Twitter will show you the things needed. It may take a few hours to get approval from Twitter to make these apps. Once it is done, head over to the settings page of the app to set some necessary fields.
Set up or edit the user authentication as needed by your app.

Edit user authentication set up

As I only need to read profile information for this minimal web app, these are the settings I used:

App permissions: Read, no request emails; App type: Web app

Callback URI: http://www.localhost:3001/oauth/twitter, Website URL: http://www.localhost:3000

Save the Twitter Client ID and client secret securely.

Note: http://www.localhost:3000 works but not http://localhost:3000.
So, I added www. in both websites.

Client

Frontend authentication button

Now we add the button in the client, which will lead to our backend for authentication.
To do so, we need to use a valid Twitter OAuth URL getter function and a button to go to the URL.

import twitterIcon from "../public/twitter.svg";
import Image from "next/image";

const TWITTER_CLIENT_ID = "T1dLaHdFSWVfTnEtQ2psZThTbnI6MTpjaQ" // give your twitter client id here

// twitter oauth Url constructor
function getTwitterOauthUrl() {
  const rootUrl = "https://twitter.com/i/oauth2/authorize";
  const options = {
    redirect_uri: "http://www.localhost:3001/oauth/twitter", // client url cannot be http://localhost:3000/ or http://127.0.0.1:3000/
    client_id: TWITTER_CLIENT_ID,
    state: "state",
    response_type: "code",
    code_challenge: "y_SfRG4BmOES02uqWeIkIgLQAlTBggyf_G7uKT51ku8",
    code_challenge_method: "S256",
    scope: ["users.read", "tweet.read", "follows.read", "follows.write"].join(" "), // add/remove scopes as needed
  };
  const qs = new URLSearchParams(options).toString();
  return `${rootUrl}?${qs}`;
}

// the component
export function TwitterOauthButton() {
  return (
    <a className="a-button row-container" href={getTwitterOauthUrl()}>
      <Image src={twitterIcon} alt="twitter icon" />
      <p>{" twitter"}</p>
    </a>
  );
}
Enter fullscreen mode Exit fullscreen mode

Note: We are hard coding code_challenge and code_verifier for simplicity. You can randomly generate it.

After adding the above code in client\components\TwitterOauthButton.tsx, we will add a twitter SVG icon (from online resources like this) on path client\public\twitter.svg.
Then we will import the component on the homepage:

import { TwitterOauthButton } from "../components/TwitterOauthButton";

const Home: NextPage = () => {
  return (
    <div className="column-container">
      <p>Hello!</p>
      <TwitterOauthButton />
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

This is how it should look like afterwards:

Webpage with Twitter icon and text, "Hello! twitter"

Clicking on the Twitter icon will lead us to the Twitter page where we can authorize the app:

Twitter interface asking whether to authorize app or cancel

Of course, clicking on the authorize app button leads to a Cannot GET /oauth/twitter response, as we haven't implemented the backend yet.

Me query

Let's request for the current logged in user from the frontend through a hook, client\hooks\useMeQuery.ts:

import { useEffect, useState } from "react";
import axios, { AxiosResponse } from "axios";

export type User = {
  id: string;
  name: string;
  username: string;
  type: "local" | "twitter";
};

export function useMeQuery() {
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState<boolean>(true);
  const [data, setData] = useState<User | null>(null);

  useEffect(() => {
    setLoading(true);
    axios
      .get<any, AxiosResponse<User>>(`http://www.localhost:3001/me`, {
        withCredentials: true,
      })
      .then((v) => {
        if (v.data) setData(v.data);
      })
      .catch(() => setError("Not Authenticated"))
      .finally(() => setLoading(false));
  }, []);

  return { error, data, loading };
}
Enter fullscreen mode Exit fullscreen mode

This will do a good enough job for our minimal app. We will use it to determine what to render. We will render the username if we get a user from the hook. Otherwise, we will render the Login with Twitter button

import type { NextPage } from "next";
import { TwitterOauthButton } from "../components/TwitterOauthButton";
import { useMeQuery } from "../hooks/useMeQuery";

const Home: NextPage = () => {
  const { data: user } = useMeQuery();
  return (
    <div className="column-container">
      <p>Hello!</p>
      {user ? (// user present so only display user's name
        <p>{user.name}</p>
      ) : (// user not present so prompt to login
        <div>
          <p>You are not Logged in! Login with:</p>
          <TwitterOauthButton />
        </div>
      )}
    </div>
  );
};

export default Home;
Enter fullscreen mode Exit fullscreen mode

The above is how the final client\pages\index.tsx will look like. Go to http://www.localhost:3000 and inspect the network window of the browser while the page is loading. You should see the Me query being executed there.

Information about the query GET http://www.localhost:3001/me which failed with status 404

Its 404 because we havent implemented it in the backend

Styling

Let's just add some basic styling while we are at it by modifying the client\styles\globals.css file:

html,
body {
  padding: 0;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
    Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}

a {
  color: inherit;
  text-decoration: none;
}

.column-container {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.row-container {
  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: center;
}

.a-button {
  border: 2px solid grey;
  border-radius: 5px;
}

.a-button:hover {
  background-color: #111133;
}

@media (prefers-color-scheme: dark) {
  html {
    color-scheme: dark;
  }
  body {
    color: white;
    background: black;
  }
}
Enter fullscreen mode Exit fullscreen mode

That is all we have to do on our client-side. The final homepage should look like this:

Webpage saying to log in by clicking on the below twitter logo button

Server

As we saw from our frontend, we need to implement GET /oauth/twitter route in our server to make the Twitter OAuth part of the app work. A look at the twitter documentation reveals the steps we need to perform so that we can read the info we mentioned in our scopes here.
These steps are summarized below:

  1. getting the access token
  2. getting the Twitter user from the access token
  3. upsert the user in our database
  4. create cookie so that the server can validate the user
  5. redirect to the client with the cookie

Note: Only the first two steps are related to Twitter OAuth Implementation

Lets add a file server\src\oauth2.ts where we will add our OAuth related codes. We will complete the steps above by defining a function there:

// the function which will be called when twitter redirects to the server at http://www.localhost:3001/oauth/twitter
export async function twitterOauth(req: Request<any, any, any, {code:string}>, res: Response) {
  const code = req.query.code; // getting the code if the user authorized the app

  // 1. get the access token with the code

  // 2. get the twitter user using the access token

  // 3. upsert the user in our db

  // 4. create cookie so that the server can validate the user

  // 5. finally redirect to the client

  return res.redirect(CLIENT_URL);
}
Enter fullscreen mode Exit fullscreen mode

Before doing any of that make sure we the Twitter OAuth client secret in our .env file. We will also add a JWT secret there so that we can encrypt the cookie we send to the client. The final .env file should look like this:

DATABASE_URL=postgres://postgres:postgres@localhost:5432/twitter-oauth2
CLIENT_URL=http://www.localhost:3000
SERVER_PORT=3001
JWT_SECRET=put-your-jwt-secret-here
TWITTER_CLIENT_SECRET=put-your-twitter-client-secret-here
Enter fullscreen mode Exit fullscreen mode

Getting the access token with the code

Add the following code(the commented lines explain what they do) to get the access token:

// add your client id and secret here:
const TWITTER_OAUTH_CLIENT_ID = "T1dLaHdFSWVfTnEtQ2psZThTbnI6MTpjaQ";
const TWITTER_OAUTH_CLIENT_SECRET = process.env.TWITTER_CLIENT_SECRET!;

// the url where we get the twitter access token from
const TWITTER_OAUTH_TOKEN_URL = "https://api.twitter.com/2/oauth2/token";

// we need to encrypt our twitter client id and secret here in base 64 (stated in twitter documentation)
const BasicAuthToken = Buffer.from(`${TWITTER_OAUTH_CLIENT_ID}:${TWITTER_OAUTH_CLIENT_SECRET}`, "utf8").toString(
  "base64"
);

// filling up the query parameters needed to request for getting the token
export const twitterOauthTokenParams = {
  client_id: TWITTER_OAUTH_CLIENT_ID,
  // based on code_challenge
  code_verifier: "8KxxO-RPl0bLSxX5AWwgdiFbMnry_VOKzFeIlVA7NoA",
  redirect_uri: `http://www.localhost:3001/oauth/twitter`,
  grant_type: "authorization_code",
};

// the shape of the object we should recieve from twitter in the request
type TwitterTokenResponse = {
  token_type: "bearer";
  expires_in: 7200;
  access_token: string;
  scope: string;
};

// the main step 1 function, getting the access token from twitter using the code that twitter sent us
export async function getTwitterOAuthToken(code: string) {
  try {
    // POST request to the token url to get the access token
    const res = await axios.post<TwitterTokenResponse>(
      TWITTER_OAUTH_TOKEN_URL,
      new URLSearchParams({ ...twitterOauthTokenParams, code }).toString(),
      {
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
          Authorization: `Basic ${BasicAuthToken}`,
        },
      }
    );

    return res.data;
  } catch (err) {
    console.error(err);

    return null;
  }
}
Enter fullscreen mode Exit fullscreen mode

Getting the Twitter User from access token

Similar code to get user from access token:

// the shape of the response we should get
export interface TwitterUser {
  id: string;
  name: string;
  username: string;
}

// getting the twitter user from access token
export async function getTwitterUser(accessToken: string): Promise<TwitterUser | null> {
  try {
    // request GET https://api.twitter.com/2/users/me
    const res = await axios.get<{ data: TwitterUser }>("https://api.twitter.com/2/users/me", {
      headers: {
        "Content-type": "application/json",
        // put the access token in the Authorization Bearer token
        Authorization: `Bearer ${accessToken}`,
      },
    });

    return res.data.data ?? null;
  } catch (err) {
    console.error(err);

    return null;
  }
}
Enter fullscreen mode Exit fullscreen mode

Checking if they work

Let's see if they successfully gets us the user. After adding all the code in the server\src\oauth2.ts file it should look like this:

import { CLIENT_URL } from "./config";
import axios from "axios";
import { Request, Response } from "express";

// add your client id and secret here:
const TWITTER_OAUTH_CLIENT_ID = "T1dLaHdFSWVfTnEtQ2psZThTbnI6MTpjaQ";
const TWITTER_OAUTH_CLIENT_SECRET = process.env.TWITTER_CLIENT_SECRET!;

// the url where we get the twitter access token from
const TWITTER_OAUTH_TOKEN_URL = "https://api.twitter.com/2/oauth2/token";

// we need to encrypt our twitter client id and secret here in base 64 (stated in twitter documentation)
const BasicAuthToken = Buffer.from(`${TWITTER_OAUTH_CLIENT_ID}:${TWITTER_OAUTH_CLIENT_SECRET}`, "utf8").toString(
  "base64"
);

// filling up the query parameters needed to request for getting the token
export const twitterOauthTokenParams = {
  client_id: TWITTER_OAUTH_CLIENT_ID,
  code_verifier: "8KxxO-RPl0bLSxX5AWwgdiFbMnry_VOKzFeIlVA7NoA",
  redirect_uri: `http://www.localhost:3001/oauth/twitter`,
  grant_type: "authorization_code",
};

// the shape of the object we should recieve from twitter in the request
type TwitterTokenResponse = {
  token_type: "bearer";
  expires_in: 7200;
  access_token: string;
  scope: string;
};

// the main step 1 function, getting the access token from twitter using the code that the twitter sent us
export async function getTwitterOAuthToken(code: string) {
  try {
    // POST request to the token url to get the access token
    const res = await axios.post<TwitterTokenResponse>(
      TWITTER_OAUTH_TOKEN_URL,
      new URLSearchParams({ ...twitterOauthTokenParams, code }).toString(),
      {
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
          Authorization: `Basic ${BasicAuthToken}`,
        },
      }
    );

    return res.data;
  } catch (err) {
    return null;
  }
}

// the shape of the response we should get
export interface TwitterUser {
  id: string;
  name: string;
  username: string;
}

// getting the twitter user from access token
export async function getTwitterUser(accessToken: string): Promise<TwitterUser | null> {
  try {
    // request GET https://api.twitter.com/2/users/me
    const res = await axios.get<{ data: TwitterUser }>("https://api.twitter.com/2/users/me", {
      headers: {
        "Content-type": "application/json",
        // put the access token in the Authorization Bearer token
        Authorization: `Bearer ${accessToken}`,
      },
    });

    return res.data.data ?? null;
  } catch (err) {
    return null;
  }
}

// the function which will be called when twitter redirects to the server at http://www.localhost:3001/oauth/twitter
export async function twitterOauth(req: Request<any, any, any, {code:string}>, res: Response) {
  const code = req.query.code;

  // 1. get the access token with the code
  const TwitterOAuthToken = await getTwitterOAuthToken(code);
  console.log(TwitterOAuthToken);

  if (!TwitterOAuthToken) {
    // redirect if no auth token
    return res.redirect(CLIENT_URL);
  }

  // 2. get the twitter user using the access token
  const twitterUser = await getTwitterUser(TwitterOAuthToken.access_token);
  console.log(twitterUser);

  if (!twitterUser) {
    // redirect if no twitter user
    return res.redirect(CLIENT_URL);
  }

  // 3. upsert the user in our db

  // 4. create cookie so that the server can validate the user

  // 5. finally redirect to the client

  return res.redirect(CLIENT_URL);
}
Enter fullscreen mode Exit fullscreen mode

Import and add the route to our express app:

app.get("/ping", (_, res) => res.json("pong"));

// activate twitterOauth function when visiting the route 
app.get("/oauth/twitter", twitterOauth);
app.listen(SERVER_PORT, () => console.log(`Server listening on port ${SERVER_PORT}`))
Enter fullscreen mode Exit fullscreen mode

Now run the client and server, and look at the server console on what happens if we click on the Twitter button in the frontend and authorize the app.

successfully fetching user and access token fron twitter

We successfully got the user from Twitter now!
The most important part, i.e. getting the user from Twitter, is done. Now we can finish up our project.

Finishing the web app

Let's finish up the rest of the steps needed for GET /oauth/twitter to work. Since they are not related to OAuth, I will add the functions in the server\src\config.ts file.

import { PrismaClient, User } from "@prisma/client"
import { CookieOptions, Response } from "express";
import { TwitterUser } from "./oauth2";
import jwt from "jsonwebtoken";

export const CLIENT_URL = process.env.CLIENT_URL!
export const SERVER_PORT = process.env.SERVER_PORT!
export const prisma = new PrismaClient()

// step 3
export function upsertUser(twitterUser: TwitterUser) {
  // create a new user in our database or return an old user who already signed up earlier 
  return prisma.user.upsert({
    create: {
      username: twitterUser.username,
      id: twitterUser.id,
      name: twitterUser.name,
      type: "twitter",
    },
    update: {
      id: twitterUser.id,
    },
    where: {  id: twitterUser.id},
  });
}

// JWT_SECRET from our environment variable file
export const JWT_SECRET = process.env.JWT_SECRET!

// cookie name
export const COOKIE_NAME = 'oauth2_token'

// cookie setting options
const cookieOptions: CookieOptions = {
  httpOnly: true,
  secure: process.env.NODE_ENV === 'production'
  sameSite: "strict"
}

// step 4
export function addCookieToRes(res: Response, user: User, accessToken: string) {
  const { id, type } = user;
  const token = jwt.sign({ // Signing the token to send to client side
    id,
    accessToken,
    type
  }, JWT_SECRET);
  res.cookie(COOKIE_NAME, token, {  // adding the cookie to response here
    ...cookieOptions,
    expires: new Date(Date.now() + 7200 * 1000),
  });
}
Enter fullscreen mode Exit fullscreen mode

Import the functions and use them in the server\src\oauth2.ts:

import { prisma, CLIENT_URL, addResCookie } from "./config";

...

// the function which will be called when twitter redirects to the server at http://www.localhost:3001/oauth/twitter
export async function twitterOauth(req: Request<any, any, any, {code:string}>, res: Response) {
  const code = req.query.code;

  // 1. get the access token with the code
  const twitterOAuthToken = await getTwitterOAuthToken(code);

  if (!twitterOAuthToken) {
    // redirect if no auth token
    return res.redirect(CLIENT_URL);
  }

  // 2. get the twitter user using the access token
  const twitterUser = await getTwitterUser(twitterOAuthToken.access_token);

  if (!twitterUser) {
    // redirect if no twitter user
    return res.redirect(CLIENT_URL);
  }


  // 3. upsert the user in our db
  const user = await upsertUser(twitterUser)

  // 4. create cookie so that the server can validate the user
  addCookieToRes(res, user, twitterOAuthToken.access_token)

  // 5. finally redirect to the client
  return res.redirect(CLIENT_URL);
}
Enter fullscreen mode Exit fullscreen mode

Note: We are sending the access token in the cookie for simplicity. For a web application, we should store it somewhere more secure, like a database.

And finally, add the me query in the server\src\index.ts file.

import { CLIENT_URL, COOKIE_NAME, JWT_SECRET, prisma, SERVER_PORT } from "./config";
import cookieParser from "cookie-parser";
import cors from "cors";
import express from "express";
import jwt from 'jsonwebtoken'
import { getTwitterUser, twitterOauth } from "./oauth2";
import { User } from "@prisma/client";

const app = express();
const origin= [CLIENT_URL];
app.use(cookieParser());
app.use(cors({
  origin,
  credentials: true
}))
app.get("/ping", (_, res) => res.json("pong"));

type UserJWTPayload = Pick<User, 'id'|'type'> & {accessToken: string}

app.get('/me', async (req, res)=>{
  try {
    const token = req.cookies[COOKIE_NAME];
    if (!token) {
      throw new Error("Not Authenticated");
    }
    const payload = await jwt.verify(token, JWT_SECRET) as UserJWTPayload;
    const userFromDb = await prisma.user.findUnique({
      where: { id: payload?.id },
    });
    if (!userFromDb) throw new Error("Not Authenticated");
    if (userFromDb.type === "twitter") {
      if (!payload.accessToken) {
        throw new Error("Not Authenticated");
      }
      const twUser = await getTwitterUser(payload.accessToken);
      if (twUser?.id !== userFromDb.id) {
        throw new Error("Not Authenticated");
      }
    }
    res.json(userFromDb)
  } catch (err) {
    res.status(401).json("Not Authenticated")
  }
})

// activate twitterOauth function when visiting the route 
app.get("/oauth/twitter", twitterOauth);
app.listen(SERVER_PORT, () => console.log(`Server listening on port ${SERVER_PORT}`))
Enter fullscreen mode Exit fullscreen mode

It is done now! Let's see what happens when we click the Twitter button on our client and authorize the app there.

Successful login and setting up cookies in FE

We see our Twitter username in there instead of the Twitter button now, which shows that the me query is being executed successfully. As a result, we now have a working user authentication system, via Twitter OAuth 2.0, in our minimal full-stack web application.

Conclusion

Thanks for reading! This is the Github repository with all the codes. Find more fun things you can do with the Twitter API here. Another example implementation of authentication via Twitter OAuth 2.0 can be found here.

Written by Rafid Hamid

Top comments (6)

Collapse
 
andypiper profile image
Andy Piper

Nice post! There's also a Twitter SDK for TypeScript, and an Awesome list of cool libraries and things you're welcome to add to!

Collapse
 
reinforz_dev profile image
Reinforz AI

Thank you! We plan to write on several topics in the future, so stay tuned!

Collapse
 
prishav profile image
priya sharma

Amazing post. It is really helpful and informative. and nodejs and expressjs content are really informative. thanks for sharing this. You can also check here : Blue Prism Certification

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
thermal_insulator profile image
Meraj Hasan

Hi,
Is it possible to only use port 3000 for callback too? Otherwise I have to run two separate instances (client and server) for it to work.
Also, Twitter says localhost wont work, but here it does. Any ideas why?

Collapse
 
reinforz_dev profile image
Reinforz AI

Hello.
It is possible to use port 3000 for the callback URL in Twitter. Next.js can be used to build the API route your callback URL would point. It is not too different from this approach and you can check the official Next.js documentation to see how to use Next.js API routes:

API Routes: Introduction | Next.js

Next.js supports API Routes, which allow you to build your API without leaving your Next.js app. Learn how it works here.

favicon nextjs.org