Phone Authentication in Ruby

Using SMS to authenticate a user has the following benefits:

  • Everybody has a phone.
  • Users don't have to remember passwords.
  • Protect against robots & duplicate accounts.
  • Familiarity.

You should not use SMS a sole authentication method for systems that require high-security.

What I am going to cover in this article is how to make an API that generates "phone tokens" that can be used to sign-in / sign-up. This way you don't need to create a user record before a phone is verified.

User stories

Below is 2 user stories we are going to cover.

As a returning user.
I want to sign-in.
I go to the login screen and enter my phone.
I receive an SMS with the verification code, enter the code.
I am logged in.


As a new user.
I want to sign-up.
I go to the login screen and enter my phone.
I receive an SMS with the verification code, enter the code.
I enter my name and email and hit submit.
I am registered and logged in.



To make the 2nd story (sign up) work, upon phone verification we want to return a secure phone token that, together with name and email can be exchanged to create a new account.

Phone token is going to be an encoded phone string signed by our secret key as proof that it originated from our system.

Pseudo-code API (Controller Code)

An important bit is that both sign-in and sign-up flows start with entering a phone number and getting a phone token. Your app can then check if a user with such phone already exists, if it does, then simply login, otherwise proceed to sign up.


# ask user for a phone and send an SMS code
POST /send-phone-verification { phone: '+19178456780' }
PhoneVerification.send_verification(phone: params[:phone])
=> {}


# verify the phone entering the code from SMS,
# get encoded phoneToken in return
POST /verify-phone { phone: '+19178456780', code: '123456' }
phone_token = PhoneVerification.code_to_phone_token(
  phone: params[:phone],
  code: params[:code]
)
=> { phoneToken: phone_token }


# exchange the phoneToken to a user information
POST /sign-in { phoneToken: 'xxxxxxxxxxxxx' }
trusted_phone = PhoneVerification.phone_token_to_phone(params[:phone_token])
user = User.find_by_phone(trusted_phone)
=> { user: user }

# OR

# use the phone token to create a new user with verified phone
POST /sign-up { phoneToken: 'xxxxxxxxxxxxx', name: 'John Doe', email: 'john@example.com' }
trusted_phone = PhoneVerification.phone_token_to_phone(params[:phone_token])
user = User.find_or_create_by!(phone: trusted_phone) do |u|
  u.name = params[:name]
  u.email = params[:email]
end
=> { user: user }

Implementation

In order to implement this we are going to need the following:

Without further ado, let's jump into code

You can read the full code at https://github.com/TheRusskiy/rails-phone-auth.
Thank you for reading!

p.s. I recommend against using SMS as a sole means of authentication for security-critical apps such as banking, admin dashboards and other systems where gaining access to a single account can give a lot of value, only use it in conjunction with passwords. A dedicated attacker can spoof SMS of a specific user.

Popular posts from this blog

HTTP server in Ruby 3 - Fibers & Ractors

Next.js: restrict pages to authenticated users

Migration locks for TypeORM