Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Add JWT Authentication in Bun API
Harsh Mangalam
Harsh Mangalam

Posted on • Edited on

     

Add JWT Authentication in Bun API

In this blog post we are going to add authentication in Bun based REST API.

Bun

Bun is relatively new javascript runtime that is built on top of JavaScriptCore engine (used in apple safari) and Zig programming language. It has built in transpiler, bundler, test runner and npm-compatible package manager.

Elysia

Elysia is a fully type-safe web framework built on top of Bun having familier syntax like express.

Prisma

Prisma is a Nodejs and Typescript ORM that reduce the burden of writting pure SQL command to interact with database. You can use both SQL and NoSQL database with prisma.

In this post we are going to use Postgresql to store user data and we will use Prisma cli to initialize new postgresql database and apply schema migrations.

Prerequisite

Install Bun (https://bun.sh/docs/installation)
Setup Postgresql (https://www.postgresql.org/download/)

Lets create new elysia project using bun command line

bun create elysia auth
Enter fullscreen modeExit fullscreen mode

Now openauth project in vscode

cdauthcode.
Enter fullscreen modeExit fullscreen mode

src/index.ts

importElysiafrom"elysia";import{auth}from"~modules/auth";import{cookie}from"@elysiajs/cookie";import{jwt}from"@elysiajs/jwt";constapp=newElysia().group("/api",(app)=>app.use(jwt({name:"jwt",secret:Bun.env.JWT_SECRET!,})).use(cookie()).use(auth)).listen(8080);console.log(`🦊 Elysia is running at${app.server?.hostname}:${app.server?.port}`);
Enter fullscreen modeExit fullscreen mode

First we created an instance ofElysia then we addedjwt andcookie plugins provided by elysia. You can install both plugins using bun command.

bun add @elysiajs/cookie @elysiajs/jwt
Enter fullscreen modeExit fullscreen mode

cookie plugin adds support for using cookie in Elysia handler andjwt plugin adds support for using JWT in Elysia handler. Internally@elysiajs/jwt usejose (https://github.com/panva/jose).

We used grouping features of elysia which allows you to combine multiple prefixes into one.

Suppose that we have these routes having repeated prefix.

/api/auth/signup
/api/auth/login
/api/auth/logout

Instead we can group them with prefix/api/.

Forjwt plugin you can explicitly register the JWT function with a different name usingname property.

You can access environment variable in Bun usingBun.env. Create a dot file on top level.env.local and addJWT_SECRET.

.env.local

JWT_SECRET="itssecret"
Enter fullscreen modeExit fullscreen mode

Then you can useBun.env.JWT_SECRET to accessJWT_SECRET value available in env file. Because Bun is Node compatible so you can also useprocess.env.JWT_SECRET.

In TypeScript, the exclamation mark (!) is known as the non-null assertion operator. It is used to assert that a value is not null or undefined.

We have registeredauth module usingapp.use(auth) so that we can keep our auth related handlers separate.

Next we are going to setup prisma.

Add Prisma CLI as a development dependency

bun add-d  prisma
Enter fullscreen modeExit fullscreen mode

Next, set up your Prisma project by creating your Prisma schema file with the following command:

bunx prisma init
Enter fullscreen modeExit fullscreen mode

bunx is similer tonpx orpnpx the primary purpose ofbunx is to facilitate the execution of packages that are listed in thedependencies ordevDependencies section of a project'spackage.json file. Instead of manually installing these packages globally or locally, you can usebunx to run them directly.

Now create user schema insideprisma/schema.prisma file

generator client {  provider = "prisma-client-js"}datasource db {  provider = "postgresql"  url      = env("DATABASE_URL")}model User {  id                String    @id @default(uuid())  name              String  username          String    @unique  email             String    @unique  salt              String  hash              String  summary           String?  links             Json?  location          Json?  profileImage      String  createdAt         DateTime  @default(now())  updatedAt         DateTime  @updatedAt}
Enter fullscreen modeExit fullscreen mode

Next we are going to apply migrations to createuser table in our database.

bunx prisma db push
Enter fullscreen modeExit fullscreen mode

Migrations are changes to your database schema, such as creating tables, altering columns, or adding indexes.

Next we are going to add prisma client package to interact with database.

bun add @prisma/client
Enter fullscreen modeExit fullscreen mode

Inside/src/libs/prisma.ts create instance of prisma client and export it.

import{PrismaClient}from"@prisma/client";exportconstprisma=newPrismaClient();
Enter fullscreen modeExit fullscreen mode

Inside/src/modules/auth/index.ts add auth related handlers

import{Elysia,t}from"elysia";import{prisma}from"~libs/prisma";import{comparePassword,hashPassword,md5hash}from"~utils/bcrypt";import{isAuthenticated}from"~middlewares/auth";exportconstauth=(app:Elysia)=>app.group("/auth",(app)=>app.post("/signup",async({body,set})=>{const{email,name,password,username}=body;// validate duplicate email addressconstemailExists=awaitprisma.user.findUnique({where:{email,},select:{id:true,},});if(emailExists){set.status=400;return{success:false,data:null,message:"Email address already in use.",};}// validate duplicate usernameconstusernameExists=awaitprisma.user.findUnique({where:{username,},select:{id:true,},});if(usernameExists){set.status=400;return{success:false,data:null,message:"Someone already taken this username.",};}// handle passwordconst{hash,salt}=awaithashPassword(password);constemailHash=md5hash(email);constprofileImage=`https://www.gravatar.com/avatar/${emailHash}?d=identicon`;constnewUser=awaitprisma.user.create({data:{name,email,hash,salt,username,profileImage,},});return{success:true,message:"Account created",data:{user:newUser,},};},{body:t.Object({name:t.String(),email:t.String(),username:t.String(),password:t.String(),}),}).post("/login",async({body,set,jwt,setCookie})=>{const{username,password}=body;// verify email/usernameconstuser=awaitprisma.user.findFirst({where:{OR:[{email:username,},{username,},],},select:{id:true,hash:true,salt:true,},});if(!user){set.status=400;return{success:false,data:null,message:"Invalid credentials",};}// verify passwordconstmatch=awaitcomparePassword(password,user.salt,user.hash);if(!match){set.status=400;return{success:false,data:null,message:"Invalid credentials",};}// generate accessconstaccessToken=awaitjwt.sign({userId:user.id,});setCookie("access_token",accessToken,{maxAge:15*60,// 15 minutespath:"/",});return{success:true,data:null,message:"Account login successfully",};},{body:t.Object({username:t.String(),password:t.String(),}),}).use(isAuthenticated)// protected route.get("/me",({user})=>{return{success:true,message:"Fetch authenticated user details",data:{user,},};}));
Enter fullscreen modeExit fullscreen mode

Here we have grouped all handlers in/auth prefix.

set is used to set status code , headers or redirect for response.
Usingbody we can parse request body data in our case this will be JSON request body.

We have used Elysia Schema to add validation for request body. Schema is used to define the strict type for the Elysia handler. Like in Login route we defined the structure of schema that we are going to receive from client in the third parameter ofapp.post(). Here we have added schema validation forbody but you can add schema validation for query, params, header etc...

Now create/src/utils/bcrypt.ts and add following codes:

import{randomBytes,pbkdf2,createHash}from"node:crypto";asyncfunctionhashPassword(password:string):Promise<{hash:string;salt:string}>{constsalt=randomBytes(16).toString("hex");returnnewPromise((resolve,reject)=>{pbkdf2(password,salt,1000,64,"sha512",(error,derivedKey)=>{if(error){returnreject(error);}returnresolve({hash:derivedKey.toString("hex"),salt});});});}asyncfunctioncomparePassword(password:string,salt:string,hash:string):Promise<boolean>{returnnewPromise((resolve,reject)=>{pbkdf2(password,salt,1000,64,"sha512",(error,derivedKey)=>{if(error){returnreject(error);}returnresolve(hash===derivedKey.toString("hex"));});});}functionmd5hash(text:string){returncreateHash("md5").update(text).digest("hex");}export{hashPassword,comparePassword,md5hash};
Enter fullscreen modeExit fullscreen mode

We added utility function to hash plain password , compare password and generate md5 hash from strings usingnode:crypto package.

/src/middlewares/auth.ts

import{Elysia}from"elysia";import{prisma}from"~libs";exportconstisAuthenticated=(app:Elysia)=>app.derive(async({cookie,jwt,set})=>{if(!cookie!.access_token){set.status=401;return{success:false,message:"Unauthorized",data:null,};}const{userId}=awaitjwt.verify(cookie!.access_token);if(!userId){set.status=401;return{success:false,message:"Unauthorized",data:null,};}constuser=awaitprisma.user.findUnique({where:{id:userId,},});if(!user){set.status=401;return{success:false,message:"Unauthorized",data:null,};}return{user,};});
Enter fullscreen modeExit fullscreen mode

derive allows you to customize Context based on existing Context. Here we have retureduser from derive nowuser will available in handlers context.

You can configuretsconfig.jsonpaths directory to resolve non-relative module names.

"paths":{"~libs/*":["./src/libs/*"],"~modules/*":["./src/modules/*"],"~utils/*":["./src/utils/*"],"~middlewares/*":["./src/middlewares/*"]},
Enter fullscreen modeExit fullscreen mode

Lets start the server

bun run dev
Enter fullscreen modeExit fullscreen mode
🦊 Elysia is running at 0.0.0.0:8080
Enter fullscreen modeExit fullscreen mode

Top comments(8)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
cholasimmons profile image
Chola
a Pasionate Creative
  • Location
    Zambia
  • Work
    Software Engineer at Simmons Studio
  • Joined
• Edited on• Edited

Seems like a fairly recent tutorial, however Prisma.ts tells me "@prisma/client" has no exported member called PrismaClient.
PS: The paths fix that you say to add topackage.json, should actually be added totsconfig.json

CollapseExpand
 
harshmangalam profile image
Harsh Mangalam
Open Sourcer | Tech Blogger | Fullstack Engineer | Tech Event Speaker | Freelancer
  • Email
  • Location
    India
  • Education
    BCA
  • Pronouns
    he/him
  • Work
    Software Engineer
  • Joined

Thanks Chola , I have fixed

CollapseExpand
 
bhumit070 profile image
Bhoomit Ganatra
MERN stack developer
  • Location
    India
  • Pronouns
    He/Him/His
  • Joined

Can you please also provide source code for this?

CollapseExpand
 
harshmangalam profile image
Harsh Mangalam
Open Sourcer | Tech Blogger | Fullstack Engineer | Tech Event Speaker | Freelancer
  • Email
  • Location
    India
  • Education
    BCA
  • Pronouns
    he/him
  • Work
    Software Engineer
  • Joined

This is the repo from where i have picked up auth part.
github.com/harshmangalam/elysia-bl...

CollapseExpand
 
bhumit070 profile image
Bhoomit Ganatra
MERN stack developer
  • Location
    India
  • Pronouns
    He/Him/His
  • Joined

I don't like the way we have to use ts-ignore to remove types if we have to move the code to other file have you find any solution for this
I am working on same
github.com/bhumit070/bun-drizzle

CollapseExpand
 
sistematico profile image
Lucas Saliés Brum
  • Joined

This is only the back-end(api)?
Great tutorial btw..

CollapseExpand
 
samlevy profile image
Samuel Levy
  • Joined
• Edited on• Edited

Thepath configuration goes intsconfig.json notpackage.json.

CollapseExpand
 
harshmangalam profile image
Harsh Mangalam
Open Sourcer | Tech Blogger | Fullstack Engineer | Tech Event Speaker | Freelancer
  • Email
  • Location
    India
  • Education
    BCA
  • Pronouns
    he/him
  • Work
    Software Engineer
  • Joined

Thanks Samuel Levy, I have corrected this

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Open Sourcer | Tech Blogger | Fullstack Engineer | Tech Event Speaker | Freelancer
  • Location
    India
  • Education
    BCA
  • Pronouns
    he/him
  • Work
    Software Engineer
  • Joined

More fromHarsh Mangalam

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp