Movatterモバイル変換


[0]ホーム

URL:


CodevoWeb

PressESC to close

tRPC API with Postgres, Prisma & Node.js: JWT Authentication

2Comments94

This article will teach you how to add JSON Web Token (JWT) Authentication to atRPC API built with PostgreSQL, Prisma, Express, Node.js, and Redis.

When we build full-stack applications with TypeScript, we end up developing different strategies to share the static types between the frontend and backend, this approach works for a few genius developers.

Those who fall into the vast majority of people either end up increasing the complexity of their project or wasting productive hours writing unnecessary code.

Up until now, GraphQL has been the dominant library used by developers to build type-safety full-stack applications.

GraphQL was a game changer for most developers since it came with more flexibility and control.

However, the flexibility GraphQL brought to developers came at a price in the form of extra complexity and the need to learn the query language.

Due to the complexities of GraphQL, frameworks, libraries, and services started popping up to streamline the development process, and provide better patterns.

Recently,tRPC was introduced to help developers statically type their API endpoints and directly share the types between the client and server.

tRPC is lightweight, haszero dependencies, doesn’t rely on code generation, leaves a tiny client-side footprint, and has adaptors forFastify/Express/Next.js.

tRPC API with React.js, Express, Prisma, and PostgreSQL Series:

  1. How to Setup tRPC API with Prisma, PostgreSQL, Node & React
  2. tRPC API with PostgreSQL, Prisma, Redis, & Node.js: JWT Authentication

Related Articles:

tRPC API with Postgres, Prisma & Node.js JWT Authentication

Prerequisites

Before we begin:

  • Basic knowledge of Node.js, and Express will be helpful.
  • An intermediate understanding of Prisma, and how ORMs and relational databases work will be highly beneficial.
  • Have Docker and Node.js installed

Setting up the Project

We will be using Express, Prisma, PostgreSQL, and tRPC along with the Express adaptor to implement the authentication flow.

Also, we will be using PostgreSQL as the primary database and Redis to store the authenticated user’s session. Despite implementing the authentication with JSON Web Tokens, we need to include Redis to add an extra layer of security.

Follow theProject Setup tutorial to set up thetRPC project with PostgreSQL, Express and Prisma before continuing with this article.

Creating the Database Models with Prisma

Prisma is an open-source ORM (Object Relational Mapping) for Node.js and TypeScript.

At the time of writing this article, Prisma supports PostgreSQL, MySQL, SQLite, SQL Server, MongoDB, and CockroachDB – making it the dominant ORM for any kind of project.

Before we start creating the database models, change the directory into thepackages/client folder and install the following dependencies:

yarn add -D prisma && yarn add @prisma/client

Next, run the Prismainit command with an optional--datasource-provider parameter to set the type of database.

By default, theinit command creates a PostgreSQL database, however, you can use the--datasource-provider parameter to change the database type.

npx prisma init --datasource-provider postgresql

The above command will create aprisma folder and either create a new.env file or update an existing one.

The newly-createdpackages/server/prisma/schema.prisma file will contain the configurations to connect to the database and the models needed to generate the database tables.

With Prisma, we define all our schemas or models in thepackages/server/prisma/schema.prisma file.

Let’s create aUser model and provide it with all the required attributes:

packages/server/prisma/schema.prisma

generator client {  provider = "prisma-client-js"}datasource db {  provider = "postgresql"  url      = env("DATABASE_URL")}model User{  @@map(name: "users")  id String  @id @default(uuid())  name String  @db.VarChar(255)  email String @unique  photo String? @default("default.png")  verified Boolean? @default(false)     password String  role RoleEnumType? @default(user)  createdAt DateTime @default(now())  updatedAt DateTime @updatedAt  provider String?}enum RoleEnumType {  user  admin}

Next, open thepackages/server/.env file and update the dummyDATABASE_URL added by Prisma with the database credentials.

packages/server/.env

NODE_ENV=developmentORIGIN=http://127.0.0.1:3000DATABASE_URL="postgresql://postgres:password123@localhost:6500/trpc_prisma?schema=public"

Database Migration with Prisma

Before we can run the database migration command, you need to have a running instance of a PostgreSQL database.

Follow theProject Setup tutorial to set up thetRPC project with PostgreSQL, Express, Docker, and Prisma before continuing with this article.

Now add the following scripts to thepackages/server/package.json file:

{"scripts": {    "start": "ts-node-dev --respawn --transpile-only src/app.ts",    "db:migrate": "npx prisma migrate dev --name user-entity --create-only && yarn prisma generate",    "db:push": "npx prisma db push"  },}
  • db:migrate – this script will run the Prismamigrate command to create a new migration file and also generate the TypeScript types based on the models defined in theschema.prisma file.
  • db:push – this script will run the Prismadb command to push the changes to the database and keep the database in sync with the Prisma schema.

Open the terminal and run the following commands to generate the migration file and push the schema to the database.

yarn db:migrate && yarn db:push

If you are familiar withpgAdmin, you can log in with the database credentials to see the table added by Prisma.

checking the prisma model attributes in postgresql using pgadmin

Creating Schemas with Zod

By default, tRPC has support for schema validation libraries likeYup,Superstruct,Zod, andmyzod. Also, it gives you the freedom to use your custom validators.

Run this command to install Zod:

yarn add zod

Next, create apackages/server/src/schemas/user.schema.ts file with the following schema definitions.

packages/server/src/schema/user.schema.ts

import { object, string, TypeOf } from 'zod';export const createUserSchema = object({  name: string({ required_error: 'Name is required' }),  email: string({ required_error: 'Email is required' }).email('Invalid email'),  photo: string({ required_error: 'Photo is required' }),  password: string({ required_error: 'Password is required' })    .min(8, 'Password must be more than 8 characters')    .max(32, 'Password must be less than 32 characters'),  passwordConfirm: string({ required_error: 'Please confirm your password' }),}).refine((data) => data.password === data.passwordConfirm, {  path: ['passwordConfirm'],  message: 'Passwords do not match',});export const loginUserSchema = object({  email: string({ required_error: 'Email is required' }).email(    'Invalid email or password'  ),  password: string({ required_error: 'Password is required' }).min(    8,    'Invalid email or password'  ),});export type CreateUserInput = TypeOf<typeof createUserSchema>;export type LoginUserInput = TypeOf<typeof loginUserSchema>;

Creating Functions to Sign and Verify JWTs

yarn add jsonwebtoken && yarn add -D @types/jsonwebtoken

To learn more about JWT Authentication with React, check outReact + Redux Toolkit: JWT Authentication and Authorization

To learn more about Refresh Tokens with React, check outReact.js + Redux Toolkit: Refresh Tokens Authentication

Generating the JWT Private and Public Keys

Follow these steps to generate the private and public keys needed to sign and verify the JWT Tokens.

Alternatively, you can use the ones I provided in thepackages/server/.env file.

Step 1: Go to thePrivate and Public Keys Generation Site, and click the“Generate New Keys” button to create the private and public keys.

Step 2: Copy the private key and visit theBase64 Encoding website to encode it inBase64.

Step 3: Copy the base64 encoded string and add it to thepackages/server/.env file asACCESS_TOKEN_PRIVATE_KEY .

Step 4: Go back to thePrivate and Public Keys Generation Site and copy the corresponding public key before encoding it in base64.

Add it to thepackages/server/.env file asACCESS_TOKEN_PUBLIC_KEY .

Step 5: Repeat the above steps for the refresh token private and public keys.

In the end, thepackages/server/.env should look somewhat like this:

packages/server/.env

NODE_ENV=developmentORIGIN=http://127.0.0.1:3000DATABASE_URL="postgresql://postgres:password123@localhost:6500/trpc_prisma?schema=public"ACCESS_TOKEN_PRIVATE_KEY=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCT1FJQkFBSkFVTWhaQjNucFJ6OEdrc0tneVZJcEZHMkJqZldHdElMWGNLUVFGMHZGbVZvOVVFcDhyOEVmCnI5T204azVTaXgrSi8rbXc0d08xVUlGb25rQTJFWnl6THdJREFRQUJBa0FDcVViOWp3K1hVRVU0S244L2dweGwKMXVHd3VvandnMnJ6aEFRZnNGaFhIKzlyQ1NWTmxNaEk0UWh4bWt3bHI2Y0NhUnFMUGg0Vk5lN3hKRDloWU5XcApBaUVBbXJ4TENiQXJJWDIvYkFETU1JdXljWFZKMnAwUk91S3FQOVBVeTB6ZG0zc0NJUUNGcGs5VDJKQ3NUVEhWCjErMWFVbk9JOFE3eUdNK1VQVGt6a200emNHcE8zUUloQUloOEU3Z2M4ejVjVzQ5WmVNSk5SbjI3VmdTRnpKL2oKTlFhTnc4SDdML0duQWlCTS9lUFJEMzg0WXpnRVV1SGZHSVNLTFNSSS8xWUZ0Y2RRR0ZqM3RSam8yUUlnS2t6ZwpVWFkwWjJRR1dqblFTdzdJRThaSDZuTHcydFUrci9LR0NZRzVKN3M9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tACCESS_TOKEN_PUBLIC_KEY=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZzd0RRWUpLb1pJaHZjTkFRRUJCUUFEU2dBd1J3SkFVTWhaQjNucFJ6OEdrc0tneVZJcEZHMkJqZldHdElMWApjS1FRRjB2Rm1WbzlVRXA4cjhFZnI5T204azVTaXgrSi8rbXc0d08xVUlGb25rQTJFWnl6THdJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tREFRESH_TOKEN_PRIVATE_KEY=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCT1FJQkFBSkFZOHRTUEZXMTk3bWgwcitCWUdLVTA4OFRPcDkrT2FObVNWQ1lMMTFhb05ZeEY1TSs1d0NSCnNDTnAxVEdHNW5zb215NW9QRitLajFsOGhjbmtUSUU2SndJREFRQUJBa0FVN2dLc1ZzbVlVQjJKWnRMS2xVSmoKZmUycGdPUG5VTWJXSDRvYmZQZlIvWGNteTdONkQyVXVQcnJ0MkdQVUpnNVJ4SG5NbVFpaDJkNHUwY3pqRDhpcApBaUVBcDFNaUtvY1BEWDJDU0lGN3c5SzVGWHlqMjIzQXJQcVJoUzNtL1dkVzVlVUNJUUNZcmxyeXRJOFkvODIzCkQ1ZTFHVExnbDlTcXN1UWdvaGF4ZCtKaXludGZHd0lnQ2xlK0xlakpTbWt1cTNLdGhzNDR1SlpLdnA2TElXWWYKcHA3T3YyMHExdTBDSVFDSy9lYWpuZ1hLLzB3NXcwTWJSUVpRK1VkTDRqRFZHRm5LVTFYUEUzOStVd0lnSEdLWgpjcDd2K3VyeG5kU05GK25MVEpZRG9abkMrKytteXRMaCtSUmU4dVU9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tREFRESH_TOKEN_PUBLIC_KEY=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZzd0RRWUpLb1pJaHZjTkFRRUJCUUFEU2dBd1J3SkFZOHRTUEZXMTk3bWgwcitCWUdLVTA4OFRPcDkrT2FObQpTVkNZTDExYW9OWXhGNU0rNXdDUnNDTnAxVEdHNW5zb215NW9QRitLajFsOGhjbmtUSUU2SndJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0t

Next, let’s add the variables to thepackages/server/src/config/default.ts file to help us provide the TypeScript types for the environment variables.

packages/server/src/config/default.ts

import path from 'path';require('dotenv').config({ path: path.join(__dirname, '../../.env') });const customConfig: {  port: number;  accessTokenExpiresIn: number;  refreshTokenExpiresIn: number;  origin: string;  dbUri: string;  accessTokenPrivateKey: string;  accessTokenPublicKey: string;  refreshTokenPrivateKey: string;  refreshTokenPublicKey: string;  redisCacheExpiresIn: number;} = {  port: 8000,  accessTokenExpiresIn: 15,  refreshTokenExpiresIn: 60,  redisCacheExpiresIn: 60,  origin: 'http://localhost:3000',  dbUri: process.env.DATABASE_URL as string,  accessTokenPrivateKey: process.env.ACCESS_TOKEN_PRIVATE_KEY as string,  accessTokenPublicKey: process.env.ACCESS_TOKEN_PUBLIC_KEY as string,  refreshTokenPrivateKey: process.env.REFRESH_TOKEN_PRIVATE_KEY as string,  refreshTokenPublicKey: process.env.REFRESH_TOKEN_PUBLIC_KEY as string,};export default customConfig;

With the above configurations, let’s create apackages/server/src/utils/jwt.ts file and add these two functions to sign and verify the JSON Web Tokens.

Signing the JWT Tokens

packages/server/src/utils/jwt.ts

import jwt, { SignOptions } from 'jsonwebtoken';import customConfig from '../config/default';export const signJwt = (  payload: Object,  key: 'accessTokenPrivateKey' | 'refreshTokenPrivateKey',  options: SignOptions = {}) => {  const privateKey = Buffer.from(customConfig[key], 'base64').toString('ascii');  return jwt.sign(payload, privateKey, {    ...(options && options),    algorithm: 'RS256',  });};

Verifying the JWT Tokens

packages/server/src/utils/jwt.ts

export const verifyJwt = <T>(  token: string,  key: 'accessTokenPublicKey' | 'refreshTokenPublicKey'): T | null => {  try {    const publicKey = Buffer.from(customConfig[key], 'base64').toString(      'ascii'    );    return jwt.verify(token, publicKey) as T;  } catch (error) {    console.log(error);    return null;  }};

Creating the Database Services with Prisma

One of the best practices of RESTful API architecture is to separate the business and application logic.

In layman’s terms, you should create services to interact with the database. That means the controllers should not be allowed to communicate directly with the database.

The rule is to push most of the business logic to the models and services, leaving us with thinner controllers and larger models.

Now let’s create apackages/server/src/services/user.service.ts file with the following services.

packages/server/src/services/user.service.ts

import { Prisma, User } from '@prisma/client';import customConfig from '../config/default';import redisClient from '../utils/connectRedis';import { signJwt } from '../utils/jwt';import { prisma } from '../utils/prisma';export const createUser = async (input: Prisma.UserCreateInput) => {  return (await prisma.user.create({    data: input,  })) as User;};export const findUser = async (  where: Partial<Prisma.UserCreateInput>,  select?: Prisma.UserSelect) => {  return (await prisma.user.findFirst({    where,    select,  })) as User;};export const findUniqueUser = async (  where: Prisma.UserWhereUniqueInput,  select?: Prisma.UserSelect) => {  return (await prisma.user.findUnique({    where,    select,  })) as User;};export const updateUser = async (  where: Partial<Prisma.UserWhereUniqueInput>,  data: Prisma.UserUpdateInput,  select?: Prisma.UserSelect) => {  return (await prisma.user.update({ where, data, select })) as User;};export const signTokens = async (user: Prisma.UserCreateInput) => {  // 1. Create Session  redisClient.set(`${user.id}`, JSON.stringify(user), {    EX: customConfig.redisCacheExpiresIn * 60,  });  // 2. Create Access and Refresh tokens  const access_token = signJwt({ sub: user.id }, 'accessTokenPrivateKey', {    expiresIn: `${customConfig.accessTokenExpiresIn}m`,  });  const refresh_token = signJwt({ sub: user.id }, 'refreshTokenPrivateKey', {    expiresIn: `${customConfig.refreshTokenExpiresIn}m`,  });  return { access_token, refresh_token };};

Creating the Authentication Handlers

We are now ready to create the authentication handlers to:

  1. Create a new user
  2. Request a new access token
  3. Sign the user into the account
  4. Log the user out from the account

To begin, let’s define the cookie options. Create apackages/server/src/controllers/auth.controller.ts file with the following imports and cookie options.

packages/server/src/controllers/auth.controller.ts

import { TRPCError } from '@trpc/server';import bcrypt from 'bcryptjs';import { CookieOptions } from 'express';import { Context } from '../app';import customConfig from '../config/default';import { CreateUserInput, LoginUserInput } from '../schema/user.schema';import {  createUser,  findUniqueUser,  findUser,  signTokens,} from '../services/user.service';import redisClient from '../utils/connectRedis';import { signJwt, verifyJwt } from '../utils/jwt';// [...] Imports// [...] Cookie Optionsconst cookieOptions: CookieOptions = {  httpOnly: true,  secure: process.env.NODE_ENV === 'production',  sameSite: 'lax',};const accessTokenCookieOptions: CookieOptions = {  ...cookieOptions,  expires: new Date(Date.now() + customConfig.accessTokenExpiresIn * 60 * 1000),};const refreshTokenCookieOptions: CookieOptions = {  ...cookieOptions,  expires: new Date(    Date.now() + customConfig.refreshTokenExpiresIn * 60 * 1000  ),};

How to Register a New User

packages/server/src/controllers/auth.controller.ts

// [...] Register User Handlerexport const registerHandler = async ({  input,}: {  input: CreateUserInput;}) => {  try {    const hashedPassword = await bcrypt.hash(input.password, 12);    const user = await createUser({      email: input.email.toLowerCase(),      name: input.name,      password: hashedPassword,      photo: input.photo,      provider: 'local',    });    return {      status: 'success',      data: {        user,      },    };  } catch (err: any) {    if (err instanceof Prisma.PrismaClientKnownRequestError) {      if (err.code === 'P2002') {        throw new TRPCError({          code: 'CONFLICT',          message: 'Email already exists',        });      }    }    throw err;  }};

Let’s evaluate the above code:

First, we hashed the plain-text password provided by the user and called thecreateUser() service to add the user to the PostgreSQL database.

Also, since we added a unique constraint to the email column, Prisma will return an error with a “P2002” code indicating that the email already exists in the PostgreSQL database.

How to Sign in the User

Now that we are able to create a user, let’s define thetRPC handler to sign in the registered user.

packages/server/src/controllers/auth.controller.ts

// [...] Register User Handler// [...] Login User Handlerexport const loginHandler = async ({  input,  ctx,}: {  input: LoginUserInput;  ctx: Context;}) => {  try {    // Get the user from the collection    const user = await findUser({ email: input.email.toLowerCase() });    // Check if user exist and password is correct    if (!user || !(await bcrypt.compare(input.password, user.password))) {      throw new TRPCError({        code: 'BAD_REQUEST',        message: 'Invalid email or password',      });    }    // Create the Access and refresh Tokens    const { access_token, refresh_token } = await signTokens(user);    // Send Access Token in Cookie    ctx.res.cookie('access_token', access_token, accessTokenCookieOptions);    ctx.res.cookie('refresh_token', refresh_token, refreshTokenCookieOptions);    ctx.res.cookie('logged_in', true, {      ...accessTokenCookieOptions,      httpOnly: false,    });    // Send Access Token    return {      status: 'success',      access_token,    };  } catch (err: any) {    throw err;  }};

Quite a lot happening in the above, let’s break it down:

  • First, we evoked thefindUser() service to check if that email exists in the PostgreSQL database.
  • Next, we validated the plain-text password with the hashed one in the database.
  • Lastly, we generated the access and refresh tokens and sent them to thetRPC client asHTTPOnly cookies.

How to Refresh the Access Token

packages/server/src/controllers/auth.controller.ts

// [...] Register User Handler// [...] Login User Handler// [...] Refresh Access Token Handlerexport const refreshAccessTokenHandler = async ({ ctx }: { ctx: Context }) => {  try {    // Get the refresh token from cookie    const refresh_token = ctx.req.cookies?.refresh_token as string;    const message = 'Could not refresh access token';    if (!refresh_token) {      throw new TRPCError({ code: 'FORBIDDEN', message });    }    // Validate the Refresh token    const decoded = verifyJwt<{ sub: string }>(      refresh_token,      'refreshTokenPublicKey'    );    if (!decoded) {      throw new TRPCError({ code: 'FORBIDDEN', message });    }    // Check if the user has a valid session    const session = await redisClient.get(decoded.sub);    if (!session) {      throw new TRPCError({ code: 'FORBIDDEN', message });    }    // Check if the user exist    const user = await findUniqueUser({ id: JSON.parse(session).id });    if (!user) {      throw new TRPCError({ code: 'FORBIDDEN', message });    }    // Sign new access token    const access_token = signJwt({ sub: user.id }, 'accessTokenPrivateKey', {      expiresIn: `${customConfig.accessTokenExpiresIn}m`,    });    // Send the access token as cookie    ctx.res.cookie('access_token', access_token, accessTokenCookieOptions);    ctx.res.cookie('logged_in', true, {      ...accessTokenCookieOptions,      httpOnly: false,    });    // Send response    return {      status: 'success',      access_token,    };  } catch (err: any) {    throw err;  }};

How to SignOut the User

packages/server/src/controllers/auth.controller.ts

// [...] Register User Handler// [...] Login User Handler// [...] Refresh Access Token Handler// [...] Logout User Handlerconst logout = ({ ctx }: { ctx: Context }) => {  ctx.res.cookie('access_token', '', { maxAge: -1 });  ctx.res.cookie('refresh_token', '', { maxAge: -1 });  ctx.res.cookie('logged_in', '', {    maxAge: -1,  });};export const logoutHandler = async ({ ctx }: { ctx: Context }) => {  try {    const user = ctx.user;    await redisClient.del(user?.id as string);    logout({ ctx });    return { status: 'success' };  } catch (err: any) {    throw err;  }};

Complete Code for the Authentication Handlers

packages/server/src/controllers/auth.controller.ts

import { TRPCError } from '@trpc/server';import bcrypt from 'bcryptjs';import { CookieOptions } from 'express';import { Context } from '../app';import customConfig from '../config/default';import { CreateUserInput, LoginUserInput } from '../schema/user.schema';import {  createUser,  findUniqueUser,  findUser,  signTokens,} from '../services/user.service';import redisClient from '../utils/connectRedis';import { signJwt, verifyJwt } from '../utils/jwt';// [...] Imports// [...] Cookie Optionsconst cookieOptions: CookieOptions = {  httpOnly: true,  secure: process.env.NODE_ENV === 'production',  sameSite: 'lax',};const accessTokenCookieOptions: CookieOptions = {  ...cookieOptions,  expires: new Date(Date.now() + customConfig.accessTokenExpiresIn * 60 * 1000),};const refreshTokenCookieOptions: CookieOptions = {  ...cookieOptions,  expires: new Date(    Date.now() + customConfig.refreshTokenExpiresIn * 60 * 1000  ),};// [...] Register User Handlerexport const registerHandler = async ({  input,}: {  input: CreateUserInput;}) => {  try {    const hashedPassword = await bcrypt.hash(input.password, 12);    const user = await createUser({      email: input.email.toLowerCase(),      name: input.name,      password: hashedPassword,      photo: input.photo,      provider: 'local',    });    return {      status: 'success',      data: {        user,      },    };  } catch (err: any) {   if (err instanceof Prisma.PrismaClientKnownRequestError) {      if (err.code === 'P2002') {        throw new TRPCError({          code: 'CONFLICT',          message: 'Email already exists',        });      }    }    throw err;  }};// [...] Login User Handlerexport const loginHandler = async ({  input,  ctx,}: {  input: LoginUserInput;  ctx: Context;}) => {  try {    // Get the user from the collection    const user = await findUser({ email: input.email.toLowerCase() });    // Check if user exist and password is correct    if (!user || !(await bcrypt.compare(input.password, user.password))) {      throw new TRPCError({        code: 'BAD_REQUEST',        message: 'Invalid email or password',      });    }    // Create the Access and refresh Tokens    const { access_token, refresh_token } = await signTokens(user);    // Send Access Token in Cookie    ctx.res.cookie('access_token', access_token, accessTokenCookieOptions);    ctx.res.cookie('refresh_token', refresh_token, refreshTokenCookieOptions);    ctx.res.cookie('logged_in', true, {      ...accessTokenCookieOptions,      httpOnly: false,    });    // Send Access Token    return {      status: 'success',      access_token,    };  } catch (err: any) {    throw err;  }};// [...] Refresh Access Token Handlerexport const refreshAccessTokenHandler = async ({ ctx }: { ctx: Context }) => {  try {    // Get the refresh token from cookie    const refresh_token = ctx.req.cookies?.refresh_token as string;    const message = 'Could not refresh access token';    if (!refresh_token) {      throw new TRPCError({ code: 'FORBIDDEN', message });    }    // Validate the Refresh token    const decoded = verifyJwt<{ sub: string }>(      refresh_token,      'refreshTokenPublicKey'    );    if (!decoded) {      throw new TRPCError({ code: 'FORBIDDEN', message });    }    // Check if the user has a valid session    const session = await redisClient.get(decoded.sub);    if (!session) {      throw new TRPCError({ code: 'FORBIDDEN', message });    }    // Check if the user exist    const user = await findUniqueUser({ id: JSON.parse(session).id });    if (!user) {      throw new TRPCError({ code: 'FORBIDDEN', message });    }    // Sign new access token    const access_token = signJwt({ sub: user.id }, 'accessTokenPrivateKey', {      expiresIn: `${customConfig.accessTokenExpiresIn}m`,    });    // Send the access token as cookie    ctx.res.cookie('access_token', access_token, accessTokenCookieOptions);    ctx.res.cookie('logged_in', true, {      ...accessTokenCookieOptions,      httpOnly: false,    });    // Send response    return {      status: 'success',      access_token,    };  } catch (err: any) {    throw err;  }};// [...] Logout User Handlerconst logout = ({ ctx }: { ctx: Context }) => {  ctx.res.cookie('access_token', '', { maxAge: -1 });  ctx.res.cookie('refresh_token', '', { maxAge: -1 });  ctx.res.cookie('logged_in', '', {    maxAge: -1,  });};export const logoutHandler = async ({ ctx }: { ctx: Context }) => {  try {    const user = ctx.user;    await redisClient.del(user?.id as string);    logout({ ctx });    return { status: 'success' };  } catch (err: any) {    throw err;  }};

Creating a User Handler

packages/server/src/controllers/user.controller.ts

import { TRPCError } from '@trpc/server';import type { Context } from '../app';export const getMeHandler = ({ ctx }: { ctx: Context }) => {  try {    const user = ctx.user;    return {      status: 'success',      data: {        user,      },    };  } catch (err: any) {    throw new TRPCError({      code: 'INTERNAL_SERVER_ERROR',      message: err.message,    });  }};

Creating the Authentication Guard

packages/server/src/middleware/deserializeUser.ts

import { TRPCError } from '@trpc/server';import { findUniqueUser } from '../services/user.service';import { Request, Response } from 'express';import redisClient from '../utils/connectRedis';import { verifyJwt } from '../utils/jwt';export const deserializeUser = async ({  req,  res,}: {  req: Request;  res: Response;}) => {  try {    // Get the token    let access_token;    if (      req.headers.authorization &&      req.headers.authorization.startsWith('Bearer')    ) {      access_token = req.headers.authorization.split(' ')[1];    } else if (req.cookies?.access_token) {      access_token = req.cookies.access_token;    }    const notAuthenticated = {      req,      res,      user: null,    };    if (!access_token) {      return notAuthenticated;    }    // Validate Access Token    const decoded = verifyJwt<{ sub: string }>(      access_token,      'accessTokenPublicKey'    );    if (!decoded) {      return notAuthenticated;    }    // Check if user has a valid session    const session = await redisClient.get(decoded.sub);    if (!session) {      return notAuthenticated;    }    // Check if user still exist    const user = await findUniqueUser({ id: JSON.parse(session).id });    if (!user) {      return notAuthenticated;    }    return {      req,      res,      user,    };  } catch (err: any) {    throw new TRPCError({      code: 'INTERNAL_SERVER_ERROR',      message: err.message,    });  }};

Connecting the Routers to the App

Before adding thetRPC endpoints, install the following dependencies:

yarn add cookie-parser && yarn add -D @types/cookie-parser morgan @types/morgan
  • cookie-parser – for parsing cookies in the request headers
  • morgan – for logging thetRPC requests

Now open thepackages/server/src/app.ts file and add the followingtRPC endpoints.

In addition, add the cookie parser to the middleware stack to enable Express to parse the cookies.

packages/server/src/app.ts

import path from "path";import dotenv from "dotenv";import express from "express";import morgan from "morgan";import cors from "cors";import { inferAsyncReturnType, initTRPC, TRPCError } from "@trpc/server";import * as trpcExpress from "@trpc/server/adapters/express";import redisClient from "./utils/connectRedis";import customConfig from "./config/default";import connectDB from "./utils/prisma";import { deserializeUser } from "./middleware/deserializeUser";import { createUserSchema, loginUserSchema } from "./schema/user.schema";import {  loginHandler,  logoutHandler,  refreshAccessTokenHandler,  registerHandler,} from "./controllers/auth.controller";import { getMeHandler } from "./controllers/user.controller";import cookieParser from "cookie-parser";dotenv.config({ path: path.join(__dirname, "./.env") });const createContext = ({ req, res }: trpcExpress.CreateExpressContextOptions) =>  deserializeUser({ req, res });export type Context = inferAsyncReturnType<typeof createContext>;export const t = initTRPC.context<Context>().create();const authRouter = t.router({  registerUser: t.procedure    .input(createUserSchema)    .mutation(({ input }) => registerHandler({ input })),  loginUser: t.procedure    .input(loginUserSchema)    .mutation(({ input, ctx }) => loginHandler({ input, ctx })),  logoutUser: t.procedure.mutation(({ ctx }) => logoutHandler({ ctx })),  refreshToken: t.procedure.query(({ ctx }) =>    refreshAccessTokenHandler({ ctx })  ),});const isAuthorized = t.middleware(({ ctx, next }) => {  if (!ctx.user) {    throw new TRPCError({      code: "UNAUTHORIZED",      message: "You must be logged in to access this resource",    });  }  return next();});const isAuthorizedProcedure = t.procedure.use(isAuthorized);const userRouter = t.router({  sayHello: t.procedure.query(async () => {    const message = await redisClient.get("tRPC");    return { message };  }),  getMe: isAuthorizedProcedure.query(({ ctx }) => getMeHandler({ ctx })),});const appRouter = t.mergeRouters(authRouter, userRouter);export type AppRouter = typeof appRouter;const app = express();if (process.env.NODE_ENV !== "production") app.use(morgan("dev"));app.use(cookieParser());app.use(  cors({    origin: [customConfig.origin, "http://localhost:3000"],    credentials: true,  }));app.use(  "/api/trpc",  trpcExpress.createExpressMiddleware({    router: appRouter,    createContext,  }));const port = customConfig.port;app.listen(port, () => {  console.log(`🚀 Server listening on port ${port}`);  // CONNECT DB  connectDB();});

Conclusion

Congratulations on reaching the end. In this article, you learned how to implement access and refresh tokens in atRPC API using Node.js, Express, PostgreSQL, Prisma, Docker-compose, and Redis.

tRPC, Prisma, PostgreSQL, & Express Source Code

Check out the:

Share Article:

How to Setup tRPC API with Prisma, PostgreSQL, Node & React

Left Arrow

CRUD RESTful API Server with Python, FastAPI, and PostgreSQL

Right Arrow

2 Comments

  1. Muhammeton May 27, 2023

    This project is working after a while because of refreshing problem. Can you correct it please?

    Reply
    • Edemon May 28, 2023

      Thank you for reaching out. I’m glad to assist you with the refreshing problem in your project.

      To better understand the issue and provide an appropriate solution, could you please provide more details about the specific problem you are facing?

      This would help me diagnose the issue more effectively and provide you with the necessary guidance. Thank you!

      Reply

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