iT邦幫忙

2023 iThome 鐵人賽

DAY 1
0
Modern Web

一些讓你看來很強的全端- trcp 伴讀系列 第 20

Day-020. 一些讓你看來很強的全端 TRPC 伴讀 -Next-Auth(CredentialsProvider)

  • 分享至 

  • xImage
  •  

CredentialsProvider setting

CredentialsProvider 他是一個 next-auth 提供客製化帳密管理的功能搭配 next-auth session 驗證機制,雖然 next-auth 官方並不推薦這種帳密管理方式,畢竟帳密安全這塊還是交給專業的平台把管比較實際,但撇除安全問題 CredentialsProvider 使用上蠻方便的,可以透過 callback 方式驗證 user 帳號,不需要再額外寫一個 api 去驗證。

signIn

首先我們先定義我們 signinpage 在哪邊因為本次loginregister 都在 / ,所以我們定義 signIn: '/',還記得我們昨日的 github 登入嗎,這邊定義好 signIn 後,日後只要呼叫 nextAuthsignIn() ,都會到 http://localhost:3000/ 中。

adapter

大家還記得在實作 register 時候有添加的 prisma schema 嗎,他主要是用來搭配 adapter 用的,每當 user 登入成功就會自動把 user info 單加到你使用的 orm 資料中, next-auth 還有提供超多種的 orm 有興趣的讀者可以 參考參考 ~

import NextAuth, { AuthOptions, getServerSession } from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials"
import GithubProvider from "next-auth/providers/github"
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import { prisma } from "@/server/db"
export const authOptions: AuthOptions = {
    
  //..
  pages: { signIn: '/' },
  adapter: PrismaAdapter(prisma),
}
export default NextAuth(authOptions)

補充一下 其實每個 adapter 只是拿以下的 codeclient 傳下去執行的各種 callBack 而已,所以如果讀者想自己寫一個 adapter 可以拿以下的 code 去修改,但要記得 returnkey 是固定的。

如果想知道其他 adpter 怎麼實作的可以參考官方寫法。
https://github.com/nextauthjs/next-auth/tree/main/packages

/** @return { import("next-auth/adapters").Adapter } */
export default function MyAdapter(client, options = {}) {
  return {
    async createUser(user) {
      return
    },
    async getUser(id) {
      return
    },
    async getUserByEmail(email) {
      return
    },
    async getUserByAccount({ providerAccountId, provider }) {
      return
    },
    async updateUser(user) {
      return
    },
    async deleteUser(userId) {
      return
    },
    async linkAccount(account) {
      return
    },
    async unlinkAccount({ providerAccountId, provider }) {
      return
    },
    async createSession({ sessionToken, userId, expires }) {
      return
    },
    async getSessionAndUser(sessionToken) {
      return
    },
   //..
  }
}

相關連結

https://next-auth.js.org/tutorials/creating-a-database-adapter

CredentialsProvider

CredentialsProvider 主要有三個部分:
name : 讓 sign() 驗證你的 name 是什麼,假如你是 Credentials ,則可以透過signIn("Credentials") 告訴 next-auth 你要透過 CredentialsProvider 方式登入。

credentials : 因為 CredentialsProvider 是一個需要客製化帳密的用法,這邊可以可你希望 user 輸入什麼 fields
authorize : 用來驗證 user authfunction ,而這邊 return 的結果將會是你希望 use 收到什麼的 infosession 中。

以下一一介紹 authorize 中比較重要的內容。

import NextAuth, { AuthOptions, getServerSession } from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials"
import bcrypt from 'bcrypt'
export const authOptions: AuthOptions = {
   //.. 
  providers: [
   //..
    CredentialsProvider({
      name: 'Credentials',
      credentials: {
        email: { label: "email", type: "text", placeholder: "email" },
        password: { label: "Password", type: "password" }
      },
      async authorize(credentials, req) {
        if (!credentials?.email || !credentials?.password) {
          throw new Error('Invalidate credentials')
        }
        const user = await prisma.user.findFirst({
          where: {
            email: credentials.email
          }
        })
        if (!user || !user?.hashedPassword) {
          throw new Error('user credentials not found or user have not register')
        }
        const isCorrectPassword = await bcrypt.compare(
          credentials.password,
          user.hashedPassword
        )
        if (!isCorrectPassword) {
          throw new Error('Invalidate password')
        }
        return user
      }
    }),


    // ...add more providers here
  ],
}
export default NextAuth(authOptions)

驗證 user 是否有 input

  if (!credentials?.email || !credentials?.password) {
      throw new Error('Invalidate credentials')
    }

prisma 中尋找是否有 user

const user = await prisma.user.findFirst({
  where: {
    email: credentials.email
  }
})
if (!user || !user?.hashedPassword) {
  throw new Error('user credentials not found or user have not register')
}

驗證 user 是否有 hashedPassword ,同時透過bcrypt.compare 驗證 input password,成功就 return user info

if (!user || !user?.hashedPassword) {
      throw new Error('user credentials not found or user have not register')
}
const isCorrectPassword = await bcrypt.compare(
  credentials.password,
  user.hashedPassword
)
if (!isCorrectPassword) {
  throw new Error('Invalidate password')
}![https://ithelp.ithome.com.tw/upload/images/20231004/20145677DfDTUT9TXN.png](https://ithelp.ithome.com.tw/upload/images/20231004/20145677DfDTUT9TXN.png)

 return user

https://ithelp.ithome.com.tw/upload/images/20231004/20145677FP7bdi6LZ7.png

callbacks

還記得我們昨天在用 github 登入時候查看 useSession 的內容嗎?你會發現這邊只有簡單的 user info 而已,但如果我們想要拿到其他 user info 呢?怎麼辦?,這時我們可以設定 next-auth 說你希望 return 什麼 datasession 中。

https://ithelp.ithome.com.tw/upload/images/20231004/20145677tPtN4RculW.png
假設我希望將 user id 送到 session ,我們可以先將 callbacks 中的 jwtprisma 中拿到 user 接著 return,此時 session 中的params token 就是上面 jwt callback return 的內容,這樣我們就可以return userIdsession 中了。

export const authOptions: AuthOptions = {
  //..
  callbacks: {
    async jwt({ token, user, account, profile, isNewUser }) {
      const userInfo = await prisma.user.findFirst({
        where: {
          email: token.email
        }
      })

      if (!userInfo) return token
      return { ...token, ...userInfo }
    },
    session({ session, token }) {
      return { ...session, user: { ...session.user, id: token.id } }
    },
  }
}
export default NextAuth(authOptions)

這時我們在看一下 console.log ,我們成功添加 userId 了~

https://ithelp.ithome.com.tw/upload/images/20231004/20145677k1M9tzqr6r.png
最後放上完整 authOptions setting 如下

import NextAuth, { AuthOptions, getServerSession } from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials"
import GithubProvider from "next-auth/providers/github"
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import { prisma } from "@/server/db"
import bcrypt from 'bcrypt'
export const authOptions: AuthOptions = {
  pages: { signIn: '/' },
  adapter: PrismaAdapter(prisma),
  // Configure one or more authentication providers
  providers: [
    GithubProvider({
      clientId: process.env.GITHUB_ID as string,
      clientSecret: process.env.GITHUB_SECRET as string,
    }),
    CredentialsProvider({
      // The name to display on the sign in form (e.g. 'Sign in with...')
      name: 'Credentials',
      // The credentials is used to generate a suitable form on the sign in page.
      // You can specify whatever fields you are expecting to be submitted.
      // e.g. domain, username, password, 2FA token, etc.
      // You can pass any HTML attribute to the <input> tag through the object.
      credentials: {
        email: { label: "email", type: "text", placeholder: "email" },
        password: { label: "Password", type: "password" }
      },
      async authorize(credentials, req) {
        if (!credentials?.email || !credentials?.password) {
          throw new Error('Invalidate credentials')
        }
        const user = await prisma.user.findFirst({
          where: {
            email: credentials.email
          }
        })
        if (!user || !user?.hashedPassword) {
          throw new Error('user credentials not found or user have not register')
        }
        const isCorrectPassword = await bcrypt.compare(
          credentials.password,
          user.hashedPassword
        )
        if (!isCorrectPassword) {
          throw new Error('Invalidate password')
        }
        return user
      }
    }),


    // ...add more providers here
  ],
  secret: process.env.AUTH_SECRET,
  callbacks: {
    async jwt({ token, user, account, profile, isNewUser }) {
      const userInfo = await prisma.user.findFirst({
        where: {
          email: token.email
        }
      })

      if (!userInfo) return token
      return { ...token, ...userInfo }
    },
    session({ session, token }) {
      return { ...session, user: { ...session.user, id: token.id } }
    },
  },
  session: {
    strategy: 'jwt',
    maxAge: 1 * 24 * 60 * 60
  }
}
export default NextAuth(authOptions)

之後我們到 authFormonSubmit 中把 signin 邏輯加上,那 signIn 哪有一個 callBack 結果,我們可以透過 callBack 去做一些登入成功或是失敗的行為。

  const onSubmit = async (value: LoginFormSchema | RegisterFormSchema) => {
    try {
      setIsLoading(true)
      if (variants === 'Login') {
        const callBack = await signIn('credentials', {
          email: value.email,
          password: value.password,
          redirect: false
        })
        if (callBack?.error) {
          toast.error(callBack.error)
        }
        if (callBack?.ok) {
          toast.success('success login')
          router.push('/posts')
        }
      }

Register 部分我們也可以優化一下,只要 user 註冊成功我們就自動幫忙 login

 const onSubmit = async (value: LoginFormSchema | RegisterFormSchema) => {
    try {
      setIsLoading(true)
      if (variants === 'Login') {
        //..
      }
      if (formSchemaTypeGuard(value) && variants === 'Register') {
        const { message } = await axios.post<{ message: string }, AxiosResponse<{ message: string }>, RegisterSchema>('/api/register', {
          name: value.name,
          email: value.email,
          password: value.password,
        }).then(res => res.data)
        toast.success(message)
        const callBack = await signIn('credentials', {
          email: value.email,
          password: value.password,
          redirect: false
        })
        if (callBack?.error) {
          toast(callBack.error)
        }
        if (callBack?.ok) {
          toast.success('success login')
          router.push('/posts')
        }
      }
    } catch (e) {
      if (e instanceof AxiosError) {
        const message = e.response?.data.message
        toast.error(message)
        return
      }
      console.log(e)
    } finally {
      setIsLoading(false)

    }
  }

因為loginregister 共用同一個 signin 邏輯所以拆成 handleSignCredentials

const handleSignCredentials = async (value: LoginFormSchema | RegisterFormSchema) => {
    const callBack = await signIn('credentials', {
      email: value.email,
      password: value.password,
      redirect: false
    })
    if (callBack?.error) {
      toast(callBack.error)
    }
    if (callBack?.ok) {
      toast.success('success login')
      router.push('/posts')
    }
  }

如此code 就簡潔多了~

const onSubmit = async (value: LoginFormSchema | RegisterFormSchema) => {
    try {
      setIsLoading(true)
      if (variants === 'Login') {
        handleSignCredentials(value)
      }
      if (formSchemaTypeGuard(value) && variants === 'Register') {
        const { message } = await axios.post<{ message: string }, AxiosResponse<{ message: string }>, RegisterSchema>('/api/register', {
          name: value.name,
          email: value.email,
          password: value.password,
        }).then(res => res.data)
        toast.success(message)
        handleSignCredentials(value)
      }
    } catch (e) {
      if (e instanceof AxiosError) {
        const message = e.response?.data.message
        toast.error(message)
        return
      }
      console.log(e)
    } finally {
      setIsLoading(false)

    }
  }

之後我們測試一下結果,如果讀者看動同一個畫面,恭喜你成功勒~

https://ithelp.ithome.com.tw/upload/images/20231004/2014567713K9jG0133.png

到這邊終於把 Next-auth 的主要功能介紹一遍了~明天我們就可以用 next-authsession 結合 trpc 做身份驗證了~

相關連結:

https://github.com/nextauthjs/next-auth/tree/main/packages
https://next-auth.js.org/tutorials/creating-a-database-adapter
https://next-auth.js.org/adapters

repo

https://github.com/Danny101201/next_demo/tree/main

✅ 前端社群 :
https://lihi3.cc/kBe0Y


上一篇
Day-019. 一些讓你看來很強的全端 TRPC 伴讀 -Next-Auth( GoogleProvider )
下一篇
Day-021. 一些讓你看來很強的全端 TRPC 伴讀 -Middleware
系列文
一些讓你看來很強的全端- trcp 伴讀30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言