Control Access with Custom Claims and Security Rules

The Firebase Admin SDK supports defining custom attributes on user accounts. This provides the ability to implement various access control strategies, including role-based access control, in Firebase apps. These custom attributes can give users different levels of access (roles), which are enforced in an application's security rules.

User roles can be defined for the following common cases:

  • Giving a user administrative privileges to access data and resources.
  • Defining different groups that a user belongs to.
  • Providing multi-level access:
    • Differentiating paid/unpaid subscribers.
    • Differentiating moderators from regular users.
    • Teacher/student application, etc.
  • Add an additional identifier on a user. For example, a Firebase user could map to a different UID in another system.

Let's consider a case where you want to limit access to the database node "adminContent." You could do that with a database lookup on a list of admin users. However, you can achieve the same objective more efficiently using a custom user claim named admin with the following Realtime Database rule:

{
  "rules": {
    "adminContent": {
      ".read": "auth.token.admin === true",
      ".write": "auth.token.admin === true",
    }
  }
}

Custom user claims are accessible via user's authentication tokens. In the above example, only users with admin set to true in their token claim would have read/write access to adminContent node. As the ID token already contains these assertions, no additional processing or lookup is needed to check for admin permissions. In addition, the ID token is a trusted mechanism for delivering these custom claims. All authenticated access must validate the ID token before processing the associated request.

Set and validate custom user claims via the Admin SDK

Because custom claims can contain sensitive data, they should only be set from a privileged server environment by the Firebase Admin SDK. You can set claims using Node.js as shown:

// Set admin privilege on the user corresponding to uid.
admin.auth().setCustomUserClaims(uid, {admin: true}).then(() => {
  // The new custom claims will propagate to the user's ID token the
  // next time a new one is issued.
});

The custom claims object should not contain any OIDC reserved key names or Firebase reserved names. Custom claims payload must not exceed 1000 bytes.

An ID token sent to a backend server can confirm the user's identity and access level using the Admin SDK as follows:

 // Verify the ID token first.
 admin.auth().verifyIdToken(idToken).then((claims) => {
   if (claims.admin === true) {
     // Allow access to requested admin resource.
   }
 });

You can also check a user's existing custom claims, which are available as a property on the UserRecord object:

 // Lookup the user associated with the specified uid.
 admin.auth().getUser(uid).then((userRecord) => {
   // The claims can be accessed on the user record.
   console.log(userRecord.customClaims.admin);
 });

You can delete a user's custom claims by passing null for customClaims.

Propagate custom claims to the client

After new claims are modified on a user via the Admin SDK, they are propagated to an authenticated user on the client side via the ID token in the following ways:

  • A user signs in or re-authenticates after the custom claims are modified. The ID token issued as a result will contain the latest claims.
  • An existing user session gets its ID token refreshed after an older token expires.
  • An ID token is force refreshed by calling currentUser.getIdToken(true).

Access custom claims on the client

Custom claims can only be retrieved through the user's ID token. Access to these claims may be necessary to modify the client UI based on the user's role or access level. However, backend access should always be enforced through the ID token after validating it and parsing its claims. Custom claims should not be sent directly to the backend, as they can't be trusted outside of the token.

Once the latest claims have propagated to a user's ID token, you can get these claims by retrieving the ID token first and then parsing its payload (base64 decoded):

// https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding
firebase.auth().currentUser.getIdToken()
  .then((idToken) => {
     // Parse the ID token.
     const payload = JSON.parse(b64DecodeUnicode(idToken.split('.')[1]));
     // Confirm the user is an Admin.
     if (!!payload['admin']) {
       showAdminUI();
     }
  })
  .catch((error) => {
    console.log(error);

Best practices for custom claims

Custom claims are only used to provide access control. They are not designed to store additional data (such as profile and other custom data). While this may seem like a convenient mechanism to do so, it is strongly discouraged as these claims are stored in the ID token and could cause performance issues because all authenticated requests always contain a Firebase ID token corresponding to the signed in user.

  • Use custom claims to store data for controlling user access only. All other data should be stored separately via the real-time database or other server side storage.
  • Custom claims are limited in size. Passing a custom claims payload greater than 1000 bytes will throw an error.

Examples and use cases

The following examples illustrate custom claims in context of specific Firebase use cases.

Defining roles via Firebase Functions on user creation

In this example, custom claims are set on a user on creation using Cloud Functions.

Custom claims can be added using Cloud Functions. and propagated immediately with Realtime Database. The function is called only on signup using an onCreate trigger. Once the custom claims are set, they propagate to all existing and future sessions. The next time the user signs in with the user credential, the token contains the custom claims.

Client side implementation (JavaScript)

const provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(provider)
.catch(error => {
  console.log(error);
});

let callback = null;
let metadataRef = null;
firebase.auth().onAuthStateChanged(user => {
  // Remove previous listener.
  if (callback) {
    metadataRef.off('value', callback);
  }
  // On user login add new listener.
  if (user) {
    // Check if refresh is required.
    metadataRef = firebase.database().ref('metadata/' + user.uid + '/refreshTime');
    callback = (snapshot) => {
      // Force refresh to pick up the latest custom claims changes.
      // Note this is always triggered on first call. Further optimization could be
      // added to avoid the initial trigger when the token is issued and already contains
      // the latest claims.
      user.getIdToken(true);
    };
    // Subscribe new listener to changes on that node.
    metadataRef.on('value', callback);
  }
});

Cloud Functions logic

A new database node (metadata/($uid)} with read/write restricted to the authenticated user is added.

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

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

// On sign up.
exports.processSignUp = functions.auth.user().onCreate(event => {
  const user = event.data; // The Firebase user.
  // Check if user meets role criteria.
  if (user.email &&
      user.email.indexOf('@admin.example.com') != -1 &&
      user.emailVerified) {
    const customClaims = {
      admin: true,
      accessLevel: 9
    };
    // Set custom user claims on this newly created user.
    return admin.auth().setCustomUserClaims(user.uid, customClaims)
      .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()});
      })
      .catch(error => {
        console.log(error);
      });
  }
});

Database rules

{
  "rules": {
    "metadata": {
      "$user_id": {
        // Read access only granted to the authenticated user.
        ".read": "$user_id === auth.uid",
        // Write access only via Admin SDK.
        ".write": false
      }
    }
  }
}

Defining roles via an HTTP request

The following example sets custom user claims on a newly signed in user via an HTTP request.

Client side implementation (JavaScript)

const provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(provider)
.then((result) => {
  // User is signed in. Get the ID token.
  return result.user.getIdToken();
})
.then((idToken) => {
  // Pass the ID token to the server.
  $.post(
    '/setCustomClaims',
    {
      idToken: idToken
    },
    (data, status) => {
      // This is not required. You could just wait until the token is expired
      // and it proactively refreshes.
      if (status == 'success' && data) {
        const json = JSON.parse(data);
        if (json && json.status == 'success') {
          // Force token refresh. The token claims will contain the additional claims.
          firebase.auth().currentUser.getIdToken(true);
        }
      }
    });
}).catch((error) => {
  console.log(error);
});

Backend implementation (Admin SDK)

app.post('/setCustomClaims', (req, res) => {
  // Get the ID token passed.
  const idToken = req.body.idToken;
  // Verify the ID token and decode its payload.
  admin.auth().verifyIdToken(idToken).then((claims) => {
    // Verify user is eligible for additional privileges.
    if (typeof claims.email !== 'undefined' &&
        typeof claims.email_verified !== 'undefined' &&
        claims.email_verified &&
        claims.email.indexOf('@admin.example.com') != -1) {
      // Add custom claims for additional privileges.
      admin.auth().setCustomUserClaims(claims.sub, {
        admin: true
      }).then(function() {
        // Tell client to refresh token on user.
        res.end(JSON.stringify({
          status: 'success'
        });
      });
    } else {
      // Return nothing.
      res.end(JSON.stringify({status: 'ineligible'});
    }
  });
});

The same flow can be used when upgrading an existing user's access level. Take for example a free user upgrading to a paid subscription. The user's ID token is sent with the payment information to the backend server via an HTTP request. When the payment is successfully processed, the user is set as a paid subscriber via the Admin SDK. A successful HTTP response is returned to the client to force token refresh.

Defining roles via backend script

A recurring script (not initiated from the client) could be set to run to update user custom claims:

admin.auth().getUserByEmail('user@admin.example.com').then((user) => {
  // Confirm user is verified.
  if (user.emailVerified) {
    // Add custom claims for additional privileges.
    // This will be picked up by the user on token refresh or next sign in on new device.
    return admin.auth().setCustomUserClaims(user.uid, {
      admin: true
    });
  }
}).catch((error) => {
  console.log(error);
});

Custom claims can also be modified incrementally via the Admin SDK:

admin.auth().getUserByEmail('user@admin.example.com').then((user) => {
  // Add incremental custom claim without overwriting existing claims.
  const currentCustomClaims = user.customClaims;
  if (currentCustomClaims.admin) {
    // Add level.
    currentCustomClaims['accessLevel'] = 10;
    // Add custom claims for additional privileges.
    return admin.auth().setCustomUserClaims(user.uid, currentCustomClaims);
  }
}).catch((error) => {
  console.log(error);
});

フィードバックを送信...

ご不明な点がありましたら、Google のサポートページをご覧ください。