Next.js + Capacitorによるクロスプラットフォームアプリの紹介 – ④Auth0によるパスワードレスログインの実装

React

はじめに

  • Shopifyストアのカスタマーがログインできる、Next.jsのクライアントアプリを提供することにした。
  • SSOを実現したかったが、Shopify Plus契約が予算的に難しいため、Auth0によるパスワードレスログインを実装することにした。

Auth0 の設定

  • Auth0 ダッシュボードにログインし、パスワードレスログインを有効にします。
  • Authentication > Passwordless に移動し、Email オプションを有効にします。

ソース

passwordless-login-page

メールアドレスを入力し、Auth0へ送信するフォームを持つページ

'use client';

import { useState } from 'react';

import Image from 'next/image'
import Link from 'next/link'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import AppleIcon from '@/assets/icons/apple.svg'
import GoogleIcon from '@/assets/icons/google.svg'
import LineIcon from '@/assets/icons/line.svg'
import LockIcon from '@/assets/icons/lock.svg'
import ProfileIcon from '@/assets/icons/profile.svg'
import { FormattedMessage, useIntl } from 'react-intl'

export default function PasswordlessLoginPage() {
  const [email, setEmail] = useState('');
  const [message, setMessage] = useState('');

  const handleLogin = async (e: React.FormEvent) => {
    e.preventDefault();

    const res = await fetch('/passwordless-login', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ email }),
    });

    if (res.ok) {
      setMessage('ログインリンクがメールに送信されました!');
    } else {
      setMessage('エラーが発生しました。もう一度お試しください。');
    }
  };

  return (
    <>
        <form className="mt-4">
            <div className="flex flex-col gap-6">
                <Input
                type="email"
                placeholder="Your email"
                icon={<ProfileIcon width={14} />}
                value={email}
                onChange={(e) => setEmail(e.target.value)}      
                />
            </div>

            <div className="mt-10 flex justify-center">
                <Button
                size="lg"
                type="button"
                className="w-48"
                data-testid="login-button"
                onClick={handleLogin}
                >
                <FormattedMessage id="general.button_login" />
                </Button>
            </div>
        </form>
            {message && <p>{message}</p>}
    </>

  );
}

passwordless-login

フォームからメールアドレスを受取、Auth0のAPIへ送信する処理

import { NextResponse } from 'next/server';
import { NextRequest } from 'next/server';

// 動的生成を強制
export const dynamic = 'force-dynamic';

export async function POST(req: NextRequest) {
  const { email } = await req.json();

  try {
    const response = await fetch(`https://${process.env.NEXT_PUBLIC_AUTH0_DOMAIN}/passwordless/start`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        client_id: process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID,
        connection: 'email',
        send: 'link',
        email,
      }),
    });

    if (response.ok) {
      return NextResponse.json({ message: 'ログインリンクを送信しました' });
    } else {
      return NextResponse.json({ message: 'エラーが発生しました' }, { status: response.status });
    }
  } catch (error) {
    return NextResponse.json({ message: 'サーバーエラーが発生しました' }, { status: 500 });
  }
}

passwordless-login-validate

Auth0からのコールバック

'use client'

import { useRouter } from 'next/navigation'
import { useEffect } from 'react'

import { useAccessToken } from '@/lib/use-access-token'
import { useUserInfo } from '@/lib/use-user-info'

export default function ValidateLogin() {
  const router = useRouter()
  const [, setAccessToken] = useAccessToken()

  useEffect(() => {
    const hashParams = new URLSearchParams(window.location.hash.substring(1));
    const accessToken = hashParams.get('access_token');

    if (accessToken) {
      setAccessToken(accessToken)
      console.log("Access Token: ", accessToken);
      router.replace('/home')
    } else {
      router.replace('/top-not-login')
    }
  }, [router, setAccessToken]); // Add dependencies for useEffect

  return null;
}

成功時の動作

  • フォームにメールアドレスを入力し、送信する
  • Auth0からのメールを受信する
  • メール上のボタンをクリックする
  • アプリへのアクセスを認可する
  • /homeへリダイレクトされる

関連記事

カテゴリー

アーカイブ

Lang »