How is the request.auth payload passed into realtime database's rules? - firebase

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)

Related

Middleware Firebase authentication clarification

I'm setting up my API routes with express and mongoose. Is this a secure way to do user authentication? Is there any way that the user could somehow inject another Firebase user.uid to get the token of an admin user (I'm using Firebase for auth)?
Backend:
myRoute.route('/sample/:id').delete((req, res, next) => {
var user = req['currentUser'];
UserModel.findById(user.uid, (error, data) => {
if (error) {
return next(error)
} else {
user = data;
if (user.admin) {
SampleModel.findByIdAndRemove(req.params.id, (error, data) => {
if (error) {
return next(error)
} else {
res.status(200).json({
msg: data
})
}
})
} else {
res.status(403).send('You are not authorised!');
}
}
})
})
async function decodeIDToken(req, res, next) {
if (req.headers?.authorization?.startsWith('Bearer ')) {
const idToken = req.headers.authorization.split('Bearer ')[1];
console.log(idToken);
try {
const decodedToken = await admin.auth().verifyIdToken(idToken);
req['currentUser'] = decodedToken;
} catch (err) {
console.log(err);
}
}
next();
}
Frontend:
const user = auth.currentUser;
const token = user && (await user.getIdToken());
axios.delete(`${this.baseApiURL}/sample/${id}`, { headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
}
}).then(() => {
console.log("Done");
})
Is this a secure way to do user authentication?
Yes, just verifying the Firebase ID Token is enough.
Is there any way that the user could somehow inject another Firebase user.uid to get the token of an admin user
Creating a JWT is pretty straightforward but you'll need to know the exact signing key that Firebase uses to sign the token else verifyIdToken() will thrown an error.

Authenticate an user on firebase rest api security rules

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!

CustomToken not working when issued through "createCustomToken" method

I am using Firebase authentication for my application and using it to authenicate users to a back-end API using JWT tokens. On the API back-end I've configured the JWT-secret, which is the asymmetric keys pulled from this url:
https://www.googleapis.com/service_accounts/v1/jwk/securetoken#system.gserviceaccount.com
This is all working fine. I recently needed to create a cloud function, which needs to call the API back-end as well. To do this, I'm using the functionality to create a Custom Token found here:
https://firebase.google.com/docs/auth/admin/create-custom-tokens
This creates my token with correct custom claims
let additionalClaims = {
'x-hasura-default-role': 'admin',
'x-hasura-allowed-roles': ['user', 'admin']
}
admin.auth().createCustomToken(userId,additionalClaims).then(function (customToken) {
console.log(customToken);
response.end(JSON.stringify({
token: customToken
}))
})
.catch(function (error) {
console.log('Error creating custom token:', error);
});
however, when I try to use it against the back-end API, I get the "JWTInvalidSignature" error. In my cloud function, I specify the service account that is in my firebase project, but it doesn't seem to help. When I view the two tokens decoded, they definitely appear coming from different services.
CustomToken
{
"aud":
"https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",
"iat": 1573164629,
"exp": 1573168229,
"iss": "firebase-adminsdk-r2942#postgrest-b4c8c.iam.gserviceaccount.com",
"sub": "firebase-adminsdk-r2942#postgrest-b4c8c.iam.gserviceaccount.com",
"uid": "mikeuserid",
"claims": {
"x-hasura-default-role": "admin",
"x-hasura-allowed-roles": [
"user",
"admin"
]
}
}
TOKEN from FireBase Auth
{
"role": "webuser",
"schema": "customer1",
"userid": "15",
"claims": {
"x-hasura-default-role": "user",
"x-hasura-allowed-roles": [
"user",
"admin"
],
"x-hasura-user-id": "OS2T2rdkM5UlhfWLHEjNExZ71lq1",
"x-hasura-dbuserid": "15"
},
"iss": "https://securetoken.google.com/postgrest-b4c8c",
"aud": "postgrest-b4c8c",
"auth_time": 1573155319,
"user_id": "OS2T2rdkM5UlhfWLHEjNExZ71lq1",
"sub": "OS2T2rdkM5UlhfWLHEjNExZ71lq1",
"iat": 1573164629,
"exp": 1573168229,
"email": "johnny1#gmail.com",
"email_verified": false,
"firebase": {
"identities": {
"email": [
"johnny1#gmail.com"
]
},
"sign_in_provider": "password"
}
}
How can I get this customToken to work with the existing JWT secret keys I have configured??
As documented in in Firebase Authentication: Users in Firebase Projects: Auth tokens, the tokens from Firebase Auth and the Admin SDK Custom Tokens are not the same, incompatible with each other and are verified differently.
Edited response after clarification:
As you are trying to identify the cloud functions instance as an authorative caller of your third-party API, you may use two approaches.
In both of the below methods, you would call your API using postToApi('/saveUserData', { ... }); in each example. You could probably also combine/support both server-side approaches.
Method 1: Use a public-private key pair
For this version, we use a JSON Web Token to certify that the call is coming from a Cloud Functions instance. In this form of the code, the 'private.key' file is deployed along with your function and it's public key kept on your third-party server. If you are calling your API very frequently, consider caching the 'private.key' file in memory rather than reading it each time.
If you ever wish to invalidate this key, you will have to redeploy all your functions that make use of it. Alternatively, you may modify the fileRead() call and store it in Firebase Storage (secure it - readable by none, writable by backend-admin). Which will allow you to refresh the private key periodically by simply replacing the file.
Pros: Only one remote request
Cons: Updating keys could be tricky
const jwt = require('jsonwebtoken');
const rp = require('request-promise-native');
const functionsAdminId = 'cloud-functions-admin';
function getFunctionsAuthToken(jwtOptions) {
jwtOptions = jwtOptions || {};
return new Promise((resolve, reject) => {
// 'private.key' is deployed with function
fs.readFile('private.key', 'utf8', (err, keyData) => {
if (err) { return reject({src: 'fs', err: err}); }
jwt.sign('cloud-functions-admin', keyData, jwtOptions, (err, token) => {
if (err) { return reject({src: 'jwt', err: err}); }
resolve(token);
});
});
});
}
Example Usage:
function postToApi(endpoint, body) {
return getFunctionsAuthToken()
.then((token) => {
return rp({
uri: `https://your-domain.here${endpoint}`,
method: 'POST',
headers: {
Authorization: 'Bearer ' + token
},
body: body,
json: true
});
});
}
If you are using express on your server, you can make use of express-jwt to deserialize the token. If configured correctly, req.user will be 'cloud-functions-admin' for requests from your Cloud Functions.
const jwt = require('express-jwt');
app.use(jwt({secret: publicKey});
Method 2: Add a cloud-functions-only user
An alternative is to avoid the public-private key by using Firebase Auth. This will have the tradeoff of potentally being slower.
Pros: No key management needed, easy to verify user on server
Cons: Slowed down by Firebase Auth calls (1-2)
const admin = require('firebase-admin');
const rp = require('request-promise-native');
const firebase = require('firebase');
const functionsAdminId = 'cloud-functions-admin';
function getFunctionsAuthToken() {
const fbAuth = firebase.auth();
if (fbAuth.currentUser && fbAuth.currentUser.uid == uid) {
// shortcut
return fbAuth.currentUser.getIdToken(true)
.catch((err) => {src: 'fb-token', err: err});
}
return admin.auth().createCustomToken(functionsAdminId)
.then(function(customToken) {
return fbAuth.signInWithCustomToken(token)
.then(() => {
return fbAuth.currentUser.getIdToken(false)
.catch((err) => {src: 'fb-token', err: err});
})
.catch((err) => {src: 'fb-login', err: err});
})
.catch((err) => {src: 'admin-newtoken', err: err});
}
Example Usage:
function postToApi(endpoint, body) {
return getFunctionsAuthToken()
.then((token) => {
return rp({
uri: `https://your-domain.here${endpoint}`,
method: 'POST',
headers: {
Authorization: 'Bearer ' + token
},
body: body,
json: true
});
});
}
On your server, you would use the following check:
// idToken comes from the received message
admin.auth().verifyIdToken(idToken)
.then(function(decodedToken) {
if (decodedToken.uid != 'cloud-functions-admin') {
throw 'not authorized';
}
}).catch(function(error) {
// Handle error
});
Or if using express, you could attach it to a middleware.
app.use(function handleFirebaseTokens(req, res, next) {
if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
var token = req.headers.authorization.split(' ')[1];
admin.auth().verifyIdToken(idToken)
.then((decodedToken) => {
req.user = decodedToken;
next();
}, (err) => {
//ignore bad tokens?
next();
});
} else {
next();
}
});
// later on: req.user.uid === 'cloud-functions-admin'
Original response:
If your client uses Firebase Authentication from an SDK to log in and your server uses the Admin SDK, you can use the client's ID token on the cloud function to speak to your server to verify a user by essentially "passing the parcel".
Client side
firebase.auth().currentUser.getIdToken(/* forceRefresh */ true).then(function(idToken) {
// Send token to your cloud function
// ...
}).catch(function(error) {
// Handle error
});
Cloud Function
// idToken comes from the client app
admin.auth().verifyIdToken(idToken) // optional (best-practice to 'fail-fast')
.then(function(decodedToken) {
// do something before talking to your third-party API
// e.g. get data from database/secret keys/etc.
// Send original idToken to your third-party API with new request data
}).catch(function(error) {
// Handle error
});
Third-party API
// idToken comes from the client app
admin.auth().verifyIdToken(idToken)
.then(function(decodedToken) {
// do something with verified user
}).catch(function(error) {
// Handle error
});

Firebase Rules are not working. Returning data even when I set up rules in Firestore

I set up Firebase Functions to make calls to my Firestore. I'm using admin.auth() and returning data. I set up custom rules in the Firestore Rules section, but the Functions are not following the Rules i.e. when I use the URL in Postman, I shouldn't be getting the data because it doesn't fulfill "if read, write: if request.auth != null". How do I address this?
Here is my Firebase Function Code:
const admin = require('firebase-admin');
module.exports = function(req, res) {
const uid = req.body.uid;
admin
.auth()
.getUser(uid)
.then(user => {
admin
.firestore()
.collection('discover')
.get()
.then(snapshot => {
res.send(
snapshot.docs.map(doc => {
const data = Object.assign({ doc_id: doc.id }, doc.data());
return data;
})
);
})
.catch(err => {
res.send({ message: 'Something went wrong!', success: false });
});
})
.catch(err => {
res.send({ error: 'Something went wrong!', success: false });
});
};
Firestore Rules:
service cloud.firestore {
match /databases/{database}/documents {
match /users/{users} {
allow read: if request.auth != null;
}
match /discover/{discover} {
allow read: if request.auth != null;
}
match /favorites/{favorite} {
allow read, write: if request.auth != null;
}
}
}
I shouldn't be able to get this data from Postman (Since I'm not authenticated) but I'm still getting the data. I don't want the data to be accessible if the user is not logged in.
Security rules don't apply when you access the database via the Admin SDK, or any other time you use a service account. It doesn't matter at all that you're using postman (or any other HTTP client). The thing actually doing the database access here is the Admin SDK.

I don't have access to custom claims from firestore security rules

I have set a custom claim using the firebase admin sdk. I have successfully use it to control access in the frontend and even with the RTDB, but I'm not able to use it with the Firestore database. Here is my security rule:
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth.token.admin == true;
}
}
}
Here is the code in my app:
const users = []
firebase.firestore().collection('users')
.get()
.then(function (querySnapshot) {
querySnapshot.forEach(function (doc) {
users.push(doc.data())
})
})
.then(() => {
commit('setUsersList', users)
commit('setLoading', false)
})
.catch(function (error) {
console.log('Error getting documents:', error)
commit('setLoading', false)
})
And here is the error I'm getting:
Error: Missing or insufficient permissions
I have fixed the error. My mistake was that I was setting the permission using the admin sdk but I wasn't passing a boolean but a string.
For example, I was setting the user with uid '1' like this:
axios.post('/admin/setadminprivileges/1/true')
In my firebase functions I was getting:
app.post('/admin/setadminprivileges/:id/:permission', (req, res) => {
const permission = req.params.permission // this is a string "true"
const uid = req.params.id // "1"
const payload = {admin: permission}
admin.auth().setCustomUserClaims(uid, payload)
})
And now with this is working fine:
app.post('/admin/setadminprivileges/:id/:permission', (req, res) => {
const permissionString = req.params.permission
const permission = permissionString === 'true' // this is now a boolean
const uid = req.params.id
const payload = {admin: permission}
admin.auth().setCustomUserClaims(uid, payload)
})
Thanks anyway. I knew it had to be a silly issue of my own, because Firebase is a solid product.

Resources