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:
- Redis
- Service to send an SMS (e.g. Twilio)
- TextEncryptor
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.