next.js x vercel x supabaseで認証 (2023.11)
Next.jp アプリ新規作成。
$ npx create-next-app
TypeScript と ESLint しかオプション聞かれなくなってる。
インストールしてビルドしたら下記警告。
Your project has `@next/font` installed as a dependency, please use the built-in `next/font` instead. The `@next/font` package will be removed in Next.js 14. You can migrate by running `npx @next/codemod@latest built-in-next-font .`. Read more: https://nextjs.org/docs/messages/built-in-next-font
いわれるままに下記を実行。
$ npx @next/codemod@latest built-in-next-font .
npm run dev して http://localhost:3000 のサンプルページが表示されることを確認しておく。
sshでgithubにpushしようとしたら、SHA-1の鍵が使えなくなってた。
You're using an RSA key with SHA-1, which is no longer allowed.
Please use a newer client or a different key type.
Please see https://github.blog/2021-09-01-improving-git-protocol-security-github/ for more information.
PuTTY Key GeneratorでECDSAの鍵を作り直す。
いったんhosting環境にデプロイする。
next.jsでサーバ側も動かそうとすると、結局vercelが楽な選択になる。
Supabaseで新規プロジェクトを作成。
DBのパスワードを決めるくらいで、特につまるところなくあっさりできる。
今回はSupabaseはIDaaSとして使用する。
Supabaseダッシュボードでの設定。
- サイドメニューから Authentication を選択
- Providers で、とりあえず Email 認証だけ有効にしておく
- デフォルトのままでよかった
- URL Configuration で Site URL を入れておく
- vercelのdomainをそのまま使ってるので
foo.vercel.app
のやつ - その下に Redirect URLs の指定欄があるが、指定方法がよくわからないのでとりあえず後回し
- vercelのdomainをそのまま使ってるので
- お好みで Email Templates でサインアップ等のメール文面をいじっておく
クライアント側の認証実装。
先にSupabaseの管理画面で、Project Settings > API から「Project URL」と「Project API keys」のanon, public というやつを控えておく。
クライアントソース側はプロジェクトルートに .env.local
ファイルを作り、上記の内容を以下のとおり定義しておく。
クライアント側で使いたい変数なので NEXT_PUBLIC_
ではじめる必要あり。
NEXT_PUBLIC_SUPABASE_URL=https://xxxxxxxxxxxxxxxx.supabase.co
NEXT_PUBLIC_SUPABASE_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx(めちゃ長ID)
そもそもこれ隠す必要ある情報なのかというのはあるけど(Firebaseではベタ書きしちゃって良いという話だった気がする)一応環境変数にしておこう。
このファイルはローカルでの開発用。リポジトリにはコミットしないやつ。
デプロイ環境へは同じ名前の環境変数をvercel側管理画面で定義しておく。
続いて認証呼び出しの実装。npmからSupabaseのライブラリをインストール。
@supabase/supabase-js
というやつでよさそう。
現在の最新は v2.38.4。
$ npm i @supabase/supabase-js
アプリ起動後に1回だけ呼ばれるどこかで以下のようにクライアントを初期化する。
import { createClient } from "@supabase/supabase-js";
// undefinedじゃないことを確認しておきましょう
const key = process.env.NEXT_PUBLIC_SUPABASE_KEY;
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabase = createClient(url, key);
サインアップのためのメール送信要求は supabase.auth.signUp()
だな。
// email: string;
// password: string;
await supabase.auth.signUp({ email, password });
Supabaseの管理画面から Authentication > Users を開くと、ユーザーが追加されているのを確認できる。
Providers > Email の設定で Confirm email を有効にしている場合は、メールリンクをクリックするまで「Waiting for verification..」のステータスになっているはず。
ついでにSupabaseのDatabase機能を使う。
ORマッパーもここで考えといたほうが後々楽なんだろうなーとか思いつつ、とりあえず素朴にCREATE TABLE。
Supabase上のコンソールから、SQL Editorで普通にCREATE TABLEクエリを実行できる。
基本的にPostgreSQLの文法そのままで問題なさそう。
あとはSupabase側で担保できるセキュリティとしてRow Level Securityを設定しておくのがよさそう。別途やる。
Schema Visualizer で自動でおしゃれなER図作ってくれるのは良い。
SupabaseのDBを使うにしても、フロントエンド側から直接Supabaseにアクセスするのではなく、APIサーバを経由して読み書きするようにしておく。密結合になりすぎんように。
フロントエンド側での認証は直接SubabaseのAPIを使う。
import { createClient } from "@supabase/supabase-js";
// 初期化
const key = process.env.NEXT_PUBLIC_SUPABASE_KEY;
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabase = createClient(url, key);
// ログイン
// const email = "xxxx";
// const password = "xxxx";
const { data, error } = await supabase.auth.signInWithPassword({ email, password });
if (error != null) {
// エラー時処理
}
// ユーザー情報
const userId = data.session.user.id;
const accessToken = data.session.access_token;
ログイン後のユーザー情報は、signInメソッドのレスポンス以外に supabase.auth.onAuthStateChange
のハンドラでも取得することができる。
このtokenをhttpリクエストのAuthorizationヘッダにBearerトークンとしてのせてサーバに送ることにする。
↓適当な GET API 呼び出しのイメージ。
const res = await fetch("/api/foo/bar", {
method:"GET",
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`
},
});
if (res.status !== 200) {
// エラー処理
}
const responseBody = res.json();
APIサーバ側の実装。Next.js。
import { createClient } from "@supabase/supabase-js";
// クライアントの初期化は同じ(どっかでやっておく)
const key = process.env.NEXT_PUBLIC_SUPABASE_KEY;
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabase = createClient(url, key);
// APIのハンドラ
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
// token検証
const getUserIdFromToken = async (authorizationHeader: string) => {
if (authorizationHeader == null) return undefined;
if (!authorizationHeader.startsWith("Bearer ")) return undefined;
const token = authorizationHeader.replace("Bearer ", "");
const { data, error } = await supabase.auth.getUser(token);
// エラーの場合
if (error != null) return undefined;
return data.user.id;
}
const userId = await getUserIdFromToken(req.headers.authorization);
if (userId == null) {
// 認証エラーのレスポンス
res.status(401).json({ error: "Unauthorizard" });
}
...
}