dpa99c / cordova-plugin-firebasex

Cordova plugin for Google Firebase
MIT License
571 stars 468 forks source link

[DOC] Firebase Authentication operations - Multilayer login #640

Open spoxies opened 3 years ago

spoxies commented 3 years ago

What:

Extending the documentation by showing how to use cordova-firebasex-plugin in conjunction with the Firebase Web JS SDK.

Why:

The cordova-firebasex-plugin does things that the Firebase Web JS SDK just cannot expose (like auth/notifications/crashalytics) in a hybrid environment. On the other hand the plugin isn't a full substitute of all (essential) features that the Web JS SDK exposes.

Since Firebase Web JS SDK is already committed to cordova compatibility I would (advise a developer that uses firebase and cordova to) use the Web JS SDK for everything else where it does not needs to interact with the Native layer. Also keeping this plugin up with things that the Firebase Web JS SDK can support seems an everlasting/draining chase for the contributor(s).

Since version 8.0.0. the auth has been reworked. Meaning it was not possible/advised to pass credentials to the Web JS SDK and login. There seem to be issues/struggles (https://github.com/dpa99c/cordova-plugin-firebasex/issues/176, https://github.com/dpa99c/cordova-plugin-firebasex/issues/433, https://github.com/dpa99c/cordova-plugin-firebasex/issues/435) derived from this change. However PR's making cross layer login possible are already posted (https://github.com/dpa99c/cordova-plugin-firebasex/pull/633, https://github.com/dpa99c/cordova-plugin-firebasex/pull/621). Documenting how to make use of these PR's does make sense.

Why (not)/ why warn:

'Unfortunately' there is overlap like some Firebase Firestore functions that are (also) added in cordova-plugin-firebasex. Having the Firebase Web JS SDK (optionally put there by the app developer) as well, means that in this example there are multiple approaches to retrieve data. That might lead to mixing (native) variables and Web JS SDK variables.

How:

Adding (a version of) the suggestion bellow in the documentation:


Passing authentication to the Firebase JavaScript SDK

Experimental: Passing authentication to the Firebase JavaScript SDK is possible for some Firebase Authentication operations that are exposed by this plugin, but this is highly experimental. Please note that mixing variables from the native (plugin) and the JavaScript SDK should be avoided as it to will lead to unexpected behaviour.

authenticateUserWithApple (Android/iOS)

Passing authentication unsupported (See https://github.com/dpa99c/cordova-plugin-firebasex/issues/551)

authenticateUserWithGoogle (Android/iOS)

Possible since version 13.0.0

var clientId = '[secret].apps.googleusercontent.com';
// Trigger plugin native authentication layer
FirebasePlugin.authenticateUserWithGoogle(clientId, function (credential) {
    var { idToken } = credential; // Extract idToken
    //Login to Firebase JS SDK
    firebase.auth.GoogleAuthProvider.credential(idToken);
}, function () {
    //err
});

verifyPhoneNumber (Android*/iOS) - w/o instantVerification

NOTE: As of version 8.0.0 passing authentication is no longer possible if instantVerification: true. (See https://github.com/dpa99c/cordova-plugin-firebasex/issues/176)

var phonenumber;
var timeout;
FirebasePlugin.verifyPhoneNumber(function (credentials) {
    // Success
    if (credentials.instantVerification) {
        return alert('passing authentication with instantVerification not possible as of version 8.0.0');
    }
    promptUserToInputCode() // you need to implement this
        .then(function (userEnteredSMSCode) {
            credential.code = userEnteredSMSCode;
            //Login to Firebase JS SDK
            var webCredential = firebase.auth.PhoneAuthProvider.credential(credential.verificationId, credential.code);
            return firebase.auth().signInWithCredential(webCredential);
        });
},
    function () {
        //err
    },
    phonenumber,
    timeout
);

Other

The above is based upon my current and limited understanding of this project and should by not be taken as truth.

At least within the iOS version of the plugin it is possible to duplicate the verification credentials for use with the plugin and web js layer. This isn't tested on Android and probably a bad idea in general.

    var credentials; // E.g. returned from FirebasePlugin.verifyPhoneNumber();
    var webCredential = firebase.auth.PhoneAuthProvider.credential(credential.verificationId, credential.code);
    // Firebase JS SDK 
    return firebase.auth.signInWithCredential(webCredential)
        .then(function () {
            // Plugin authentication layer
            FirebasePlugin.signInWithCredential(credential,
                function () {
                    console.log('Plugin logged in');
                },
                function (error) {
                    console.log('Plugin logged in failed', error);
                });
        }); // Promise

~

Sources

https://github.com/dpa99c/cordova-plugin-firebasex/pull/633 https://github.com/dpa99c/cordova-plugin-firebasex/commit/78bff37626fecd92ddc5de189de94e9864d87b6c

helpmetheroadassistanceapp commented 2 years ago

You managed to find a solution? from my side, I could only use the web code on Android and the plugin code on iOS.

spoxies commented 1 year ago

@helpmetheroadassistanceapp Yes I just stared with finalising the Android app thus I could not ignore this issue any longer. So I came across my own posts (and I over looked you question), I just found out how it is possible and assuming this is a wide spread issue, I'm gonna reply months later for those who come across.

In short both the plugin and the web layer expose the signInUserWithCustomToken / signInWithCustomToken. It is easy to generate such a token on firebase functions with an already logged in user.

The resulting token you pass to the either one (web or native) you have not logged in to yet. With this custom token you can login as the same user no matter what initial authentication method you had chosen.

What you do is expose an endpoint in firebase functions e.g:

Exposing an endpoint in Firebase functions:

firebase.json:

{
    "hosting": {
      "public": "public",
      "ignore": [
        "firebase.json",
        "**/.*",
        "**/node_modules/**"
      ],
      "rewrites": [
        {
          "source": "{/*,/**}",
          "function": "getApp"
        }
      ]
    },
    "functions": {
      "source": "functions/",
      "predeploy": [
        "npm --prefix \"$RESOURCE_DIR\" run"
      ]
    },
    "firestore": {
      "rules": "firestore.rules",
      "indexes": "firestore.indexes.json"
    },
    "storage": {
      "rules": "storage.rules"
    },
    "database" : {
      "rulesFile": "firestore.rules"
    }
  }

functions/index.js:

const functions = require('firebase-functions');
const admin = require('firebase-admin');
const express = require('express');

const app = express();

// https://github.com/firebase/firebase-admin-node/pull/285
admin.initializeApp({
    serviceAccountId: [the-service-account-that-has-generate-custom-token-permission]
});

api.myAuth_get = require('./api/my/auth/get').http_get;
api.myAuth_options = require('./api/my/auth/options').http_options;

// HTTP :: /my/auth
app.get('/my/auth', (req, res) => {
    res.set('Content-Type', 'application/json');
    return api.myAuth_get(req, res);
});

app.options('/my/auth', (req, res) => {
    res.set('Content-Type', 'application/json');
    return api.myAuth_options(req, res);
});

exports.getApp = functions.https.onRequest(app);

functions//api/my/auth/options.js:

// == api.myAuth_options ===================================
exports.http_options = (req, res) => {
    // Set CORS headers
    res.set({
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'DELETE,GET,PATCH,POST,PUT',
        'Access-Control-Allow-Headers': 'Content-Type,Access-Control-Allow-Headers,Authorization,X-Requested-With',
    });

    res.status(200).send();

    return;
};

functions//api/my/auth/get.js:

// == api.myAuth_get ===================================
const admin = require('firebase-admin');

exports.http_get = (req, res) => {
    // Set CORS headers
    res.set({
            'Access-Control-Allow-Origin': '*',
            'Access-Control-Allow-Methods': 'DELETE,GET,PATCH,POST,PUT',
            'Access-Control-Allow-Headers': 'Content-Type,Access-Control-Allow-Headers,Authorization,X-Requested-With',
    });

    if ((!req.headers.authorization || !req.headers.authorization.startsWith('Bearer '))) {
        res.status(403).send('Unauthorized');
        return;
    } else {
        // Read the ID Token from the Authorization header.
        idToken = req.headers.authorization.split('Bearer ')[1];
    }

    return admin.auth().verifyIdToken(idToken)
        .then((decodedIdToken) => {
            return admin.auth().createCustomToken(decodedIdToken.uid);
        }).then((customToken) => {
            if (customToken) {
                res.status(200).send({'customToken': customToken});
                return;
            }
            res.status(404).send({
                'success': false,
            });

        }).catch((error) => {
            console.log('Error while creating a Firebase ID token:', error);
            res.status(400).send({ error: 'Invalid request' });
        });
};

So assuming you have logged in on the weblayer (e.g with phoneAuth), you call the above endpoint to retrieve a custom token.

The example bellow shows you could intermix passing credentials from FirebasePlugin.verifyPhoneNumber to the weblayer firebase.auth().signInWithCredential and from that retrieve the custom token from 'Admin Functions' an pass it to FirebasePlugin.signInUserWithCustomToken.

Of course it would make more sense to do FirebasePlugin.verifyPhoneNumber > FirebasePlugin.signInWithCredential > [retrieve-custom-token-from-admin-functions] > firebase.auth().signInWithCustomToken

The APP:


window.FirebasePlugin.verifyPhoneNumber(function (credential) {
  // if instant verification is true use the code that we received from the firebase endpoint,
 // otherwise ask user to input verificationCode:
 var code = (credential.instantVerification) ? credential.code : false;

   var verificationCredential = credential;

   if (!code) {
       showSmsInput(verificationCredential);
        return;
    }
     validateCredential(credential);
     return;
}, console.error, {timeOutDuration : 120, requireSmsValidation : true});

function showSmsInput(){
  // Show some UI
...
// Bind to some submit event
  $$(document).on('submit', function(){
    var currentInput = $$("input#phonenumber_sms_code").val();
    validateCredential(app.data.verificationCredential, currentInput);
  });
}
}
function validateCredential(credential, code) {

   var webCredential = firebase.auth.PhoneAuthProvider.credential(
      credential.verificationId,
      code || credential.code
   );

   // Sign in weblayer
   return firebase.auth().signInWithCredential(webCredential)
      .then((auth) => {
       if(!auth || !auth.user){
         return;
       }
       // Retrieve a custom token from your endpoint
       // using a XHR request
       return auth.user.getIdToken().then(function (idToken) {
            return app.request.promise({
               url: [your - exposed - admin - endpoint] / my / auth,
               method: 'GET',
               dataType: 'json',
               headers: { 'Authorization': `Bearer ${idToken}` },
            }).then(function (customTokenData) {
               // Sign in to the native layer
               window.FirebasePlugin.signInUserWithCustomToken(
                  customTokenData.customToken,
                  console.log,
                  console.warn
               );
            });
         });
      });
}
spoxies commented 1 year ago

@dpa99c Would the above (but reworked a bit), be something that you like to have added to the docs 'Achieve multilayer login' ? If so I'll write a bit and propose a PR.

dpa99c commented 1 year ago

@spoxies Yes, any additional documentation to assist other plugin users for common use cases like this would be most welcome and appreciated.

elloboblanco commented 1 year ago

Just here to say that I encountered this problem recently and the proposed solution worked on iOS for me. Really appreciate you adding this information.