Authentication
An Admin Section
管理画面を /admin
に置くことは、合理的なことです。これを実現するために、URLが /posts
で始まる4つのルートを更新し、代わりに /admin/posts
で始まるようにしましょう:
- JavaScript
- TypeScript
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
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
ブラウザで http://localhost:8910/admin/posts を開くと、生成された scaffold ページ表示されるはずです。名前付きルートのおかげでページの name
は変更されなかったので、scaffold によって生成された <Link>
を更新する必要はありません!
管理者が別のパスにいることは素晴らしいことですが、誰かがその新しいパスをブラウズして、私たちのブログ記事をいじくることを止めるものは何もないのです。どうすれば、覗き見されないようにできるでしょうか?
Authentication
"Authentication(認証)" とは、ユーザ(しばしばメールアドレスとパスワードで識別される)が、何かにアクセスすることを許可するための包括的な用語です。認証は、技術的観点からも開発者の幸福の観点からも、正しく行うことが famously fickle (継続的な実現が非常に難しいことで有名)であることがよくあります。
"Credentials" (クレデンシャル)とは、ユーザが自分自身を証明するために提供する情報のことです:一般的にはユーザ名(だいたいはメールアドレス)とパスワードがこれにあたります。
Redwoodには2つの認証方法があります:
- セルフホスト:ユーザ認証情報はユーザ自身のデータベースに保存される
- サードパーティホスト:ユーザ認証情報はサードパーティに保存される
どちらの場合も、最終的にはWebサイドとAPIサイドの両方でアクセスできる認証済みユーザを得ることができます。
Redwood には、最も人気のあるいくつかのサードパーティの認証プロバイダの integrations が含まれています:
- Auth0
- Clerk
- Netlify Identity
- Netlify GoTrue-JS
- Magic
- Nhost
- Firebase's GoogleAuthProvider
- Supabase
- SuperTokens
- WalletConnect
このチュートリアルのブログでは、セルフホストの認証(Redwoodでは dbAuth という名前)を使用することにします。これは最も簡単で、サードパーティの認証サービスへのユーザ登録が不要だからです。
ログインを語る上で必ずと言っていいほど登場する、"A "で始まり "ation "で終わる、文字数の多い2つの用語があります(つまり、韻を踏もうと思えば踏めるということです):
- Authentication
- Authorization
Redwoodはこれらの用語を以下のように使用しています:
- Authentication (認証)は、一般的にメールアドレスとパスワード、またはAuth0のようなサードパーティプロバイダで "logging in" することによって、誰かがその人であるかどうかを決定することを扱う
- Authorization (認可)は、ユーザ(通常、すでに認証されている)が、彼らがしたい何かをすることを許可されているかどうか。一般的に、URLやサイト機能へのアクセスを許可する前に、ロールと権限チェックを組み合わせて判定する
チュートリアルのこのセクションでは Authentication (認証)のみに焦点を当てています。Redwood の Authorization については チュートリアルの 7 章 を参照してください。
Auth Setup
おそらくお分かりのように、Redwood にはいくつかのジェネレータがあります。一つは dbAuth に必要なバックエンドコンポーネントをインストールするもので、もう一つはログイン、サインアップ、パスワード忘れのページを作成するものです。
このセットアップコマンドを実行すると、アプリに dbAuth が追加されます:
yarn rw setup auth dbAuth
既存のファイル /api/src/lib/auth.ts
を上書きするかどうか尋ねられたら "yes" と答えてください。新しいアプリに作成されるシェル auth.ts
は @requireAuth
ディレクティブのようなものを動作するようにしますが、今度はこれを実際の実装に置き換えることにします。 "Enable WebAuthn support" と聞かれたら、"no" を選びます - これはこのチュートリアルでは必要ない別の機能です。
このプロセスでは、いくつかのファイルが作成され、最後に必要なカスタマイズのためのインストール後の説明も含まれていることがわかります。では、それらを見ていきましょう。
Create a User Model
まず User
モデルにいくつかのフィールドを追加する必要があります。まだ User
モデルがないので、必要なフィールドと一緒に作成します。
schema.prisma
を開いて追加します:
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
binaryTargets = "native"
}
model Post {
id Int @id @default(autoincrement())
title String
body String
createdAt DateTime @default(now())
}
model Contact {
id Int @id @default(autoincrement())
name String
email String
message String
createdAt DateTime @default(now())
}
model User {
id Int @id @default(autoincrement())
name String?
email String @unique
hashedPassword String
salt String
resetToken String?
resetTokenExpiresAt DateTime?
}
これにより、名前とメールアドレス持つユーザと、dbAuthが制御する4つのフィールドが得られます:
- hashedPassword : ユーザのパスワードを
salt
と組み合わせて ハッシュ化 した結果 - salt : レインボーテーブル攻撃を防ぐためにhashedPasswordと結合されるユニークな文字列
- resetToken : ユーザがパスワードを忘れた場合、dbAuthはここにトークンを挿入するので、ユーザがパスワードをリセットするために戻ってきたときここにデータ存在しなければならない
- resetTokenExpiresAt :
resetToken
が期限切れで無効になるタイムスタンプ (ユーザはパスワード忘れフォームに再度記入する必要がある)
データベースに"create user" のような名前をつけたマイグレーションを実行して、 ユーザモデルを作ってみましょう:
yarn rw prisma migrate dev
データベースの設定は以上です!
Private Routes
ブログ記事管理画面を再読み込みしてみれば、50%正しいものが表示されるはずです:
adminセクションに行くと、ログインしていないユーザがブログ記事を見ることができなくなりました。素晴らしい!これは api/src/graphql/posts.sdl.ts
にある @requireAuth
ディレクティブの結果です:あなたは認証されていないので、GraphQLはあなたのデータ要求に応答しません。しかし、理想を言えば、管理ページそのものを見えないようにしたいところです。Routesファイルにある新しいコンポーネント <Private>
でそれを解決しましょう:
- JavaScript
- TypeScript
import { Private, Router, Route, Set } from '@redwoodjs/router'
import ScaffoldLayout from 'src/layouts/ScaffoldLayout'
import BlogLayout from 'src/layouts/BlogLayout'
const Routes = () => {
return (
<Router>
<Private 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>
</Private>
<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
import { Private, Router, Route, Set } from '@redwoodjs/router'
import ScaffoldLayout from 'src/layouts/ScaffoldLayout'
import BlogLayout from 'src/layouts/BlogLayout'
const Routes = () => {
return (
<Router>
<Private 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>
</Private>
<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
プライベートなルート(ログインしているときだけアクセスできるルート)を <Private>
コンポーネントでラップし、認証されていないルートをどこに送ればよいかをアプリに知らせます。この場合、ルートは home
に送られます。
今 http://localhost:8910/admin/posts に戻ってみると - なんと!
さて、管理画面には行けませんでしたが、ブログ記事も見れなくなってしまいました。なぜ、ブログ記事の管理画面で見たのと同じメッセージがここで表示されているかわかりますか?
posts.sdl.ts
の posts
クエリが、ホームページとブログ記事管理ページの 両方で 使用されるからです。これは @requireAuth
ディレクティブを持つので、ログインしているときだけアクセスできるようにロックされています。しかし、ログインしていない人がホームページのブログ記事を見ることができるように したい のです!
管理ページが <Private>
ルートの配下になったので、代わりに posts
クエリを @skipAuth
に設定したらどうでしょうか。試してみましょう:
- JavaScript
- TypeScript
export const schema = gql`
type Post {
id: Int!
title: String!
body: String!
createdAt: DateTime!
}
type Query {
posts: [Post!]! @skipAuth
post(id: Int!): Post @requireAuth
}
input CreatePostInput {
title: String!
body: String!
}
input UpdatePostInput {
title: String
body: String
}
type Mutation {
createPost(input: CreatePostInput!): Post! @requireAuth
updatePost(id: Int!, input: UpdatePostInput!): Post! @requireAuth
deletePost(id: Int!): Post! @requireAuth
}
`
export const schema = gql`
type Post {
id: Int!
title: String!
body: String!
createdAt: DateTime!
}
type Query {
posts: [Post!]! @skipAuth
post(id: Int!): Post @requireAuth
}
input CreatePostInput {
title: String!
body: String!
}
input UpdatePostInput {
title: String
body: String
}
type Mutation {
createPost(input: CreatePostInput!): Post! @requireAuth
updatePost(id: Int!, input: UpdatePostInput!): Post! @requireAuth
deletePost(id: Int!): Post! @requireAuth
}
`
ホームページを再読み込みすると:
戻ってきました!一応それぞれのブログ記事が見られるかもチェックしてみましょう...ぐぬぬ:
このページでは単一のブログ記事を表示するのに posts
ではなく post
クエリを使っています!そのためこちらにも @skipAuth
が必要です:
- JavaScript
- TypeScript
export const schema = gql`
type Post {
id: Int!
title: String!
body: String!
createdAt: DateTime!
}
type Query {
posts: [Post!]! @skipAuth
post(id: Int!): Post @skipAuth
}
input CreatePostInput {
title: String!
body: String!
}
input UpdatePostInput {
title: String
body: String
}
type Mutation {
createPost(input: CreatePostInput!): Post! @requireAuth
updatePost(id: Int!, input: UpdatePostInput!): Post! @requireAuth
deletePost(id: Int!): Post! @requireAuth
}
`
export const schema = gql`
type Post {
id: Int!
title: String!
body: String!
createdAt: DateTime!
}
type Query {
posts: [Post!]! @skipAuth
post(id: Int!): Post @skipAuth
}
input CreatePostInput {
title: String!
body: String!
}
input UpdatePostInput {
title: String
body: String
}
type Mutation {
createPost(input: CreatePostInput!): Post! @requireAuth
updatePost(id: Int!, input: UpdatePostInput!): Post! @requireAuth
deletePost(id: Int!): Post! @requireAuth
}
`
指をくわえてリロード!
やったね!アプリに認証を追加すると、このように、デフォルトでロックされているページやクエリへのアクセスを再許可しなければならない状況に何度か遭遇するでしょう。Redwoodはデフォルトでセキュアであることを忘れないでください -- アプリの意図しない公開は 多い より 少ない ほうがいいでしょう!
さて、ページがログイン後にあるため、実際にログインページを作成して、再び見ることができるようにしましょう。
posts
and post
feels bad somehow...ああ、良い目の付け所ですね。ブログ記事は今のところ特に秘密の情報を公開しませんが、いずれ publishStatus
のようなフィールドを追加して、ブログ記事を draft
(下書き)としてマークし、ホームページには表示されないようにしたらどうでしょう。しかし、もしあなたがGraphQLについて十分に知っていれば、データベース内のすべてのブログ記事を簡単にリクエストでき、すべての下書きを読むことができるようになります!
publicPosts
や publicPost
のような公開されたブログ記事を取得する 新しい エンドポイントを作成するほうが将来有望でしょう。
その場合、新しいエンドポイントは最小限のデータしか返さないようにするためのロジックを組み込み、デフォルトの posts
と post
クエリはブログ記事の全データを返し管理者だけがアクセスできるようにします(逆に posts
と post
を public にして、機密情報を含むことができる adminPosts
と adminPost
というエンドポイントを新たに作成することもできます)。
Login & Signup Pages
今回は、ログイン、サインアップ、パスワード忘れなどのページを作成するもうひとつのジェネレータをご紹介します。
yarn rw g dbAuth
またここでいくつかのページが作成され、インストール後の手順が説明されます。しかし、とりあえず、http://localhost:8910/login にアクセスしてみてください:
簡単でしたね!ログインするユーザがいないので、代わりにサインアップページに行ってみてください(ログインボタンの下にリンクがあります、または http://localhost:8910/signup を開いてください)。
dbAuthのデフォルトでは、最初のフィールドは一般的な "Username" ですが、私たちの場合、ユーザ名はメールアドレスになります(このラベルはすぐに変更可能です)。メールアドレスメールとパスワードでユーザを作成します:
そして "Signup" をクリックすると、ホームページに戻り、すべてが同じように見えるはずです!やったー?しかし、今度は http://localhost:8910/admin/posts に行ってみてください:
すごい!サインアップすると自動的にログインします(この動作は 変更可能 )。 SignupPage
のコードを見れば、ホームページへのリダイレクトが行われる場所がわかります(ヒント:21行目を確認してください)。
Add a Logout Link
さて、ログインしたところで、どうやってログアウトするのでしょうか?すべてのページに表示されるように BlogLayout
にリンクを追加し、実際に誰としてログインしているかの表示も追加しましょう。
Redwood は hook の useAuth
を提供しており、これをコンポーネント内で使用して、ユーザのログイン状態を判断したり、ユーザ情報を取得したりすることができます。 BlogLayout
では、 useAuth()
から isAuthenticated
、 currentUser
、 logOut
プロパティを再構築したいと思います:
- JavaScript
- TypeScript
import { useAuth } from '@redwoodjs/auth'
import { Link, routes } from '@redwoodjs/router'
const BlogLayout = ({ children }) => {
const { isAuthenticated, currentUser, logOut } = useAuth()
return (
<>
<header>
<h1>
<Link to={routes.home()}>Redwood Blog</Link>
</h1>
<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
import { useAuth } from '@redwoodjs/auth'
import { Link, routes } from '@redwoodjs/router'
type BlogLayoutProps = {
children?: React.ReactNode
}
const BlogLayout = ({ children }: BlogLayoutProps) => {
const { isAuthenticated, currentUser, logOut } = useAuth()
return (
<>
<header>
<h1>
<Link to={routes.home()}>Redwood Blog</Link>
</h1>
<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
たぶん名前でわかると思いますが:
- isAuthenticated : ユーザがログインしているかどうかのブール値
- currentUser : アプリが持つそのユーザの詳細(詳細は後述)
- logOut : ユーザのセッションを削除し、ログアウトさせる
ページの右上には、ユーザのメールアドレス(ログインしている場合)とログアウトのためのリンクを表示しましょう。ログインしていない場合は、ログインのリンクを表示します:
- JavaScript
- TypeScript
import { useAuth } from '@redwoodjs/auth'
import { Link, routes } from '@redwoodjs/router'
const BlogLayout = ({ children }) => {
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
import { useAuth } from '@redwoodjs/auth'
import { Link, routes } from '@redwoodjs/router'
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
まあ、だいたい合ってるんですけどね!メールアドレスはどこでしょ?デフォルトでは、 currentUser
に何が入っているかを判断する関数は、セキュリティ上の理由から、そのユーザの id
フィールドのみを返します(公開しすぎるよりは、少なすぎる方が良いことを覚えておいてください!)。そのリストにemailを追加するには、 api/src/lib/auth.ts
をチェックしてください:
- JavaScript
- TypeScript
import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server'
import { db } from './db'
export const getCurrentUser = async (session) => {
return await db.user.findUnique({
where: { id: session.id },
select: { id: true },
})
}
export const isAuthenticated = () => {
return !!context.currentUser
}
export const hasRole = (roles) => {
if (!isAuthenticated()) {
return false
}
const currentUserRoles = context.currentUser?.roles
if (typeof roles === 'string') {
if (typeof currentUserRoles === 'string') {
// roles to check is a string, currentUser.roles is a string
return currentUserRoles === roles
} else if (Array.isArray(currentUserRoles)) {
// roles to check is a string, currentUser.roles is an array
return currentUserRoles?.some((allowedRole) => roles === allowedRole)
}
}
if (Array.isArray(roles)) {
if (Array.isArray(currentUserRoles)) {
// roles to check is an array, currentUser.roles is an array
return currentUserRoles?.some((allowedRole) =>
roles.includes(allowedRole)
)
} else if (typeof currentUserRoles === 'string') {
// roles to check is an array, currentUser.roles is a string
return roles.some((allowedRole) => currentUserRoles === allowedRole)
}
}
// roles not found
return false
}
export const requireAuth = ({ roles } = {}) => {
if (!isAuthenticated()) {
throw new AuthenticationError("You don't have permission to do that.")
}
if (roles && !hasRole(roles)) {
throw new ForbiddenError("You don't have access to do that.")
}
}
import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server'
import { db } from './db'
import type { DbAuthSession } from '@redwoodjs/api'
export const getCurrentUser = async (session: DbAuthSession<number>) => {
return await db.user.findUnique({
where: { id: session.id },
select: { id: true },
})
}
export const isAuthenticated = (): boolean => {
return !!context.currentUser
}
type AllowedRoles = string | string[] | undefined
export const hasRole = (roles: AllowedRoles): boolean => {
if (!isAuthenticated()) {
return false
}
const currentUserRoles = context.currentUser?.roles
if (typeof roles === 'string') {
if (typeof currentUserRoles === 'string') {
// roles to check is a string, currentUser.roles is a string
return currentUserRoles === roles
} else if (Array.isArray(currentUserRoles)) {
// roles to check is a string, currentUser.roles is an array
return currentUserRoles?.some((allowedRole) => roles === allowedRole)
}
}
if (Array.isArray(roles)) {
if (Array.isArray(currentUserRoles)) {
// roles to check is an array, currentUser.roles is an array
return currentUserRoles?.some((allowedRole) =>
roles.includes(allowedRole)
)
} else if (typeof currentUserRoles === 'string') {
// roles to check is an array, currentUser.roles is a string
return roles.some((allowedRole) => currentUserRoles === allowedRole)
}
}
// roles not found
return false
}
export const requireAuth = ({ roles }: { roles?: AllowedRoles } = {}) => {
if (!isAuthenticated()) {
throw new AuthenticationError("You don't have permission to do that.")
}
if (roles && !hasRole(roles)) {
throw new ForbiddenError("You don't have access to do that.")
}
}
getCurrentUser()
関数は、マジックが起こる場所です:この関数が返すのは、Webサイドと APIサイドの両方で currentUser
の内容なのです! dbAuth の場合、唯一の引数として渡される session
には、ログインしているユーザの id
が含まれます。 そして、Prismaでデータベース内のユーザを検索し、 id
だけを選択します。ここに email
を追加してみましょう:
- JavaScript
- TypeScript
export const getCurrentUser = async (session) => {
return await db.user.findUnique({
where: { id: session.id },
select: { id: true, email: true},
})
}
export const getCurrentUser = async (session) => {
return await db.user.findUnique({
where: { id: session.id },
select: { id: true, email: true},
})
}
これでメールアドレスがホームページの右上に表示されるはずです:
このファイルから離れる前に requireAuth()
を見てみましょう。 @requireAuth
ディレクティブの話をしたとき、最初に認証をインストールしたときに "You don't have permission to do that" というメッセージが表示されたのを覚えていますか?あのメッセージはここからきています!
Session Secret
最初の setup
コマンドで dbAuth をインストールした後、プロジェクトのルートにある .env
ファイルが編集されたことにお気づきでしょうか。 setup
スクリプトは SESSION_SECRET
という新しい環境変数に、数字と文字からなるランダムな長い文字列を追加していました。これは、ユーザがログインしたときにブラウザに保存されるクッキーの暗号化キーです。この秘密は決して共有してはいけませんし、あなたのリポジトリにチェックインしてもいけません。また、デプロイする環境ごとに再作成しなければなりません。
新しい値は yarn rw g secret
コマンドで生成できます。このコマンドはターミナルに出力されるだけなので、 .env
ファイルにコピー&ペーストする必要があります。本番環境でこのキーを変更すると、すべてのユーザが次のリクエストでログアウトしてしまうことに注意してください!なぜなら、現在持っているクッキーを新しいキーで復号することができないからです。ユーザは新しいキーで暗号化された新しいクッキーで再度ログインする必要があります。
Wrapping Up
信じられないかもしれませんが、これで認証はほぼ完了です! @requireAuth
と @skipAuth
ディレクティブの組み合わせで GraphQL クエリ/ミューテーションへのアクセスをロックしたり、 <Private>
コンポーネントを使用してアプリのページ全体へのアクセスを制限したりできます。特定のコンポーネント、またはコンポーネントの特定の部分へのアクセスのみを制限したい場合は、常に useAuth()
フックから isAuthenticated
を取得して個別にレンダリングしなければなりません。
Redwood のドキュメントを読み込むなら self-hosted authentication と third-party authentication をどうぞ。
One More Thing
Creating a Contact の最後にあるGraphQL Playgroundを覚えていますか?認証が実装された今の状態でもう一度実行してみると、先ほど説明した @requireAuth
ディレクティブが原因でエラーが発生するはずです!しかし、新しい お問い合わせの追加は問題なく行えるはずです(このミューテーションでは @skipAuth
を使用しているからです)。
しかしながら、GraphQL Playgroundを通じてログインしたユーザをシミュレートすることは、そう楽しいことではありません。しかし、私たちはこの体験を改善するために努力しています!