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

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

Related

Firebase email/password authentication - how to require email verification?

Whenever I use the email/password authentication provider in Firebase, the provider sends a bearer token upon successful sign-up even though the emailVerified is false. Is there a way, out of the box, to configure the email/password auth provider to not send a bearer token (and return a 403 error) until the user has verified their email address?
Note that I'm aware of how to create a user, sign in a user, send a verification email, etc... using firebase v9.x via the methods createUserWithEmailAndPassword, signInWithEmailAndPassword, signOut, sendEmailVerification from firebase/auth. I'm just asking if there is a way to set the behavior of the provider without having to write my own handler function for this. I'd like this to behave like Cognito does whenever the email verification is required.
There is no way to require the user's email address to be verified before they can sign in to Firebase Authentication.
The closest you can get is by using email-link sign-in, which combines signing in and verifying the user's email address in one action.
But this is how you'll typically want to implement this in your application code:
User enters their credentials
You sign them in to Firebase with those credentials
You check whether their email address is verified
If not, you stop them from further using the app - and (optionally) send them a verification email.
Same with data access: if you have a custom backend code, you can check whether the email address is verified in the ID token there too, as well as in Firebase's server-side security rules.
As per the documentation, you can use blocking functions to require email verification for registration (only that it doesn't work):
exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
const locale = context.locale;
if (user.email && !user.emailVerified) {
// Send custom email verification on sign-up.
return admin.auth().generateEmailVerificationLink(user.email).then((link) => {
return sendCustomVerificationEmail(user.email, link, locale);
});
}
});
exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => {
if (user.email && !user.emailVerified) {
throw new functions.auth.HttpsError(
'invalid-argument', `"${user.email}" needs to be verified before access is granted.`);
}
});
generateEmailVerificationLink always returns the following error:
"err": {
"message": "There is no user record corresponding to the provided identifier.",
"code": "auth/user-not-found"
},
but the user is created anyway given that beforeCreate don't return an exception.
If you want to check by yourself just log the error:
return admin.auth().generateEmailVerificationLink(user.email)
.then((link) => {
functions.logger.info("link", {user: user, context: context, link: link})
})
.catch((err) => {
functions.logger.info("error", {user: user, context: context, err: err});
});
The createUserWithEmailAndPassword() will sign in user right after the account is created. Also there isn't any way to prevent users from logging in even if their email is not verified but you can actually check if email is verified in security rules or using Admin SDK to prevent users with unverified email from accessing your resources. You can use this rule in Firestore:
allow read, write: if request.auth.token.email_verified == true;
One workaround would be creating users using a Cloud function and Admin SDK which won't sign in users but do note that users can sign in.
If you want to prevent login unless the email is verified strictly, then you can disable account right after it is created. Now you may not be able to use sendEmailVerification() which requires user to be signed in at first place, you can always create your own solution for verifying email. The process might look something like:
Create a user and disable the account in a Cloud function
Generate some token or identifier for verifying email and send an email to user from same cloud function
Once the user visits that link and verifies the email you can enable it
Additionally, users can still create accounts by using REST API but you can disable sign ups so users can be created via Cloud function only which disables the user immediately.

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 Auth verify this user

I am currently verifying my user using the Auth JS SDK and Admin Auth SDK combined. I am doing in the following approach:
In the front-end:
firebase.auth().onAuthStateChanged(function (user) {
if (user) {
var current_user = firebase.auth().currentUser;
current_user.getIdToken(true).then(function (idToken) {
$.getJSON('/firebase_token', { token: idToken }, function (user) {
In the back-end:
router.get("/firebase_token", (req, res, next) => {
admin.auth().verifyIdToken(req.query.token).then(function(decodedToken) {
res.send(decodedToken);
})
})
I am wondering if this is a secured approach, because the user can just send whatever token they want from the front-end. For example, an invalid user can send a valid token they copied from a valid account to pass the token verification.
I am wondering if in the admin SDK. There is a way to detect the currently signed in user. In other words, detect this user who is using this instance of the app with the admin SDK?
I am wondering if this is a secured approach, because the user can just send whatever token they want from the front-end. For example, an invalid user can send a valid token they copied from a valid account to pass the token verification.
Yes, that's possible. But then again, if the user got access to a token, that means they probably are the user represented by that token, or they know the credentials of that account. That's not a problem at all - this is the way authentication systems work.
I am wondering if in the admin SDK. There is a way to detect the currently signed in user. In other words, detect this user who is using this instance of the app with the admin SDK?
No, the Admin SDK can't possibly know what all is going on for all of the users using your application. The ID token is exactly the piece of information it needs to verify users. A valid token proves that the user is who they say they are.

firebase auth is it possible to restrict login to some users?

currently I have implemented a login function with firestore.
btnLogin.addEventListener('click', e => {
// Get email and password
const email = txtEmail.value;
const pass = txtPassword.value;
const auth = firebase.auth();
// Sign in
const promise = auth.signInWithEmailAndPassword(email, pass);
promise.catch(e => console.log(e.message));
})
I am wondering if it's possible to restrict the login to only a particular email, the only way I am thinking is manually checking for the email before authenticating - but wondering if there are any official ways of doing this. Even extended for more than one email.
My temporary solution (?)
btnLogin.addEventListener('click', e => {
// Get email and password
const email = txtEmail.value;
const pass = txtPassword.value;
const auth = firebase.auth();
// Sign in
if (email == "email_desired" | email == "email_desired2"){
const promise = auth.signInWithEmailAndPassword(email, pass);
promise.catch(e => console.log(e.message));
}
else{
console.log("Email is not accepted at login")
}
})
The purpose I am doing this is to only allow fixed users to access the admin login-panel.
In ur app, dont give option to create new user.
Only keep ur app limted to Login with email and password.
You cam manually create users on firebase web console.
This way only users u created will be able to login.
If you have email/password sign-in enabled, you can't stop people from trying to create accounts and signing in. What you've suggested in your question isn't really "security" since client code can be compromised and bypassed. However, you can use security rules to determine who can actually access your database. Since you tagged this google-cloud-firestore, I'll assume that's what you're using.
Firestore has security rules that you can use to determine who has access to what data. For an admin panel where you trust a certain set of accounts under your control (that you can create manually yourself), you can simply whitelist them in your rules. Assuming that you have a list of UIDs, the easiest thing to do, if you trust them all with any actions in the database, is simply give them all full access:
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth.uid in ['uid1', 'uid2', 'uid3'];
}
}
}
You might want something more sophisticated, in which case, you will really have to learn the security rules system and make the best use of it. Or, you can route all user actions through a backend you control, and verify the user's Firebase Auth ID token using the Firebase Admin SDK before committing the action.
You can block those users in firebase console by disable those accounts in Authentication Section.
Elaborating a bit on Doug's answer above, I've found something like this helpful:
Create a collection (assuming Firestore) called cms_users or similar where you will keep the users that should be able to access the application. I also store more granular permissions on the table.
Create a Firestore rule function that will get whether or not the requesting user appears in that table, e.g.
function getCMSUser() { return get(/databases/$(database)/documents/cms_users/$(request.auth.uid)); }
Create a rule that ensures that the collection is only readable by users that are in that collection:
match /phoenixcms_users/{userId} { allow read: if getCMSUser() != null; }
And now, for example, if you have data that you want to restrict read/writes to only those users, something like:
match /cms_secret_collection/{document=**} { allow read: if getCMSUser() != null; allow write: if getCMSUser() != null && getCMSUser().data.permissionLevel in ["owner", "admin"]; }
In the above example, you can see that all 'CMS' users can view the data, but only certain CMS users with owner or admin permission can write the data.

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