We’ve all seen it at least once before: that little extra layer of security when we create an account on Gmail, Netflix, and possibly on every login into our online bank accounts. A verification code is sent to our device which we have to enter to get past that glorious second layer.
If you want this in your app and you’re having trouble finding out how, look no further. I’m here to tell you it’s piss easy and you can get it set up in a minute… well maybe a bit more than that, but not much more.
This variant of the verification code uses Firebase Authentication, a service from Firebase which lets you authenticate through different methods and providers, one of which is Phone Verification.
If you’ve never encountered Firebase before, don’t you worry fam. This tutorial is designed for the dummy who’s never encountered it…. no offense.
Add Firebase Authentication to your App
I explain this in slightly more depth here, but I’ll list a quick way to do this here that’ll barely take a minute. If you’re not yet connected to Firebase:
- Open the Firebase Assistant by going to Tools > Firebase
- Click on the Analytics Menu
- Click Connect to Firebase and choose to either create a new Firebase project or connect to an existing one.
- Click the button below the previous one to add the Firebase Core dependencies to your build.gradle files.
- Go back to the Firebase Assistant home, go to Authentication and add the Authentication dependencies from there.
Or if you’re already connected to Firebase, you can just add this dependency to your app/build.gradle file:
implementation 'com.google.firebase:firebase-auth:16.1.0'
Now you’re good to go (ง ͠° ͟ل͜ ͡°)ง
A few things to note
You can use Firebase Authentication for a complete authentication solution, but you’ll want to pair up phone verification with any of its other sign-in methods (Recommended: the standard Email/Password) to keep your app well secure.
Otherwise, if you just want to use Firebase Auth for the phone verification, you can let the user sign-in through this way then pass the Firebase Authentication Token to your own Authentication Provider.
Enable Phone Verification on the Console
Head to the Firebase Console, open up your project there and go into Authentication (on the sidebar). Under Sign-In Method, enable ‘Phone’.
Initialise Firebase Auth
val auth = FirebaseAuth.getInstance()
Dump this as a global variable in your activity.
Send Verification Code to User’s Phone
Create an interface for your user to submit their phone number. For legal reasons or just best practice standards, inform your users if they use phone verification that they’ll be receiving an SMS for it and that standard rates apply.
Then pass in their phone number to PhoneAuthProvider.verifyPhoneNumber with a suitable timeout for your app.
PhoneAuthProvider.getInstance().verifyPhoneNumber( phoneNumber, // Phone number to verify 60, // Timeout duration TimeUnit.SECONDS, // Unit of timeout this, // Activity (for callback binding) callbacks) // OnVerificationStateChangedCallbacks
*Calling verifyPhoneNumber multiple times won’t trigger it while a request still hasn’t timed out.
Receiving Verified Code and Creating a Credential
Notice in the verifyPhoneNumber method, you pass in a callbacks object.
callbacks = object: PhoneAuthProvider.OnVerificationStateChangedCallbacks() {...}
This has 3 important methods you’ll be overriding:
onCodeSent
override fun onCodeSent(verificationId: String?, token: PhoneAuthProvider.ForceResendingToken) { // The SMS verification code has been sent to the provided phone number, we // now need to ask the user to enter the code and then construct a credential // by combining the code with a verification ID. Log.d(TAG, "onCodeSent:" + verificationId!!) // Save verification ID and resending token so we can use them later storedVerificationId = verificationId resendToken = token // ... }
When the code is sent, you’ll get a verificationId.
val credential = PhoneAuthProvider.getCredential(verificationId!!, code)
You’ll be able to combine this and the verification code sent to the user’s phone to create a credential which you can use to authenticate the user.
onVerificationCompleted
override fun onVerificationCompleted(credential: PhoneAuthCredential) { // This callback will be invoked in two situations: // 1 - Instant verification. In some cases the phone number can be instantly // verified without needing to send or enter a verification code. // 2 - Auto-retrieval. On some devices Google Play services can automatically // detect the incoming verification SMS and perform verification without // user action. Log.d(TAG, "onVerificationCompleted:$credential") signInWithPhoneAuthCredential(credential) }
In the two situations listed above, verification will happen in the background (so your user doesn’t need to manually enter the verification code) and you get a credential straight like that. Wow.
onVerificationFailed
override fun onVerificationFailed(e: FirebaseException) { // This callback is invoked in an invalid request for verification is made, // for instance if the the phone number format is not valid. Log.w(TAG, "onVerificationFailed", e) if (e is FirebaseAuthInvalidCredentialsException) { // Invalid request // ... } else if (e is FirebaseTooManyRequestsException) { // The SMS quota for the project has been exceeded // ... } // Show a message and update the UI // ... }
Because ? happens.
Signing in with the Credential
auth.signInWithCredential(credential) .addOnCompleteListener(this) { task -> if (task.isSuccessful) { // Sign in success, update UI with the signed-in user's information Log.d(TAG, "signInWithCredential:success") val user = task.result?.user // ... } else { // Sign in failed, display a message and update the UI Log.w(TAG, "signInWithCredential:failure", task.exception) if (task.exception is FirebaseAuthInvalidCredentialsException) { // The verification code entered was invalid } } }
After running this method, your user might just be signed in. Hooray! Hold your horses though, now we’re going to combine this with other Authentication methods to meet our security standards (Firebase or not).
Linking Phone Verification with other Sign-In Methods
Option 1: Firebase Email/Password Auth
By no regular standards should 2-factor authentication be possible with Firebase Authentication, but we’re going to need some help from the Database (or Firestore if that’s your thing).
Normally, signing-in through different methods gives the user different user IDs or “accounts” should I say. To optimize things, Firebase lets you link multiple providers to authenticate to the same account, and that’s done through the simple method:
auth.currentUser.linkWithCredential(credential)
Doing so links together the credentials from the different providers. Now when the user adds his phone number, we link it to the current account and add it to the database.
private fun enableTwoFactorAuthentication() { auth.currentUser!!.linkWithCredential(phoneCredential) .addOnSuccessListener { result -> val userRef = database.child("users/${result.user}") userRef.child("twoFactorEnabled").setValue(true) userRef.child("phoneNumber").setValue(phoneNumber) } }
Now when the user signs in, we check the database if two-factor is enabled, then we immediately sign out and send the phone verification.
auth.signInWithEmailAndPassword(email, password).addOnSuccessListener { database.child("users/${it.user.uid}").addListenerForSingleValueEvent(object: ValueEventListener { override fun onDataChange(snapshot: DataSnapshot) { val twoFactorEnabled = snapshot.child("twoFactorEnabled").value as Boolean val phoneNumber = snapshot.child("twoFactorEnabled").value as String if (twoFactorEnabled) { auth.signOut() sendPhoneVerification(phoneNumber) } } }) }
And voila! A two-factor authentication solution.
Now keep in mind this isn’t a perfect solution. It isn’t two-factor authentication in its purest form as it’s performed through a series of UI gimmicks. It does still do the trick though. It should increase your app’s security a fair bit and it’s so much quicker to implement than “true” two-factor solutions out there.
Option 2: Your own Authentication Server.
The best way to do things here is to get the Authentication Token of your authenticated user (this string is unique to each user and a new one can be generated upon request, like a hash fingerprint) and pass it to your own servers to identify the user authenticated on Firebase.