@supabase/ssrを使ってローカル環境のSupabaseでクライアントサイドでTwitter(X)認証する
2023年10月下旬にauth-helpersが非推奨となってしまいました。
代わりに @supabase/ssr
の使用をするようにアナウンスされています。
現在公開されているドキュメントだけでも十分に動かせるぐらいの情報量はあるので、それに従いながら進めていきます。
※Supabaseは急速に進化しているため、この記事を鵜呑みにせずに必ず公式ドキュメントに目を通すことをおすすめします。
公式ドキュメント
▼認証まわり
0.事前準備
- Dockerをインストールしておくこと。
- Next.js(App Router)が動く状態にしておく
- Supabase CLIをインストールしておくこと。
- Twitter Developer Portalにサインインできる状態。(審査が無くなった?っぽいので気軽に使えるかと思います!)
npm install @supabase/ssr @supabase/supabase-js
0. 環境変数を設定しよう
ここはすごく単純なことなので下記ページをご覧ください。
1. Twitter Developer Portalで API Key と API Key Secretを取得しよう。
Twitter Developer Portalで API Key
と API Key Secret
を取得する必要があります。
Keys and tokens タブの「Consumer Keys」から生成できます。
生成された2つのAPIキーをメモしておきましょう。
2. Twitter(X)認証の設定をする。
Settingsタブの「User authentication settings」の「User authentication set up」のEditをクリックします。
【App Permissions】
上段のRead / Write権限はプロジェクトに応じて選択すれば良いですが、
必ず「Request email from users」を有効にしておきましょう。(Supabaseはメールアドレスが取得できないとサービスプロバイダ認証ができないため。)
【Type of App】
ご自身のアプリケーションの種類で選んでください。
Webサービスであれば、「Web App, Automated App or Bot」を選択すればOKです。
【App info】
項目名 | 値 |
---|---|
Callback URI RedirectURL |
http://localhost:54321/auth/v1/callback |
Website URL | 公開予定のドメインを入れておけばOK |
Terms of service | 公開予定のドメイン + /terms を入れておけばOK |
Privacy policy | 公開予定のドメイン + /privacy を入れておけばOK |
これでTwitter(X)での設定は完了です。
次にローカルで Supabase が動くようにしておきましょう。
3. ローカルでSupabaseを立ち上げる
どこでもいいので、Supabaseを入れるようのフォルダを作成してください。
作成したフォルダまで ターミナル等で移動しておきましょう。
仮に sample-supabase
というフォルダを作ったのであれば、 sample-supabase
フォルダの中に入っている状態で下記を進めてください。
Supabaseをインストールする
supabase init
このコマンドを実行するとSupabaseのインストールが開始します。
終了したら、
Docker Desktopを起動した後で、
supabase start
を実行してSupabaseを立ち上げましょう。
ブラウザで http://localhost:54323/
にアクセスしてみてください。
Supabaseのダッシュボードが開けば成功です!
4. SupabaseでTwitter(X)認証を有効化する
Supabase をインストールしたフォルダを開くと、supabaseディレクトリの中に config.toml
というファイルがあるかと思います。
このファイルの末尾に下記を追記してください。
[auth.external.twitter]
enabled = true
client_id = "【ここにAPI Key】"
secret = "【ここにAPI Key Secret】"
redirect_uri = "http://localhost:54321/auth/v1/callback"
記事冒頭の(1)で取得したTwitter(X)のAPIキーを入力しましょう。
仮にAPIキーが「hogehogehogehoge」だとしたら、下記のように書きます。
client_id = "hogehogehogehoge"
ダブルクォーテーションは必要なので注意してください。
この設定ファイルを反映させるには Supabase を再起動させなければなりません。
supabase stop
で一旦Supabaseを停止し、 supabase start
でもう一度起動させましょう。
これで Supabase側の準備も完了しました!
5. Next.js に記述をしてクライアントサイドでTwitter(X)認証できるようにする
認証時のコールバックAPIを用意しておきましょう。
app/auth/callback/route.ts
ファイルを作成してください。
import { NextResponse } from "next/server";
// The client you created from the Server-Side Auth instructions
import { createClient } from "@/utils/supabase/server";
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get("code");
// if "next" is in param, use it as the redirect URL
const next = searchParams.get("next") ?? "/";
if (code) {
const supabase = await createClient();
const { error } = await supabase.auth.exchangeCodeForSession(code);
if (!error) {
const forwardedHost = request.headers.get("x-forwarded-host"); // original origin before load balancer
const isLocalEnv = process.env.NODE_ENV === "development";
if (isLocalEnv) {
// we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host
return NextResponse.redirect(`${origin}${next}`);
} else if (forwardedHost) {
return NextResponse.redirect(`https://${forwardedHost}${next}`);
} else {
return NextResponse.redirect(`${origin}${next}`);
}
}
}
// return the user to an error page with instructions
return NextResponse.redirect(`${origin}/auth/auth-code-error`);
}
上記は完全コピペで大丈夫です。
※認証ミスると /auth/auth-code-error
にリダイレクトされるので、それ用のページは別途用意しておく必要があります。(認証成功パターンとしては使わないので、この記事では割愛します。)
最後に認証を実行開始するためのボタンをクライアントコンポーネントとして設置しておきましょう。
"use client";
import { createBrowserClient } from '@supabase/ssr'
export const ButtonSignup = () => {
const supabase = createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
const handleSignUp = async () => {
// signInWithOAuthという名前だが、新規会員登録もログインも全く同じ関数を使う
// 新規なのかログインなのかはSupabaseがうまいことやってくれる。
await supabase.auth.signInWithOAuth({
provider: "twitter",
options: {
redirectTo: "http://localhost:3000/auth/callback/",
},
});
};
return (
<button onClick={handleSignUp}>
<span>新規登録</span>
</button>
);
};
<ButtonSignup />
をどこかに出力すれば完成です!
6. 認証を実行し、登録できているか確認して終わろう!
それでは実際にボタンを押して認証してみましょう。
Twitter認証画面が開いた後、 localhost:3000/
に戻ってくるはずです。
戻ってきたら、 http://localhost:54323/project/default/auth/users
にアクセスしましょう。
ここにユーザーが追加されていれば成功です!
お疲れ様でした!
データ取得とかどうするのか?
'@supabase/ssr' を使ったデータ取得をする際は、
クライアントサイドでリクエストしたい場合は createBrowserClient
。
サーバーサイドでリクエストしたい場合は createServerClient
を使うことになります。
引数には環境変数で設定したSupabase URLやAnon Keyを入れたり、cookieの中身を渡すなどすることで使えるようになります。詳しくは下記の公式ドキュメントをご覧ください。
おまけ【個人的に詰まったところ紹介】
hosts で localhost-sample.com:3000
として試していました。
コイツが原因で認証が成功しないというミスを犯していました。
hosts使うと辛いことになるので大人しく localhost でやっておいた方が楽です...!!!
おまけ2【Supabase CLIのデータベース周り】
コマンド適当にやってるとえらい目喰らうので先にある程度触ってDB壊したり、PC再起動するとどういう挙動になるかとか確認しておくと吉です!
おまけ3【クライアントサイドでSELECTでデータ取得することの是非を考える】
クライアントサイドでデータ取得する場合、 supabase.from('hoge').select(``)
でデータ取得するとヤバそうな予感がしたので上記の記事書きました。まだ正解が見えていませんが、現時点ではポリシーでSELECTのリクエストを FALSE にしておき、 .rpc()
で関数呼び出して制御できる範囲でデータ取得するべきかなと落ち着いています。
この件については皆さんのご意見も聞いてみたいところです。
おまけ4 ログイン状態のときに /login と /signup のアクセスを禁止する
import { type NextRequest } from "next/server";
import { updateSession } from "@/utils/supabase/middleware";
export async function middleware(request: NextRequest) {
return await updateSession(request);
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* Feel free to modify this pattern to include more paths.
*/
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
],
};
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
export async function updateSession(request: NextRequest) {
let supabaseResponse = NextResponse.next({
request,
});
const supabase = createServerClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, {
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value));
supabaseResponse = NextResponse.next({
request,
});
cookiesToSet.forEach(({ name, value, options }) => supabaseResponse.cookies.set(name, value, options));
},
},
});
const {
data: { user },
} = await supabase.auth.getUser();
if (user && (request.nextUrl.pathname.startsWith("/login") || request.nextUrl.pathname.startsWith("/signup"))) {
const url = request.nextUrl.clone();
url.pathname = "/";
return NextResponse.redirect(url);
}
return supabaseResponse;
}
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export async function createClient() {
const cookieStore = await cookies();
return createServerClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, {
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options));
} catch {
// The `setAll` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
});
}
処理の主な箇所は下記です。
const {
data: { user },
} = await supabase.auth.getUser();
if (user && (request.nextUrl.pathname.startsWith("/login") || request.nextUrl.pathname.startsWith("/signup"))) {
const url = request.nextUrl.clone();
url.pathname = "/";
return NextResponse.redirect(url);
}
この条件によってアクセス禁止(リダイレクト)しています。
Discussion