I have a basic security rule that checks if the user is authenticated.
{
"rules": {
"users": {
"$user_id": {
".write": "auth != null"
}
}
}
}
How can I get firebase security rules to acknowledge auth data from cloud functions when sent an access token from the client app.
Request Method = Post
import * as admin from 'firebase-admin'
const DEPLOYED = false;
admin.initializeApp()
const ValidateToken = (request: any, response: any) => {
const params = {
a: request.body.token, // Client Validation
}
const ValidateToken = admin.auth().verifyIdToken(params.a).catch((error) => { throw { Message:error }});
return Promise.all([ValidateToken]).then((res: any) => {
return DEPLOYED ? res : response.status(200).json(res);
}).catch(error => {
return DEPLOYED ? error : response.status(400).json(error);
});
}
export default ValidateToken;
Gives 200 responses and user data.
Update Username
import FBApp from '../utils/admin'
FBApp
const UpdateUsername = (request: any, response: any) => {
const params = {
a: request.body.UID,
b: request.body.username
}
const UpdateProfile = FBApp.database().ref('users').child(`${params.a}/username`).set(`#${params.b}`).catch((error) => { throw { Message:error }});
return Promise.all([UpdateProfile]).then((res: any) => {
response.status(200).json(res);
}).catch(error => {
response.status(400).json(error);
});
}
export default UpdateUsername;
Gives permission denied
For the Cloud Functions to work and run properly, they have administrative rights, which means, that they "bypass" the security rules set on your Firebase. For this reason, you just need to have your rules set to secure your application from unauthenticated users.
Besides that, I found this article below, which should provide you more information as well, on the use of rules with Cloud Functions and Firebase.
Patterns for security with Firebase: combine rules with Cloud Functions for more flexibility
Let me know if the information helped you!
Related
I am setting my auth rules in my realtime database (in firebase) but my understanding is weak in how it works despite reading the documentation.
I have authentication set up in firebase as well as firebase functions.
The specific rule that I am struggling with is confirming the user accessing the part in the database:
"$uid":{
".write": "$uid === auth.uid",
".read": "$uid === auth.uid",
},
Simply, is the path equal to the auth id?
I have the proper auth token coming to the backend as a header, and it is being retrieved using a middleware:
const idToken = req.headers.authorization.split("Bearer ")[1];
await adminApp
.auth()
.verifyIdToken(idToken)
.then((decodedIdToken) => {
req.auth = decodedIdToken;
next();
})
.catch((error) => {
console.log(error);
res.status(403).send(`Unauthorized ${error.message}`);
});
When I console req.auth before making the request in the function, it comes back with the full data:
{
name: 'Fake user',
iss: '---',
aud: '---',
auth_time: 1669298408,
user_id: '---',
sub: '---',
iat: 1669342950,
exp: 1669346550,
email: '---',
email_verified: true,
firebase: { identities: { email: [Array] }, sign_in_provider: 'password' },
uid: '---'
}
(obviously censored out here)
using this function:
console.log("GET ALL", req.auth);
const { uid } = req.auth;
get(child(dbRef, `${uid}/workouts`))
.then((snapshot) => {
if (snapshot.exists()) {
return snapshot.val();
} else {
console.log("No workouts available");
return null;
}
})
I know it has to do with the rules above because the error logged is a permissions denied type. The function also works perfectly fine when i have no rules in place...
edit:
this is the sign in function I have implemented for getting the auth token...
export const signIn = async (email, password) => {
try {
await setPersistence(auth, browserLocalPersistence);
const userCredential = await signInWithEmailAndPassword(
auth,
email,
password
);
const user = userCredential.user;
const token = await user.getIdToken(true);
return { user, token };
} catch (error) {
return { error: error.message };
}
};
decodedIdToken.uid is indeed the same ID as in those security rules. However, the security rules are bypassed entirely when accessed via one of the Admin SDKs anyway.
Additionally, I would recommend taking a look at the sample express middleware function: validateFirebaseIdToken() (as your version will throw an uncaught exception when the authorization header is not present)
I am trying to build callable cloud functions, when users delete a post, it also try to delete the comments, which is a sub-collection of the post. so I saw the example and implemented just like a documentation example
const admin = require('firebase-admin');
const firebase_tools = require('firebase-tools');
const functions = require('firebase-functions');
admin.initializeApp({
serviceAccountId: 'xxxxxx-xxxxx#appspot.gserviceaccount.com'
}
);
exports.mintAdminToken = functions.https.onCall(async (data: any, context: any) => {
const uid = data.uid;
const token = await admin
.auth()
.createCustomToken(uid, { admin: true });
return { token };
});
exports.recursiveDelete = functions
.runWith({
timeoutSeconds: 540,
memory: '2GB'
})
.https.onCall(async (data: any, context: any) => {
// Only allow admin users to execute this function.
if (!(context.auth && context.auth.token && context.auth.token.admin)) {
throw new functions.https.HttpsError(
'permission-denied',
'Must be an administrative user to initiate delete.'
);
}
const path = data.path;
console.log(
`User ${context.auth.uid} has requested to delete path ${path}`
);
await firebase_tools.firestore
.delete(path, {
project: process.env.GCLOUD_PROJECT,
recursive: true,
yes: true,
token: functions.config().fb.token
});
return {
path: path
};
});
and I succeeded in receiving the custom token to the client. but what I have to do now? after getting token I called the "recursiveDelete" function from client but it occurs error PERMISSION_DENIED
Should the user who received the token be initialized with a new custom admin token? (if I misunderstand let me know)
Is the admin token really necessary when deleting a sub collection like this? It's difficult to use, so I ask.
I don't think that you really need a custom token for this use case and I suggest that you use firebase firestore rules rather than implementing your own role based authentication.
Steps to follow:
1- create a collection that you may call "users" and include in it a field of the role that this user may have such as "ADMIN". every document id in this collection can be the auth uid of users that firebase auth generates. you can get this uid from your frontend by using the currentUser prop and it's all explained here
2- protect your database with firestore rules as such:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// only admins can remove posts
match /posts/{postID} {
allow read, write: if isAdmin();
}
// only admins can remove comments
match /comments/{commentID} {
allow read, write: if isAdmin();
}
// this function will check if the caller has an admin role and allow or disallow the task upon that
function isAdmin() {
return get(/databases/$(database)/documents/
users/$(request.auth.uid)).data.role == "ADMIN";
}
}
}
3- after you succefully deletes a post document you can create a function with onDelete trigger that get invoked and deletes the comments subcollection recursivley and to do that you should include this bit of code:
const client = require('firebase-tools');
exports.recursiveDelete = functions.firestore
.document('posts/{postID}')
.onDelete((snap, context) => {
.....
await client.firestore
.delete(collectionPath, {
project: process.env.GCLOUD_PROJECT,
recursive: true,
yes: true
});
}
I'm using firebase for my web/mobile apps and now have a back-end API I'm looking to use as well.
The API requires a JWT token to authenticate the requests and to set it up, I need to specify the JWT Secret that is used to encrypt/decrypt the token.
In firebase, I believe I retrieve the token using
const token = await firebase.auth().currentUser.getIdToken(); This is what I pass to the API.
However, I have not figured out where I get the JWT-secret to configure? I have tried the API key that is shown in firebase console, I have also tried the server/client keys found at my console at https://console.developers.google.com.
however, no matter what, I'm getting a JWSInvalidSignature when trying to make requests to the API Call.
Has anyone got this working? Where do I get the JWT-secret from firebase to configure on the API back-end? Thanks in advance.
Here are the details:
1. I am using a service called postGrest which auto-creates a web API on top of postgres DB. In order to authenticate the requests, you configure the service by specifying a custom claim called "role", and you also need to specify the JWT-secret so it can decode the token.
Here is my simple call to the API:
const fetchdata = async () => {
const token = await firebase.auth().currentUser.getIdToken();
let axiosConfig = {
headers: {
'Authorization': 'Bearer' + token
}
}
const data = await axios.get(`http://localhost:8080/users`,
axiosConfig);
}
Also note, I can simulate this in the bash command line using the following code: Note here that I'm getting the token from the getIdToken() above.
export TOKEN="eyJhbGciOiJSUzI1NiIsImtpZCI6ImQ2YzM5Mzc4YWVmYzA2YzQyYTJlODI1OTA0ZWNlZDMwODg2YTk5MjIiLCJ0eXAiOiJKV1QifQ.eyJ1c2VyaWQiOiI1NSIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9wb3N0Z3Jlc3QtYjRjOGMiLCJhdWQiOiJwb3N0Z3Jlc3QtYjRjOGMiLCJhdXRoX3RpbWUiOjE1NzExNTIyMjQsInVzZXJfaWQiOiJNMXZwQ3A2ZjlsaFdCblRleHh1TjlEdXIzUXAyIiwic3ViIjoiTTF2cENwNmY5bGhXQm5UZXh4dU45RHVyM1FwMiIsImlhdCI6MTU3MTE1OTQ0NSwiZXhwIjoxNTcxMTYzMDQ1LCJlbWFpbCI6InNwb25nZWJvYkBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsic3BvbmdlYm9iQGdtYWlsLmNvbSJdfSwic2lnbl9pbl9wcm92aWRlciI6InBhc3N3b3JkIn19.nKuovs0Gx_ZKp17dI3kfz6GQofIMEOTA8RqTluwEs-5r-oTbKgpG33uS7fs7txVxvWIb_3fbN3idzfDHZevprMkagbHOd73CxTFBM7pr1bD2OKSK9ZPYfSt9OhvgJL51vBN3voLcNAb5iWVVl2XMqkcXeDoBi8IOKeZr27_DsRx48GSi7HieHWscF1lujSEr2C9tdAek3YyNnr3IcGI8cTSPHPaIbYl-8CaHQO2fUiGHEAaD7sqHxp3otJio56zOoNAy44P_nwORlMFZC0Rm8SaATpbmIkgbGYWHZHty70lmlYGVHTuM_hr2s7z2YhAjuacvBMgusZpyoVnoe3FQeA"
curl http://localhost:8080/contacts -H "Authorization: Bearer $TOKEN"
What's returned is: {"message":"JWSError JWSInvalidSignature"}
For the JWT-secret, I have tried several values, but none seem to work. This includes the "API Key" from firebase project, as well as trying "Generate key" which downloads a new .json file and inside there is a "private_key": that is along string.
From your service account downloaded file, use the private_key value to validate/decode the JWT token you got from getIdToken()...
The steps for using a third-party library to validate a Firebase Auth ID token describe it in more detail.
First you need to get your service_account.json
Then setup firebase on your server.
Example:
// set_up_firebase.js
const admin = require("firebase-admin");
var serviceAccount = require("./service_account.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
storageBucket: "your-app.appspot.com"
});
exports.admin = admin;
Then create a middleware function to check jwt.
Example:
// verifyJWT.js
const fire = require("./set_up_fire.js");
function _getToken(req) {
return req.headers.authorization !== undefined
&& req.headers.authorization !== ""
&& req.headers.authorization.split(' ').length === 2
&& req.headers.authorization.split(' ')[1] !== undefined
&& req.headers.authorization.split(' ')[1] !== ""
? req.headers.authorization.split(' ')[1]
: "";
}
function getJWTExpiredPayload() {
return { "status": 401, "result": "ERROR", "error": "Unauthorized!" };
}
function getJWTNotValidPayload() {
return { "status": 403, "result": "ERROR", "error": "Forbidden!" };
}
module.exports = async (req, res, next) => {
const token = _getToken(req);
let verified = undefined;
if (token !== undefined && token !== null && token !== "") {
try {
verified = await fire.admin.auth().verifyIdToken(token, true);
if (verified !== undefined && verified["exp"] !== undefined
&& Date.now() < verified["exp"] * 1000) {
req.user = verified;
return next();
}
return res.status(401).json(getJWTExpiredPayload());
} catch (error) {
console.log(error);
}
}
return res.status(403).json(getJWTNotValidPayload());
}
Then in your router, call it before your api:
Example with Express:
const verifyJWT = require("./jwt/verifyJWT.js");
router.post("/verifyJWT", verifyJWT, (req, res) => {
return res.status(200);
})
I'm beginning to use custom claims in my Firebase project to implement a role-based authorization system to my app.
I'll have a firebase-admin script which is going to set {admin: true} for a specific user's uid. This will help me write better and clearer Firestore security rules.
admin.auth().setCustomUserClaims(uid, {admin: true})
So far, so good. My problem is that I'll also need a dashboard page to let me know which users are currently admins inside my app.
Basically I'll need a way to query/list users based on custom claims. Is there a way to do this?
From this answer, I can see that it's not possible to do this.
But maybe, Is there at least a way to inspect (using Firebase Console) the customUserClaims that were set to a specific user?
My current solution would be to store that information (the admins uid's) inside an admin-users collection in my Firestore and keep that information up-to-date with the any admin customClaims that I set or revoke. Can you think of a better solution?
I solved this use case recently, by duplicating the custom claims as "roles" array field into the according firestore 'users/{uid}/private-user/{data}' documents. In my scenario I had to distinguish between two roles ("admin" and "superadmin"). The documents of the firestore 'users/' collection are public, and the documents of the 'users/{uid}/private-user/' collection are only accessible from the client side by the owning user and "superadmin" users, or via the firestore Admin SDK (server side) also only as "superadmin" user.
Additionally, I only wanted to allow "superadmin" users to add or remove "superadmin" or "admin" roles/claims; or to get a list of "superadmin" or "admin" users.
Data duplication is quite common in the NoSQL world, and is NOT considered as a bad practice.
Here is my code (Node.js/TypeScript)
First, the firebase cloud function implementation (requires Admin SDK) to add a custom user claim/role.
Note, that the "superadmin" validation line
await validateUserClaim(functionName, context, "superadmin")
must be removed until at least one "superadmin" has been created that can be used later on to add or remove additional roles/claims to users!
const functionName = "add-admin-user"
export default async (
payload: string,
context: CallableContext,
): Promise<void> => {
try {
validateAuthentication(functionName, context)
validateEmailVerified(functionName, context)
await validateUserClaim(functionName, context, "superadmin")
const request = parseRequestPayload<AddAdminUserRoleRequest>(
functionName,
payload,
)
// Note, to remove a custom claim just use "{ [request.roleName]: null }"
// as second input parameter.
await admin
.auth()
.setCustomUserClaims(request.uid, { [request.roleName]: true })
const userDoc = await db
.collection(`users/${request.uid}/private-user`)
.doc("data")
.get()
const roles = userDoc.data()?.roles ?? []
if (roles.indexOf(request.roleName) === -1) {
roles.push(request.roleName)
db.collection(`users/${request.uid}/private-user`)
.doc("data")
.set({ roles }, { merge: true })
}
} catch (e) {
throw logAndReturnHttpsError(
"internal",
`Firestore ${functionName} not executed. Failed to add 'admin' or ` +
`'superadmin' claim to user. (${(<Error>e)?.message})`,
`${functionName}/internal`,
e,
)
}
}
Second, the firebase cloud function implementation (requires Admin SDK) that returns a list of "superadmin" or "admin" users.
const functionName = "get-admin-users"
export default async (
payload: string,
context: CallableContext,
): Promise<GetAdminUsersResponse> => {
try {
validateAuthentication(functionName, context)
validateEmailVerified(functionName, context)
await validateUserClaim(functionName, context, "superadmin")
const request = parseRequestPayload<GetAdminUsersRequest>(
functionName,
payload,
)
const adminUserDocs = await db
.collectionGroup("private-user")
.where("roles", "array-contains", request.roleName)
.get()
const admins = adminUserDocs.docs.map((doc) => {
return {
uid: doc.data().uid,
username: doc.data().username,
email: doc.data().email,
roleName: request.roleName,
}
})
return { admins }
} catch (e) {
throw logAndReturnHttpsError(
"internal",
`Firestore ${functionName} not executed. Failed to query admin users. (${
(<Error>e)?.message
})`,
`${functionName}/internal`,
e,
)
}
}
And third, the validation helper functions (require the Admin SDK).
export type AdminRoles = "admin" | "superadmin"
export const validateAuthentication = (
functionName: string,
context: CallableContext,
): void => {
if (!context.auth || !context.auth?.uid) {
throw logAndReturnHttpsError(
"unauthenticated",
`Firestore ${functionName} not executed. User not authenticated.`,
`${functionName}/unauthenticated`,
)
}
}
export const validateUserClaim = async (
functionName: string,
context: CallableContext,
roleName: AdminRoles,
): Promise<void> => {
if (context.auth?.uid) {
const hasRole = await admin
.auth()
.getUser(context.auth?.uid)
.then((userRecord) => {
return !!userRecord.customClaims?.[roleName]
})
if (hasRole) {
return
}
}
throw logAndReturnHttpsError(
"unauthenticated",
`Firestore ${functionName} not executed. User not authenticated as ` +
`'${roleName}'. `,
`${functionName}/unauthenticated`,
)
}
export const validateEmailVerified = async (
functionName: string,
context: CallableContext,
): Promise<void> => {
if (context.auth?.uid) {
const userRecord = await auth.getUser(context.auth?.uid)
if (!userRecord.emailVerified) {
throw logAndReturnHttpsError(
"unauthenticated",
`Firestore ${functionName} not executed. Email is not verified.`,
`${functionName}/email-not-verified`,
)
}
}
}
Finally, custom claims can be added or removed only on the server side as the according "setCustomUserClaims" function belong to the firebase Admin SDK, whereas the "get-admin-users" function could be implemented also on the client side. Here and here you will find more information about custom claims, including firestore rules for client side queries protected by a custom user claim/role.
I need to prevent user authentication if their email isn't found in allowedUsers. It doesn't really affect the app, since all the actions will be performed on the users list, but it will be nice if the authentication doesn't happen.
loginWithGoogle() {
const userDetails = this.afAuth.auth.signInWithPopup(new firebase.auth.GoogleAuthProvider())
.then(user => {
console.log(user.user.uid);
const queryObservable = this.db.list('/allowedUsers', {
query: {
orderByChild: 'email',
equalTo: user.user.email,
}
}).subscribe(data => {
if (data.length > 0) {
const userObj = {
admin: false,
...
email: data[0].email,
name: data[0].name,
verified: false,
};
this.addUser(userObj, user.user.uid);
}
});
});
return userDetails;
}
There is no way to prevent a user from authenticating with Firebase Authentication. All they do when they authenticate is say "I am X Yz" and prove it.
What you can do however is prevent that they have access to you database. For example to only allow white-listed users read-access to your database:
{
"rules": {
".read": "root.child('allowedUsers').child(auth.uid).exists()"
}
}
Also see:
How to disable Signup in Firebase 3.x
Firebase Authentication State Change does not fire when user is disabled or deleted (inverted version of your whitelist)