2

I'm trying to build role-based authentication using firebase, react and redux. Is it any way to add a custom property (such as 'permission' or 'role') to the firebase user object while creating new user from app level, or should I make it done some other way?

@UPDATE - problem resolved I built the role-based firebase authentication using custom claims as @frank-van-puffelen suggested. There are three roles: admin, worker and customer. Only admin is able to create worker accounts, customers are logging in via google+.

I used firebase cloud functions to create worker accounts, (because when i was trying to do this locally, i was automatically signed in with newly created account), then to set custom claims containing user's role. Cloud function is also decoding a token, which contains custom claims.

So here's my code snippets:

Cloud functions

const functions = require('firebase-functions');
const cors = require('cors');
const corsHandler = cors({origin: true});
const admin = require('firebase-admin');
admin.initializeApp();

exports.createWorkerAccount = functions.https.onRequest((request, response) => {
  corsHandler(request, response, () => {
    response.status(200).send('Hello from Cloud Function');
  });
  admin.auth().createUser({
   email: request.body.email,
   password: request.body.password
  }).then(() => {
    admin.auth().getUserByEmail(request.body.email).then((user) => {
      admin.auth().setCustomUserClaims(user.uid, {role: 'worker'});
    });
  });
});

exports.getToken = functions.https.onRequest((request, response) => {
  const token = admin.auth().verifyIdToken(request.body.token) //decoding token that contains custom claims
.then((t) => { 
    corsHandler(request, response, () => {
      response.status(200).send(t.role); //sending role as a response
  });
   });
});

Both functions are called by simple ajax requests.

The user's role comes back as a request response, then it's pushed to redux state so I am able to access it from everywhere in the app.

To redirect logged in user to specific path and restrict it I'am using react-router public and private paths and also firebase function onAuthStateChanged.

Detecting firebase auth state changes - it's redirecting user to specific path based on custom claim.

firebase.auth().onAuthStateChanged((user) => {
  if (user) {
    store.dispatch(login(user.uid));

    firebase.auth().currentUser.getIdToken() //getting current user's token
    .then((t) => {
      getTokenRequest(t) // Promise containing the reqest
      .then((role) => {
        store.dispatch(permissions(role)); //dispatching role to store
        renderApp();
        if (role == "admin") {
          console.log('admin');
          history.push('/admin/dashboard'); //redirecting to user-role-specific path
        }
        else if (role == "worker") {
          history.push('/worker/dashboard');
        } 
        else {
          history.push('/customer/dashboard');
        }
      });
    });

  } else {
    store.dispatch(logout());
    renderApp();
    history.push('/');
  }
});

React-router public route component which is disallowing the user to access wrong paths.

export const PublicRoute = ({
  isAuthenticated,
  role,
  component: Component,
  ...rest 
}) => (
    <Route {...rest} component={(props) => {
      if(isAuthenticated && role == "admin") {
        return <Redirect to="/admin/dashboard" />
      } else if (isAuthenticated && role == "") {
        return <Redirect to ="/customer/dashboard" />
      }
      else {
        return <Component {...props} />
      }
    }} />
  );

const mapStateToProps = (state) => ({
  isAuthenticated: !!state.auth.uid,
  role: state.role
});

export default connect(mapStateToProps)(PublicRoute);
Gibolt
  • 42,564
  • 15
  • 187
  • 127

1 Answers1

1

No, you cannot attach any profile information to a user object. Fields that apparently exist for this purpose inside the Firebase user object are used for Social Authentication (e.g. logging with Facebook, Google+ ...).

If you need to add your own app specific fields, you will need to add them to one of their database solutions (Firestore for example).

An example of the flow would be:

  1. Create the user using firebase SDK.
  2. When user is created you can get the ID of the newly created user. Push a new JSON containing the user profile with the following route to the db: users/${userId}

If you use such pattern you will always be able to pull a related user profile (that has the same id as the user) from the database, without actually needing to link it with the user account in any manner.

Vlatko Vlahek
  • 1,839
  • 15
  • 20
  • Great answer! Although for storing user roles, I'd nowadays recommend [using custom claims](https://firebase.google.com/docs/auth/admin/custom-claims). – Frank van Puffelen Aug 08 '18 at 13:19
  • Vlatko, logicaly it's understandable approach, but I'm stuck technically. I don't need any security rules (yet) but just need a simple information about user role to create routes. I got 3 other routes in my app, one for each role. If logged in user is an admin, get him to admin page, if simple user - to simple user page etc.. How to make it the simplest way? – Bartek Hajder Aug 08 '18 at 13:38
  • The simplest way would be to use Custom claims that @frank-van-puffelen suggested. When you register a user, add a custom claim to his account, and also save that info to Redux reducer so you can use it app wide. In case you are persisting logins (remember me option on login screen) save the role in a cookie or local storage, depending on what you are using. Next time your user opens the website, you can check their cookie or local storage to rehydrate the auth state automatically. In case you aren't persisting logins, you can check the claim on firebase next time user logs in to the website. – Vlatko Vlahek Aug 08 '18 at 14:38
  • I just tried to use custom claims, but when I try to import admin SDK to my project, I got bunch of compiling errors like "Module not found: Error: Can't resolve 'fs'". – Bartek Hajder Aug 08 '18 at 15:15
  • You're likely trying to use the Admin SDK into your client-side code. That won't work (and would be a big security risk if you made it work). Since this code sets the user's roles, you cannot have just any user run it. It needs to run in a trusted environment, which only you and your collaborators have access to. This could be your development machine, a server you control, or Cloud Functions for Firebase. – Frank van Puffelen Aug 08 '18 at 15:30
  • Thank You both for your help and explanations. Everything works fine. I'll put my code snippets for others. I'd be glad if you take a look at it. – Bartek Hajder Aug 09 '18 at 11:18
  • Congrats ! I'm glad if you managed to help you at least a little bit. – Vlatko Vlahek Aug 09 '18 at 13:28