この記事について
この記事は、全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>
が追加される
+ 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
を実行する
model User { id Int @id @default(autoincrement()) name String? email String @unique hashedPassword String salt String resetToken String? resetTokenExpiresAt DateTime? }
UUIDを使用する場合
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を使用している場合も下記のような関数が生成されている。
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 }, }); };
このままだとログイン時にエラーが出てしまうので、下記のように修正する必要がある。
- if (!session || typeof session.id !== 'number') { + if (!session || typeof session.id !== 'string') {
暗号化キー
.env
にSESSION_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
が追加される。
web/src/pages/LoginPage/LoginPage.tsx
web/src/pages/SignupPage/SignupPage.tsx
web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx
web/src/pages/ResetPasswordPage/ResetPasswordPage.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
コマンドで作成されたgetCurrentUser
はid
しか返さないようになっている。
id
以外も取得したい場合は下記のように追加する必要がある。
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を使ってみた所感をまとめる。