🥑

RedwoodJSに入門してみた(第4回: dbAuthによる認証)

2022/12/19に公開

この記事について

この記事は、全5回の第4回です。
RedwoodJSに入門してみた(第1回: アプリ作成〜モデル作成)
RedwoodJSに入門してみた(第2回: CRUD作成 API編)
RedwoodJSに入門してみた(第3回: CRUD作成 WEB編)
RedwoodJSに入門してみた(第4回: dbAuthによる認証)
RedwoodJSに入門してみた(第5回: 実際に触ってみて感じたこと)

今回はRedwoodアプリに認証を実装する
Redwoodでは組み込みで認証のライブラリが用意されている。
Auth0やFirebaseなど、サードパーティの認証も利用できるが、この記事では組み込みの認証システムであるdbAuthを使用する。

認証のセットアップ

yarn rw setup auth dbAuthを実行する

? Overwrite existing /api/src/lib/auth.[jt]s? › (y/N)

上書きして問題ないのでyを入力

? Enable WebAuthn support (TouchID/FaceID)? See https://redwoodjs.com/docs/auth/dbAuth#webAuthn › (y/N)

こちらは今回必要ないのでNを入力

package.jsonに@redwoodjs/authも追加されている

web/src/App.tsx<AuthProvider>が追加される

web/src/App.tsx
+ import { AuthProvider } from '@redwoodjs/auth'
+
import { FatalErrorBoundary, RedwoodProvider } from '@redwoodjs/web'
import { RedwoodApolloProvider } from '@redwoodjs/web/apollo'

...

const App = () => (
  <FatalErrorBoundary page={FatalErrorPage}>
    <RedwoodProvider titleTemplate="%PageTitle | %AppTitle">
+     <AuthProvider type="dbAuth">
        <RedwoodApolloProvider>
          <Routes />
        </RedwoodApolloProvider>
+     </AuthProvider>
    </RedwoodProvider>
  </FatalErrorBoundary>
)

User モデルを追加

schema.prismaにUserモデルを追加して、yarn rw prisma migrate devを実行する

schema.prisma
model User {
  id                  Int @id @default(autoincrement())
  name                String?
  email               String    @unique
  hashedPassword      String
  salt                String
  resetToken          String?
  resetTokenExpiresAt DateTime?
}

UUIDを使用する場合

schema.prismaは下記のようになっている想定

schema.prisma
model User {
  id                  String    @id @default(uuid())
  name                String?
  email               String    @unique
  hashedPassword      String
  salt                String
  resetToken          String?
  resetTokenExpiresAt DateTime?
}

setupコマンドで生成されるapi/src/lib/auth.tsでは、どうやらDBのUserの定義を参照していないらしく、UUIDやCUIDを使用している場合も下記のような関数が生成されている。

api/src/lib/auth.ts
export const getCurrentUser = async (session: Decoded) => {
  if (!session || typeof session.id !== "number") {
    throw new Error("Invalid session");
  }

  return await db.user.findUnique({
    where: { id: session.id },
    select: { id: true },
  });
};

このままだとログイン時にエラーが出てしまうので、下記のように修正する必要がある。

api/src/lib/auth.ts
-  if (!session || typeof session.id !== 'number') {
+  if (!session || typeof session.id !== 'string') {

暗号化キー

.envSESSION_SECRETという変数が追加されている。この変数がユーザーログイン時のクッキーの暗号化キーになる。デプロイ先を変更する際など、yarn rw g secretを実行すると新しい値を生成することができる。

ログイン・会員登録・パスワード忘れの画面追加

yarn rw g dbAuthを実行すると、ログイン・サインアップ・パスワード忘れのページを追加できる

? Enable WebAuthn support (TouchID/FaceID) on LoginPage? See https://redwoodjs.com/docs/auth/dbAuth#webAuthn › (y/N)

またしても聞かれるが、今回は不要なのでNを入力。

下記の4つのPageとそれぞれのRouteが追加される。

  1. web/src/pages/LoginPage/LoginPage.tsx
  2. web/src/pages/SignupPage/SignupPage.tsx
  3. web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx
  4. web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx

web/src/Routes.tsx

web/src/Routes.tsx
const Routes = () => {
  return (
    <Router>
+     <Route path="/login" page={LoginPage} name="login" />
+     <Route path="/signup" page={SignupPage} name="signup" />
+     <Route path="/forgot-password" page={ForgotPasswordPage} name="forgotPassword" />
+     <Route path="/reset-password" page={ResetPasswordPage} name="resetPassword" />
      ...
    </Router>
  )
}

非常に便利。

Web側での認証

useAuth()

useAuth()というhooksから下記のように様々な認証情報にアクセスできる

import { useAuth } from "@redwoodjs/auth";

export const MyComponent = () => {
  const { currentUser, isAuthenticated, logIn, logOut } = useAuth();

  return (
    <ul>
      <li>The current user is: {currentUser}</li>
      <li>Is the user logged in? {isAuthenticated}</li>
      <li>
        Click to{" "}
        <button type="button" onClick={logIn}>
          login
        </button>
      </li>
      <li>
        Click to{" "}
        <button type="button" onClick={logOut}>
          logout
        </button>
      </li>
    </ul>
  );
};

ログイン・ログアウトの実行、現在のログイン状態や、ログインしているユーザー情報の取得などができる。

Route

<Set>コンポーネントにprivateを指定することで認証されていないユーザーのアクセスを制限できる。

認証されていないユーザーがアクセスした場合のリダイレクト先はunauthenticatedで指定できる。hasRoleを指定することでユーザーのロールを制限することもできる。

ちなみに、<Private>コンポーネントを使うことで<Set private>と同様に扱える。基本的には<Private>を使ったほうが良さそう。

const Routes = () => {
  return (
    <Router>
      ...
      {/* PrivateとSetを分けているパターン  */}
      <Private unauthenticated="login" hasRole={["author", "editor"]}>
        <Set wrap={FooLayout}>
          <Route path="/foo" name="foo" page={FooPage} />
        </Set>
      </Private>
      {/* Privateにまとめて書くことができる */}
      <Private unauthenticated="login" wrap={FooLayout}>
        <Route path="/foo" name="foo" page={FooPage} />
      </Private>
      <Route notfound page={NotFoundPage} />
    </Router>
  );
};

API側での認証

API側ではcontext.currentUserとすることでユーザーの情報にアクセスできる。

getCurrentUser()

createGraphQLHandlerにgetCurrentUserを渡すことで、デコードされた情報をユーザーオブジェクトにマッピングできる。

クライアントに返す情報は慎重に定義したいので、setupコマンドで作成されたgetCurrentUseridしか返さないようになっている。

id以外も取得したい場合は下記のように追加する必要がある。

api/src/lib/auth.ts
export const getCurrentUser = async (session: Decoded) => {
  if (!session || typeof session.id !== 'number') {
    throw new Error('Invalid session')
  }

  return await db.user.findUnique({
    where: { id: session.id },
    select: {
     id: true,
+    email: true,
    },
  })
}

GraphQL Directive

api/src/lib/auth.tsが上書きされることにより、すべてtrueを返していたrequireAuth()が認証情報をチェックするようになる。

デフォルトで作成されたQueryやMutationは@requireAuthで定義されている。認証の設定をする前はauthorizedがtrueになっているため実行できるが、認証の設定後はログインしていないとfalseが返るようになり、実行が許可されない。

認証をスキップしたい場合は@skipAuthに書き換えることで、認証していないユーザーも実行できるようになる。

次回

次回はRedwoodJSを使ってみた所感をまとめる。

GitHubで編集を提案

Discussion