Closed7

next.js x vercel x supabaseで認証 (2023.11)

A KidA Kid

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 のサンプルページが表示されることを確認しておく。

A KidA Kid

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の鍵を作り直す。

A KidA Kid

いったんhosting環境にデプロイする。
next.jsでサーバ側も動かそうとすると、結局vercelが楽な選択になる。

A KidA Kid

Supabaseで新規プロジェクトを作成。
DBのパスワードを決めるくらいで、特につまるところなくあっさりできる。

今回はSupabaseはIDaaSとして使用する。
Supabaseダッシュボードでの設定。

  • サイドメニューから Authentication を選択
  • Providers で、とりあえず Email 認証だけ有効にしておく
    • デフォルトのままでよかった
  • URL Configuration で Site URL を入れておく
    • vercelのdomainをそのまま使ってるのでfoo.vercel.appのやつ
    • その下に Redirect URLs の指定欄があるが、指定方法がよくわからないのでとりあえず後回し
  • お好みで Email Templates でサインアップ等のメール文面をいじっておく
A KidA Kid

クライアント側の認証実装。

先に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..」のステータスになっているはず。

A KidA Kid

ついでにSupabaseのDatabase機能を使う。
ORマッパーもここで考えといたほうが後々楽なんだろうなーとか思いつつ、とりあえず素朴にCREATE TABLE。
Supabase上のコンソールから、SQL Editorで普通にCREATE TABLEクエリを実行できる。
基本的にPostgreSQLの文法そのままで問題なさそう。
あとはSupabase側で担保できるセキュリティとしてRow Level Securityを設定しておくのがよさそう。別途やる。

Schema Visualizer で自動でおしゃれなER図作ってくれるのは良い。

A KidA Kid

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" });
    }

    ...
}
このスクラップは5ヶ月前にクローズされました