Next.js + SupabaseでSMS認証を作成する方法
- 土門 大貴
- 記事制作日2024年3月6日
- 更新日2024年3月6日
- 0いいね!
前回はNext.jsとSupabaseを使って、認証付きチャットアプリを作成しました。
ここまでくれば、もう一つのアプリとして成り立つレベルです。
しかしながら、サービスとしてローンチするにはまだまだ問題があります。
人が集まれば集まる程、Botが投稿をし始めたり、違法なことを書き込んだりする人が出てきます。
そのようなユーザーへの対応は基本運営が行わなければなりませんが、手動で削除やBAN等を行っているのでは、貴重なリソースやコストがどんどん浪費されてしまいます。
そこで今回は、以前作成した認証機能のリポジトリに『SMS認証機能』を追加してみましょう。
これにより、Botの登録を防ぎ、責任ある書き込みを促すことが出来ます。
更にはユーザーが簡単・安全にログインできるという効果もあるため、非常に有用な機能です。
Supabase側の設定
①:Supabaseプロジェクトの作成
Supabaseにログイン(登録していなければアカウント登録から)して、
トップページの「New Project」を押します。
すると、下記の様な画面が表示されます。
適当なプロジェクト名とデータベースのパスワードを入れて新しいプロジェクトを作成しましょう。
※Regionはできれば自分の住んでいる地域の近くがいいです。私は日本にしました。
Next.js事前準備
①:認証機能のリポジトリをクローン
今回はこちらの記事で紹介している認証機能(Supabase Auth)を元に実装を進めたいので、
まずはこちらの記事内にあるgithubのリンク から`git clone`してリポジトリを持ってきてください。
②:起動確認
Supabase Authが正しく動くかをまず確認しましょう。
クローンしたプロジェクトの直下に`.env.local`を新規作成し、
内容を下記の通り入力します
# Update these with your Supabase details from your project settings > API# https://app.supabase.com/project/_/settings/apiNEXT_PUBLIC_SUPABASE_URL={ここにProject URLを入れる}NEXT_PUBLIC_SUPABASE_ANON_KEY={ここにProject API Anon Keyを入れる}
それぞれここに〜〜入れると書かれている部分に
Supabaseのダッシュボードの`Project Settings`の`API`から必要な情報をコピーします。
コピーが終わったら、
npm install
を実行した上で
npm run dev
を実行します。
下記の画面が表示されるか確認してください。
Sign Upで登録したメールアドレスでLoginを行い、Profileページが表示されたらOKです。
Twilio設定
※すでにTwilioに登録済みの方は対応不要です。
今回SMS認証機能を提供するために、CpaaSであるTwilioを利用します。
https://www.twilio.com/ja-jp
上記サイトにアクセスし、右上の`無料トライアルボタン`からアカウント登録しましょう。
登録してはじめに表示されるダッシュボード画面で`Get a trial phone number`をクリックし、無料で使える電話番号を取得します。
電話番号を取得すると、ダッシュボード画面の`Account Info`にも電話番号が追加されます。
こちらの情報をSupabaseのダッシュボードの`Authentication`→`Providers`→`Phone`に入れていきます。
Account SIDとAuth Tokenはそのまま同じ値を入れ、Message Service SIDは先程取得した電話番号を入力します。
これで保存すればSMS認証をするための下準備はOKです。
ここからNext.js側での実装に移ります。
Next.js側の実装
既存のサインインフォームに追加すると分かりづらくなるため、SMS認証用のフォームを別で作成します。
事前準備
Twilioに送る電話番号には国際コードが必要になりますが、
わざわざそのために国際コード用のセレクタを作成するのも面倒なのでライブラリを利用します。
https://www.npmjs.com/package/react-international-phone
npm i react-international-phone
components/modal/modalType.ts
分岐させるためにSMS認証用のTypeを追加します。
export enum ModalType { SignIn = 1, SignUp = 2, SMSAuth = 3}
components/modal/smsAuthForm.tsx
SMS認証向けのフォームです。
"use client"import { useState } from "react";import { createClientComponentClient } from "@supabase/auth-helpers-nextjs";import { PhoneInput } from 'react-international-phone';import 'react-international-phone/style.css';export default function SMSAuthForm() { const supabase = createClientComponentClient(); const [phoneNumber, setPhoneNumber] = useState(''); const [otp, setOtp] = useState(''); const handleSendOtp = async () => { try { const { error } = await supabase.auth.signInWithOtp({ phone: phoneNumber }); if (error) throw error; } catch (error: any) { console.error('Error sending OTP:', error.message); } }; return ( <> <form action="/auth/smsLogin" method="post" className="space-y-4"> <div> <PhoneInput name="phone" defaultCountry="jp" value={phoneNumber} onChange={(e) => setPhoneNumber(e)} /> <button className="bg-white hover:bg-gray-100 text-gray-800 font-semibold py-1 px-4 border border-gray-400 rounded shadow block mt-2" type="button" onClick={handleSendOtp}>Send OTP</button> </div> <div> <input className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" type="text" placeholder="Enter OTP" name="otp" value={otp} onChange={(e) => setOtp(e.target.value)} /> <button className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded px-4 py-1 text-center mt-2" type="submit">Verify OTP</button> </div> </form> </> );}
ここでサインイン処理を行います。
実行が成功すると設定した電話番号に対してワンタイムパスワードが送られます。
const handleSendOtp = async () => { try { const { error } = await supabase.auth.signInWithOtp({ phone: phoneNumber }); if (error) throw error; } catch (error: any) { console.error('Error sending OTP:', error.message); } };
app/auth/smsLogin/route.ts
smsAuthForm.tsxのフォームからPOSTされるとこちらに来てワンタイムパスワードによる認証が行われます。
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'import { cookies } from 'next/headers'import { NextResponse } from 'next/server'export async function POST(request: Request) { const requestUrl = new URL(request.url) const formData = await request.formData() const phone = String(formData.get('phone')) const otp = String(formData.get('otp')) const supabase = createRouteHandlerClient({ cookies }) const { data: { session }, error } = await supabase.auth.verifyOtp({ phone: phone, token: otp, type: "sms" }); if (error) { throw error } return NextResponse.redirect(requestUrl.origin + '/profile', { status: 301, })}
その他実装
components/modalCore.tsx
import { useState } from 'react';import { ModalType } from './modal/modalType';import SignUpForm from './modal/signupForm';import SignInForm from './modal/signinForm';import SMSAuthForm from './modal/smsAuthForm';interface Props { modalType: ModalType;}const ModalCore = ({ modalType }: Props) => { const [showModal, setShowModal] = useState(false); let title = ""; let headerButton = ""; let formElement = <p>フォームを読み込めませんでした。</p>; switch (modalType) { case ModalType.SignIn: title = "ログインフォーム"; headerButton = "Login"; formElement = <SignInForm showModal={setShowModal}></SignInForm>; break; case ModalType.SignUp: title = "ユーザ登録フォーム"; headerButton = "Sign Up"; formElement = <SignUpForm showModal={setShowModal}></SignUpForm>; break; case ModalType.SMSAuth: title = "SMS認証フォーム"; headerButton = "SMS認証"; formElement = <SMSAuthForm></SMSAuthForm>; } return ( <> <button className="text-gray-600 hover:text-blue-600" type="button" onClick={() => setShowModal(true)} > {headerButton} </button> {showModal ? ( <> <div className="overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 max-h-full bg-black-rgba"> <div className="m-auto relative p-4 w-full max-w-md max-h-full"> <div className="relative bg-white rounded-lg shadow"> <div className="flex items-center justify-between p-4 md:p-5 border-b rounded-t"> <h3 className="text-xl font-semibold text-gray-900"> {title} </h3> <button type="button" className="end-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center" data-modal-hide="authentication-modal" onClick={() => setShowModal(false)} > <svg className="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14" > <path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" /> </svg> <span className="sr-only">モーダルを閉じる</span> </button> </div> <div className="p-4 md:p-5">{formElement}</div> </div> </div> </div> </> ) : null} </> );};export default ModalCore;
追加されているのはこの部分です。
import SMSAuthForm from './modal/smsAuthForm';~~~~~~~switch (modalType) { ~~~~~~~~ case ModalType.SMSAuth: title = "SMS認証フォーム"; headerButton = "SMS認証"; formElement = <SMSAuthForm></SMSAuthForm>; }
components/navigation.tsx
'use client';import type { Session } from '@supabase/auth-helpers-nextjs';import Link from 'next/link';import { usePathname, useRouter } from 'next/navigation';import ModalCore from './modalCore';import { ModalType } from './modal/modalType';const Navigation = ({ session }: { session: Session | null }) => { const pathname = usePathname(); const router = useRouter(); if (session === null && pathname?.includes('/profile')) { router.push('/'); } return ( <header> <div className="flex items-center justify-between px-4 py-2 bg-white shadow-md"> <nav className="hidden md:flex space-x-4"> <div> <Link className="text-gray-600 hover:text-blue-600" href="/"> Home </Link> </div> {session ? ( <div> <Link className="text-gray-600 hover:text-blue-600" href="/profile" > Profile </Link> </div> ) : ( <> <div> <ModalCore modalType={ModalType.SignIn}></ModalCore> </div> <div> <ModalCore modalType={ModalType.SignUp}></ModalCore> </div> <div> <ModalCore modalType={ModalType.SMSAuth}></ModalCore> </div> </> )} </nav> </div> </header> )}export default Navigation
こちらはSignUpの下にSMS認証のモーダルを追加しています
<div> <ModalCore modalType={ModalType.SMSAuth}></ModalCore></div>
動作確認
※動かないときは...
Twilioのトライアルアカウントでは認証された電話番号しか利用することができません。
Twilioのダッシュボード→`Phone Numbers`→`Manage`→`Verified Caller IDs`に電話番号を追加しましょう。
※もし認証されているのにエラーが発生する場合は再度追加し直すと解消されることがあります。
ワンタイムパスワード発行
`SMS認証`ボタンを押し、上記認証フォームの電話番号を入力、その後`Send OTP`でワンタイムパスワードを発行します。
電話番号に送信されたワンタイムパスワード
発行されたワンタイムパスワードを入力して`Verify OTP`をクリックしてProfileページにアクセスできたら成功です。
ログインに成功すると、Supabaseのダッシュボード→`Authentication`に認証した電話番号が表示されます。
これでSMS認証の作成が確認できました!
その他参考資料など
今回のgithubはこちらになります。
またTodoONada株式会社では、チャットシステム以外にも、ストレージを利用した画像アプリやTodoアプリ等の作成方法についてご紹介しています。
ぜひこちらもご覧ください!
- Next.js + SupabaseでTodoアプリ作成 CRUDの基本を学ぼう
- Next.js + Supabaseでリアルタイムチャットを作ろう
- Next.js + SupabaseでStorageを利用した画像投稿アプリ作成
- Next.js + SupabaseでRLSを利用して安全なアプリを作ろう。
- Next.js + SupabaseでAuth + Storageのストレージサービスを作る方法
- Next.jsとSupabaseで認証機能ありのリアルタイムチャットを作成する。
- Next.jsとSupabaseで認証つきチャットアプリを作成する(SNS風UI)
お問合せ&各種リンク
お問合せ:GoogleForm
ホームページ:https://libproc.com
運営会社:TodoONada株式会社
Twitter:https://twitter.com/Todoonada_corp
Instagram:https://www.instagram.com/todoonada_corp/
Youtube:https://www.youtube.com/@todoonada_corp/
Tiktok:https://www.tiktok.com/@todoonada_corp
presented by
- この記事にいいね!する
この記事を書いた人
- 29いいね!
稼働ステータス
◎現在対応可能
- 土門 大貴
職種
エンジニア
システムエンジニア(SE)
希望時給単価
10,000円~30,000円
▼実績例 ・公共インフラ事業者様向け管理システム開発(Windows、Python、PostgreSQL) ・官公庁様向け地図情報アプリのインフラ開発(Windows、PostgreSQL) ・自治体様向けポイント管理サービスのAPI開発(Linux、PostgreSQL、JS、Python) ・大手製造業様向けクラウド環境開発支援(AWS全般、Terraform) ・公共事業様向け顔認証決算システム基盤開発(Windows、PostgreSQL、JS、Python) ・リース業様向け代理店向けWebAPI開発(AWS全般、GoLang、JS) ・通販サイトインフラ構築支援、要件定義~開発(AWS, ECCube) ・結婚相談所様向けオウンドメディア制作(WordPress、JS、ウェブディレクトションな)
スキル
Python
AWS
React
・・・(登録スキル数:6)
スキル
Python
AWS
React
・・・(登録スキル数:6)