【Supabase × React】認証機能の実装方法
はじめに
今回は、Supabase
とReact
を用いて認証機能を実装する方法についてまとめました。
ここでいう認証機能は以下です。
- サインアップ(ユーザー登録)機能
- ログイン機能
- ログアウト機能
- パスワードリセット機能
- パスワード再設定用URLメール送信機能
- パスワード再設定機能
余談ですが、他にも良質な記事はありました。しかしながら、どの記事もNext.js
での実装が前提となっていました。
私が作成したものはVite
×React
のSPAでの実装で、React
のみの実装の記事もあったら良いなと思い記事にさせていただきました。
環境
- typescript 5.2
- vite 4.5.1
- react 18.2.0
- @types/react 7.5.3
- react-router-dom 6.19.0
- react-hook-form 7.48.2
- @hookform/resolver 3.3.2
- zod 3.22.4
- @supabase/supabase-js 2.39.2
- @supabase/auth-ui-react 0.4.7
- @supabase/auth-ui-shared 0.1.8
- @mui/material 5.14.18
- @mui/icons-material 5.14.18
- uuid 9.0.1
実装
それでは実装の解説に入ります。
まずは、Supabase
とアプリケーションの連携をして、Supabase
のAuth API
を実行できるようにします。
※ 既存のSupabase
のProjectがない場合は、作成しておく必要があります
(本記事は、Supabase
のProjectが既存で存在している場合を前提としています)
Supabaseとアプリケーションの連携
ここでは、Supabase
のProjectのIDとProject URL
・API Key
を取得します。
また、メール認証を実装するため、その設定も行います。
Project IDの取得
まず、Supabase
のDashboard画面から該当のProjectを選択します。
ProjectのHome画面に遷移できたら、「Project Settings」をクリックし、Projectの設定画面に遷移します。
(設定画面の初期画面であるGeneral
に遷移できればOKです)
ここで、以下のReference ID
をコピーします。
Supabaseの型生成
今回のアプリケーションではTypeScript
で開発をしているので、Supabase
の型を生成します。
以下の公式ドキュメントを参考に実施しまう。
まず、ターミナルで以下のコマンドを実行します。。
※ アプリケーションのディレクトリに移動しておいてください
npx supabase login
ブラウザが立ち上がり、Supabase
の画面が表示され、「アクセストークンが取得できた」旨のメッセージが表示されていればOKです。
ターミナルでは以下のようになっていればOKです。
Token cli_username@MacBook-hoge_XXXXXXXXXXXX created successfully.
次に以下のコマンドを実行します。
その際、$PROJECT_REF
を、先程コピーしたReference ID
に置き換える必要があります。
また、types/supabase.ts
の部分ですが、typesディレクトリを事前に作成しておく必要があります。
もちろん、lib
など任意のディレクトリを指定することも可能です。
npx supabase gen types typescript --project-id "$PROJECT_REF" --schema public > types/supabase.ts
Project URL・API Keyの取得
Projectの設定画面から「API」のメニューをクリックし、画面遷移します。
以下のProject URL
とProject API Key
のanon public
をコピーします。
コピーしたProject URL
とProject API Key
を環境変数ファイルに記述します。
VITE_SUPABASE_URL = "Project URL"
VITE_SUPABASE_API_KEY = "Project API Key"
Vite
ではimport.meta.env.VITE_SUPABASE_URL
のように環境変数にアクセスします。
Authentication Providerの設定
Projectの設定画面のサイドメニューから「Authentication」のをクリックし、画面遷移します。
その後、「Providers」を選択し、以下のようにEmail
をEnabled
とし、有効化します。
ここまでで、React
アプリケーションとのSupabase
の連携・管理画面での設定は完了です。
認証機能
ここからが本題となる認証機能の実装です。
ですが、その前に、必要なライブラリをインストールし、SupabaseClient
を作成する必要があります。
SupabaseClient
により、アプリケーション全体でSupabase
のAPIを使用できるようになります。
まずは、ライブラリの導入から行います。
以下の公式のクイックスタートを参照し、行います。
※ React
アプリケーション×Supabase
を0から作成する場合にも以下を参照して行ってください。
yarn add @supabase/supabase-js @supabase/auth-ui-react @supabase/auth-ui-shared
次にSupabaseClient
を作成します。
以下のようにutils
などのディレクトリにファイルを作成し、記述を行います。
import { createClient } from "@supabase/supabase-js";
import type { Database } from "../types/supabase";
export const supabase = createClient<Database>(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_API_KEY,
);
Supabase
のcreateClient
に、supabaseUrl
とsupabaseKey
を渡して、SupabaseClient
を作成します。
Database
はSupabase
の型を生成した際に作成されたinterface
です。
これで、Supabase
のAuth API
をsupabase.〇〇
のような形式で使用することができます。
サインアップ機能
以下、ドキュメントを参照し、機能実装します。
実装したロジックやUIは以下となります。
import { zodResolver } from "@hookform/resolvers/zod";
import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
import PersonOutlineOutlinedIcon from "@mui/icons-material/PersonOutlineOutlined";
import { Box } from "@mui/material";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { AccountFormFooter } from "../../../../components/ui-elements/AccountFormFooter";
import AccountTextField from "../../../../components/ui-elements/AccountTextField";
import { supabase } from "../../../../utils/supabase";
import { signUpInputSchema } from "../../types/SignUpFormInput";
import type { SignUpFormInput } from "../../types/SignUpFormInput";
import type { SubmitHandler } from "react-hook-form";
export const LoginForm = () => {
const navigate = useNavigate();
const {
handleSubmit,
control,
formState: { isSubmitting, errors },
reset,
} = useForm<SignUpFormInput>({
resolver: zodResolver(signUpInputSchema),
defaultValues: {
email: "",
password: "",
},
});
const onSubmit: SubmitHandler<SignUpFormInput> = async (data) => {
try {
const { email, password } = data;
const { error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${window.location.origin}/welcome`
}
});
if (error) {
throw new Error(error.message);
}
navigate("/");
} catch (err) {
console.log(err);
} finally {
reset();
}
};
const handleClick = () => {
navigate("/login", { state: { referrer: "signUp" } });
};
return (
<Box
component="form"
sx={{
display: "flex",
flexDirection: "column",
gap: "32px",
width: "100%",
}}
onSubmit={handleSubmit(onSubmit)}
>
<AccountTextField
id="email"
name="email"
control={control}
error={errors.email?.message}
type="text"
label="メールアドレス"
secondaryLabel="メールアドレスを入力..."
icon={<PersonOutlineOutlinedIcon />}
disabled={isSubmitting}
/>
<AccountTextField
id="password"
name="password"
control={control}
type="password"
error={errors.password?.message}
label="パスワード"
secondaryLabel="パスワードを入力..."
icon={<LockOutlinedIcon />}
disabled={isSubmitting}
/>
<AccountFormFooter
disabled={isSubmitting}
text="サインアップ"
icon={<ArrowForwardIcon />}
secondaryText="アカウントを持っている場合"
onClick={handleClick}
/>
</Box>
);
};
サインアップフォームのUIは以下のような簡易的なものになります。
実装部分の解説をします。
APIの部分はドキュメント通りなので実装自体は簡単です。
email
とpassword
、options
を引数に渡すことで実装ができます。
Supabase
のAuth API
の関数は基本的に返り値として、data
やerror
を返してくれるので、それを受け取り、error
があれば例外をthrow
するようにしています。
(この処理は、他の認証機能の実装でも行っています)
emailRedirectTo
に任意のURLを渡すことでメールで送信されるURLのリダイレクト先が指定したURLとなります。
そのため、emailRedirectTo
で指定したページが存在しないと当然、404ページに遷移するなどの挙動になります。
また、Supabase
を用いたメール送信機能ではSupabase
側のRedirect URLs
という設定も必要です。
Redirect URLsの設定
以下は、Redirect URLs
のドキュメントです。
こちらを参照し、設定していきます。
まず「Supabase Project画面」のサイドメニューからAuthentication
をクリックします。
画面遷移できたら、「URL Configuration」をクリックします。
その後、Redirect URLs
のAdd URL
でURLを追加します。
Add URL
をクリックすると、モーダルが開くので入力欄にリダレクトをさせたいURLのドメインを入力します。
今回の場合は、http://localhost:3000/*
を入力します。
これを入力すると、http://localhost:3000/foo
やhttp://localhost:3000/bar
をemailRedirectTo
に指定すると、それらのURLがメールに記載されるURLのリダイレクト先になります。
(今回の場合はhttp://localhost:3000/welcom
がメールのURLリンクとなります)
ログイン機能
続いて、ログイン機能です。
以下、ドキュメントを参照し、実装します。
実際に実装したUIやロジックは以下のとおりです。
import { zodResolver } from "@hookform/resolvers/zod";
import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
import PersonOutlineOutlinedIcon from "@mui/icons-material/PersonOutlineOutlined";
import { Box } from "@mui/material";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { AccountFormFooter } from "../../../../components/ui-elements/AccountFormFooter";
import AccountTextField from "../../../../components/ui-elements/AccountTextField";
import { supabase } from "../../../../utils/supabase";
import { loginInputSchema } from "../../types/LoginFormInput";
import type { LoginFormInput } from "../../types/LoginFormInput";
import type { SubmitHandler } from "react-hook-form";
export const LoginForm = () => {
const navigate = useNavigate();
const {
handleSubmit,
control,
formState: { isSubmitting, errors },
reset,
} = useForm<LoginFormInput>({
resolver: zodResolver(loginInputSchema),
defaultValues: {
email: "",
password: "",
},
});
const onSubmit: SubmitHandler<LoginFormInput> = async (data) => {
try {
const { email, password } = data;
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
throw new Error(error.message);
}
navigate("/");
} catch (err) {
console.log(err);
} finally {
reset();
}
};
const handleClick = () => {
navigate("/reset_password", { state: { referrer: "login" } });
};
return (
<Box
component="form"
sx={{
display: "flex",
flexDirection: "column",
gap: "32px",
width: "100%",
}}
onSubmit={handleSubmit(onSubmit)}
>
<AccountTextField
id="email"
name="email"
control={control}
error={errors.email?.message}
type="text"
label="メールアドレス"
secondaryLabel="メールアドレスを入力..."
icon={<PersonOutlineOutlinedIcon />}
disabled={isSubmitting}
/>
<AccountTextField
id="password"
name="password"
control={control}
type="password"
error={errors.password?.message}
label="パスワード"
secondaryLabel="パスワードを入力..."
icon={<LockOutlinedIcon />}
disabled={isSubmitting}
/>
<AccountFormFooter
disabled={isSubmitting}
text="ログイン"
icon={<ArrowForwardIcon />}
secondaryText="パスワードを忘れた場合"
onClick={handleClick}
/>
</Box>
);
};
ログインフォームのUIは以下のような簡易的なものになります。
ここもAPIの部分はドキュメント通りなので実装自体は簡単にできます。
email
とpassword
を引数に渡すことでログイン機能の実装ができます。
次はログアウトを見ていきましょう。
ログアウト機能
ログアウト機能もドキュメントどおりに実装していきます。
実際のログアウトの実装は以下のとおりです。
import { Logout } from "@mui/icons-material";
import { Button } from "@mui/material";
import { useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { supabase } from "./utils/supabase";
const SideBar = () => {
const navigate = useNavigate();
const onLogout = useCallback(async () => {
try {
const { error } = await supabase.auth.signOut();
if (error) {
throw new Error(error.message);
}
navigate("/login");
} catch (err) {
console.log(err);
}
}, [navigate]);
return (
<Button
color="error"
variant="text"
startIcon={<Logout />}
onClick={onLogout}
>
ログアウト
</Button>
);
};
export default SideBar;
このように簡単に実装できます。
パスワードリセット機能
本機能は以下のドキュメントを参照し、実装します。
それでは、実際の実装を見ていきましょう。
前もって認識していただきたいのですが、この機能のみコンポーネントを、実装上2つに分けています。
またカスタムフックを1つ作成しています。
それぞれ解説します。
import { zodResolver } from "@hookform/resolvers/zod";
import { Box, Typography } from "@mui/material";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { useIsLocationState } from "../../../hooks/useIsLocationState";
import { loginInputSchema } from "../../login/types/LoginFormInput";
import { ResetPasswordForm } from "./ResetPasswordForm";
export const ResetPasswordPage = () => {
useIsLocationState("/login");
const emailSchema = z.object({
email: loginInputSchema.shape.email,
});
const {
handleSubmit,
control,
formState: { isSubmitting, isSubmitSuccessful, errors },
reset,
} = useForm<{ email: string }>({
resolver: zodResolver(emailSchema),
defaultValues: { email: "" },
});
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
gap: "32px",
width: "100%",
}}
>
<Typography variant="h4" align="left">
パスワード再設定
</Typography>
{isSubmitSuccessful ? (
<Typography variant="body1">
パスワードの再設定URLを送信しました。メールをご確認ください。
</Typography>
) : (
<ResetPasswordForm
handleSubmit={handleSubmit}
control={control}
isSubmitting={isSubmitting}
errors={errors}
reset={reset}
/>
)}
</Box>
);
};
まず、ResetPasswordPage.tsx
ですが、ここはフォームの型定義と、画面の切り替えを行っています。
react-hook-form
のformState: { isSubmitSuccessful }
で送信が成功した場合に、メールが送信された旨のメッセージを送信するようにしています。
続いてカスタムフックのuseIsLocationState
を解説します。
import { useEffect } from "react";
import { useLocation, useNavigate } from "react-router-dom";
export const useIsLocationState = (path: string) => {
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
if (!location.state) {
navigate(path);
}
}, [location.state, navigate, path]);
};
このカスタムフックは、ブラウザバックを制御しています。
useNavigate
でnavigate("/reset_password", { state: { referrer: "login" } });
のようにstate
がない画面遷移の場合、引数に渡したpath
に強制的に遷移するようにしています。
今回の場合、ログイン画面からの遷移でない場合は、ログイン画面に遷移させるようにしています。
例えば、ホーム画面から/reset_password
とURLを直打ちした場合に、location.state
が存在しないので、強制的にログイン画面に遷移します。
そうすると/reset_password
にはログイン画面からの遷移でしか行けないので、ブラウザバックした際には必ずログイン画面が表示されるという要件を満たすことができます。
最後に、パスワードリセットのフォームのコンポーネントです。
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import PersonOutlineOutlinedIcon from "@mui/icons-material/PersonOutlineOutlined";
import SendIcon from "@mui/icons-material/Send";
import { Box } from "@mui/material";
import { useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { v4 as uuidv4 } from "uuid";
import { AccountFormFooter } from "../../../../components/ui-elements/AccountFormFooter";
import AccountTextField from "../../../../components/ui-elements/AccountTextField";
import { supabase } from "../../../../utils/supabase";
import type {
Control,
FieldErrors,
SubmitHandler,
UseFormHandleSubmit,
UseFormReset,
} from "react-hook-form";
type Props = {
handleSubmit: UseFormHandleSubmit<{ email: string }>;
control: Control<{ email: string }>;
isSubmitting: boolean;
errors: FieldErrors<{ email: string }>;
reset: UseFormReset<{ email: string }>;
};
export const ResetPasswordForm: React.FC<Props> = ({
handleSubmit,
control,
isSubmitting,
errors,
reset,
}) => {
const navigate = useNavigate();
const goBack = useCallback(() => {
navigate("/login");
}, [navigate]);
const onSubmit: SubmitHandler<{ email: string }> = async (data) => {
const { email } = data;
const token = uuidv4();
try {
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${window.location.origin}/reset_password/${token}`,
});
if (error) {
throw new Error(error.message);
}
alert("パスワード再設定メールを送信しました。メールを確認してください。");
} catch (err) {
console.log(err);
} finally {
reset();
}
};
return (
<Box
component="form"
sx={{
display: "flex",
flexDirection: "column",
gap: "32px",
width: "100%",
}}
onSubmit={handleSubmit(onSubmit)}
>
<AccountTextField
id="email"
name="email"
control={control}
error={errors.email?.message}
type="text"
label="メールアドレス"
secondaryLabel="メールアドレスを入力..."
icon={<PersonOutlineOutlinedIcon />}
disabled={isSubmitting}
/>
<AccountFormFooter
disabled={isSubmitting}
text="パスワードを再設定する"
secondaryText="ログイン画面に戻る"
icon={<SendIcon />}
secondaryIcon={<ArrowBackIcon />}
onClick={goBack}
/>
</Box>
);
};
パスワードリセットでは、パスワード再設定のメールが送信される関数を実装します。
その関数がresetPasswordForEmail
です。
引数に入力したemail
とサインアップ機能の実装と同様にメールに記載されるURLのリンク(リダイレクト先)となるパスを引数に渡します。
今回の場合、redirectTo
で指定したパスがhttp://localhost:3000/reset_password/hogefuga
のようになります。
そのため、Supabase
側のRedirect URLs
の設定で、URLを追加する必要があります。
以下にRedirect URLs
のワイルドカードの詳細な記述があります。
上記を参照すると、http://localhost:3000/*
の場合は、http://localhost:3000/foo
やhttp://localhost:3000/bar
をメールに記載されるURLのリンクとして設定できます。
しかし、http://localhost:3000/foo/baz
はメールに記載されるURLのリンクとして設定できない事がわかります。
そのため新たに、http://localhost:3000/**
をSupabase
のProject > Authentication
> URL Configuration
から追加する必要があります。
この**
のURLドメインを追加することでhttp://localhost:3000/foo/baz
のようなURLもメールに記載されるURLのリンクとして設定することができます。
つまり、http://localhost:3000/reset_password/:token
のURLを設定できるということになります。
この機能のUIは、以下のようになります。
続いて、メールの件名は本文の設定をしていきます。
パスワード再設定用URLメール送信機能
まずSupabase
のProject > Authentication
> Email Templates
で画面遷移します。
以下のReset Password
タブからパスワード再設定用のメールの設定が行なえます。
その他にも、サインアップ時のメールの設定なども行えます。
`
.ConfirmationURL
にresetPasswordForEmail
のredirectTo
で指定したURLがリダイレクトリンクとして渡されます。
最後にパスワード再設定機能の実装を見ていきましょう。
パスワード再設定機能
以下のドキュメントを参照し、パスワードの再設定機能を実装します。
以下が、実際の実装です。
import { zodResolver } from "@hookform/resolvers/zod";
import DoneIcon from "@mui/icons-material/Done";
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
import { Box } from "@mui/material";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { AccountFormFooter } from "../../../../components/ui-elements/AccountFormFooter";
import AccountTextField from "../../../../components/ui-elements/AccountTextField";
import { supabase } from "../../../../utils/supabase";
import { resetPasswordFormInputSchema } from "../../types/ResetPasswordIFormInput";
import type { ResetPasswordFormInput } from "../../types/ResetPasswordIFormInput";
import type { SubmitHandler } from "react-hook-form";
export const ResetPasswordInputForm = () => {
const navigate = useNavigate();
const {
handleSubmit,
control,
formState: { isSubmitting, errors },
reset,
} = useForm<ResetPasswordFormInput>({
resolver: zodResolver(resetPasswordFormInputSchema),
defaultValues: { password: "", passwordConfirmation: "" },
});
const onSubmit: SubmitHandler<ResetPasswordFormInput> = async (data) => {
try {
const { password, passwordConfirmation } = data;
const { error } = await supabase.auth.updateUser({
password,
});
if (error) {
throw new Error(error.message);
}
navigate("/login", {
state: { referrer: "login" },
});
} catch (err) {
console.log(err);
} finally {
reset();
}
};
return (
<Box
component="form"
sx={{
display: "flex",
flexDirection: "column",
gap: "32px",
width: "100%",
}}
onSubmit={handleSubmit(onSubmit)}
>
<AccountTextField
id="password"
name="password"
control={control}
error={errors.password?.message}
type="password"
label="新規パスワード"
secondaryLabel="パスワードを入力..."
icon={<LockOutlinedIcon />}
disabled={isSubmitting}
/>
<AccountTextField
id="passwordConfirmation"
name="passwordConfirmation"
control={control}
error={errors.passwordConfirmation?.message}
type="password"
label="新規パスワード確認"
secondaryLabel="パスワードをもう一度入力..."
icon={<LockOutlinedIcon />}
disabled={isSubmitting}
/>
<AccountFormFooter
disabled={isSubmitting}
text="パスワードを設定"
icon={<DoneIcon />}
/>
</Box>
);
};
基本的には、これまでの実装と同じです。
updateUser
にオブジェクトで入力された新規のpassword
を渡すことでパスワードの再設定が完了です。
以下は、パスワードの再設定画面のUIです。
おわりに
ここまで読んでくださり、ありがとうございます!
Supabase
のAuthentication
の機能を使用すれば、認証周りは面倒な処理も簡単に実装できると痛感しました。
ぜひ実装してみてください。
参考文献
Discussion