Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Firebase verify email password in cloud function

I have a requirement to accept custom usernames into my site (Requirement from biller with strict limitations of alpha numeric.) And these usernames should be interchangeable with the user's email address for login purposes.

I allow the user to signup and login with their email and password through the standard firebase email password authentication. The user is registered at the biller, who then returns a custom generated username to the application via a postback.

I have created a usernames table that contains the UID of the user each username belongs to (Initially there is the email, and biller generated usernames)

When the user attempts to login, I go to the usernames table and lookup the UID. At this point I would like to use the UID just looked up, and the password supplied by the user to log the user in through the standard firebase authentication system.

I have been unable to find any way to verify the user's password is valid against a looked up user account inside of cloud functions so that I can generate a custom token.

I could lookup the user by the username, find the email, send it back to the client and allow the login to take place with that email and the password the user supplied, but would prefer to avoid it as that will allow usernames and email addresses to be associated with each other by bad actors.

like image 468
major-mann Avatar asked Aug 08 '18 17:08

major-mann


2 Answers

In your cloud function you can install and use the firebase package besides firebase-admin, and initialize it as if you were initializing a web page. That way you can use the admin SDK to find the email of the username, and then use firebase from the cloud function to authenticate, using signInWithEmailAndPassword. If it succeeds you can then generate a custom token and send it to the client.

I don't know if this is the best approach but it works.

like image 173
Ricardo Smania Avatar answered Sep 23 '22 10:09

Ricardo Smania


Below is an implementation of Ricardo's answer (using REST). The goal is to allow an alternate login system in parallel with email login. What this does is:

  1. Take a username
  2. Lookup the matching email in a DB
  3. Validate the provided password against that email
  4. Return the email, to be used client-side with signInWithEmailAndPassword()

It expects a database collection named users, keyed by username and containing users' email addresses. I've called usernames code internally (can be changed). Make sure to update the API key:

// Firebase dependencies.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();

// Axios, for REST calls.
const axios = require('axios');
const apiKey = '[YOUR API KEY]';
const signInURL = 'https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=' + apiKey;


exports.getEmailWithCodeAndPassword = functions.https.onCall((data, context) => {
  // Require code and passowrd.
  data = data || {};
  if (!(data.code && data.password)) {
    throw new functions.https.HttpsError('failed-precondition', 'The function must be called with fields: code and password.');
  }

  // Search for user's email, sign in to verify email, and return the email for client-side login.
  return db.collection('users').doc(data.code).get().then(doc => {
    // Throw if the code is not in the users DB.
    if (!doc.data()) {
      throw {
        code: 'auth/user-not-found',
        message: 'There is no user record corresponding to this identifier. The user may have been deleted.',
      };
    }

    // Retrieve the email and attempt sign-in via REST.
    const email = doc.data().email;
    return axios.post(signInURL, {
      email: email,
      password: data.password,
      returnSecureToken: true,
    }).catch(e => {
      throw {
        code: 'auth/wrong-password',
        message: 'The password is invalid or the user does not have a password.',
      };
    });
  }).then(res => {
    // Return the email after having validated the login details.
    return res.data.email;
  }).catch(e => {
    // Throw errors.
    throw new functions.https.HttpsError('unknown', e.message);
  });
});

It's not the most efficient (~500ms in my tests), but it works. An alternative could be to do steps 1-2 using admin.auth().listUsers, which also gives the salt/hash, then use Firebase's custom scrypt to check the provided password against the hash. This would prevent the need for the REST call, which is the bulk of the time lost, but it would be difficult because the custom scrypt isn't in JS.

I also tried implementing with the Firebase client-side SDK instead of REST, but it's about as slow and has much larger dependencies (90MB and 6400 files, vs 500KB/67 files for Axios). I'll copy that solution below too, in case anyone's curious:

// Firebase dependencies.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const firebaseClient = require('firebase');
admin.initializeApp();
const db = admin.firestore();

// Configure and initialise Firebase client SDK.
var firebaseConfig = {
  // [COPY YOUR CLIENT CONFIG HERE (apiKey, authDomain, databaseURL, etc)]
};
firebaseClient.initializeApp(firebaseConfig);

exports.getEmailWithCodeAndPassword = functions.https.onCall((data, context) => {
  // Require code and passowrd.
  data = data || {};
  if (!(data.code && data.password)) {
    throw new functions.https.HttpsError('failed-precondition', 'The function must be called with fields: code and password.');
  }

  // Search for user's email, sign in to verify email, and return the email for client-side login.
  let email;
  return db.collection('users').doc(data.code).get().then(doc => {
    if (!doc.data()) {
      throw {
        code: 'auth/user-not-found',
        message: 'There is no user record corresponding to this identifier. The user may have been deleted.',
      };
    }

    // Retrieve the email and attempt sign-in.
    email = doc.data().email;
    return firebaseClient.auth().signInWithEmailAndPassword(email, data.password);
  }).then(res => email).catch(e => {
    throw new functions.https.HttpsError('unknown', e.message);
  });
});
like image 44
Erik Koopmans Avatar answered Sep 23 '22 10:09

Erik Koopmans