I am struggling with security rules in Firebase for anonymously authenticated users. I want users to be able to create, read, update, and delete their own projects using anonymous authentication. When I use the code below, Firebase denies permission to the database: Error: permission_denied: Client doesn't have permission to access the desired data.
Does my Angular code need to first create a users folder of some sort in Firebase even though I'm using anonymous authentication?
[EDIT: I've included my routes and some additional code in case it helps.]
Javascript:
myApp.factory('fbAuth', function($firebaseAuth, $firebase) {
var ref = new Firebase('https://xxxxxxxxxx.firebaseio.com');
var authData = ref.getAuth();
if (authData) {console.log('Authenticated user with uid:', authData.uid); } else {
ref.authAnonymously(function (error, authData) {
if (error) {
console.log('Login Failed!', error);
} else {
console.log('Authenticated successfully with payload:', authData);
}
});
}
return authData;
});
myApp.factory('Projects', function($firebase, fbURL) {
return $firebase(new Firebase(fbURL+'/projects')).$asArray();
});
myApp.factory('Selections', function($firebase, fbURL) {
return $firebase(new Firebase(fbURL+'/services')).$asArray();
});
myApp.controller('ProjectListCtrl', function ProjectListCtrl(Projects) {
var projectList = this;
projectList.projects = Projects;
projectList.total = function(){
var total = 0;
angular.forEach(projectList.projects, function(project) {
total += project.type.cost;
});
return total;
};
});
myApp.controller('SelectionListCtrl', function (Selections) {
var selectionList = this;
selectionList.selections = Selections;
this.selectedServices = Selections;
});
Routes:
myApp.config(function ($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/');
$stateProvider
.state('selection', {
url: '/',
views: {
'list': {
url: '',
templateUrl: 'views/list.html',
controller: 'ProjectListCtrl as projectList',
}
},
'selectionlist': {
templateUrl: 'views/selectionlist.html',
controller: 'SelectionListCtrl as selectionList',
}
})
Security Rules:
{
"rules": {
"projects": {
"$project_id" : {
"$uid": {
".read": "auth != null && auth.uid === $uid",
".write": "auth !=null && auth.uid === $uid" }
}
},
"$other": {".validate": false }
}
}
Firebase Data Structure
Root
- projects
+ -JiVDL4RUSladYTqqHl6
+ -JiVIdH8QIQ8o8q3iKvf
+ -JiYY44i6AOGzTjPDNVM
Related
Since fragments are not supported in aad redirect_uris, I made the redirect_uri my homepage with navigateToLoginRequestUrl. After sign-in, instead of being directed to my_host/#code=...reest-of-aad-response, vue router seems to jump in and hashbang the url to my_host/#/code=...rest-of-aad-response which 404s.
Do I need to switch to history or is there something I am missing and a way to accomplish this in hash mode? Should I use loginPopup instead of loginRedirect?
msal service
import * as msal from '#azure/msal-browser';
export default class msalAuth {
constructor(config) {
const msalConfig = {
auth : {
clientId : config.clientId,
authority : config.authority,
redirectUri : config.redirectUrl,
navigateToLoginRequestUrl : true
},
cache : {
cacheLocation : 'localStorage',
storeAuthStateInCookie : true
},
system: {
loggerOptions: {
loggerCallback: (level, message, containsPii) => {
if (containsPii) {
return;
}
switch (level) {
case msal.LogLevel.Error:
console.error(message);
return;
case msal.LogLevel.Info:
console.info(message);
return;
case msal.LogLevel.Verbose:
console.debug(message);
return;
case msal.LogLevel.Warning:
console.warn(message);
return;
}
}
}
}
};
let graphScopes = Object.values(config.graphScopes);
let state = window.location.origin;
let postLogoutRedirectUri = config.logoutRedirect;
let graphUrl = config.graphUrl;
this.msalAppConfig = {
graphScopes,
state,
loginRequest: {
scopes: graphScopes,
state
},
postLogoutRedirectUri,
graphUrl
};
this.app = new msal.PublicClientApplication(msalConfig);
}
login() {
this.app.loginRedirect(this.msalAppConfig.loginRequest);
}
logout(userName) {
const logoutRequest = {
account : this.app.getAccountByUsername(userName),
postLogoutRedirectUri : this.msalAppConfig.postLogoutRedirectUri,
mainWindowRedirectUri : this.msalAppConfig.postLogoutRedirectUri
}
this.app.logoutPopup(logoutRequest);
}
async handleRedirectPromise() {
return await this.app.handleRedirectPromise();
}
processRedirectResponse(response) {
let accountId = '';
console.log('processRedirectResponse', response);
if (response) {
accountId = response.account.homeAccountId;
// Display signed-in user content, call API, etc.
} else {
// In case multiple accounts exist, you can select
const currentAccounts = this.app.getAllAccounts();
if (currentAccounts.length === 0) {
// no accounts signed-in, attempt to sign a user in
//this.loginRedirect();
} else if (currentAccounts.length > 1) {
// Add choose account code here
accountId = currentAccounts[0].homeAccountId;
} else if (currentAccounts.length === 1) {
accountId = currentAccounts[0].homeAccountId;
}
}
return accountId;
}
}
redirectUri is http://localhost:8080 as am still developing
Thanks!
I switched vue router mode to history instead of hash, and it resolved the issue for anyone coming here with the same problem
Edit: for anyone coming to this and being dismayed that I switched to history mode and are using Azure static webapps. I added a staticwebapp.config.json to my public folder (or anywhere which will place it in root of output when built). This file lets you provide some configuration to the static web app. You can read about it in the ms docs but mine was the following which you can edit / build off of
{
"routes": [
{
"route": "/*",
"serve": "/index.html",
"statusCode": 200
}
],
"navigationFallback": {
"rewrite": "/index.html",
"exclude": [
"/icons/*.{png,jpg,gif,webp,svg}",
"/css/*",
"favicon.ico",
"/fonts/*"
]
},
"mimeTypes": {
".woff2": "font/woff2",
".woff": "font/woff",
".json": "text/json",
".ico": "image/x-icon"
}
}
I am working on a project with angularfire and I am trying to implement the method to update user password. Due the messed documentation about it, please help me to find a solution to re-authenticate an user. I've already read this stackoverflow question
account.js:
vm.updateUserPassword = function() {
if (vm.oldPassword && vm.newPassword && vm.confirmNewPassword) {
if (vm.newPassword === vm.confirmNewPassword) {
var currentCredential = firebaseAuth.EmailAuthProvider.credential(vm.currentAuth.email, vm.oldPassword);
vm.currentAuth.reauthenticate(currentCredential)
.then(function() {
Database.updateUserPassword(vm.newPassword);
}, function(error) {
console.error('[Account]', error);
});
} else {
toastr.error('A nova senha não confere');
}
} else {
toastr.error('Preencha todos os campos corretamente');
}
};
database.js service:
vm.updateUserPassword = function(newPassword) {
firebaseAuth.$updatePassword(newPassword)
.then(function() {
console.log('[Database] Password changed successfully!');
}).catch(function(error) {
switch (error.code) {
case 'auth/requires-recent-login':
vm.translationId = 'FIREBASE.AUTH.REQUIRES_RECENT_LOGIN.ERROR_MSG';
break;
default:
vm.translationId = error.message;
}
$translate(vm.translationId)
.then(function(translated) {
toastr.error(translated);
}, function(translationId) {
vm.translationId = translationId;
});
});
};
Console error:
TypeError: Cannot read property 'credential' of undefined
You can get credential using:
firebase.auth.EmailAuthProvider.credential(user.email, userProvidedPassword);
instead of:
firebase.auth().EmailAuthProvider.credential(user.email, userProvidedPassword);
I'm having a problem with a publish function. If I run the code without the roles package it works, however, when I add the roles if statement it doesn't. What am I missing here?
Path: publish.js (working)
Meteor.publish('userProfile', function(id) {
check(id, String);
return Meteor.users.find({_id: id}, {
fields: {
"profile.firstName": 1,
"profile.familyName": 1
}
});
});
Path: publish.js (not working)
Meteor.publish('userProfile', function(group, id) {
if (Roles.userIsInRole(this.userId, ['is_admin'], group)) {
check(id, String);
return Meteor.users.find({_id: id}, {
fields: {
"profile.firstName": 1,
"profile.familyName": 1
}
});
} else {
// user not authorized. do not publish secrets
this.stop();
return;
}
});
I created the rule by using the Firebase Bolt compiler.
// ####### Root
path / {
read() = true;
write() = false;
}
// ######## USERS PHOTOS
// Allow anyone to read the list of Photos.
path /users_photos {
read() = true;
}
// All individual Photos are writable by anyone.
path /users_photos/$id is Photos {
write() = isSignedIn();
}
type Photos {
image: String,
user_id: String,
removed: Boolean,
dt_created: InitialTimestamp,
dt_updated: CurrentTimestamp
}
type CurrentTimestamp extends Number {
validate() = this == now;
}
type InitialTimestamp extends Number {
validate() = initial(this, now);
}
//
// Helper Functions
//
isSignedIn() = auth != null;
// Returns true if the value is intialized to init, or retains it's prior
// value, otherwise.
initial(value, init) = value == (prior(value) == null ? init : prior(value));
Ref: https://github.com/firebase/bolt/blob/master/docs/guide.md
My script:
/*Upload*/
VigiApp.controller('UploadController', ['$scope', 'Upload', '$timeout', 'FirebaseURL', function ($scope, Upload, $timeout, FirebaseURL) {
// upload on file select or drop
$scope.upload = function (file, id) {
$('.page-spinner-bar').removeClass('ng-hide hide').addClass('ng-show show');
id = typeof id !== 'undefined' ? id : null;
Upload.base64DataUrl(file).then(function(base64){
//auth
var fbAuth = FirebaseURL.getAuth();
//Ref
var usersPhotosRef = FirebaseURL.child("users_photos");
usersPhotosRef.push({'image': base64,'removed': true, 'user_id': fbAuth.uid}, function(error){
if (error) {
alert('Error: Something went wrong when creating your post please try again');
} else {
var newID = usersPhotosRef.key();
if(id !== null){
$('#'+id).css("background-image", "url('"+base64+"')");
$('#'+id).css("background-size", "100% 100%");
}
}
$('.page-spinner-bar').removeClass('ng-show show').addClass('ng-hide hide');
});
});
}
}]);
Compile ...
>firebase-bolt mmgv-vigiapp.bolt -o rules.json
bolt: Generating rules.json...
And deploy ...
>firebase deploy:rules
=== Deploying to 'vivid-heat-2144'...
i deploying rules
+ Deploy complete!
Dashboard: https://vivid-heat-2144.firebaseio.com
But I'm getting the error:
FIREBASE WARNING: set at /users_photos/-K5VL1m04oF8s2xp8oTf failed: permission_denied
The rules created:
{
"rules": {
".read": "true",
"users_photos": {
".read": "true",
"$id": {
".validate": "newData.hasChildren(['image', 'user_id', 'removed', 'dt_created', 'dt_updated'])",
"image": {
".validate": "newData.isString()"
},
"user_id": {
".validate": "newData.isString()"
},
"removed": {
".validate": "newData.isBoolean()"
},
"dt_created": {
".validate": "newData.isNumber() && newData.val() == (data.val() == null ? now : data.val())"
},
"dt_updated": {
".validate": "newData.isNumber() && newData.val() == now"
},
"$other": {
".validate": "false"
},
".write": "auth != null"
}
}
}
}
When I remove the date, it works.
...
type Photos {
image: String,
user_id: String,
removed: Boolean,
}
...
How can I generate the creation date and update?
Where is my wrong, please?
When you are adding a Photo, you pass this information:
usersPhotosRef.push({'image': base64,'removed': true, 'user_id': fbAuth.uid}
Your security rules, require these properties:
".validate": "newData.hasChildren(['image', 'user_id', 'removed', 'dt_created', 'dt_updated'])",
There is no magic "default value" for dt_created and dt_updated, so you'll need to pass these in from your application code:
usersPhotosRef.push({
'image': base64,
'removed': true,
'user_id': fbAuth.uid,
'dt_created': Firebase.ServerValue.TIMESTAMP,
'dt_updated': Firebase.ServerValue.TIMESTAMP
}
Since this snippet is adding a new record, dt_created and dt_updated are set to the same value. When you update a record, you'll only want to set dt_updated.
Meteor.publishComposite('jobs', {
find: function() {
var user = null;
if (this.userId) {
user = Meteor.users.findOne(this.userId);
if ( user && user.profile && user.profile.isAdmin ) {
return Jobs.find({}, { sort: { createdAt: -1 }});
} else if(user && user._id) {
return Jobs.find({'createdBy': user._id});
}
} else {
return this.ready();
}
},
children: [
{
find: function(job) {
// Find post author. Even though we only want to return
// one record here, we use "find" instead of "findOne"
// since this function should return a cursor.
return Meteor.users.find(
{
_id: job.createdBy
},
{
fields: {
'profile': 1,
'createdAt': 1
}
}
);
}
}
]
});
This is the code I'm using from meteor-publishComposite package. I do not get the profile on my subscriptions for some reason. I can get the user.services to show up, but not the user.profile.