Table of Contents
はじめに
Chapter 4は認証機能を実装する内容となります。
Note
デプロイについても取り上げていましたが、RailwayやNetlifyなど普段私が使わないサービスでしたので今回はスキップします。
前回の記事
管理画面
まずはルーティングを変更してscaffoldで生成したPostリソースの画面群を全て/admin
以下に設定します。
// web/src/Routes.tsx
import { Router, Route, Set } from '@redwoodjs/router'
import ScaffoldLayout from 'src/layouts/ScaffoldLayout'
import BlogLayout from 'src/layouts/BlogLayout'
const Routes = () => {
return (
<Router>
<Set wrap={ScaffoldLayout} title="Posts" titleTo="posts" buttonLabel="New Post" buttonTo="newPost">
<Route path="/admin/posts/new" page={PostNewPostPage} name="newPost" />
<Route path="/admin/posts/{id:Int}/edit" page={PostEditPostPage} name="editPost" />
<Route path="/admin/posts/{id:Int}" page={PostPostPage} name="post" />
<Route path="/admin/posts" page={PostPostsPage} name="posts" />
</Set>
<Set wrap={BlogLayout}>
<Route path="/article/{id:Int}" page={ArticlePage} name="article" />
<Route path="/contact" page={ContactPage} name="contact" />
<Route path="/about" page={AboutPage} name="about" />
<Route path="/" page={HomePage} name="home" />
</Set>
<Route notfound page={NotFoundPage} />
</Router>
)
}
export default Routes
セットアップ
次にコマンド実行して、認証のセットアップします。
yarn rw setup auth dbAuth
➤ YN0000: · Yarn 4.3.0
➤ YN0000: ┌ Resolution step
➤ YN0085: │ + @redwoodjs/auth-dbauth-setup@npm:7.7.4, @simplewebauthn/browser@npm:7.4.0, and 1 more.
➤ YN0000: └ Completed in 3s 509ms
➤ YN0000: ┌ Post-resolution validation
➤ YN0086: │ Some peer dependencies are incorrectly met by dependencies; run yarn explain peer-requirements for details.
➤ YN0000: └ Completed
➤ YN0000: ┌ Fetch step
➤ YN0013: │ 3 packages were added to the project (+ 120.16 KiB).
➤ YN0000: └ Completed in 0s 917ms
➤ YN0000: ┌ Link step
➤ YN0000: └ Completed in 0s 308ms
➤ YN0000: · Done with warnings in 4s 884ms
✔ Enable WebAuthn support (TouchID/FaceID)? See https://redwoodjs.com/docs/auth/dbAuth#webAuthn … no
↓ Setting up Auth from scratch
✔ Generating auth api side files...
✔ Successfully wrote file `./api/src/functions/auth.ts`
✔ Successfully wrote file `./api/src/lib/auth.ts`
✔ Updating web/src/App.{jsx,tsx}
✔ Creating web/src/auth.ts
✔ Updating Routes file...
✔ Adding auth config to GraphQL API...
✔ Adding required web packages...
✔ Adding required api packages...
✔ Installing packages...
✔ Adding SESSION_SECRET var to .env...
✔ Create auth decoder function
✔ One more thing...
Done! But you have a little more work to do:
You will need to add a couple of fields to your User table in order
to store a hashed password and salt:
model User {
id Int @id @default(autoincrement())
email String @unique
hashedPassword String // <─┐
salt String // <─┼─ add these lines
resetToken String? // <─┤
resetTokenExpiresAt DateTime? // <─┘
}
If you already have existing user records you will need to provide
a default value for `hashedPassword` and `salt` or Prisma complains, so
change those to:
hashedPassword String @default("")
salt String @default("")
If you expose any of your user data via GraphQL be sure to exclude
`hashedPassword` and `salt` (or whatever you named them) from the
SDL file that defines the fields for your user.
You'll need to let Redwood know what fields you're using for your
users' `id` and `username` fields. In this case we're using `id` and
`email`, so update those in the `authFields` config in
`/api/src/functions/auth.js` (this is also the place to tell Redwood if
you used a different name for the `hashedPassword`, `salt`,
`resetToken` or `resetTokenExpiresAt`, fields:`
authFields: {
id: 'id',
username: 'email',
hashedPassword: 'hashedPassword',
salt: 'salt',
resetToken: 'resetToken',
resetTokenExpiresAt: 'resetTokenExpiresAt',
},
To get the actual user that's logged in, take a look at `getCurrentUser()`
in `/api/src/lib/auth.js`. We default it to something simple, but you may
use different names for your model or unique ID fields, in which case you
need to update those calls (instructions are in the comment above the code).
Finally, we created a SESSION_SECRET environment variable for you in
/path/to/project/my-first-redwood-app/.env. This value should NOT be checked
into version control and should be unique for each environment you
deploy to. If you ever need to log everyone out of your app at once
change this secret to a new value and deploy. To create a new secret, run:
yarn rw generate secret
Need simple Login, Signup and Forgot Password pages? We've got a generator
for those as well:
yarn rw generate dbAuth
実行すると、パッケージがインストールされたり、WebAuthnサポートについて聞かれたり、色々ファイルが上書きされたりしました。
実行ログにあるように手動で手を加えないと部分があるので、やっていきます。
Userモデルの作成
まずはUserモデルを作成します。
// schema.prisma
model User {
id Int @id @default(autoincrement())
name String?
email String @unique
hashedPassword String
salt String
resetToken String?
resetTokenExpiresAt DateTime?
}
認証に関するフィールドは以下です。
hashedPassword
: パスワードとソルトの組み合わせをハッシュ化したものを保存しますsalt
: パスワードと混ぜるためのユニークな文字列。Rainbow Table Attack防止のためですresetToken
: ユーザーがパスワードを忘れてリセットする際に使用するトークンですresetTokenExpiresAt
: パスワードリセットの有効期限です
Userモデルのスキーマ定義ができたらマイグレーションを実行します。
yarn rw prisma migrate dev
これでデータベースのセットアップが完了しました。
プライベートルート
この段階で/admin/posts
にアクセスすると「You don't have permission to do that.」とエラーメッセージが表示されます。
これをアクセスできるように修正します。
import { PrivateSet, Router, Route, Set } from '@redwoodjs/router'
import ScaffoldLayout from 'src/layouts/ScaffoldLayout'
import BlogLayout from 'src/layouts/BlogLayout'
import { useAuth } from './auth'
const Routes = () => {
return (
<Router useAuth={useAuth}>
<PrivateSet unauthenticated="home">
<Set wrap={ScaffoldLayout} title="Posts" titleTo="posts" buttonLabel="New Post" buttonTo="newPost">
<Route path="/admin/posts/new" page={PostNewPostPage} name="newPost" />
<Route path="/admin/posts/{id:Int}/edit" page={PostEditPostPage} name="editPost" />
<Route path="/admin/posts/{id:Int}" page={PostPostPage} name="post" />
<Route path="/admin/posts" page={PostPostsPage} name="posts" />
</Set>
{/* ... */}
</PrivateSet>
</Router>
)
}
PrivateSet
コンポーネントを挟みました。
再度/admin/posts
へアクセスしてみると、未認証のためhome(/
)へリダイレクトされました。
ホーム画面ではPost一覧が見えるようにしたいので、SDLファイルを編集します。
// api/src/graphql/posts.sdl.ts
export const schema = gql`
# ...
type Query {
posts: [Post!]! @skipAuth
post(id: Int!): Post @skipAuth
}
# ...
`
Queryのpostsとpostを@requireAuth
から@skipAuth
へ変更しました。
これでPost一覧と詳細が認証なしで表示できるようになりました。
ログイン&サインアップページ
RedwoodJSにはログインページとサインアップページを生成してくれるコマンドがあるので実行します。
yarn rw g dbAuth
✔ Determining UI labels...
✔ Username label: "Username"
✔ Password label: "Password"
✔ Querying WebAuthn addition: WebAuthn addition not included
✔ Creating pages...
✔ Successfully wrote file `./web/src/pages/SignupPage/SignupPage.tsx`
✔ Successfully wrote file `./web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx`
✔ Successfully wrote file `./web/src/pages/LoginPage/LoginPage.tsx`
✔ Successfully wrote file `./web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx`
✔ Adding routes...
✔ Adding scaffold import...
✔ One more thing...
Pages created! But you're not done yet:
You'll need to tell your pages where to redirect after a user has logged in,
signed up, or reset their password. Look in LoginPage, SignupPage,
ForgotPasswordPage and ResetPasswordPage for these lines:
if (isAuthenticated) {
navigate(routes.home())
}
and change the route to where you want them to go if the user is already
logged in. Also take a look in the onSubmit() functions in ForgotPasswordPage
and ResetPasswordPage to change where the user redirects to after submitting
those forms.
Happy authenticating!
/login
と/signup
へアクセスすると以下のような画面ができています。
そのままサインアップした後、/admin/posts
へアクセスすると無事に見れるようになりました。
ログアウトリンク
ログインできたので次はログアウトリンクを追加しましょう。 BlogLayoutコンポーネントに追加します。
import { Link, routes } from '@redwoodjs/router'
import { useAuth } from 'src/auth'
type BlogLayoutProps = {
children?: React.ReactNode
}
const BlogLayout = ({ children }: BlogLayoutProps) => {
const { isAuthenticated, currentUser, logOut } = useAuth()
return (
<>
<header>
<div className="flex-between">
<h1>
<Link to={routes.home()}>Redwood Blog</Link>
</h1>
{isAuthenticated ? (
<div>
<span>Logged in as {currentUser.email}</span>{' '}
<button type="button" onClick={logOut}>
Logout
</button>
</div>
) : (
<Link to={routes.login()}>Login</Link>
)}
</div>
<nav>
<ul>
<li>
<Link to={routes.home()}>Home</Link>
</li>
<li>
<Link to={routes.about()}>About</Link>
</li>
<li>
<Link to={routes.contact()}>Contact</Link>
</li>
</ul>
</nav>
</header>
<main>{children}</main>
</>
)
}
export default BlogLayout
useAuth
フックを使うと、ログインユーザー情報やログイン状況、ログアウト関数が取れます。
ただしこのままだとcurrentUser.email
が取れないのでapi/src/lib/auth.tsを編集します。
// api/src/lib/auth.ts
export const getCurrentUser = async (session) => {
return await db.user.findUnique({
where: { id: session.id },
select: { id: true, email: true},
})
}
これでログインしているユーザーのEmailが表示できました。
セッションシークレットについて
setup
コマンドを実行した際に、SESSION_SECRET
という環境変数が.envに追加されたはずです。
これはログイン時にユーザーのブラウザに保存されるクッキーの暗号化キーです。これは漏洩しないように注意してください。リポジトリへのプッシュもするべきではありません。
再度生成したい場合はyarn rw g secret
で新しい値を出力できます。
もし本番環境のSESSION_SECRET
を変更する場合は全てのユーザーがログアウトしてしまうので注意してください。
これでChapter 4(Authenticationのみ)は終了です。