Create users with roles on client side with firebase - firebase

I'm trying to do my first login and authentication with firebase and I need three roles
Student, teacher, admin
My NavBar should render different things depends the role of the user signed in and I've seen that there is a way to use roles with firebase https://firebase.google.com/docs/auth/admin/custom-claims
but if I'm not missunderstanding it, it should be done on the backend (I'm using spring)
I should know the role at every moment on client side How could I do it?

You can't create custom claims on the client. That would be a security hole, because users could just give themselves access to anything simply by modifying your code. You need to use the Admin SDK on the server for that.
There is also documentation for accessing custom claims on the client. You use getIdTokenResult() on the Firebase auth user object. For example:
firebase.auth().currentUser.getIdTokenResult()
.then((idTokenResult) => {
// Confirm the user is an Admin.
if (!idTokenResult.claims.admin) {
// Show admin UI.
showAdminUI();
} else {
// Show regular user UI.
showRegularUI();
}
})
.catch((error) => {
console.log(error);
});

Related

Set custom claims & role-based access for specific Google sign-in account

My firebase functions app makes use of Firebase & Google for authentication, checking for valid domains to grant access.
My concern is that a rogue employee can come along and destroy sensitive data. Thus, I wish to set custom claims for each account access the site from "admin" user of sorts.
There is already 2 questions (here and here) relating to this, being able to set claims in the Firebase console but this isn't yet a feature
My question has 2 parts:
How can I determine who my "admin" user is (I know the email, how do I set this user as the admin user)
How can this admin user set permissions for these accounts.
Possible solution:
The only solution I can think of is, upon for login, the account is saved in firestore e.g. /portal-users/${email}/mode
Modes:
none: on initial registration, the "admin" user needs to accept this person as a valid portal user
user: valid system user
revoked: revoked email, cannot access system
admin: well, admin I guess
My implementation using Firebase Functions & express follows this flow:
When signing in to POST:/user/createSession, I read the Firestore database and set custom claims using this as a base:
// if you are wondering what happened to CSRF - I couldn't get that to work :(
const idToken = req.body.idToken.toString();
const expiresIn = 60 * 60 * 24 * 5 * 1000;
const auth = admin.auth();
auth.verifyIdToken(idToken).then(value => {
console.log("Token verified")
auth.setCustomUserClaims(value.uid, {mode: `insert mode here from firestore`}).then(value1 => {
return auth.createSessionCookie(idToken, {expiresIn})
.then((sessionCookie) => {
// Set cookie policy for session cookie.
const options = {maxAge: expiresIn, httpOnly: true, secure: true};
res.cookie('session', sessionCookie, options);
res.end(JSON.stringify({status: 'success'}));
}).catch((error) => {
console.error(error);
res.status(401).send('UNAUTHORIZED REQUEST!');
});
})
}).catch(reason => {
console.error("Unable to verify token");
console.error(reason);
res.status(401).send('INVALID TOKEN!');
});
When requesting a auth /any/route/with/private/data route, the following check should be done (roughly) this way when validating a request:
admin
.auth()
.verifySessionCookie(sessionCookie, true)
.then((decodedClaims) => {
// Where the magic happens
switch (decodedClaims.mode) {
case "none": {
// return to login screen with awaiting verification message
break;
}
case "revoked": {
// return to login screen with revoked permissions message
break;
}
case "user":
case "admin":{
// continue
break;
}
}
res.status(401).send('UNAUTHORIZED REQUEST!');
})
.catch((error) => {
// Session cookie is unavailable or invalid. Force user to login.
res.redirect('/login');
});
To manage users' permissions, the admin user has a special page to set modes for each user on the system (which will eventually call setCustomClaims(), see above).
Are there any issues or security problems I might face? (except the CSRF issue ofc)
A few things to be aware of. Custom claims are cached from the client, these can lead to valid tokens with expired information making false-positive modifications to your database. This is an inherit flaw with Firebase's Auth system being restful by design, to get around this you must revoke the users' Auth token when their custom claims have changed, there are several ways to handle this:
Propagate: https://firebase.google.com/docs/auth/admin/custom-claims#propagate_custom_claims_to_the_client
Revoke: https://firebase.google.com/docs/auth/admin/manage-sessions#revoke_refresh_tokens
The main issue is that the user will have to re-authenticate to update these changes and should only be used on extreme account-wide conditions, such as setting administrator privileges, etc. In contrast, Cloud Functions have the luxury of validating the user object and accessing auth directly for the updated claims but these are not reflected in Security Rules. (this would be my dream fix)
Since Firestore and Realtime do suffer from cached claims, it's often more viable to have these flags set on the user based on a database profile or 'role' table with their UID associated with the correct Role. doing multiple reads and writes is also helpful as these reads from within Firestore are Cached per request, up to a maximum of 10 unique documents.
Source: https://firebase.google.com/docs/firestore/security/rules-conditions#access_other_documents
Custom Claims currently is not a viable solution to Secure your database, only validation on Cloud Functions and user flags on the client. Using a combination of the above methods and Database roles is the best solution.
Finding the user
You can setup a onUser create trigger within cloud functions that checks the user's credentials and confirms it with your backend, ie: is the user auth'd from "google.com" and has a specific prefix, etc.
or if the user is already within your Firebase Auth collection, you can target them by UID or email - Email is not as secure compared to the user's UID, as this does not validate the auth provider.
One popular solution is a Cloud Function Extention that adds claims based on Firestore document changes
Source:
Cloud Function Extention: https://github.com/FirebaseExtended/experimental-extensions/blob/next/firestore-auth-claims/README.md
Custom Cloud Function: https://github.com/digimbyte/FirebaseDevelopers/blob/master/Cloud%20Functions/Firestore%20Admin%20Manager.md

How to verify custom auth claim upon login?

Similar to Uber, I have two applications, one for clients and one for drivers. Is it possible to know which role type the user has upon login? For instance, if I have a client account and I try to log in on the driver's application I should get the error: "client accounts cannot be used to log into the driver application".
Let's say I stored the user's account type (driver or client) in a custom auth claim, would it be possible to access that while firebase auth is verifying the email and password, or does the user have to log in successfully before I can verify the value of the custom auth claim?
Essentially, if the user tries logging into the wrong application, I want it to come back as an error without actually logging them in. So far I've only been able to check for this after the user logs in using getIDTokenResult.
Any help is appreciated! :)
Essentially, if the user tries logging into the wrong application, I want it to come back as an error without actually logging them in.
You seem to be mixing authentication (the user enters credentials that prove who they are) with authorization (the user is allowed to do certain things based on who the are). Firebase Authentication solely is concerned with the former: allowing the user to sign in once they enter the correct credentials for their account. Once the user is signed in, your application code can then determine whether they're allowed to perform certain actions.
For your specific use-case for example, the idiomatic approach is to:
Sign the user in to Firebase Authentication.
Check whether their token contains the necessary claim for the app they're trying to use.
If so, allow them to continue to the main screen of your app.
If not, inform them of that fact and don't allow them to continue.
As you can see here, it is your application logic that handles all authorization logic, while Firebase takes care of the authentication.
The user must be logged in before checking the claims and anyways you cannot prevent anyone from logging in if it's a same firebase project. You should check the claim after login and if the user has logged into wrong application, just force them to logout. Security Rules can be used to prevent unauthorized access.
firebase.auth().signInWithEmailAndPassword().then(async ({user}) => {
const claims = await user.getIdTokenResult()
// check for claim
// if not valid then logout or redirect to relevant pages
await firebase.auth(can ).signOut()
})
You can show your error alerts after signing out.
If you really want to check the claim before logging the user in then you would have to use cloud functions which checks claims for the entered email but this method may not be useful in other sign in providers such as Google or Facebook.
Although I won't recommend using Cloud functions just to check the claims before users logs in as it just can be bypassed on the frontend and as mentioned above, forcing the user to logout should be enough. But here's a cloud function you can use to check the claims.
exports.checkClaim = functions.https.onCall((data, context) => {
const {email} = data;
return admin
.auth()
.getUser(uid)
.then((userRecord) => {
const {customClaims: {driver, client}} = userRecord;
if (driver) return {role: "driver"}
if (client) return {role: "client"}
return {error: "No role found"}
})
.catch((error) => {
console.log('Error fetching user data:', error);
});
});
Then call the function before you run the signInWithEmailAndPassword method.
const checkUserRole = firebase.functions().httpsCallable('checkClaim');
checkUserRole({ email: "user#domain.tld" })
.then((result) => {
const {role, error} = result;
if (error) {
alert("Something went wrong. No roles found")
} else {
console.log(`Your role is: ${role}`)
}
});
Again as mentioned above this sounds a bit overkill but if it's necessary or you prefer to do it that way then you use this function.

Firebase create user with customClaim

I'm using the Firebase SDK on a React Native app. I'm authenticating users with onAuthStateChanged - works great.
If it doesn't return a user, they can sign up using their phone number.
For that I use the following on submitting the phone activation code:
...
const credential = firebase.auth.PhoneAuthProvider.credential(
verificationId,
verificationCode
);
await firebase.auth().signInWithCredential(credential).then((response) => {// creating a record on firestore. onAuthStateChanged will be re-triggered and store the user and token in state});
...
I would also like to set custom claims for the user. How do I do that? I cannot use admin SDK since this is the frontend and I also don't want to. I could fire a call to my graphQL to do it, but there is probably a way to add a custom claim in the flow above. How?
There is no supported way to modify custom claims from within a client app. Since custom claims are normally used to give special secure authorizations, it obviously be a security hole to allow user to assign claims to themselves. That's why it's recommended to use the Admin SDK on a secure backend you control.
Custom claims can only be set from a trusted environment. Otherwise anyone could make any claim they want about themselves, which defeats their purpose of securely adding information to a user profile.

Does using Firebase Anonymous authentication allow me to secure my Firestore data and Cloud functions to only be accessed by my application?

I'm creating a mobile app that does not have any reason for the users to authenticate. However I don't want other people to write apps or websites that can access my data in Firestore, or call any of my Cloud Functions.
Does this mean I need to implement Anonymous Authentication and then write access rules that require the request to come from an authenticated user?
Or is there a way to write the rules to just say they must come from my application?
We had the same problem: an app for deployment to many people where it needs to be able to only read from firestore documents, but another admin app (actually just a web page) that will not be distributed, that needs to be able to write to those documents.
Our solution is to:
create another user for the database, with email and password authentication. (We are not allowing users to create accounts -- just having the one static email/password we've created.)
use only anonymous login for the regular app
have email/password signin for the "admin app".
add rules like these for each document:
allow read;
allow write: if request.auth.uid == 'notshowingyououridhere-sorry';
We are using ionic with typescript, so the code to do the user/password login is relatively simple:
firebase.initializeApp(credentials);
firebase.auth()
.signInWithEmailAndPassword('obfuscated#sorry.com', 'sorrynotshowingyouourpassword')
.catch(err => {
console.log('Something went wrong:', err.message);
});
firebase.auth().onAuthStateChanged((user) => {
if (user) {
// User is signed in.
const isAnonymous = user.isAnonymous;
const uid = user.uid;
console.log('onAuthStatChanged: isAnon', isAnonymous);
console.log('userid', user.uid);
} else {
console.log('onAuthStateChanged: else part...');
}
});
Hope this helps.

How to prove to the server that I as a client am logged in with the given uid?

Heres my problem:
I wan't to be able to create new users for my website, from my website. This is only aloud though, if I have the "isAdmin" flag set to true in the realtime db under /users/myid.
Generally I would have done this with a security rule but the problem here is that I need to use the admin SDK, since the normal "createNewUser" method signs in to the newly created user automatically. I as an admin though, want to be able to create a new user and stay logged in as myself. So what I wan't to do is use ajax post request to my server with my uid und the new userdata which is to be created. My server then checks if the given uid has the isAdmin flag and if so creates the user with the firebase admin SDK which provides such a method.
But, anyone, if they have an admins uid, could hit up that request and create a new user. (My clients definetely get uid's from other users).
So how would I go about proving to the server that I am actually logged in with that uid.
From my understanding, tokens are used to be able to write to the database, but I don't need that permission, I just need to prove that I'm actually logged in with that uid.
Is there something I'm missing? Thanks a lot guys!
Was easier then I thought. This will generate a token on the client side:
firebase.auth().currentUser.getToken(true).then(function(token) {
// send request to server with generated token
}).catch(function(error) {
// handle error
});
Which I can then verify on the server like so:
admin.auth().verifyIdToken(idToken)
.then(function(decodedToken) {
var uid = decodedToken.uid;
// user is logged in
}).catch(function(error) {
// user is not logged in, or other error occured
});
Taken from https://firebase.google.com/docs/auth/admin/verify-id-tokens

Resources