I wrote following simple presence code in JavaScript (based upon https://firebase.google.com/docs/database/web/offline-capabilities#section-sample):
var app = firebase.initializeApp(config);
var mainRef = app.database().ref();
var session = null;
var connected = false;
function do_sessionSubscribe(subscription) {
if (!subscription.entry) {
subscription.entry = subscription.parent.push(true);
subscription.entry.onDisconnect().remove();
}
}
function do_sessionUnsubscribe(subscription) {
if (subscription.entry) {
subscription.entry.remove();
subscription.entry = null;
}
}
mainRef.child(".info/connected").on("value", function(snap) {
connected = snap.val() === true;
if (session) {
if (connected) {
do_sessionSubscribe(session.subscription);
} else {
// workaround
//do_sessionUnsubscribe(session.subscription);
}
}
});
function closeSession() {
if (session) {
do_sessionUnsubscribe(session.subscription);
session = null;
}
}
function openSession(uid) {
session = { uid: uid, subscription: { parent: mainRef.child("session/user/" + uid), entry: null } };
if (connected) {
do_sessionSubscribe(session.subscription);
}
}
app.auth().onAuthStateChanged(function(user) {
closeSession();
if (user && user.uid) {
openSession(user.uid);
}
});
Security rules:
"session": {
"user": {
"$uid": {
".read": "auth.uid === $uid",
".write": "auth.uid === $uid",
"$session": {
".validate": "newData.val() === true"
}
}
},
}
The idea is that each active connection of a user will create /session/user/$uid/$session upon connecting/signing in and delete it when disconnecting/signing out.
Therefore in order to obtain a list of online users it should be sufficient to get /session/user with shallow=true.
The problem is that sometimes a session isn't cleaned up and stays under /session/user/$uid forever. This is then interpreted like if a user was online all the time.
I discovered that in order to easily reproduce the issue it is sufficient to block access to securetoken.googleapis.com (I use Google authentication), wait an hour and close the browser.
I tried to workaround the problem by calling remove() on disconnection. This cleans up the stale session as soon as the client gets reconnected (this is too late, but better late than never...). However, when user closes it's browser after loosing internet connection and then the auth token expires before sockets time out, the stale session persists forever.
What value of auth.uid is used during checking security rules when auth token used for registering onDisconnect() action is already expired?
How to make this presence system fully reliable without compromising security?
Related
My understanding is that I need to undertake the following steps:
Make the users' roles read-only
Use security rules on the data which access the roles to control access
Check for the role in the router
There are various examples on the official documentation how to deal with the security rules, but I couldn't figure out how to check for the role in the router. Let's assume I have an admin-only area, if someone who is not an admin tries to access that page I want that user to be redirected.
I'm currently following the official example using UI-Router, so this is my code:
app.config(["$stateProvider", function ($stateProvider) {
$stateProvider
.state("home", {
// the rest is the same for ui-router and ngRoute...
controller: "HomeCtrl",
templateUrl: "views/home.html",
resolve: {
// controller will not be loaded until $waitForSignIn resolves
// Auth refers to our $firebaseAuth wrapper in the factory below
"currentAuth": ["Auth", function(Auth) {
// $waitForSignIn returns a promise so the resolve waits for it to complete
return Auth.$waitForSignIn();
}]
}
})
.state("account", {
// the rest is the same for ui-router and ngRoute...
controller: "AccountCtrl",
templateUrl: "views/account.html",
resolve: {
// controller will not be loaded until $requireSignIn resolves
// Auth refers to our $firebaseAuth wrapper in the factory below
"currentAuth": ["Auth", function(Auth) {
// $requireSignIn returns a promise so the resolve waits for it to complete
// If the promise is rejected, it will throw a $stateChangeError (see above)
return Auth.$requireSignIn();
}]
}
});
}]);
I'm guessing I'll have to check in the resolve for a user role, but how would I access the data from the database there?
Update:
I tried André's solution, but "waitForAuth" (console.log("test1") never triggers. "waitForSignIn" does though, but then nothing happens - there is no error message.
.state('superadmin-login', {
url: '/superadmin',
templateUrl: 'views/superadmin-login.html',
'waitForAuth': ['Auth', function (Auth) {
console.log('test1');
// $requireAuth returns a promise so the resolve waits for it to complete
// If the promise is rejected, it will throw a $stateChangeError (see above)
return Auth.refAuth().$waitForSignIn();
}],
})
.state('superadmin', {
url: '/center-of-the-universe',
templateUrl: 'views/superadmin.html',
resolve: {
// YOUR RESOLVES GO HERE
// controller will not be loaded until $requireAuth resolves
// Auth refers to our $firebaseAuth wrapper in the example above
'currentAuth': ['Auth', function (Auth) {
console.log('test2');
// $requireAuth returns a promise so the resolve waits for it to complete
// If the promise is rejected, it will throw a $stateChangeError (see above)
return Auth.refAuth().$requireSignIn();
}],
//Here i check if a user has admin rights, note that i pass currentAuth and waitForAuth to this function to make sure those are resolves before this function
hasAdminAccess: function (currentAuth, waitForAuth, Rights) {
console.log('test');
return Rights.hasAdminAccess(currentAuth);
}
}
})
Here's how i did it.
First i made a factory to check if the user has the correct rights:
angular.module('rights.services', [])
.factory('Rights', function ($q) {
var ref = firebase.database().ref();
return {
hasAdminAccess: function (user) {
var deferred = $q.defer();
ref.child("Rights").child("Admin").child(user.uid).once('value').then(function (snapshot) {
if (snapshot.val()) {
deferred.resolve(true);
}
else{
deferred.reject("NO_ADMIN_ACCESS");
}
});
return deferred.promise;
}
};
});
And secondly i use this factory inside the resolve:
.state('logged', {
url: '',
abstract: true,
templateUrl: helper.basepath('app.html'),
resolve: {
// YOUR RESOLVES GO HERE
// controller will not be loaded until $requireAuth resolves
// Auth refers to our $firebaseAuth wrapper in the example above
"currentAuth": ["Auth", function (Auth) {
// $requireAuth returns a promise so the resolve waits for it to complete
// If the promise is rejected, it will throw a $stateChangeError (see above)
return Auth.refAuth().$requireSignIn();
}],
"waitForAuth": ["Auth", function (Auth) {
// $requireAuth returns a promise so the resolve waits for it to complete
// If the promise is rejected, it will throw a $stateChangeError (see above)
return Auth.refAuth().$waitForSignIn();
}],
//Here i check if a user has admin rights, note that i pass currentAuth and waitForAuth to this function to make sure those are resolves before this function
hasAdminAccess: function (currentAuth, waitForAuth, Rights) {
return Rights.hasLightAccess(currentAuth);
}
})
})
Keep in mind the way you save user roles in firebase can be different from how i do it in this example. This is (part of) how it looks in firebase:
{"moderators":
{
"0123eeca-ee0e-4ff1-9d13-43b8914999a9" : true,
"3ce9a153-eea8-498f-afad-ea2a92d79950" : true,
"571fa880-102d-4372-be8d-328ed9e7c9de" : true
}
},
{"Admins":
{
"d3d4effe-318a-43e1-a7b6-d7faf3f360eb" : true
}
}
And the security rules for these nodes:
"Admins": {
"$uid": {
//No write rule so admins can only be added inside the firebase console
".read": "auth != null && auth.uid ==$uid"
}
},
"Moderators" : {
//Admins are able to see who the moderators are and add/delete them
".read" : "(auth != null) && (root.child('Admins').hasChild(auth.uid))",
".write" : "(auth != null) && (root.child('Admins').hasChild(auth.uid))",
"$uid": {
".read": "auth != null && auth.uid ==$uid"
}
}
I'm having trouble getting Firebase authentication to work with an anonymous user. I'm building a chat app using Firechat.js, which attaches a onDisconnect() handler to set the user "offline".
However, it seems when I call firebase.unauth() it logs the user out before the onDisconnect() has a chance to set the user "offline", so it fails with a permission error (this is my theory).
The log shows exactly how everything transpires as I login and then logout:
app.js: using username: John Smith <<logged in
app.js: calling firebase.unauth() <<logged out
firechat.js: Firechat Warning: Firechat requires an authenticated Firebase reference. Pass an authenticated reference before loading.
app.js: using username: Anonymous3aa-437b <<after logout, reauthenticate as anonymous
firebase.js: FIREBASE WARNING: set at /chat/user-names-online/john smith/-KE9LcpieTwxj_A4sBHz failed: permission_denied
As you see the user is anonymous before Firechat has a chance to set the previous user offline. Here is the onDisconnect() bit in firechat.js
Here's my app code:
var $firebase = new Firebase(...);
var $firechat = new Firechat(...);
$firebase.onAuth(function(authData) {
if (authData) {
var username = authData[authData.provider].displayName;
var anonusername = "Anonymous" + authData.uid.substr(10, 8);
console.log('using username: ' + username || anonusername);
//set firechat user and resume chatting session (user becomes "online")
$firechat.setUser(authData.uid, username || anonusername, function() {
$firechat.resumeSession();
});
} else {
//if not logged in, authenticate anonymously
$firebase.authAnonymously(function(error) {
if (error) {
console.log(error);
}
});
}
});
Here's the security rule for user-names-online:
"user-names-online": {
// A mapping of active, online lowercase usernames to sessions and user ids.
".read": true,
"$username": {
"$sessionId": {
".write": "(auth != null) && (!data.exists() || !newData.exists() || data.child('id').val() === auth.uid || auth.provider === 'anonymous')",
"id": {
".validate": "(newData.val() === auth.uid)"
},
"name": {
".validate": "(newData.isString())"
}
}
}
},
It seems firechat is built for this scenario, so why is it failing?
You seem to be confusing a few things:
calling unauth() drop the authentication session for the user.
It does not disconnect the user.
It does cancel any listeners for location that require authentication.
the code you attach to onDisconnect() runs on the Firebase servers, once they detect that the client has disconnected.
If the onDisconnect() handler removes the user from some sort of presence system, other clients can see that the user disappears.
the client that disconnected cannot immediately see the result of its own disconnect, because it has already disconnected.
if a client wants to know when it has disconnected, monitor the .info/connected value.
I'd like to secure my presence logic so that the app automatically sets the username/userid from the current auth token/session and has permission to only set the logged in user's presence.
1. How can I get currently logged in user's info from the Firebase class when we are outside of the scope of the auth method?
myConnectionsRef.childByAppendingPath(Fierbase.currentUserId) //there is no such thing as Firebase.currentUserId
2. How can I set up the permissions so that a user only updates their own child node and not others - is the following the right way:
{
"rules": {
"members":
{
"$room_id": {
".read": "auth.uid !== null"
"$member_id" : {
".write": "auth.uid === $member_id"
}
}
},
}
You can read about this in the docs, under the section titled Monitoring Authentication State. What is here is just a re-iteration of the existing docs.
Use the onAuth() method to listen for changes in user authentication state.
// Create a callback which logs the current auth state
function authDataCallback(authData) {
if (authData) {
console.log("User " + authData.uid + " is logged in with " + authData.provider);
} else {
console.log("User is logged out");
}
}
// Register the callback to be fired every time auth state changes
var ref = new Firebase("https://<your-firebase>.firebaseio.com");
ref.onAuth(authDataCallback);
Additionally, you can use the getAuth() method to synchronously check authentication state.
var ref = new Firebase("https://<your-firebase>.firebaseio.com");
var authData = ref.getAuth();
if (authData) {
console.log("User " + authData.uid + " is logged in with " + authData.provider);
} else {
console.log("User is logged out");
}
Firebase Simple login provides an email/password option, how do I use it? Starting from from creating a user, storing data for that user, to logging them in and out.
There are three distinct steps to be performed (let's assume you have jQuery):
1. Set up your callback
var ref = new Firebase("https://demo.firebaseio-demo.com");
var authClient = new FirebaseAuthClient(ref, function(error, user) {
if (error) {
alert(error);
return;
}
if (user) {
// User is already logged in.
doLogin(user);
} else {
// User is logged out.
showLoginBox();
}
});
2. User registration
function showLoginBox() {
...
// Do whatever DOM operations you need to show the login/registration box.
$("#registerButton").on("click", function() {
var email = $("#email").val();
var password = $("#password").val();
authClient.createUser(email, password, function(error, user) {
if (!error) {
doLogin(user);
} else {
alert(error);
}
});
});
}
3. User login
function showLoginBox() {
...
// Do whatever DOM operations you need to show the login/registration box.
$("#loginButton").on("click", function() {
authClient.login("password", {
email: $("#email").val(),
password: $("#password").val(),
rememberMe: $("#rememberCheckbox").val()
});
});
}
When the login completes successfully, the call you registered in step 1 will be called with the correct user object, at which point we call doLogin(user) which is a method you will have to implement.
The structure of the user data is very simple. It is an object containing the following properties:
email: Email address of the user
id: Unique numeric (auto-incrementing) ID for the user
FirebaseAuthClient will automatically authenticate your firebsae for you, not further action is required. You can now use something like the following in your security rules:
{
"rules": {
"users": {
"$userid": {
".read": "auth.uid == $userid",
".write": "auth.uid == $userid"
}
}
}
}
This means, if my User ID is 42, only I can write or read at example.firebaseio-demo.com/users/42 - when I am logged in - and no-one else.
Note that Simple Login does not store any additional information about the user other than their ID and email. If you want to store additional data about the user, you must do so yourself (probably in the success callback for createUser). You can store this data as you normally would store any data in Firebase - just be careful about who can read or write to this data!
Just incase someone is reached to this thread and looking for some example application using the firebase authentication. Here are two examples
var rootRef = new Firebase('https://docs-sandbox.firebaseio.com/web/uauth');
......
.....
....
http://jsfiddle.net/firebase/a221m6pb/embedded/result,js/
http://www.42id.com/articles/firebase-authentication-and-angular-js/
I have made an facebook app. Now i need to take user information using a pop-up permission box. If a user has authenticated the app, facebook should not open dialog box for permission but if a user comes to app first time then it must open a dialog box. What I am trying to do here is...and getting error like...
Cannot call method 'showPermissionDialog' of undefined
FB.getLoginStatus(function (response) {
if (response.status === 'connected') {
alert("1");
// the user is logged in and has authenticated your
// app, and response.authResponse supplies
// the user's ID, a valid access token, a signed
// request, and the time the access token
// and signed request each expire
var uid = response.authResponse.userID;
//alert(uid);
var accessToken = response.authResponse.accessToken;
jQuery("#<%= accessToken.ClientID %>").val(accessToken);
// alert(accessToken);
fqlQuerynew();
} else if (response.status === 'not_authorized') {
// the user is logged in to Facebook,
// but has not authenticated your app
alert('not_authorized');
OnRequestPermission();
} else {
alert("3");
//alert('the user isnt logged in to Facebook');
}
});
};
function OnRequestPermission() {
var myPermissions = "publish_stream, manage_pages"; // permissions your app needs
FB.Connect.showPermissionDialog("email,offline_access", function (perms) {
if (!perms) {
alert("hi");
// document.location.href = 'YouNeedToAuthorize.html';
} else {
alert("buy");
document.location.href = 'homePage.html';
}
});
}
If you have just copied and pasted it from your code then I think you have added one extra closing bracket after FB.getLoginStatus '};'.
After removing that try your code. If it doesn't work then can we know when you want to check login status like after clicking some social button or while loading page.
Here's a modified version of your code, I haven't tested it and it's not complete, but should give you an idea of what to do:
FB.getLoginStatus(function (response) {
if (response.status === 'connected') {
var uid = response.authResponse.userID;
var accessToken = response.authResponse.accessToken;
jQuery("#<%= accessToken.ClientID %>").val(accessToken);
fqlQuerynew();
} else if (response.status === 'not_authorized') {
OnRequestPermission();
} else {
...
}
});
function OnRequestPermission() {
var myPermissions = "publish_stream, manage_pages"; // permissions your app needs
FB.login(function(response) {
if (response.status === 'connected') {
FB.api("me/permissions", checkPermissions);
}
else {
....
}
}, { scope: "email,offline_access" });
}
function checkPermissions(response) {
if (response.data && response.data.legth == 1) {
var perms = response.data[0];
// iterate over perms and check if the user has all needed permissions
}
}