Firebase - Prevent user authentication - firebase

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)

Related

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

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)

Create a new User when authenticating with Google

How can I create a document user in firebase when I first authenticate or multiple times ???
Example: I choose to authenticate by google, Twitter .... by account abc#gmail.com ... and I want to save to collection "user" with document = User UId (google account).
With the certificate authentication method I don't know how to get the Google User User UID until it's added to the Authentication List.
signInWithGoogleAsync = async () => {
try {
const { type, accessToken } = await Google.logInAsync({
iosClientId: '1058889369323-kh44jtru0ar24qu24ebv1bu0ebrgvokd.apps.googleusercontent.com',
scopes: ['profile', 'email'],
});
if (type === 'success') {
this.setState({ spinner: true });
const credential = firebase.auth.GoogleAuthProvider.credential(null, accessToken);
firebase.auth().signInWithCredential(credential).catch(error => {console.log(error);});
///I know how to do this, authenticate 1 user and put it in the Authentication list when authenticating successfully the first time
} else {
return { cancelled: true };
}
} catch (e) {
return { error: true };
}
}

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!

How to get users from Firebase auth based on custom claims?

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.

How to add additional information to firebase.auth()

How can I add extra attributes phone number and address to this data set? It seems like Firebase documentation doesn't specify anything about that.
I have implemented the login, register and update using firebase.auth()
Login :
//Email Login
firebase.auth().signInWithEmailAndPassword(email, password).then(
ok => {
console.log("Logged in User",ok.user);
},
error => {
console.log("email/pass sign in error", error);
}
);
Register:
//Sign Up
firebase.auth().createUserWithEmailAndPassword(email, password).then(
ok => {
console.log("Register OK", ok);
},
error => {
console.log("Register error", error);
}
)
Update:
//User Authentication
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
$scope.data=user;
} else {
// No user, Redirect to login page
}
});
//Save Function
$scope.save=function(values){
$scope.data.updateProfile({
displayName: "Test User",
email: "test#gmail.com",
/* phone: 123412341,
address: "Temp Address",*/
photoURL: "www.example.com/profile/img.jpg"
}).then(function() {
// Update successful.
}, function(error) {
// An error happened.
});
};
As far as I know, you have to manage the users profiles by yourself if you want to have more fields than the default user provided by Firebase.
You can do this creating a reference in Firebase to keep all the users profiles.
users: {
"userID1": {
"name":"user 1",
"gender": "male"
},
"userID2": {
"name":"user 2",
"gender": "female"
}
}
You can use onAuthStateChanged to detect when the user is logged in, and if it is you can use once() to retrieve user's data
firebaseRef.child('users').child(user.uid).once('value', callback)
Hope it helps
This can be done by directly storing your custom data in Firebase Auth as "custom claims" on each user via the Admin SDK on your backend.
Note this can't be done purely client-side, your server (or you can use a Cloud Function as per the linked guide if you don't already have a server/API set up) needs to make a request through the Admin SDK to securely set the data using the admin.auth().setCustomUserClaims() method:
https://firebase.google.com/docs/auth/admin/custom-claims#defining_roles_via_an_http_request
You could write some code that combines data from firebase auth and firestore document and expose that to the app as a single data entity. To take subscriptions and notify that changes to the whole app, you would be better served with event libraries like Rxjs. Bellow, I wrote the example below using a simple library that implements an event bus.
// auth.js
import { publish } from '#joaomelo/bus'
import { fireauth, firestore } from './init-firebase.js'
const authState = {
userData: null
};
fireauth.onAuthStateChanged(user => {
if (!user) {
authState.userData = null;
publish('AUTH_STATE_CHANGED', { ...authState });
return;
}
// we must be carefull
// maybe this doc does not exists yet
const docRef = firestore
.collection('profiles')
.doc(user.uid);
docRef
// 'set' secures doc creation without
// affecting any preexisting data
.set({}, { merge: true })
.then(() => {
docRef.onSnapshot(doc => {
// the first data load
// and subsequent updates
// will trigger this
authState.userData = {
id: user.uid,
email: user.email,
...doc.data()
};
publish('AUTH_STATE_CHANGED', { ...authState });
});
});
});
// some-place-else.js
import { subscribe } from '#joaomelo/bus'
subscribe('AUTH_STATE_CHANGED',
authState => console.log(authState));
You can expand on that in a post I wrote detailing this solution and also talking about how to update those properties. There is too a small library that encapsulates the answer with some other minor features with code you could check.

Resources