Creating Anonymous sign-in flow with Firebase

Have you noticed that, these days, most services and apps we use require an account before starting the experience of actually using the app/service? It certainly can feel annoying sometimes, especially when there are many steps involved and the creation flow is not as straightforward designed, or refined. In reality, at first glance, we might not even need the user to have an account so early, but rather have some sort of identification of who’s using your application. However, we need to encourage more users to try our platform. So, how can we facilitate this while maintaining the security of the app?

An anonymous sign-in is actually a good option to let users try out your app first without having to compromise their info.

Firebase Authentication

Chances are, if you’re reading this, you might have heard or even used services from Firebase, which is a solid platform that can work in many platforms with high-quality and reliable features, such as authentication.

Firebase Authentication supports authentication with:

  • E-mail and password
  • Email link
  • Phone number
  • Federated identity: Google, Facebook, Github, Apple, and some others.
  • SAML

You can start using the auth module with a few lines of JavaScript:

(I’m omitting the whole SDK setup for brevity concerns so we can focus on just the functionality of the module instead.)

// firebase.js
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";

const firebaseConfig = {...};

export const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);

For the remaining examples in this post, I’ll keep using JavaScript, but, luckily, Firebase SDK supports a wide range of languages and platforms such as Android, iOS, Flutter, and even C++.

import { auth } from './firebase'

const email = ...
const password = ...

auth().signInWithEmailAndPassword(email, password)
  .then((userCredential) => {
    // User is now signed in
    const user = userCredential.user;
  })
  .catch((error) => {
    // Handle common like invalid credentials errors by the error.code
  });

The user returned from any of the authentication providers now becomes the current user of the Auth module.


import { auth } from './firebase'

const user = auth().currentUser;

if (user) {
  // User is signed in, see docs for a list of available properties
  // https://firebase.google.com/docs/reference/js/auth.user
} else {
  // No user is signed in.
}

Handling multi-authentication options

It turns out that e-mail and password might not be the preferable choice amongst some users, so, to attend more users, we need more sign-in methods. But the good news is Firebase has you covered!

We can enable a federated identity provider, for example, Github, on the Firebase console to sign in and retrieve the account info from the provider.

import { GithubAuthProvider, signInWithPopup } from "firebase/auth";
import { auth } from './firebase'

// our instance
const githubProvider = new GithubAuthProvider();

signInWithPopup(auth, githubProvider)
  .then((result) => {
    // This gives you a GitHub Access Token. You can use it to access the GitHub API.
    const credential = GithubAuthProvider.credentialFromResult(result);
    const token = credential.accessToken;

    // The signed-in user info.
    const user = result.user;
    const userData = getAdditionalUserInfo(result)
  }).catch((error) => {
    // Handle Errors here.
    const errorCode = error.code;
    const errorMessage = error.message;
    // The email of the user's account used.
    const email = error.customData.email;
    // The AuthCredential type that was used.
    const credential = GithubAuthProvider.credentialFromError(error);
    // ...
  });

You can check more authentication providers at the official docs.

Cool, now we can have two or more options to create or sign in users in our application!

But it doesn’t end here yet, let’s suppose our demand now is to have anonymous and regular users to auth through our application to sign-in or convert from different authentication providers.

Let’s set out our plan:

Now, given our new flow, we will start by defining the anonymous sign-in flow.

Anonymous sign-in

You can use the same auth module to create and use temporary anonymous accounts to authenticate with Firebase by using the signInAnonymously() method, which creates a new unique anonymous user for each call.

Reminder: You need to enable the Anonymous sign-in method in your project’s Firebase console first.

import { auth } from './firebase' 

auth()
  .signInAnonymously()
  .then(onSuccess)
  .catch(onError);

The internal ID of this guest user is created automatically on the Firebase server.

From this point forward, you can redirect the anonymous user to the intended home page or any non-sensitive part of your application.

Firebase auth dashboard list of anonymous users

Converting anonymous accounts to real users

Later on, at some point in your app, you need to decide when you’re going to prompt the users to upgrade their account. It is important to have this to prevent user’s sensitive data from being exposed and avoid stale data from anonymous users.

If an anonymous user decides to sign up for your app, you can link their sign-in credentials to the anonymous account so that they can continue to work with their protected data in future sessions.

We first start by getting the credentials for the required authentication providers:

E-mail

const emailCredential = auth.EmailAuthProvider.credential(
  email,
  password,
);

Google

const googleCredential = auth.GoogleAuthProvider.credential(
  idToken,
  accessToken,
);

Apple

const appleCredential = auth.AppleAuthProvider.credential(
  identityToken,
  nonce,
);

You can get the authentication details (e.g. access token and secrets) from the respective auth providers’ sign-in methods as we saw at the beginning of the post.

Then, we link the credentials of the current assigned anonymous user to upgrade this account.

import { auth } from './firebase'

const currentUser = auth().currentUser;
const credential = ...

if (currentUser?.isAnonymous) {
    try {
        await auth().currentUser?.linkWithCredential(credential);
        // "User successfully converted"

        **goToFeature()**
    } catch {
        // handle errors
    }
} else {
    try {
        await auth().signInWithCredential(credential);
    } catch {
        // handle errors
    }
}

Once this linking call succeeds, you can then redirect the newly converted user to the desired feature, trigger a welcome e-mail, or use just their data from Firebase.

Firebase auth dashboard converted user

Handling errors

What happens if we try to convert a user to an account that already exists? Has the verification code been expired? Is it pending e-mail verification?

Some things can go wrong while linking user’s credentials, and that means we need to take care of them as well:

import { auth } from './firebase'
import { FirebaseAuthTypes } from '@react-native-firebase/auth';

try {
    await auth().currentUser?.linkWithCredential(credential);
} catch (err: FirebaseAuthTypes.NativeFirebaseAuthError) {
    // If the user is already linked to another account, 
    // we request the user to logout and login with the correct account
  if (error?.code === 'auth/credential-already-in-use') {
    await auth().signOut();
    throw new Error('This account is already linked to another user');
  }
  if (error?.code === 'auth/invalid-verification-code') {
    throw new Error('Invalid verification code');
  }
  if (error?.code === 'auth/invalid-credential') {
      throw new Error('Invalid credentials');
  }
}

As of the above example, you can catch the errors of any of these authentication functions thrown by Firebase, identify and display a proper message to the end-user.

As always, you can check out all the possible auth error codes at the docs.

A couple gotchas

As you might’ve imagined now, if a full account is not required at the first step, your application will likely end up having users who didn’t finish their experience or will never come back to the app again, so you need to ensure a couple of things:

  • Limit certain areas of your app only for real users
    • Generally speaking, it is not ideal to have anonymous users accessing features that either rely on connections (social), profiles, or any sensitive information, as they are essentially unidentifiable, meaning it is not easy to recognize who you are interacting with.
  • Cleanup anonymous user data as they are not active regularly
    • It is not optimal to have data of users that are hard to identify and potentially inactive, so it is good to make sure not to keep anonymous data forever.
    • Tip: You can enable automatic clean-up on Firebase Identity Platform, this will automatically trigger a cleanup, given a certain period of time, of all user’s stale data.
  • Avoid targeting anonymous users for marketing campaigns or analytics reports
    • Now, this might not be directly related to the app experience, but since we’re letting new users experiment and tinker with our application without having to comprise, they might feel annoyed if suddenly they start receiving campaigns they weren’t signed up for. This is not to say you should never collect any data about their initial experience, let’s say a successful sign-in, but rather avoid notifying them unnecessarily.
  • Optional – Migrate existing accounts to Firebase auth
    • If you’re dealing with a mixture of a database of existing accounts and a Firebase instance with, both, new anonymous and upgraded users, it can be worth migrating the users that do their regular sign-in to also have a Firebase UID.

By taking care of these, we can significantly enhance the integrity of the app’s database. It allows us to clearly distinguish between regular users and anonymous users. It also helps us provide a more personalized experience for regular users, while still accommodating those who prefer to use our app anonymously.

Bonus – Listening for authentication events

Suppose we need to fetch the user’s data or display a warning message depending on whether the user has sign-in, logout, or linked credentials, Firebase allows us to listen to authentication events with onAuthStateChanged.

The onAuthStateChanged function observes changes in the current instantiated user state (auth().currentUser)

import { getAuth, onAuthStateChanged } from "firebase/auth";

const auth = getAuth();

onAuthStateChanged(auth, (user) => {
  if (user) {
    // User is signed in, see docs for a list of available properties
    // https://firebase.google.com/docs/reference/js/auth.user
    const uid = user.uid;
    const data = user.toJSON();
    if (user.emailVerified) // do something...
    if (user.isAnonymous) // do something...
  } else {
    // User is signed out
    // ...
  }
});

Wrap up

Long story short, Authentication is hard to get well done. However, thankfully enough, today, there are tools available that can handle the heavy lifting for us so we can focus on the flow and overall experience.

Firebase provides a method for creating an anonymous sign-in flow, enabling users to test an app without giving up their personal information. This involves using Firebase’s authentication module, which supports various sign-in methods such as email, phone number, and federated identity.

Anonymous accounts can be converted to regular accounts when users decide to sign up, linking their sign-in credentials to the anonymous account. It’s crucial to handle errors during this process and to clean up inactive anonymous user data.

Hope you had a good read, see you next time!

We want to work with you. Check out our "What We Do" section!