Search code examples
reactjsfirebaseauthenticationoauth-2.0oauth

React App on Firebase - Fetching User Last Login Time


I have a React App hosted in Firebase. I'm tring to get "lastLoginAt" information of the user. I can get this via Firebase Auth Rest API "Get user data" but problem is FIREBASE_ID_TOKEN is needed at sample request.

curl 'https://identitytoolkit.googleapis.com/v1/accounts:lookup?key=[API_KEY]' \
-H 'Content-Type: application/json' --data-binary '{"idToken":"[FIREBASE_ID_TOKEN]"}'

In order to get FIREBASE_ID_TOKEN, user have to be logged in but then this overwrites "lastLoginAt" value and makes it useless for my case. Maybe I should look for another data elsewhere but I'm not very experienced in Firebase.

Another thing I tried was trying to import firebase-admin to my app, but as it is a browser based client side app it is not possible. Then for the last I authenticated a user via this separate js standalone module which worked fine;

import admin            from "firebase-admin"
import serviceAccount   from "./serviceAccountKey.json" assert { type: "json" } // Initialize the Firebase Admin SDK with your project credentials

admin.initializeApp({
    
    credential  : admin.credential.cert( serviceAccount ),
    databaseURL : 'DATABASE_URL'
});

// Get a reference to the Firebase Authentication user management API
const auth = admin.auth()

// Set custom claims for the user that grant the necessary permissions to retrieve user information
const uid = 'UID_USER' // The UID of the user to authorize

const customClaims = {
    
    authorizedUser: true // A custom claim that grants permission to retrieve user information
}

auth.setCustomUserClaims( uid, customClaims)
    
    .then(() => {
        
        console.log( 'User authorized successfully' );
    })
    
    .catch(error => {
        
        console.error( error );
    })

With this authorized user I tried to grab last login time of users without these users logging in and overwriting their lastloginat values.

The curl API call I tried was;

curl --location --request POST 'https://identitytoolkit.googleapis.com/v1/accounts:lookup?key=APP_KEY' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer LOOOONG_TOKEN'

Finally this gave me following reply;

{
    "error": {
        "code": 401,
        "message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
        "errors": [
            {
                "message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
                "domain": "global",
                "reason": "unauthorized"
            }
        ],
        "status": "UNAUTHENTICATED"
    }
}

I know I can do this in another way by recording login times to a DB table and fetching them but knowing that this data is already available tickles me to do it trough Firebase. Anyone has an idea what else can I try or what I am doing wrong ?


Solution

  • If you are looking to track when the user was last active, take a look at this thread.


    If you are looking to track when users actually log in, you'll need to write to the RTDB/Firestore immediately after a call to a signIn* method with the value of new Date(currentUser.metadata.lastSignInTime).getTime() (reference: Legacy SDK / Modern SDK). As long as that user doesn't need to call another signIn method, their old lastLoginAt time won't change.

    // Legacy syntax
    import * as firebase from "firebase/app";
    import "firebase/auth";
    import "firebase/database";
    
    const auth = firebase.auth();
    const rtdb = firebase.database();
    
    const { user } = await auth.signInWithEmailAndPassword(email, password);
    
    await rtdb.ref("users/" + user.uid + "/lastLoginAt")
      .set(new Date(user.metadata.lastSignInTime).getTime());
    
    // Modern syntax
    import { getAuth, signInWithEmailAndPassword } from "firebase/auth";
    import { getDatabase, ref, set } from "firebase/database";
    
    const auth = getAuth();
    const rtdb = getDatabase();
    
    const { user } = await signInWithEmailAndPassword(auth, email, password);
    
    await set(
      ref(rtdb, "users/" + user.uid + "/lastLoginAt"),
      new Date(user.metadata.lastSignInTime).getTime()
    );
    

    If the intent is to track how many days ago the user last logged in, instead of using a basic set operation, update the new login time using a transaction:

    // Legacy syntax
    import * as firebase from "firebase/app";
    import "firebase/auth";
    import "firebase/database";
    
    const auth = firebase.auth();
    const rtdb = firebase.database();
    
    const { user } = await auth.signInWithEmailAndPassword(email, password);
    
    let previousLoginTime;
    await rtdb.ref("users/" + user.uid + "/lastLoginAt")
      .transaction((serverPreviousLoginTime) => {
        previousLoginTime = serverPreviousLoginTime;
        return new Date(user.metadata.lastSignInTime).getTime();
      });
    
    // previousLoginTime now contains the previous login, in milliseconds, if any (it can be null).
    if (previousLoginTime) {
      console.log("User's last login: " + String(new Date(previousLoginTime)));
    }
    
    // Modern syntax
    import { getAuth, signInWithEmailAndPassword } from "firebase/auth";
    import { getDatabase, ref, runTransaction } from "firebase/database";
    
    const auth = getAuth();
    const rtdb = getDatabase();
    
    const { user } = await signInWithEmailAndPassword(auth, email, password);
    
    let previousLoginTime;
    await runTransaction(
      ref(rtdb, "users/" + user.uid + "/lastLoginAt"),
      (serverPreviousLoginTime) => {
        previousLoginTime = serverPreviousLoginTime;
        return new Date(user.metadata.lastSignInTime).getTime();
      });
    );
    
    // previousLoginTime now contains the previous login, in milliseconds, if any (it can be null).
    if (previousLoginTime) {
      console.log("User's last login: " + String(new Date(previousLoginTime)));
    }
    

    If the intent is to track recent login sessions, you can alternatively push data to the database rather than overwrite it.

    // Legacy syntax
    import * as firebase from "firebase/app";
    import "firebase/auth";
    import "firebase/database";
    
    const auth = firebase.auth();
    const rtdb = firebase.database();
    
    const { user } = await auth.signInWithEmailAndPassword(email, password);
    
    await rtdb.ref("userLogins/" + user.uid)
      .push({
        timestamp: new Date(user.metadata.lastSignInTime).getTime()
        /* ... other data? ... */,
      });
    
    
    // Modern syntax
    import { getAuth, signInWithEmailAndPassword } from "firebase/auth";
    import { getDatabase, ref, push } from "firebase/database";
    
    const auth = getAuth();
    const rtdb = getDatabase();
    
    const { user } = await signInWithEmailAndPassword(auth, email, password);
    
    await push(
      ref(rtdb, "userLogins/" + user.uid),
      {
        timestamp: new Date(user.metadata.lastSignInTime).getTime()
        /* ... other data? ... */,
      }
    );
    

    With the release of Cloud Functions v2, you also create a beforeUserSignedIn function to handle the same logic discussed above. In most situations, the code above can be reused by swapping out "firebase/*" for "firebase-admin/*".