56

I want users to be able to authenticate to my Firebase application using multiple different auth providers, such as Facebook, Twitter, or Github. Once authenticated, I want users to have access to the same account no matter which auth method they used.

In other words, I want to merge multiple auth methods into a single account within my app. How can I do this in a Firebase app?

Andrew Lee
  • 10,127
  • 3
  • 46
  • 40

4 Answers4

56

Update (20160521): Firebase just released a major update to its Firebase Authentication product, which now allows a single user to link accounts from the various supported providers. To find out more about this feature, read the documentation for iOS, Web and Android. The answer below is left for historical reasons.


The core Firebase service provides several methods for authentication: https://www.firebase.com/docs/security/authentication.html

At its core, Firebase uses secure JWT tokens for authentication. Anything that results in the production of a JWT token (such as using a JWT library on your own server) will work to authenticate your users to Firebase, so you have complete control over the authentication process.

Firebase provides a service called Firebase Simple Login that is one way to generate these tokens (this provides our Facebook, Twitter, etc auth). It's intended for common auth scenarios so that you can get up and running quickly with no server, but it is not the only way to authenticate, and isn't intended to be a comprehensive solution. 

Here's one approach for allowing login with multiple providers using Firebase Simple Login:

  1. Store one canonical user identifier for each user, and a mapping for each provider-specific identifier to that one canonical id.
  2. Update your security rules to match any of the credentials on a given user account, instead of just one.

In practice, the security rules might look like this, assuming you want to enable both Twitter and Facebook authentication (or allow a user to create an account with one and then later add the other):

{
  "users": {
    "$userid": {
      // Require the user to be logged in, and make sure their current credentials
      // match at least one of the credentials listed below, unless we're creating
      // a new account from scratch.
      ".write": "auth != null && 
        (data.val() === null || 
        (auth.provider === 'facebook' && auth.id === data.child('facebook/id').val() || 
        (auth.provider === 'twitter' && auth.id === data.child('twitter/id').val()))"
    }
  },
  "user-mappings": {
    // Only allow users to read the user id mapping for their own account.
    "facebook": {
      "$fbuid": {
        ".read": "auth != null && auth.provider === 'facebook' && auth.id === $fbuid",
        ".write": "auth != null && 
          (data.val() == null || 
          root.child('users').child(data.val()).child('facebook-id').val() == auth.id)"
      }
    },
    "twitter": {
      "$twuid": {
        ".read": "auth != null && auth.provider === 'twitter' && auth.id === $twuid",
        ".write": "auth != null && 
          (data.val() == null || 
          root.child('users').child(data.val()).child('twitter-id').val() == auth.id)"
      }
    }
  }
}

In this example, you store one global user id (which can be anything of your choosing) and maintain mapping between Facebook, Twitter, etc. authentication mechanisms to the primary user record. Upon login for each user, you'll fetch the primary user record from the user-mappings, and use that id as the primary store of user data and actions. The above also restricts and validates the data in user-mappings so that it can only be written to by the proper user who already has the same Facebook, Twitter, etc. user id under /users/$userid/(facebook-id|twitter-id|etc-id).

This method will let you get up and running quickly. However, if you have a complicated use case and want complete control over the auth experience, you can run your own auth code on your own servers. There are many helpful open source libraries you can use to do this, such as everyauth and passport.

You can also authenticate using 3rd party auth providers. For example, you can use Singly, which has a huge variety of integrations out-of-the-box without you needing to write any server-side code.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
Andrew Lee
  • 10,127
  • 3
  • 46
  • 40
  • 1
    I have a few questions for you about this strategy found here: http://stackoverflow.com/questions/22886053/can-the-firebase-auth-object-handle-simultaneous-authentication-types – Dan Kanze Apr 05 '14 at 19:58
  • I can't see the difference between `.child('twitter/id').val()` and `.child('twitter-id').val()`. Are both different references? I didn't catch it. – Jobsamuel May 15 '14 at 22:48
  • Thanks a lot for the mapping example. Would it be possible to follow up with some code on how to do this on iOS? – Holger Sindbaek Mar 06 '15 at 00:16
  • I've used a very similar system on a production app for a while now. My data structure is just a bit different. Under user-mappings I just have a list of UIDs. I haven't found any reason to break them up into provider buckets... although each node does end up getting tagged with the provider name. This slightly more shallow data structure allows you to run queries against all of the user-mappings, so you can quickly relate users that log in with different methods but the same email address. Here's some documentation: http://www.christopheresplin.com/post/124334674865/firebase-data-structures – Chris Esplin Sep 23 '15 at 16:06
  • This could be a great solution, but not ever network provide the user email and not ever user allow to read their email... what can be the "glue" if not email to "relate" the both users o user-mappings? – Tiago Gouvêa Jan 10 '16 at 17:58
  • Note that now Firebase uses `auth.uid` instead of `auth.id`, and when using providers like Facebook, etc., it appends the provider name to the `auth.uid` value: `{uid: google:}` – Roland Feb 15 '16 at 15:58
  • I feel like I'm missing something obvious here, but in the scenario where you signup with twitter, then log off, then sign in with facebook, wouldn't the facebook userMapping be empty? How can you determine that that is the same user when they switch login portals? The only mapping I can think of where you could join two login instances would be if the e-mail is the same for the twitter and facebook accounts. Can someone help clarify what I'm missing? – Brett Nottingham Apr 25 '16 at 16:15
16

I know this post exists for months but when I faced this problem, it took lot of my time to make the code more flexible. Base on Andrew code above, I tweaked the code a little.

Sample data store:

userMappings
|---facebook:777
|   |---user:"123"
|---twitter:888
    |---user:"123"
users
|---123
    |---userMappings
        |---facebook: "facebook:777"
        |---twitter: "twitter:888"

Security rules:

"userMappings": {
  "$login_id": {
    ".read": "$login_id === auth.uid",
    ".write": "auth!== null && (data.val() === null || $login_id === auth.uid)"
  }
},

"users": {
  "$user_id": {
    ".read": "data.child('userMappings/'+auth.provider).val()===auth.uid",
    ".write": "auth!= null && (data.val() === null || data.child('userMappings/'+auth.provider).val()===auth.uid)"
  }
}

So userMappings is still the first information we look up when login by Facebook, Twitter.... the userMappings' user will point to the main account in users. So after login by Facebook or Twitter, we can look up the main user account. In users we keep list of userMapping that can access to its data.

When create new user, we have to create an account in users first. The id for user in users could be anything we want. This is flexible because we can provide more login method like Google, Github without adding more security rule.

Kuma
  • 2,703
  • 1
  • 12
  • 12
  • 1
    This seems like a great solution, but it leaves me wondering/blocked in one respect: requests with the correct auth.uid will be able to access the "userMapping" it needs, naturally (check!), and will also be able to access the "user" it needs by way of a lookup on its "userMappings" child (slightly clunky, but check!). But what about when we have other data/fields, lets say "userActivity," or "userPosts," how would one make those fields only writable by the user that created them? It seems like they would also have to have a "userMappings" child with all of the login keys for that user.. – tydbwjiiofj23iof2jiojf2ifj3i2 Dec 14 '14 at 15:55
  • @MathewHuuskoV: I think in "userActivity," or "userPosts,"..., we can check security rule by using predifined variable "root" to look for user ID in userMappings. E.g. ".write": "root.child('userMappings/'+auth.uid).child('user').val()===data.child('author').val()". Which 'author' contains the user ID of the author of the post. – Kuma Jan 08 '15 at 06:16
  • 1
    Has someone implemented something like this already? Are there any code examples for this kind of solution? I want to add Firebase login - to the same account - using either email/password or Facebook to my iOS app. I see a couple of scenarios which make the entire thing rather complicated, depending on how to user logs in, and if the user has previously created the account using email or Facebook. For example, if the user logs in using Facebook, but has previously used only email/password to login, how am I supposed to find the matching user in my Firebase database? Or vice versa. – rodskagg Nov 03 '15 at 14:34
2

I've just created an angularfire decorator to handle this for us: angularfire-multi-auth

Douglas Correa
  • 1,015
  • 12
  • 25
0

I've spent quite some time thinking in a good solution, and IMHO to be able to register from any provider is just confusing. In my front end I always ask for a email registration, so a user logging with facebook and google+, for example, would be logged as the same user when he informs his email.

This way, the Sample Data proposed by Kuma don't need to duplicate the userMappings.

Sample Data Store:

userMappings
|---facebook:777
|   |---user:"123"
|---twitter:888
    |---user:"123"
users
|---123
    |---user data
Jp_
  • 5,973
  • 4
  • 25
  • 36