In my NextJS serversideprops, I am trying to pre fetch data from a firebase collection which has the rule that the user must be logged in (allow read, write: if request.auth.uid != null;)
I do save and get the user cookies using nookies and I am also able to verify the users token via
const { token } = nookies.get(context);
const user = await admin.auth().verifyIdToken(token);
This does return me the user data, however even though it returns me the data, I do not think the user is logged in in the server, hence why the firestore rule blocks it. How do I get around this?
Related
My application is a company-internal software. I want to enable all authenticated users to access all documents in the Firestore for tests.
I ran into a mistake while doing this.
The user logs in to the iOS app and has access to the documents.
I delete the user from Firebase Auth (via Firebase console)
As long as the user has the app open, he receives updates and can read and write.
Here is the code from the rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read : if request.auth != null
allow write: if request.auth != null
}
}
}
How can I prevent deleted users from continuing to have access?
The user logs in to the iOS app and has access to the documents.
That's the expected behavior.
I delete the user from Firebase Auth (via the console) As long as the user has the app open, he receives updates and can read and write.
When a user signs in with Firebase, he receives a token that is valid for about an hour. Unfortunately, such a token cannot be revoked, due to expensive checks on each call.
If you delete a user account right from the Firebase Console, the user can still have access for up to an hour. After that period of time, the token needs to be refreshed. But this operation will fail since that account doesn't exist anymore. So that access will automatically be disabled within an hour.
However, if you want to remove that access before the token expires, then you should consider keeping an additional list of banned UIDs and maintaining it over time. For instance, you can keep a global list/array of bannedUIDs into a document, and add the UID to that. Lastly, in your security rules, you can check if that particular UID is not banned. If that UID exists inside that list, then Firebase servers will reject the operation.
Edit:
Another option might be to disable the user account. This accomplishes the same as above and the user won't be able to get a new token after the current token expires. It also prevents the user from signing up again with the same credentials.
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
Am calling these functions in sequence (javascript 'firebase`):
firebase
.auth()
.signInWithEmailLink(email, url)
.then(userCred => {
return firebase.firestore().collection('users').doc('someDocID').get()
....
I have security rules setup in firestore that allows read of 'users/:docId' only if the email field in that document equals the auth email:
allow get: if request.auth.token.email == resource.data.email
The problem is we get a missing or insufficient permissions error on the get().
For sure the email in the database is correct so the only problem could be is that request.auth.token.email is not valid.
Could it be that signing in (using any sign in function), even if resolves, doesn't guarantee the auth token is updated and can be immediately used ?
Is there propagation time? should we call getIdToken() after sign in resolves to make sure the token is valid before making database/firestore calls?
When a user actively signs in (by calling any of the signInWith... methods), the client gets an ID token, which contains the up-to-date values for that user. That ID token is valid for an hour, and is then auto-refreshed by the client/SDK.
So right after the signIn... call, the profile should contain the latest values. If you think something is going wrong there, I'd recommend logging the values right before making the call to Firestore.
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.
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.