รู้จักกับ Next Auth + Prisma

/ 16 min read

Share on social media

Next Auth คืออะไร ?

NextAuth.js (https://next-auth.js.org/) ****คือ library ระบบยืนยันตัวตนแบบ open-source ที่ออกแบบมาสำหรับ Next.js โดยเฉพาะ library นี้ช่วยแก้ปัญหาในการติดตั้งระบบยืนยันตัวตนและระบบให้สิทธิ์ผู้ใช้งาน โดยรองรับระบบการยืนยันตัวตนหลากหลายรูปแบบ ไม่ว่าจะเป็น OAuth 1.0, 1.0A, 2.0, OpenID Connect การยืนยันตัวตนด้วยอีเมล และการยืนยันตัวตนแบบไร้รหัสผ่าน (passwordless) ได้เช่นเดียวกัน

โดยจุดเด่นของ NextAuth.js มีตั้งแต่

  • สามารถเชื่อมต่อกับผู้ให้บริการยอดนิยม ไม่ว่าจะเป็น Google, Facebook และอื่น ๆ อีกมากมาย
  • มีตัวเลือกให้ใช้งาน Session ร่วมกับฐานข้อมูล หรือ JSON Web Tokens (JWT) ในการเก็บข้อมูล Session เอาไว้ โดยรองรับทั้ง Session บน Client และ Server
  • มี feature ด้าน security ที่ได้รับการออกแบบตาม practice ของ security อยู่แล้ว เช่น signed, prefixed, server-only cookies, การตรวจสอบ CSRF Token บนการส่งข้อมูลแบบ HTTP POST รวมถึงการใช้ JWT ร่วมกับ JWS/JWE/JWK สำหรับการเข้ารหัสด้วย
  • อนุญาตให้ปรับแต่งหน้าลงชื่อเข้าใช้และออกจากระบบได้ รวมถึงมี function callback สำหรับจัดการ Event ต่างๆ ที่เกิดขึ้นระหว่างการเข้าสู่ระบบได้

NextAuth.js นั้นจุดเด่นจริงๆคือ ช่วยลดความยุ่งยากในการติดตั้งระบบ authentication ให้กับ application Next.js โดยทำให้กระบวนการต่าง ๆ ไม่ว่าจะเป็นการจัดการ Session ขั้นตอนเข้าสู่ระบบ/ออกจากระบบ และการจัดการข้อมูล user ทำได้ง่ายขึ้นผ่าน NextAuth.js

ทีนี้ในปัจจุบัน หากเราเข้า document ของ NextAuth.js ก็จะเจอว่า NextAuth.js ได้กลายมาเป็นส่วนหนึ่งของ Auth.js (https://authjs.dev/) ไปเป็นที่เรียบร้อยแล้ว โดย Auth.js คือ open source package ที่ได้เตรียม library สำหรับการทำ authentication ของ web application เอาไว้ โดย design ไว้ให้ใช้สำหรับ “modern web framework ใดๆก็ได้” แทน ทำให้เราสามารถใช้ idea ของการ implement ที่แต่เดิมมีอยู่แค่ใน NextAuth.js สามารถใช้งานกับ framework ใดก็ได้ โดยเปลี่ยนมาเรียกใช้ผ่าน Auth.js แทน

เพราะฉะนั้น แม้ว่าในหัวข้อนี้เราจะพูดถึง NextAuth.js ก็ตามแต่ library หลายๆตัว (เช่น adapter สำหรับการต่อ database) ได้โดยย้ายไปที่ Auth.js เป็นที่เรียบร้อย ดังนั้นให้สังเกตให้ดีว่า library ทีใช้อยู่นั้นอยู่ใน NextAuth.js หรือ Auth.js เพื่อให้สามารถอ่าน document จากถูกที่ได้เช่นกัน

next-auth-01.webp

เราจะทำอะไรกันในหัวข้อนี้บ้าง

เพื่อให้เห็นภาพการใช้งาน NextAuth.js เราจะมาลองทำระบบ Sign in / Sign up ด้วย Email / Password กันก่อน ทีนี้เพื่อให้ต่อเนื่องกับหัวข้อที่เราเคยทำมาของ Next.js เราจะขอหยิบ PostgreSQL มาเป็น database และ Prisma มาเป็น ORM สำหรับการต่อพูดคุยกับ database (เนื่องจาก NextAuth.js มี adapter ที่ support กับ Prisma อยู่แล้ว ทำให้เราสามารถใช้งาน Prisma กับ NextAuth โดยตรงได้เลย

โจทย์ของเราในวันนี้คือ เราจะทำ 3 อย่างกัน

  1. Sign in / Sign up ด้วย Prisma และ NextAuth ผ่าน Email และ Password

  2. ลองใช้งานร่วมกับ Role เพื่อดึงข้อมูลอื่นๆมาใช้งานร่วมกัน (สำหรับการควบคุมสิทธิ์)

  3. ลองใช้งานร่วมกับ Social Login อย่าง Google และทำให้สามารถใช้งานร่วมกับ Email / Password ได้

เริ่มต้น เราจะทำการ setup project กันด้วยท่าประจำเอกสารของ Next.js นั่นคือ

Terminal window
npx create-next-app@latest <ชื่อ project next>
next-auth-02.webp

โดย project นี้เราจะขอเลือกเป็น javascript เพื่อให้ทุกคนสามารถจับ concept จาก javascript กันก่อน (รวมถึงตัวอย่าง typescript มีเยอะมากแล้วด้วย) หลังจาก create project มาเรียบร้อย ให้ทำการ

Terminal window
npm install
npm run dev

หาก run เว็บได้ที่ localhost:3000 ก็ถือว่าพร้อมสำหรับ Next.js และ Step ต่อมาเราจะทำการลง Database ไว้ให้พร้อม ซึ่งเป็นวิธีการเดียวกันกับบทความ Prisma ORM ที่ใช้ในหัวข้อก่อนหน้านี้

https://blog.mikelopster.dev/next-prisma/

ดังนั้น docker ที่ใช้สำหรับการ run postgres จะเป็นตัวเดียวกันกับในบทความของ Prisma ให้ทำการวาง docker-compose.yml ใน root project

version: '3.8'
services:
postgres:
image: postgres:latest
environment:
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
POSTGRES_DB: mydb
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
pgadmin:
image: dpage/pgadmin4
environment:
PGADMIN_DEFAULT_EMAIL: admin@admin.com
PGADMIN_DEFAULT_PASSWORD: root
ports:
- "5050:80"
depends_on:
- postgres
volumes:
postgres-data:

และทำการ run ด้วย docker-compose up -d --build ขึ้นมา และเมื่อ run ทุกอย่างถูกต้องจะต้องได้ผลลัพธ์แบบนี้ออกมา

https://mikelopster.dev/img/blogs/next-prisma/blog-02.gif

ตอนนี้เราก็มีทั้ง project Next.js และ database (PostgreSQL) พร้อมแล้วเรียบร้อย มาเข้าสู่ step สุดท้ายคือการสร้าง Schema ของ Prisma เริ่มต้นเอาไว้ก่อน (ก่อนที่เราจะเพิ่มต่อในหัวข้อถัดไป)

สุดท้ายให้ทำการเริ่มต้น Schema file ของ Prisma ขึ้นมาด้วยการลง package Prisma และ ทำการ init ผ่านคำสั่งของ prisma ออกมา

Terminal window
npm install prisma --save-dev
npm install @prisma/client # สำหรับใช้ใน Next.js ทำการลงไว้ก่อนเลย

หลังจากนั้น ให้ทำการพิมพ์คำสั่งนี้เพื่อเริ่มต้น schema file ของ Prisma ออกมา

Terminal window
npx prisma init

เพียงเท่านี้ก็จะได้ไฟล์เริ่มต้นของ Schema ออกมาเป็นที่เรียบร้อยเป็นอันเสร็จพิธีกรรมการติดตั้ง Next.js + PostgreSQL + Prisma

https://mikelopster.dev/img/blogs/next-prisma/prisma-03.webp

1. Sign In / Sign up ด้วย Email

สำหรับโจทย์แรกสิ่งที่เราจะต้องทำคือ

  1. ต้องเพิ่ม database สำหรับการเก็บ account user เอาไว้ โดยจะต้องเก็บ email เป็น credential และ password เป็นตัวเช็ค
  2. เพิ่ม API สำหรับการทำ Login ผ่าน NextAuth และการ Sign up ผ่าน Prisma
  3. เพิ่ม Frontend สำหรับหน้าจอของการ Login (เป็นตัวอย่างไว้ให้ สำหรับหน้า Register สามารถไปเพิ่มต่อเองได้)

และนี่คือ structure เริ่มต้นของโจทย์ (หยิบมาแค่ตัวสำคัญๆเท่านั้น)

Terminal window
.
├── app
├── api
└── auth
├── [...nextauth]
└── route.js --> API สำหรับ NextAuth
└── signup
└── route.js --> API สำหรับ Sign up
├── components
└── SessionProvider.jsx
├── layout.js
├── page.js --> สำหรับหน้าหลัก Login
└── profile --> สำหรับหน้าแสดง Profile หลัง Login
└── page.js
├── docker-compose.yml
├── prisma
├── migrations --> สำหรับเก็บ migration
└── schema.prisma
└── tailwind.config.js

จาก Structure ด้านบนเรามีการแบ่งส่วนออกจากกันตามนี้

  • folder api สำหรับการเก็บ API ทั้งหมดเอาไว้โดยจะเก็บ API ของ NextAuth และ Signup (ผ่าน Prisma) เอาไว้
  • folder component จะทำการเก็บ SessionProvider ที่ใช้สำหรับเก็บ session ของการ Login ไว้ (เดี๋ยวเรามาอธิบายเพิ่มอีกที)
  • ที่เหลือก็จะเป็นการแยกหน้าแต่ละหน้าผ่าน Page Component (page.js) ทั้งหน้า Login และหน้า Profile (สำหรับการ Sign up จริงๆไอเดียจะคล้ายๆกับการยิง API ทั่วไปจะขอไม่ทำใน Session นี้ สามารถใช้ idea ของหัวข้อ Prisma มาประยุกต์ใช้ได้เลย)

เราจะเริ่มต้นจากการสร้าง database กันก่อน

สร้าง database สำหรับเก็บ user

เริ่มต้นสร้าง schema ของ User ขึ้นมาผ่าน prisma/schema.prisma เพื่อสร้าง table สำหรับเก็บ email และ password

generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
name String?
email String @unique
password String
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

โดย model User นั้นได้ทำการสร้าง email, password สำหรับเก็บเป็นข้อมูลของการ login ไว้ (เก็บเป็น string) และมีการเก็บ name (ชื่อ), image (ภาพของ user) เอาไว้เป็น optional และ createdAt (วันที่สร้าง), updatedAt (วันที่มีการ update) สำหรับเป็นวันที่อ้างอิงในกรณีที่มีการแก้ไขข้อมูล

step ต่อมาต้องมี config สำหรับการชี้ไปยัง database นั่นก็คือ DATABASE_URL โดยการเพิ่มผ่าน .env เข้ามา

DATABASE_URL="postgresql://myuser:mypassword@localhost:5432/mydb?schema=public"

หลังจากนั้นทำการ sync database โดยการ migrate model เข้า PostgreSQL เพื่อสร้าง table user ขึ้นมา

Terminal window
npx prisma migrate dev --name init

เมื่อ run คำสั่งแล้วก็จะเจอ table user ขึ้นมาใน postgreSQL (ผ่าน pgAdmin) หากมี table user และ column แสดงออกมาถูกต้องตาม model User ถือว่าทำได้ถูกต้องแล้ว

next-auth-03.webp

step ต่อมาเราพร้อมสำหรับการสร้าง API สำหรับเก็บข้อมูล user แล้ว

สร้าง API สำหรับ Signup

เราจะเริ่มทำจาก idea ง่ายๆแบบนี้คือ

  • เราจะสร้าง api หนึ่งเส้นขึ้นมา POST /api/auth/signup โดยจะทำการรับข้อมูล name, email และ password ผ่าน API เข้ามา
  • โดย password นั้นจะทำการเข้า hash ด้วยวิธีการ bcrypt เพื่อทำการเข้ารหัส password ทางเดียว (เพื่อใช้สำหรับแค่เปรียบเทียบใน database และไม่สามารถถอดรหัสย้อนกลับมาได้)

ดังนั้น เพิ่ม API ขึ้นมาด้วยวิธี Route Handler ของ Next.js ที่ app/api/auth/signup/route.js

pages/api/auth/signup.js
import { PrismaClient } from '@prisma/client'
import bcrypt from 'bcrypt'
const prisma = new PrismaClient()
export async function POST(request) {
try {
const { email, password, name } = await request.json()
const hashedPassword = bcrypt.hashSync(password, 10)
const user = await prisma.user.create({
data: {
email,
password: hashedPassword,
name,
},
})
return Response.json({ message: 'User created', user })
} catch (error) {
return Response.json({ error: 'User could not be created' })
}
}

อธิบายจาก code

  • code ส่วนนี้เป็น API route handler method POST สำหรับฝั่ง server โดยทำหน้าที่เฉพาะในส่วนของการสมัครใช้งานของผู้ใช้
  • มีการ import PrismaClient ****จาก @prisma/client ****เพื่อใช้ติดต่อกับฐานข้อมูล และ bcrypt สำหรับการเข้ารหัส (hashing) รหัสผ่าน
  • มีการสร้างตัวแปร prisma เพื่อเก็บ instance ของ PrismaClient ที่เป็นตัวแทนของการเรียกใช้งาน database ผ่าน Prisma ORM
  • โดย API นี้มีการรับข้อมูล email, password และ name ผ่าน JSON body เข้ามา โดย password นั้นมีการเข้า hash password ผ่าน bcrypt
  • หลังจากจัดการข้อมูลเรียบร้อยก็ทำการ save ข้อมูลผ่าน prisma เข้าไปใน user เพื่อสร้างเป็น record ใหม่ผ่าน prisma.user.create เข้าไป

เมื่อสร้าง API เรียบร้อยให้ลองทดสอบยิงผ่าน postman ดู หากลองยิงผ่าน postman แล้วมาตรวจสอบผ่าน database ว่าข้อมูล user เข้าเรียบร้อย และเข้ารหัส password แล้วก็ถือว่า API สำหรับการสมัครเสร็จสิ้นเป็นที่เรียบร้อย

next-auth-04.webp next-auth-05.webp

step ต่อมา เราจะลองนำข้อมูลที่ทำการสมัครเข้าไปมาทำการลองยิงผ่าน Login กันบ้าง

สร้าง API สำหรับ NextAuth

สำหรับการ Login นั้น จะแตกต่างกัน Sign up ตรงที่เราจะทำผ่าน NextAuth.js เลย เนื่องจาก จะได้ใช้ feature ของการตรวจสอบ CSRF Token และการ login ด้วย Session และ JWT ไปในตัวเลย (โดยที่ไม่ต้องมา implement เอง) โดยเราจะทำการเพิ่มไปยัง path app/api/auth/[...nextauth]/route.js

เวลาที่เราใช้ NextAuth.js การเพิ่ม route ในรูปแบบ app/api/auth/[...nextauth]/route.js จะเป็นเหมือนข้อตกลง (convention) ในการ setting routes สำหรับระบบ authentication ซึ่ง NextAuth.js จะนำ routes เหล่านี้ไปใช้จัดการขั้นตอนการ authentication ต่างๆ ซึ่ง NextAuth.js ก็ใช้ route เหล่านี้สำหรับการ signing in, signing out, callbacks ในการทำ Authentication ออกมาได้

โดยหน้าตาของ code ที่เพิ่มไป ก็จะประมาณนี้

import NextAuth from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'
import { PrismaClient } from '@prisma/client'
import bcrypt from 'bcrypt'
import { PrismaAdapter } from '@auth/prisma-adapter'
const prisma = new PrismaClient()
export const authOptions = {
providers: [
CredentialsProvider({
name: 'Credentials',
credentials: {
email: { label: 'Email', type: 'email', placeholder: 'john@doe.com' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials, req) {
if (!credentials) return null
const user = await prisma.user.findUnique({
where: { email: credentials.email },
})
if (
user &&
(await bcrypt.compare(credentials.password, user.password))
) {
return {
id: user.id,
name: user.name,
email: user.email
}
} else {
throw new Error('Invalid email or password')
}
},
})
],
adapter: PrismaAdapter(prisma),
session: {
strategy: 'jwt',
},
callbacks: {
jwt: async ({ token, user }) => {
if (user) {
token.id = user.id
}
return token
},
session: async ({ session, token }) => {
if (session.user) {
session.user.id = token.id
}
return session
}
},
}
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }

จาก code ตัวอย่างนี้แสดงวิธีการ setting NextAuth.js โดย code ได้กำหนดค่าเพื่อใช้การ authentication ผ่านผู้ใช้แบบ Custom Credentials (ใช้ อีเมล และ รหัสผ่าน) พร้อมเชื่อมต่อกับฐานข้อมูลผ่าน Prisma เข้ามา โดย

  • มีการเรียกใช้ NextAuth จาก library next-auth สำหรับระบบยืนยันตัวตน (authentication)
  • CredentialsProvider จาก next-auth/providers/credentials เพื่อสร้างระบบยืนยันตัวตนแบบกำหนดเองโดยอ้างอิงจากข้อมูลของผู้ใช้ (credentials)
  • PrismaClient จาก @prisma/client เพื่อใช้ติดต่อกับฐานข้อมูล และ bcrypt สำหรับการเข้ารหัส (hashing) และตรวจสอบความถูกต้องของรหัสผ่าน
  • PrismaAdapter จาก @auth/prisma-adapter เพื่อเชื่อมต่อ NextAuth เข้ากับ Prisma ORM

โดย การตั้งค่า NextAuth นั้น มีการเรียกใช้ NextAuth พร้อมกับ object สำหรับการกำหนด config ต่างๆเอาไว้ โดยมี

  • Providers array ที่ระบุวิธีการยืนยันตัวตน (authentication providers) โดยเคสนี้ เราใช้เพียง CredentialsProvider ซึ่งหมายถึงการเปิดให้ผู้ใช้สามารถเข้าสู่ระบบโดยใช้อีเมลและรหัสผ่าน
  • function authorize จะรับข้อมูลของผู้ใช้ (credentials) ที่ถูกส่งมา พร้อมกับ request object (user และ password) จากนั้นจะค้นหาผู้ใช้ในฐานข้อมูลโดยใช้อีเมล และใช้ bcrypt เทียบรหัสผ่านกับฐานข้อมูล หากข้อมูลทุกอย่างถูกต้องจะส่งกลับข้อมูลของผู้ใช้ (user object) กลับไป
  • Adapter ทำการเรียกใช้งาน PrismaAdapter เพื่อเชื่อมต่อ NextAuth เข้ากับ Prisma ORM ซึ่งจะทำให้ NextAuth สามารถสื่อสารกับฐานข้อมูลผ่าน Prisma ได้
  • Session ใช้สำหรับกำหนด รูปแบบของ session ซึ่งใน code นี้เราจะใช้เป็น jwt หมายถึงจะใช้ JSON Web Tokens ในการจัดการ session
  • Callbacks หรือ function ที่จะทำการดำเนินการหลังจาก authentication เรียบร้อย โดยกำหนด callback 2 ตัวคือ
    • jwt จะถูกเรียกใช้เมื่อใดก็ตามที่มีการสร้างหรือ update JWT โดยถ้ามีข้อมูล user object จะมีการเพิ่ม user ID เข้าไปใน token ด้วย
    • session จะถูกเรียกใช้เมื่อใดก็ตามที่มีการตรวจสอบ session หลัง authentication เสร็จ โดยจะเพิ่ม ID ของผู้ใช้ลงใน session object (ไว้สำหรับเป็น identity ของ user)
  • สุดท้าย เมื่อประกอบของทั้งหมดเรียบร้อย จะมีการส่งออก handler ของ NextAuth ซึ่งได้รับการตั้งค่าไว้เป็นทั้ง GET และ POST เพื่อรับมือกับ HTTP method สำหรับ routes ต่างๆที่ใช้ในการยืนยันตัวตน เนื่องจาก NextAuth.js จำเป็นต้องจัดการใช้ทั้ง GET (เช่น ตอนดึง session ปัจจุบัน) และ POST (เช่น การเข้าสู่ระบบ)

โดยสรุปทั้งหมดนั้น code ส่วนนี้ได้ทำการตั้งค่าระบบ authentication ด้วย NextAuth.js โดยทำการ custom credentials provider (ใช้เป็น email, password) ร่วมกับ Prisma และ bcrypt เพื่อจัดการการเข้าสู่ระบบของผู้ใช้ด้วยอีเมลและรหัสผ่าน นอกจากนี้ยังได้มีการกำหนดค่า JWT เพื่อใช้ในการจัดการ session และปรับแต่งการจัดการ token และ session ให้ตรงความต้องการด้วยการใช้ callbacks ออกมา

และเพื่อให้ใช้งานทั้งหมดออกมาได้จำเป็นต้องมีการเพิ่ม secret (สำหรับใช้งานใน jwt token ซึ่ง NextAuth จะเป็นคนนำไปใช้งาน) และ nextauth_url สำหรับกำหนด base url ของ ระบบ ดังนั้น เพิ่มเข้าไปใน .env ตามนี้

NEXTAUTH_SECRET=98E3B2CC28F61492C6934531C828C
NEXTAUTH_URL=http://localhost:3000/

โดยทั้ง 2 ส่วนนี้สามารถอ่านเพิ่มเติมจาก docs ต้นฉบับได้

เพียงเท่านี้ ส่วนของการ login ก็เป็นการ implement เรียบร้อย step ต่อไปเราจะมาลองเชื่อม login เพื่อทดสอบการ login กัน

ทำ Frontend สำหรับ Login และดึง Profile

หลังจากที่ implement sign in เรียบร้อย เราจะลองนำ sign in ที่ implement ผ่าน NextAuth มาใช้กัน โดย file ที่เกี่ยวข้องจะเกี่ยวข้องกันทั้งหมด 2 หน้าใหญ่ๆคือ

  1. หน้า Login (หน้าแรกสุด) สำหรับการเชื่อมต่อ Login และเข้าสู่ระบบผ่าน NextAuth
  2. หน้า Profile (path: /profile) สำหรับการดึงข้อมูลผ่าน Session ที่เก็บไว้ผ่าน NextAuth
.
├── app
├── layout.js --> เพิ่ม Session Login
├── page.js --> สำหรับหน้าหลัก Login
└── profile --> สำหรับหน้าแสดง Profile หลัง Login
└── page.js

เริ่มต้นเราจะลองเพิ่มหน้า Login ออกมา ที่ app/page.js

app/page.js
'use client'
import { useState } from 'react'
import { signIn } from 'next-auth/react'
import { useRouter } from 'next/navigation'
export default function SignIn() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const router = useRouter()
const handleSubmit = async (e) => {
e.preventDefault()
try {
const result = await signIn('credentials', {
redirect: false,
email,
password,
})
if (result.error) {
console.error(result.error)
} else {
router.push('/profile')
}
} catch (error) {
console.log('error', error)
}
}
return (
<div className="flex h-screen items-center justify-center">
<form
onSubmit={handleSubmit}
className="bg-white p-6 rounded-md shadow-md"
>
<div className="mb-4">
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full border border-gray-300 px-3 py-2 rounded" // Added border
/>
</div>
<div className="mb-4">
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="w-full border border-gray-300 px-3 py-2 rounded" // Added border
/>
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white py-2 rounded mb-4"
>
Sign In
</button>{' '}
</form>
</div>
)
}

และที่หน้า Profile app/profile/page.js

'use client'
import { useSession, signOut } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
export default function Profile() {
const { data: session, status } = useSession()
const router = useRouter()
useEffect(() => {
if (status === 'unauthenticated') {
router.push('/')
}
}, [status, router])
// When after loading success and have session, show profile
return (
status === 'authenticated' &&
session.user && (
<div className="flex h-screen items-center justify-center">
<div className="bg-white p-6 rounded-md shadow-md">
<p>
Welcome, <b>{session.user.name}!</b>
</p>
<p>Email: {session.user.email}</p>
<p>Role: {session.user.role}</p>
<button
onClick={() => signOut({ callbackUrl: '/' })}
className="w-full bg-blue-500 text-white py-2 rounded"
>
Logout
</button>
</div>
</div>
)
)
}

สุดท้ายเพื่อให้เรียกใช้งาน Session ได้จากทุกจุด เรียกใช้งาน SessionProvider จาก Layout แต่เพื่อให้ Layout นั้นไม่ต้องแปลงออกมาเป็น Client component (เนื่องจาก SessionProvider ใช้ Context API ที่ใช้งานได้เฉพาะ React บน Client เท่านั้น) เราจึงมีการแยกเป็น client component ออกมา และเรียกใช้งานผ่านการ import จาก component เข้ามาแทน

ดังนั้นที่ app/components/SessionProvider.jsx ทำการ import SessionProvider และทำการ export ออกไป

'use client'
import { SessionProvider } from 'next-auth/react'
export default SessionProvider

หลังจากนั้นที่ app/layout.js ทำการเรียกใช้งาน SessionProvider เข้ามา

import { Inter } from 'next/font/google'
import { getServerSession } from 'next-auth'
import SessionProvider from './components/SessionProvider'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default async function RootLayout({ children}) {
const session = await getServerSession()
return (
<html lang="en">
<body className={inter.className}>
<SessionProvider session={session}>{children}</SessionProvider>
</body>
</html>
)
}

ทีนี้ เมื่อรวมทุกอย่างเรียบร้อย ลองทดสอบ Login ดู

next-auth-06.gif

2. เพิ่ม Role เข้า Token

โจทย์ต่อมา เราจะลองเพิ่มความสามารถให้ NextAuth สามารถใช้งานร่วมกับ Role ได้ ดังนั้น สิ่งที่เราจะทำ เราจะทำทั้งหมด 3 Step คือ

  1. เพิ่ม field role เข้า database
  2. ทำการใส่ role เข้าไปใน token และ session
  3. เรียกใช้งานผ่าน Session ในแต่ละจุด

เราจะมาลองไล่ทำทีละ step กัน

เพิ่ม role เข้า database

step แรกให้ทำการเพิ่ม column role เข้า User เพื่อเพิ่มการใช้งาน role โดยทำการกำหนดเอาไว้ว่า ทุกคนที่สมัครเข้ามาใหม่ จะต้องเป็น role member ก่อนเสมอ

generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
name String?
email String @unique
password String
image String?
role String @default("member") // Add a role field
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

หลังจากนั้น ให้ทำการ migrate database เพื่อเพิ่ม role เข้า database เข้าไป

Terminal window
npx prisma migrate dev --name add-role

และนี่คือ ผลลัพธ์ผ่าน database ก็จะเจอว่า table User มี column ใหม่ โผล่มาเรียบร้อย

next-auth-07.webp

ทำการใส่ role เข้า Session

ที่ app/api/auth/[...nextauth]/route.js ทำการเพิ่มการเรียก role จาก database ประกอบเข้า session เข้าไป

/* import เหมือนเดิม */
export const authOptions = {
providers: [
CredentialsProvider({
name: 'Credentials',
credentials: { /* เหมือนเดิม */ },
async authorize(credentials, req) {
if (!credentials) return null
const user = await prisma.user.findUnique({
where: { email: credentials.email },
})
if (
user &&
(await bcrypt.compare(credentials.password, user.password))
) {
return {
id: user.id,
name: user.name,
email: user.email,
role: user.role, // ทำการเพิ่ม role จากการดึงผ่าน database ส่งออกไป
}
} else {
throw new Error('Invalid email or password')
}
},
}),
],
/* adapter, session เหมือนเดิม */
callbacks: {
jwt: async ({ token, user }) => {
if (user) {
token.id = user.id
token.role = user.role // เพิ่ม role เข้าไป
}
return token
},
session: async ({ session, token }) => {
if (session.user) {
session.user.id = token.id
session.user.role = token.role // เพิ่ม role เข้าไป
}
return session
},
},
}
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }

ลองเรียกใช้งาน

และนี่คือตัวอย่างการเรียกใช้งานในแต่ละจุด โดยเราสามารถเรียกใช้งาน role ได้ผ่าน Session ของ user (ที่มีการเพิ่มเข้าไปผ่าน callback ได้)

โดยสามารถเรียกใช้งานได้ตั้งแต่ฝั่ง Client ที่ app/profile/page.js

/* import เหมือนเดิม */
export default function Profile() {
const { data: session, status } = useSession()
/* code เหมือนเดิม */
// When after loading success and have session, show profile
return (
status === 'authenticated' &&
session.user && (
<div>
<h1>Profile</h1>
<p>Welcome, {session.user.name}!</p>
<p>Email: {session.user.email}</p>
<p>Role: {session.user.role}</p>{/* แค่เพิ่มตรงนี้เข้ามา */}
<button onClick={() => signOut({ callbackUrl: '/' })}>Logout</button>
</div>
)
)
}

ฝั่ง Middleware (ส่วนของ Edge Runtime) ที่ middleware.js ที่สามารถเรียกใช้โดยการแกะ Token ผ่าน cookie (โดยหยิบจาก request) จาก getToken โดยทำการเรียกใช้ secret ผ่าน NEXTAUTH_SECRET ที่เป็น secret เดียวกันกับที่ NextAuth ใช้เพื่อให้สามารถดึงข้อมูล token ออกมาได้

app/middleware.js
import { getToken } from 'next-auth/jwt'
import { NextResponse } from 'next/server'
export async function middleware(request) {
const user = await getToken({
req: request,
secret: process.env.NEXTAUTH_SECRET,
})
// Get the pathname of the request
const { pathname } = request.nextUrl
// If the pathname starts with /protected and the user is not an admin, redirect to the home page
if (
pathname.startsWith('/protected') &&
(!user || user.role !== 'admin')
) {
return NextResponse.redirect(new URL('/', request.url))
}
// Continue with the request if the user is an admin or the route is not protected
return NextResponse.next()
}

หรือลองเพิ่มที่ Route Handler api POST /api/profile เพื่อเรียกใช้งาน ก็สามารถใช้วิธีนี้ได้ผ่าน app/api/profile/route.js โดยการใช้คำสั่ง getServerSession เข้ามาได้ โดยการ import authOption จากที่ set config ของ NextAuth.js ไว้

import { getServerSession } from 'next-auth/next'
import { authOptions } from '../auth/[...nextauth]/route'
export async function GET(request) {
const session = await getServerSession(authOptions)
console.log('session', session)
return Response.json({
message: 'test',
})
}

(ตัวอย่างนี้ เราจะทำไว้ประมาณนี้ก่อน เดี๋ยวในอนาคตมีโอกาสเราจะกลับมาเล่าแบบจัดเต็มในหัวข้อ RBAC สำหรับการจัดการ Design Software แบบ Role ขึ้นมานะครับ)

3. ใช้ร่วมกับ Google Login

โจทย์สุดท้าย เราจะลองมาใช้งานร่วมกันหลาย Credential กัน โดยเราจะลอง design ใช้งานได้ทั้ง email / password และ Social Login อย่าง Google Login โดย step ที่เราจะทำคือ

  1. สร้าง credential oauth สำหรับ google provider เพื่อให้สามารถใช้งาน service authentication ของ google ได้
  2. เพิ่ม database รับรองการทำ google sign in
  3. เพิ่ม Google sign in ที่หน้าเว็บ

เราจะมาลองทำไปทีละ step กัน

สร้าง credential สำหรับ google provider

จาก link https://next-auth.js.org/providers/google จะมีคำแนะนำไว้ว่าเราสามารถสร้าง credential GOOGLE_CLIENT_ID และ GOOGLE_CLIENT_SECRET ออกมาได้ ผ่าน Google Cloud Console

ให้เราทำการเข้าผ่าน url https://console.developers.google.com/apis/credentials สร้าง Google Cloud 1 project ขึ้นมา (ไม่จำเป็นต้องสร้าง service อะไรก็ได้) หลังจากนั้นมายังหน้า API & Services > Cerdentials (ตาม url) และทำการกด CREATE CREDENTIAL เพื่อทำการสร้าง credential ขึ้นมา

next-auth-08.webp

หลังจากนั้นใส่รายละเอียดให้ครบ โดย

  • เลือก Application type เป็น web application
  • ทำการใส่ Authorized Javascript origin เป็น localhost:3000 (สำหรับ host จริงก็ใส่เป็น domain จริงได้เลย)
  • Authorized redirect URIs เป็น http://localhost:3000/api/auth/callback/google (สำหรับ host จริงก็ใส่เป็น domain จริงได้เลย)

เมื่อใส่ข้อมูลครบเรียบร้อยทำการ Create ออกมา

next-auth-09.webp

หลังจากสร้างเรียบร้อย ก็จะขึ้นมา OAuth client created แล้ว ให้ทำการเก็บ Client ID และ Client secret เอาไว้

next-auth-10.webp

และหลังจากนั้น นำค่าที่ได้มาเพิ่มใน .env

Terminal window
GOOGLE_CLIENT_ID=xxx
GOOGLE_CLIENT_SECRET=yyy

เป็นอันเสร็จสิ้นการสร้าง credential และการนำ credential ของ google มาใช้

เพิ่ม database สำหรับการรองรับ Google Sign in

step ต่อมาเราจะทำการเพิ่ม database เพื่อให้สามารถรองรับ ทั้ง email / password และ social sign in เราจะไม่ลบ field password ออก แต่เราจะทำการเพิ่ม table Accounts และ ทำการผูก User เข้ากับ Account แทน

  • table Account เป็น pattern ของ NextAuth ที่จะทำการเก็บ credential ของ google sign in อย่าง oauth token, refresh token ที่ใช้สำหรับการดึงข้อมูลและยืนยันตัวตน
  • table นี้จะถูกใช้โดย NextAuth โดยอัตโนมัติ (เมื่อ implement ผ่าน NextAuth)
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
name String?
email String @unique
password String?
image String?
role String @default("member") // Add a role field
emailVerified DateTime?
accounts Account[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Account {
id String @id @default(cuid())
userId Int
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}

หลังจากเพิ่มเรียบร้อย ทำการ migrate database เพื่อเพิ่ม field และ table ขึ้นมา

Terminal window
npx prisma migrate dev --name add-account

ผลลัพธ์จากการเพิ่ม database

next-auth-11.webp

เป็นอันเสร็จสิ้นเรียบร้อย ต่อไปเราจะทำการเพิ่ม google sign in เข้ามา

** สำหรับข้อมูลเรื่อง table Account สามารถอ่านเพิ่มเติมได้ที่นี่เช่นกัน https://github.com/nextauthjs/next-auth/discussions/7967

เพิ่ม google sign in

ที่ app/api/auth/[...nextauth]/route.js ทำการเพิ่ม google provider เข้ามา

/* ที่เหลือ import เหมือนเดิม */
import GoogleProvider from 'next-auth/providers/google'
const prisma = new PrismaClient()
export const authOptions = {
providers: [
CredentialsProvider({ /* จากหัวข้อก่อนหน้า */ }),
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
profile(profile) {
return {
id: profile.sub,
name: `${profile.given_name} ${profile.family_name}`,
email: profile.email,
image: profile.picture,
}
},
}),
],
/* session adapter เหมือนเดิม */
callbacks: {
jwt: async ({ token, user }) => { /* เหมือนเดิม */},
session: async ({ session, token }) => {
if (session.user) {
session.user.id = token.id
session.user.role = token.role
session.user.image = token.picture // เพิ่มการรับรูปภาพเข้ามา
}
return session
},
async redirect({ baseUrl }) {
return `${baseUrl}/profile`
},
},
}
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }

โดยส่วนที่เพิ่มมาคือ

  • GoogleProvider ถูกตั้งค่าโดยใช้ environment variables สำหรับ clientId และ clientSecret ซึ่งเราได้รับค่าเหล่านี้จาก Google API Console โดย function profile จะทำหน้าที่แปลงข้อมูลผู้ใช้จาก Google มาให้อยู่ในรูปแบบที่ NextAuth.js สามารถนำไปใช้ต่อได้ (ส่งออกไปเป็น user ที่ใช้ใน jwt token ต่อได้)
  • ใน Callback ให้เพิ่ม redirect การใช้ callback นี้จะทำการเปลี่ยนเส้นทาง (redirect) ผู้ใช้ไปยังหน้า /profile หลังจากที่เข้าสู่ระบบสำเร็จ

ที่ฝั่ง UI app/page.js ทำการเพิ่มปุ่ม Sign in with google เข้ามา

app/page.js
/* import เหมือนเดิม */
export default function SignIn() {
/* handle state เหมือนเดิม */
return (
<div className="flex h-screen items-center justify-center">
<form
onSubmit={handleSubmit}
className="bg-white p-6 rounded-md shadow-md"
>
{/* code ส่วนนี้เหมือนเดิม */}
{/* Google Sign in Button */}
<button
type="button"
onClick={() => signIn('google')}
className="w-full flex items-center justify-center gap-2 bg-white border border-gray-300 text-gray-700 py-2 rounded"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 488 512"
width="20"
height="20"
>
<path d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z" />
</svg>
Sign in with Google
</button>
</form>
</div>
)
}

และที่หน้า app/profile/page.js ทำการเพิ่มการดึงรูปภาพเข้ามา

/* import เหมือนเดิม */
export default function Profile() {
const { data: session, status } = useSession()
/* เหมือนเดิม */
return (
status === 'authenticated' &&
session.user && (
<div className="flex h-screen items-center justify-center">
<div className="bg-white p-6 rounded-md shadow-md">
{/* ทำการเพิ่มส่วนรูปภาพเข้ามา */}
<div className="text-center mb-4">
<img
src={session.user.image}
className="rounded-full w-20 h-20 mx-auto"
/>
</div>
<p>
Welcome, <b>{session.user.name}!</b>
</p>
<p>Email: {session.user.email}</p>
<p>Role: {session.user.role}</p>
<button
onClick={() => signOut({ callbackUrl: '/' })}
className="w-full bg-blue-500 text-white py-2 rounded"
>
Logout
</button>
</div>
</div>
)
)
}

เมื่อลอง run ทั้งหมดดู

next-auth-12.gif

ข้อสังเกตคือ เราไม่ได้เพิ่มอะไรเกี่ยวกับการ sign up เลย แต่เมื่อเราลองมาดูผ่าน database ก็จะเจอว่ามีข้อมูลสมัครเข้า database แล้วเป็นที่เรียบร้อย

next-auth-13.webp

ความแตกต่างระหว่าง Next Auth และ Auth0

ทีนี้พอลองมาเทียบกันแล้ว NextAuth.js และ Auth0 (ซึ่งเป็นหัวข้อที่เราเคยทำไปก่อนหน้านี้) ก็เป็น solution สำหรับระบบ authentication ใน web application ทั้งคู่ จุดสำคัญที่ทำให้ 2 ตัวนี้แตกต่างกันคือ

NextAuth.js

  • เป็น open-source สำหรับ authentication ที่ถูกออกแบบมาสำหรับ Next.js โดยเฉพาะ
  • เราสามารถเลือก host เองได้ (self-hosted)
  • รองรับผู้ให้บริการยืนยันตัวตนที่หลากหลายและสามารถเชื่อมต่อตรงกับ database ได้อย่างง่ายดายผ่าน adapter
  • จัดการ session ด้วย JWT หรือ database session รวมถึงมี hooks อย่าง useSession สำหรับดึงข้อมูล session ในฝั่ง client ออกมาได้
  • สามารถปรับแต่งที่ระดับ code ได้โดยตรงจาก Next.js (เนื่องจากมันเป็นแค่ library สำหรับการทำ authentication)

Auth0

  • เป็น commercial platform สำหรับยืนยันตัวตนและกำหนดสิทธิ์ (authentication และ authorization) ให้บริการในรูปแบบ software as a service
  • รองรับ feature เกี่ยวกับการยืนยันตัวตนและการกำหนดสิทธิ์ครอบคลุมใน platform ทั้งหมด รวมถึงการสมัครใช้งาน เข้าสู่ระบบ การยืนยันตัวตนแบบ MFA และการจัดการความยินยอม (consent) ได้
  • มาพร้อมกับ feature ที่ครอบคลุมตั้งแต่เริ่มต้น รวมถึงมีมาตรฐานความปลอดภัยรวมไว้แล้วเรียบร้อย
  • ใช้รูปแบบการยืนยันตัวตนแบบ Authorization Code Grant ซึ่ง Auth0 เป็นผู้จัดการกระบวนการยืนยันตัวตน และจะมีการมอบ token ให้ web application หลังผ่านการยืนยันตัวตน
  • Auth0 ออกแบบมาเพื่อเป็น solution ด้านการระบุตัวตนที่ครบวงจร ใช้ได้กับหลากหลาย framework ไม่จำกัดแค่ Next.js (แต่ปัจจุบัน NextAuth.js ที่เปลี่ยนมาเป็น Auth.js เองก็สามารถใช้งานร่วมกับหลาย framework ได้เช่นเดียวกัน)

การเลือกใช้ Auth0 โดยปกติแล้วจะเป็นการ redirect ผู้ใช้ไปยังบริการของ Auth0 เพื่อยืนยันตัวตน จากนั้นจัดการกับ authorization code ในฝั่ง server

ส่วน NextAuth.js จะจัดการกระบวนการยืนยันตัวตนทั้งหมดภายใน Next.js เอง (ซึ่งเอาจริงๆ NextAuth ก็มีตัวเลือกให้ใช้ Auth0 ได้ด้วยเช่นกัน)

สรุปแล้ว การเลือกใช้ NextAuth.js หรือ Auth0 จะขึ้นอยู่กับ requirement เฉพาะแต่ละ project เช่น จำเป็นต้อง host authentication server เอง (เพื่อใช้ภายในเท่านั้น), ต้องการปรับแต่งทุกระดับของ code ตามที่ต้องการ, เพิ่มความซับซ้อนของเงื่อนไขในการยืนยันตัวตน เป็นต้น ถ้าเป็นเคสเหล่านี้ NextAuth.js ก็จะตอบโจทย์กว่า แต่ถ้ารูปแบบเป็น Authentication ทั่วไป, ไม่มีปัญหา flow login ที่ต้อง redirect รวมถึง สามารถใช้ feature Auth0 ได้ไม่มีปัญหาอะไร ก็สามารถเลือกใช้เป็น Auth0 เพื่อประหยัดเวลาในการพัฒนาได้เช่นกัน

สรุปส่งท้าย

และนี่คือ NextAuth ในหัวข้อนี้เราได้เรียนรู้การเชื่อม Authentication ผ่าน Next.js ว่าสามารถใช้งาน NextAuth ได้ยังไงบ้าง รวมถึง สามารถประยุกต์ใช้กับ Role และสามารถใช้งานร่วมกับหลาย Provider ได้ยังไงบ้าง หวังว่าทุกคนจะลองนำ NextAuth ไปประยุกต์ใช้กันต่อได้นะครับ 😁

Reference แนะนำ

Related Post

Share on social media