Open3

Blitz.js + passport.js + firebase authenticationで認証

Tsuyoshi HiguchiTsuyoshi Higuchi

Blitz.jsでのOAuth認証事始め

Blitz.jsのOAuth認証はBlitz.jsがラップしているpassport.jsの passportAuth/api/auth/[...auth].ts という感じでAPI生やして、任意Strategyを追加してやるように公式ドキュメントに記載がある。

Facebookの場合はこんな感じ。

import { passportAuth } from "blitz"
import db from "db"
import { Strategy as FacebookStrategy } from "passport-facebook"

export default passportAuth({
  successRedirectUrl: "/",
  errorRedirectUrl: "/",
  secureProxy: true,
  strategies: [
    {
      authenticateOptions: { scope: "email" },
      strategy: new FacebookStrategy(
        {
          clientID: process.env.FACEBOOK_APP_ID as string,
          clientSecret: process.env.FACEBOOK_APP_SECRET as string,
          callbackURL: "http://localhost:3000/api/auth/facebook/callback",
        },
        async function (_token, _tokenSecret, profile, done) {
          const email = profile.emails && profile.emails[0]?.value

          if (!email) {
            return done(new Error("Facebook OAuth response doesn't have email."))
          }

          const user = await db.user.upsert({
            where: { email },
            create: {
              email,
              name: profile.displayName,
            },
            update: { email },
          })

          const publicData = {
            userId: user.id,
            roles: [user.role],
            source: "facebook",
          }
          done(null, { publicData })
        }
      ),
    },
  ]
})
Tsuyoshi HiguchiTsuyoshi Higuchi

ユーザ情報をあまり持ちたくないのでFirebase Authenticationを使う

ユーザ情報をFirebase Authenticatonに集約したいのでFacebookやGoogleなど個別のStrategyは使わず、Custom Strategyを使ってFirebase Authとの連携を試みる。


Client

※このfirebaseインスタンスはclient向けのもの。

  1. firebase.auth().signInWithFacebook() でFirebase経由でFacebook認証
  2. 非同期で結果を受け取ってfirebase.auth().currentUser!.getIdToken(true) でidTokenを取得
  3. idTokenをつけてfetchで /api/auth/custom/callback?idToken=${idToken} としてたたく
Server
  1. passport-customのverifiedCallback内でidTokenを firebase.auth().verifyIdToken(idToken) でFacebookのUser情報(email, name辺り)を取得
  2. emailとnameを db.user.upsert() で追加 or 生成する
  • emailが取得出来ない場合は done(Error)
  • emailがある場合も done(null, { ...publicData })

import { passportAuth } from "blitz"
import db from "db"
import { Strategy as FacebookStrategy } from "passport-facebook"
import { Strategy as CustomStrategy } from "passport-custom"
import firebase from "../../firebase/admin"

export default passportAuth({
  successRedirectUrl: "/",
  errorRedirectUrl: "/",
  secureProxy: true,
  strategies: [
    {
      strategy: new CustomStrategy((req, done) => {
        if (!req.query.idToken) return done(new Error("Request doesn't have idToken."))
        const idToken = req.query.idToken as string

        try {
          const { email, name } = await firebase.auth().verifyIdToken(token)

          if (!email) {
            return done(new Error("Facebook OAuth response doesn't have email."))
          }

          const user = await db.user.upsert({
            where: { email },
            create: {
              email,
              name,
            },
            update: { email },
          })

          const publicData = { userId: user.id, roles: [user.role], source: "facebook" }
          done(null, { publicData })
        } catch (error) {
          done(new Error(`Facebook OAuth failed...`))
        }
      }),
    }
  ],
})

他のOAuth認証を追加する場合はqueryで分岐するなどすればよいかと。 client側だけでこっちはここまでで充分。
で、Firebase Authentcatonが使えるということなのでメールリンク認証も使えるわけです。
Firebaseを使わない場合はMagic Link Strategyがあったのでそちらを使うと良いと思います。

Tsuyoshi HiguchiTsuyoshi Higuchi

client側には以下レスポンスがある。

{
  ...
  redirected: true
  status: 200
  statusText: "OK"
  type: "basic"
  url: "http://localhost:3000/"
}

他のStrategyを指定すると passportAuth() で指定した successRedirectUrlerrorRedirectUrl で指定したいずれかにリダイレクトするが、Custom Strategyにするとclient fetchするためリダイレクト処理はクライアント側で行うことになる。はず。