How can I login with multiple social services with Firebase? - authentication

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?

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:
Store one canonical user identifier for each user, and a mapping for
each provider-specific identifier to that one canonical id.
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.

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.

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

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

Related

Using Firestore for Survey Data - everybody can write, a few can read

I'm setting up an online survey. This survey will be anonymous - to fill it all you need to have is the survey's URL. I want to store the survey answers in Firestore, and later run scripts that retrieve the data and generate reports.
I want to set it up so that everybody can write to it, but only specific accounts that have access to the project can read the data. I've set up the following rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read: if request.auth.uid != null;
allow write;
}
}
}
Now I want to create the script that reads the data, and I'm not sure - which API key should I use? Firestore automatically created a firebase-sdk-admin service account - should I use this account? There are also the Browser Key and Web client Key that were created automatically. Are those the ones to use?
What I would really want is to set up the script in a way that asks me for my Google Credentials (much like the gcloud sdk does it). That way there's no sensitive information in the script at all - if the script user logs in to Google with an account that has access to the database - it works. If it doesn't - it doesn't.
Can I do that?
You can either user Firebase Custom Claims which are basically like roles in Discord so or store UIDs of authorized users in Firestore or RTDB. Then you can write you security rules like this:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read: if request.auth.uid != null;
allow write: if request.auth.token.admin == true;
}
}
}
Now only the users with "admin" claim set to true will be able to write to those documents. If you go with storing the user UIDs in a Firestore document then you can check if user's UID is present their as shown below:
allow write: get(/databases/$(database)/documents/users/$(request.auth.uid)).data.admin == true;
Regarding the Service Account, you should not use them from frontend. They grant privileged access to your Firebase resources i.e. security rules are redundant. So you usually use them with the Firebase Admin SDK in a secure server env like Cloud functions. You can create a Firebase Cloud Function and then allow only whitelisted users to invoke it. That means if unauthenticated users or someone you haven't whitelisted won't be able to invoke it.
If you want to give access to your database to a teammate, then you can add members to your project from the Firebase Console. Let me know if you have any further queries.
I think I got it. Since I want to let everybody write, and only read data from scripts run by admins and not users, I can set up the following access rules:
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read: if false;
allow write;
}
}
}
For reading, my scripts will authenticate as a service account (keeping the service account json file a secret), and will be able to read the data properly.

Cloud Firestore: setting security rule based on authentication

I would like to understand how secure it is a security rule based on authentication, like this:
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth != null;
}
}
}
I have collections where each document is relative to a specific user.
I am using Cloud Firestore only from mobile, both Android and iOS, not from web.
Is there any way for a user to get authenticated outside my mobile apps, and hence going to read or write some other user's documents?
If you want to make sure that users cannot read each other's information, you should implement stronger rules than auth != null.
For example, these rules make it so you can only read and write the data at /users/userId if you are authenticated as userId.
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
// Anybody can write to their ouser doc
allow read, write: if request.auth.uid == userId;
}
}
}
This will make it impossible for someone to "get authenticated outside my mobile apps, and hence going to read or write some other user's documents" as you mentioned.

Firebase Auth linking anonymous auth user with custom auth user

So I saw here: https://firebase.google.com/docs/auth/web/account-linking#link-auth-provider-credentials-to-a-user-account That it is now possible to link user accounts in Firebase. I also saw that Firebase provides the functionality of anonymous authentication, where it creates a user session for a user, without any credentials.
In our application we normally use CustomAuthentication with Firebase, as we have our own authentication service. This works perfectly, and we are able to use the same Auth system in-between systems that use Firebase, and the ones that don't.
Now we got to the point where we wanted to take advantage of the anonymous authentication of Firebase, to allow users to use the apps without registering, and just transfer their details after they log in. So I thought that account linking is what I need. But I can't find a way to link an anonymous account with a custom authentication account. Is something like this possible with Firebase?
I have not found linking between anonymous and custom signIns too, so I just use signInAnonymously and pass it's uid to signInWithCustomToken.
Step 1.
To be able acheive this, you should grab an uid from signInAnonymously function like this:
var uid;
auth().signInAnonymously().then(user => {
uid = user.uid;
});
Step 2.
Save current user data like this:
database().ref(`users/${uid}`).set({ some: 'data' });
Step 3.
Send this uid to your server which returns custom token. On your server you just pass this uid to firebase.auth().createCustomToken(uid) function and send this custom token back. It will contain the uid.
Step 4.
When you receive custom token, you can sign in with it like this:
auth().signInWithCustomToken(authKey);
And that's it. You are signed in with your custom token and can access your anonymous user data saved on the step 2.
I had a similar problem but I believe Firebase does not allow this type of linking because it would require linking two separate firebase user ids: the id of the custom token created user and the id of the anonymous user.
Firebase docs say:
Users are identifiable by the same Firebase user ID regardless of the authentication provider they used to sign in.
So handling linking between two user ID's would cause inconsistencies.
I got around this problem by doing a merge of the data between the two accounts.
For example (from Firebase docs):
// Get reference to the currently signed-in user
var prevUser = auth.currentUser;
// Sign in user with another account
auth.signInWithCredential(credential).then(function(user) {
console.log("Sign In Success", user);
var currentUser = user;
// Merge prevUser and currentUser accounts and data
// ...
}, function(error) {
console.log("Sign In Error", error);
});
Caveat: I've never done this. Have you seen this page? Down at the bottom, under the heading "Email-password sign-in", it lists this code fragment:
var credential = firebase.auth.EmailPasswordAuthProvider.credential(email, password);
You can then (apparently) link the credential like this:
auth.currentUser.link(credential).then(function(user) {
console.log("Anonymous account successfully upgraded", user);
}, function(error) {
console.log("Error upgrading anonymous account", error);
});

Limit logins via Github organizations in Firebase

I am working on an application using Firebase as the backend. I have a working implementation of GitHub authentication using Firebase. However, I want to limit my complete application (all read/write operations) to people in a specific GitHub organization.
From what I gather, it is to be done via adding Security Rules in Forge. However, the organization of a user is not part of the "basic auth data" that GitHub provides to Firebase for use. Is there any way for me to make the app available to members of a particular organization only?
Current Code:
var FB = new Firebase('https://XXXXX.firebaseio.com');
var auth = new FirebaseSimpleLogin(FB, function(error, user){
if(user==null){
auth.login('github',{rememberMe:true});
}
else{
//We are logged in
$.getJSON("https://api.github.com/user/orgs?access_token="+user.accessToken+"&callback=?", function(data){
var orgs = data.data.map(function(org){return org.login});
if(orgs.indexOf(config.github_org)>-1){
//Public Member of Chosen Github Org
//Now we fetch the data and render it
}
else{
//Throw them out
alert("Join the group "+config.github_org+" on GitHub to get access to Eon");
document.location="https://github.com/"+config.github_org;
}
})
}
});
To use FB Security Rules you'll need to compare the auth data with something in the Firebase (like a list of organizations), but it looks like you are trying to get those on the fly every time. Instead I'd recommend keeping a collection of users in your Firebase organized by their auth.uid that have organization lists you can check against:
users
uid1
...
uidX
orgs
org1
...
orgX
So when a user tries to access a given orgX you can check to see if root.child('users').child(auth.uid).child('orgs/orgX') exists in your Security Rules.

Cakephp with OpenID and User Authentication

I have a table "users" and I want to enable my visitors to login with their openID Account. For this I use the OpenId Component for Cakephp and it works fine (When I login with the Google URL I receive the "successfully authenticated!" notification).
But now I'm kind of stuck because I don't know how to go on from there.
Do I have to create a User-Entry for every user which has a new entry in the "oid_associations" table (I save all OpenId interaction in the mysql database)?
Do I have to authenticate the User after the login (or is the OpenID-component doing that automatically?).
Am I completely misunderstanding the concept?
No, you don't have to access the "oid_associations" table, it is a table which is only used by the OpenID library.
Instead, you can use the identity_url to figure out whether it is a new user. If that's the case, you can then create an entry in your "users" table. For example (assuming your "users" table has an "openid" column):
$response = $this->Openid->getResponse($returnTo);
if ($response->status == Auth_OpenID_SUCCESS) {
$user = $this->User->findByOpenid($response->identity_url);
if ($user) {
// existing user
} else {
// new user -> create user entry in the database
}
}
I'm not sure I understand your second question correctly. If someone logs in with an OpenID and you get an Auth_OpenID_SUCESS response, then this means this user was sucessfully authenticated. How you use this information in your application is up to you.
I hope this answers your questions.