Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Setting custom claims for Firebase auth from flutter

I'm using Firebase auth for an app, but as part of user creation I need to set some custom claims.

I've written a cloud function to set the claims when a user is created:

    const functions = require('firebase-functions');
    const admin = require('firebase-admin');
    admin.initializeApp(functions.config().firebase);

    // On sign up.
    exports.processSignUp = functions.auth.user().onCreate(user => {

    let customClaims;

    // Set custom user claims on this newly created user.
    return admin.auth().setCustomUserClaims(user.uid, {
            'https://hasura.io/jwt/claims': {
                'x-hasura-default-role': 'user',
                'x-hasura-allowed-roles': ['user'],
                'x-hasura-user-id': user.uid
            }
        })
        .then(() => {
            // Update real-time database to notify client to force refresh.
            const metadataRef = admin.database().ref("metadata/" + user.uid);
            // Set the refresh time to the current UTC timestamp.
            // This will be captured on the client to force a token refresh.
            return metadataRef.set({
                refreshTime: new Date().getTime()
            });
        })
        .then(() => {
            return admin.auth().getUser(user.uid);
        })
        .then(userRecord => {
            console.log(userRecord);
            return userRecord.toJSON();

        })
        .catch(error => {
            console.log(error);
        });
});

When I print out to the console the userRecord I can see the custom claims are set correctly.

Then in flutter I get the token from the created user, but it then doesn't seem to have the custom claims attached.

I'm using this code to create the user and print the claims in flutter

    Future<FirebaseUser> signUp({String email, String password}) async {
    final FirebaseUser user = (await auth.createUserWithEmailAndPassword(
      email: email,
      password: password,
    )).user;

    IdTokenResult result = await (user.getIdToken(refresh: true));
    print('claims : ${result.claims}');

    return user;
  }

If I inspect the token itself in a jwt debugger I can see its not got the custom claims on it. Is it that I need some additional steps to try and get an updated token once the claims have been set? I've tried user.reload() and user.getIdToken(refresh: true) but they don't seem to help.

Any ideas on how to get the token that has the custom claims?

like image 688
Andrew Avatar asked Dec 06 '22 08:12

Andrew


2 Answers

For future reference, I managed to get this working with Doug's suggestions.

Here's my firebase sdk admin function.

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();

const firestore = admin.firestore();
const settings = {timestampsInSnapshots: true};
firestore.settings(settings);


// On sign up.
exports.processSignUp = functions.auth.user().onCreate(async user => {

// Check if user meets role criteria:
// Your custom logic here: to decide what roles and other `x-hasura-*` should the user get
let customClaims;
// Set custom user claims on this newly created user.

return admin.auth().setCustomUserClaims(user.uid, {
    'https://hasura.io/jwt/claims': {
    'x-hasura-default-role': 'user',
    'x-hasura-allowed-roles': ['user'],
    'x-hasura-user-id': user.uid
}
  })
.then(async () => {
    await firestore.collection('users').doc(user.uid).set({
        createdAt: admin.firestore.FieldValue.serverTimestamp()
    });
 })
 .catch(error => {
    console.log(error);
 });
});

Then on the flutter side of things

Future<FirebaseUser> signUp({String email, String password}) async {
    final FirebaseUser user = (await auth.createUserWithEmailAndPassword(
      email: email,
      password: password,
    )).user;
    currentUser = user;

    await waitForCustomClaims();
    return user;
}

Future waitForCustomClaims() async {
    DocumentReference userDocRef = 
    Firestore.instance.collection('users').document(currentUser.uid);
    Stream<DocumentSnapshot> docs = userDocRef.snapshots(includeMetadataChanges: false);

    DocumentSnapshot data = await docs.firstWhere((DocumentSnapshot snapshot) => snapshot?.data !=null && snapshot.data.containsKey('createdAt'));  
    print('data ${data.toString()}');

    IdTokenResult idTokenResult = await (currentUser.getIdToken(refresh: true));
    print('claims : ${idTokenResult.claims}');
}

Hopefully this will help somebody else looking to do similar.

like image 101
Andrew Avatar answered Jan 04 '23 17:01

Andrew


The code you're showing is likely trying to get custom claims too soon after the account is created. It will take a few seconds for the function to trigger after you call auth.createUserWithEmailAndPassword. It runs asynchronously, and doesn't at all hold up the process of user creation. So, you will need to somehow wait for the function to complete before calling user.getIdToken(refresh: true).

This is precisely the thing I address in this blog post. The solution I offer does the following:

  1. Client: Creates a user
  2. Client: Waits for a document with the user's UID to be created in Firestore
  3. Server: Auth onCreate function triggers
  4. Server: Function does its work
  5. Server: At the end, function writes data to a new document with the new user's UID
  6. Client: Database listener triggers on the creation of the document

Then, you would add more more step on the client to refresh the ID token after it sees the new document.

The code given in the post is for web/javascript, but the process applies to any client. You just need to get the client to wait for the function to complete, and Firestore is a convenient place to relay that information, since the client can listen to it in real time.

Also read this post for a way to get a client to refresh its token immediately, based on claims written to a Firestore document.

Bottom line is that you're in for a fair amount of code to sync between the client and server.

like image 23
Doug Stevenson Avatar answered Jan 04 '23 17:01

Doug Stevenson