Phone authentication with Supabase

August 4, 2021
views 6 min read

This post is complementary of Authentication with NextJS and Supabase.

We'll be using the same repository, implementing SMS OTP, make sure to use the same code.

What we'll cover?

  • Creating and setting up a Twilio account
  • Setup Supabase with Twilio credentials
  • Implement login with phone number

Twilio

Navigate to Twilio and create an account. Once you're done setting up everything you'll need to request a trial phone, scroll down and click on Get a trial phone number:

If should see a Your new Phone Number is +13345∙∙∙∙∙∙ message. Great.

You should now see the credentials we'll be using over at Supabase:


Supabase

Access your Supabase account, navigate to Settings → Auth settings, under Phone Auth toggle Enable Phone Signup.

Fill in the details with Twilio credentials accordingly. Click on Save.


Our application

We'll now change our application to add the ability to sign up either with email or with a phone number.

Open pages/auth.tsx, file should look like this:

import { useState } from 'react'
import { useRouter } from 'next/router'

import supabase from '../lib/supabase'

const Auth: React.FC = () => {
  const [email, setEmail] = useState<string>()
  const { push } = useRouter()

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

    const { error } = await supabase.auth.signIn({ email })

    if (!error) push('/')
  }

  return (
    <div className="border rounded-lg p-12 w-4/12 mx-auto my-48">
      <h3 className="font-extrabold text-3xl">Ahoy!</h3>

      <p className="text-gray-500 text-sm mt-4">
        Fill in your email, we'll send you a magic link.
      </p>

      <form onSubmit={handleSubmit}>
        <input
          type="email"
          placeholder="Your email address"
          className="border w-full p-3 rounded-lg mt-4 focus:border-indigo-500"
          onChange={e => setEmail(e.target.value)}
        />

        <button
          type="submit"
          className="bg-indigo-500 text-white w-full p-3 rounded-lg mt-8 hover:bg-indigo-700"
        >
          Let's go!
        </button>
      </form>
    </div>
  )
}

export default Auth

First, we'll need to add 2 new states, one to save the phone number value and another one to toggle between email and phone number.

import { useState } from 'react'
import { useRouter } from 'next/router'

import supabase from '../lib/supabase'

const Auth: React.FC = () => {
  const [email, setEmail] = useState<string>()
  const [phone, setPhone] = useState<string>()
  const [signupWithPhone, setSignupWithPhone] = useState(false)
  const { push } = useRouter()

  // ...
}

export default Auth

Great, now we'll need to add a new input for the phone number:

<form onSubmit={handleSubmit}>
  <input
    type="text"
    placeholder="Your phone number"
    className="border w-full p-3 rounded-lg mt-4 focus:border-indigo-500"
    onChange={e => setPhone(e.target.value)}
  />

  <input
    type="email"
    placeholder="Your email address"
    className="border w-full p-3 rounded-lg mt-4 focus:border-indigo-500"
    onChange={e => setEmail(e.target.value)}
  />

  <button
    type="submit"
    className="bg-indigo-500 text-white w-full p-3 rounded-lg mt-8 hover:bg-indigo-700"
  >
    Let's go!
  </button>
</form>

At this point both inputs are showing up, we need to show depending on the value of signupWithPhone, let's take care of that:

<form onSubmit={handleSubmit}>
  {signupWithPhone ? (
    <input
      type="text"
      placeholder="Your phone number"
      className="border w-full p-3 rounded-lg mt-4 focus:border-indigo-500"
      onChange={e => setPhone(e.target.value)}
    />
  ) : (
    <input
      type="email"
      placeholder="Your email address"
      className="border w-full p-3 rounded-lg mt-4 focus:border-indigo-500"
      onChange={e => setEmail(e.target.value)}
    />
  )}

  <button
    type="submit"
    className="bg-indigo-500 text-white w-full p-3 rounded-lg mt-8 hover:bg-indigo-700"
  >
    Let's go!
  </button>
</form>

Awesome, now we just need to create a button to switch between the 2 inputs:

<form onSubmit={handleSubmit}>
  {/* ... */}

  <div className="my-4">
    <button
      className="text-sm text-gray-500 hover:underline"
      onClick={() => setSignupWithPhone(!signupWithPhone)}
    >
      Use {signupWithPhone ? 'email' : 'phone'} instead
    </button>
  </div>

  <button
    type="submit"
    className="bg-indigo-500 text-white w-full p-3 rounded-lg mt-8 hover:bg-indigo-700"
  >
    Let's go!
  </button>
</form>

Alright, what are we doing here? First every time we click on the button it'll set the value of signupWithPhone to the opposite value, what does this means?

The way booleans work with this technique:

let signupWithPhone = true;

!signupWithPhone; // false
!signupWithPhone; // true
!signupWithPhone; // false
!signupWithPhone; // true

We could do this all day! Secondly, we're simply displaying a different context depending if signupWithPhone is true or false.

Awesome, we'll need to change a little bit our handleSubmit function:

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

  const { error } = await supabase.auth.signIn({
    ...signupWithPhone ? { phone } : { email },
  })

  if (!error) push('/')
}

I love this trick, since Supabase uses the same signIn() for both email and phone we're simply checking if the signup is being made through phone, otherwise we're sending the value of email, voilà.


OTP Code

Right, now we're getting the code via SMS, we need a way of verifying that the code is valid, we're currently redirecting the user to the root of the project. Let's change that.

For the sake of keeping our code clean, we'll create a new component:

touch pages/verify-otp.tsx

Inside that file we'll create a simple component:

const VerifyOTP: React.FC = () => {
  return (
    <>
      We'll verify OTP here!
    </>
  )
}

export default VerifyOTP

Great, now we need to make a tiny change on our pages/auth.tsx component:

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

  const { error } = await supabase.auth.signIn({
    ...signupWithPhone ? { phone } : { email },
  })

  if (!error) push(signupWithPhone ? '/verify-otp' : '/')
}

Now when authenticating with phone we'll redirect the user to verify the OTP code.

Back to VerifyOTP, first we'll create the input where the user can type their code:

const VerifyOTP: React.FC = () => {
  return (
    <div className="border rounded-lg p-12 w-4/12 mx-auto my-48">
      <h3 className="font-extrabold text-3xl">Verify OTP</h3>

      <p className="text-gray-500 text-sm mt-4">
        You should've received an SMS with a code.
      </p>

      <form>
        <input
          type="token"
          placeholder="Your OTP code"
          className="border w-full p-3 rounded-lg mt-4 focus:border-indigo-500"
        />

        <button
          type="submit"
          className="bg-indigo-500 text-white w-full p-3 rounded-lg mt-8 hover:bg-indigo-700"
        >
          Verify
        </button>
      </form>
    </div>
  )
}

export default VerifyOTP

Awesome, we now to save the value of our input as well as handling the form submit:

import { useState } from 'react'
import { useRouter } from 'next/router'

import supabase from '../lib/supabase'

const VerifyOTP: React.FC = () => {
  const [token, setToken] = useState<string>('')
  const phone = 'YOUR_PHONE_NUMBER'
  const { push } = useRouter()

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

    const { session, error } = await supabase.auth.verifyOTP({ phone, token })

    console.log({ session })

    if (!error) push('/')
  }

  return (
    <div className="border rounded-lg p-12 w-4/12 mx-auto my-48">
      <h3 className="font-extrabold text-3xl">Verify OTP</h3>

      <p className="text-gray-500 text-sm mt-4">
        You should've received an SMS with a code.
      </p>

      <form onSubmit={handleSubmit}>
        <input
          type="token"
          placeholder="Your OTP code"
          className="border w-full p-3 rounded-lg mt-4 focus:border-indigo-500"
          onChange={e => setToken(e.target.value)}
        />

        <button
          type="submit"
          className="bg-indigo-500 text-white w-full p-3 rounded-lg mt-8 hover:bg-indigo-700"
        >
          Verify
        </button>
      </form>
    </div>
  )
}

export default VerifyOTP

You might notice that I'm setting the phone number as a raw value, there are several ways this can be handled, I don't want to go through any because it's not the point of this article, but you could save the value of the phone number with localStorage, or have all this logic in one single component.

Alright, hopefully everything will work just fine. Go through the whole flow and you should get a session object back from Supabase.

Read next
Tech Twitter is broken
April 7, 2021
views