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:
- Take a username
- Lookup the matching email in a DB
- Validate the provided password against that email
- 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);
});
});