I want to make a system which allows for username login. This process requires the following:
User must register with an email/password
The user can set a unique username
The user can sign in with either email or username
The user can recover their password via email or username
The functionality must work on a persistence enabled database
This question has been answered previously, but it disabled the functionality for the user to use password recovery. They also didn't address case-sensitivity, one person could register as "scooby" and another as "Scooby".
DISCLAIMER: This code is now over two years old. While this doesn't mean it's deprecated, I would strongly recommend investigating alternative methods before assuming this is the best approach. I personally wouldn't want the standard login process for Firebase to be dictated by my initial approach to a problem while Firebase wasn't as heavily adopted as it is now.
After multiple iterations of development I've come up with the following design to address this. I will post my code snippets in Swift, but they will typically be translatable directly into Android with ease.
Create a Firebase registration process for email/password.
This is required as the backbone of the user's sign-in experience. This can be implemented completely from the stock Firebase API documentation provided here
Prompt the user to enter their username
The username entry should be completed at registration, I'd recommend an additional field in the registration flow. I also recommend checking if the user has a username whenever they log in. If they don't, then display a SetUsername interface that prompts them to set a username before progressing further into the UI. A user might not have a username for a few reasons; it could be revoked for being rude or reserved, or they might have signed up prior to the username being required at registration.
Make sure that if you're using a persistence-enabled Firebase build that you use Firebase Transactions. The transactions are necessary, otherwise your app can make assumptions about the data in the username table, even though a username might have been set for a user only seconds earlier.
I would also advise enforcing the username to be nearly alphanumeric (I allow for some harmless punctuation). In Swift I can achieve this with the following code:
static var invalidCharacters:NSCharacterSet {
let chars = NSMutableCharacterSet.alphanumericCharacterSet()
// I add _ - and . to the valid characters.
chars.addCharactersInString("_-.")
return chars.invertedSet
}
if username.rangeOfCharacterFromSet(invalidCharacters) != nil {
// The username is valid
}
Saving the user data
The next important step is knowing how to save the user's data in a way that we can access it in the future. Below is a screenshot of the way I store my user data:
A few things to note:
The usernames are stored twice, once in usernames and again in details/[uid]/username. I recommend this as it allows you to be case sensitive with usernames (see the next point) and it also allows you to know the exact database reference to check a username (usernames/scooby) rather than having to query or check through the children of details to find a username that matches (which would only become more complicated when you have to factor in case-sensitivity)
the usernames reference is stored in lowercase. When I check the values in this reference, or when I save to this reference, I ensure that I only save data in lowercase. This means if anyone wants to check if the username 'scoobY' exists, it will fail because in lowercase it's the same username as the existing user "Scooby".
The details/[uid]/username field contains capitals. This allows for the username to display in the case of preference for the user, rather than enforcing a lowercase or Capitalised word, the user can specify their name as "NASA Fan" and not be converted over to "Nasa Fan", while also preventing anyone else from registering the username "NASA FAN" (or any other case iterations)
The emails are being stored in the user details. This might seem peculiar because you can retrieve the current user's email via Firebase.auth().currentUser.email?. The reason this is necessary is because we need references to the emails prior to logging in as the user.
Logging in with email or username
For this to work seamlessly, you need to incorporate a few checks at login.
Since I've disallowed the # character in usernames, I can assume that a login request containing an # is an email request. These requests get processed as normal, using Firebase's FIRAuth.auth().signInWithEmail(email, password, completion) method.
For all other requests, we will assume it's a username request. Note: The cast to lowercase.
let ref = FIRDatabase.database().reference()
let usernameRef = ref.child("users/usernames/\(username.lowercaseString)")
When you perform this retrieval, you should consider if you have persistence-enabled, and if there's a possibility that a username could be revoked. If a username could be revoked and you have persistence-enabled, you will want to ensure you retrieve the username value within a Transaction block, to make sure you don't get a cached value back.
When this retrieval succeeds, you get the value from username[username], which is the user's uid. With this value, you can now perform a retrieval on the user's email value:
let ref = FIRDatabase.database().reference()
let usernameRef = ref.child("users/details/[uid]/email")
Once this request succeeds, you can then perform the standard Firebase email login with the email string you just retrieved.
The exact same retrieval methods can be used to retrieve an email from a username to allow for password recovery.
A few points to be wary of for advanced functionality:
- If you allow the user to update their email using FIRUserProfileChangeRequest, make sure you update it both on the auth AND the details[uid]email field, otherwise you will break the username login functionality
- You can significantly reduce the code required to handle all the different failure cases in the retrieval methods by using success and failure blocks. Here's an example of my get email method:
static func getEmail(username:String, success:(email:String) -> Void, failure:(error:String!) -> Void) {
let usernameRef = FIRDatabase.database().reference().child("users/usernames/\(username.lowercaseString)")
usernameRef.observeSingleEventOfType(.Value, withBlock: { (snapshot) in
if let userId = snapshot.value as? String {
let emailRef = FIRDatabase.database().reference().child("users/details/\(userId)/email")
emailRef.observeSingleEventOfType(.Value, withBlock: { (snapshot) in
if let email = snapshot.value as? String {
success(email: email)
} else {
failure(error: "No email found for username '\(username)'.")
}
}) { (error) in
failure(error: "Email could not be found.")
}
} else {
failure(error: "No account found with username '\(username)'.")
}
}) { (error) in
failure(error: "Username could not be found.")
}
}
This success/failure block implementation allows the code I call in my ViewControllers to be much cleaner. Å login calls the following method:
if fieldText.containsString("#") {
loginWithEmail(fieldText)
} else {
// Attempt to get email for username.
LoginHelper.getEmail(fieldText, success: { (email) in
self.loginWithEmail(email)
}, failure: { error in
HUD.flash(.Error, delay: 0.5)
})
}
Related
I've read the Firebase docs on Stucturing Data. Data storage is cheap, but the user's time is not. We should optimize for get operations, and write in multiple places.
So then I might store a list node and a list-index node, with some duplicated data between the two, at very least the list name.
I'm using ES6 and promises in my javascript app to handle the async flow, mainly of fetching a ref key from firebase after the first data push.
let addIndexPromise = new Promise( (resolve, reject) => {
let newRef = ref.child('list-index').push(newItem);
resolve( newRef.key()); // ignore reject() for brevity
});
addIndexPromise.then( key => {
ref.child('list').child(key).set(newItem);
});
How do I make sure the data stays in sync in all places, knowing my app runs only on the client?
For sanity check, I set a setTimeout in my promise and shut my browser before it resolved, and indeed my database was no longer consistent, with an extra index saved without a corresponding list.
Any advice?
Great question. I know of three approaches to this, which I'll list below.
I'll take a slightly different example for this, mostly because it allows me to use more concrete terms in the explanation.
Say we have a chat application, where we store two entities: messages and users. In the screen where we show the messages, we also show the name of the user. So to minimize the number of reads, we store the name of the user with each chat message too.
users
so:209103
name: "Frank van Puffelen"
location: "San Francisco, CA"
questionCount: 12
so:3648524
name: "legolandbridge"
location: "London, Prague, Barcelona"
questionCount: 4
messages
-Jabhsay3487
message: "How to write denormalized data in Firebase"
user: so:3648524
username: "legolandbridge"
-Jabhsay3591
message: "Great question."
user: so:209103
username: "Frank van Puffelen"
-Jabhsay3595
message: "I know of three approaches, which I'll list below."
user: so:209103
username: "Frank van Puffelen"
So we store the primary copy of the user's profile in the users node. In the message we store the uid (so:209103 and so:3648524) so that we can look up the user. But we also store the user's name in the messages, so that we don't have to look this up for each user when we want to display a list of messages.
So now what happens when I go to the Profile page on the chat service and change my name from "Frank van Puffelen" to just "puf".
Transactional update
Performing a transactional update is the one that probably pops to mind of most developers initially. We always want the username in messages to match the name in the corresponding profile.
Using multipath writes (added on 20150925)
Since Firebase 2.3 (for JavaScript) and 2.4 (for Android and iOS), you can achieve atomic updates quite easily by using a single multi-path update:
function renameUser(ref, uid, name) {
var updates = {}; // all paths to be updated and their new values
updates['users/'+uid+'/name'] = name;
var query = ref.child('messages').orderByChild('user').equalTo(uid);
query.once('value', function(snapshot) {
snapshot.forEach(function(messageSnapshot) {
updates['messages/'+messageSnapshot.key()+'/username'] = name;
})
ref.update(updates);
});
}
This will send a single update command to Firebase that updates the user's name in their profile and in each message.
Previous atomic approach
So when the user change's the name in their profile:
var ref = new Firebase('https://mychat.firebaseio.com/');
var uid = "so:209103";
var nameInProfileRef = ref.child('users').child(uid).child('name');
nameInProfileRef.transaction(function(currentName) {
return "puf";
}, function(error, committed, snapshot) {
if (error) {
console.log('Transaction failed abnormally!', error);
} else if (!committed) {
console.log('Transaction aborted by our code.');
} else {
console.log('Name updated in profile, now update it in the messages');
var query = ref.child('messages').orderByChild('user').equalTo(uid);
query.on('child_added', function(messageSnapshot) {
messageSnapshot.ref().update({ username: "puf" });
});
}
console.log("Wilma's data: ", snapshot.val());
}, false /* don't apply the change locally */);
Pretty involved and the astute reader will notice that I cheat in the handling of the messages. First cheat is that I never call off for the listener, but I also don't use a transaction.
If we want to securely do this type of operation from the client, we'd need:
security rules that ensure the names in both places match. But the rules need to allow enough flexibility for them to temporarily be different while we're changing the name. So this turns into a pretty painful two-phase commit scheme.
change all username fields for messages by so:209103 to null (some magic value)
change the name of user so:209103 to 'puf'
change the username in every message by so:209103 that is null to puf.
that query requires an and of two conditions, which Firebase queries don't support. So we'll end up with an extra property uid_plus_name (with value so:209103_puf) that we can query on.
client-side code that handles all these transitions transactionally.
This type of approach makes my head hurt. And usually that means that I'm doing something wrong. But even if it's the right approach, with a head that hurts I'm way more likely to make coding mistakes. So I prefer to look for a simpler solution.
Eventual consistency
Update (20150925): Firebase released a feature to allow atomic writes to multiple paths. This works similar to approach below, but with a single command. See the updated section above to read how this works.
The second approach depends on splitting the user action ("I want to change my name to 'puf'") from the implications of that action ("We need to update the name in profile so:209103 and in every message that has user = so:209103).
I'd handle the rename in a script that we run on a server. The main method would be something like this:
function renameUser(ref, uid, name) {
ref.child('users').child(uid).update({ name: name });
var query = ref.child('messages').orderByChild('user').equalTo(uid);
query.once('value', function(snapshot) {
snapshot.forEach(function(messageSnapshot) {
messageSnapshot.update({ username: name });
})
});
}
Once again I take a few shortcuts here, such as using once('value' (which is in general a bad idea for optimal performance with Firebase). But overall the approach is simpler, at the cost of not having all data completely updated at the same time. But eventually the messages will all be updated to match the new value.
Not caring
The third approach is the simplest of all: in many cases you don't really have to update the duplicated data at all. In the example we've used here, you could say that each message recorded the name as I used it at that time. I didn't change my name until just now, so it makes sense that older messages show the name I used at that time. This applies in many cases where the secondary data is transactional in nature. It doesn't apply everywhere of course, but where it applies "not caring" is the simplest approach of all.
Summary
While the above are just broad descriptions of how you could solve this problem and they are definitely not complete, I find that each time I need to fan out duplicate data it comes back to one of these basic approaches.
To add to Franks great reply, I implemented the eventual consistency approach with a set of Firebase Cloud Functions. The functions get triggered whenever a primary value (eg. users name) gets changed, and then propagate the changes to the denormalized fields.
It is not as fast as a transaction, but for many cases it does not need to be.
I have two types of users--client and event member-- i saved their name, email, password, etc. to a firebase database. How will I restrict a client's email that he can't login in the event member activity; and how can I also restrict an event member's email that he can't login in the client activity? Please help me. Thank you!
Below image shows my firebase structure:
My Database structure
You got 2 type of users but as I can see you save the exact information to client and event users. So first of all, you can make only one firebase structure for both user and just add a boolean variable CLIENT that indicates if a user is a client or event member. And to answer your other question you add another boolean variable MEMBER that indicates if a user can login to event or client activity. So here is my firebase model!!
users
-KrvzqA4GnTsomTzRHtT
client: false
birthday: "23/2/1986"
country: "Philippines"
emailAdd: "client's email"
fname: "Steph"
lname:"Diaz"
mobileNum:"Client's mobile"
password:"password"
member: true
Add one more value into your firebase user database which is your "member_type".
"member_type" : "true" if your member is client otherwise save it as a value "member_type" : "false".
Then in your application after user logged in to your application retrieve that particular user's information and check if that user's "member_type" is client or event that is true or false?
If "member_type" is client then allow access to clientactivity otherwise allow access to EventActivity.
You database model would look something like this:
client
-KrvzqA4GnTsomTzRHtT
birthday: "23/2/1986"
country: "Philippines"
emailAdd: "client's email"
fname: "Steph"
lname:"Diaz"
mobileNum:"Client's mobile"
password:"password"
member_type: true
eventMember
-Ks-RHCoq0bUWJySDvCF
birthday: "21/8/1992"
country: "American Samoa"
emailAdd: "eventmember's email"
fname: "Patricia"
lname:"Ortega"
mobileNum:"eventmember's mobile"
password:"password"
member_type: false
You can also differentiate member_type either by boolean value or by integer or any other datatype you found useful.
I am creating an android application using the Firebase authentication and database.
For my app I need to identify the users after they logged in and redirect them to two different activities. (admin/simple user)
In my app I have only 10-20 users all time, and there isn't a way to register, it's a company only app.
Thanks for help.
If max users you expect is 20 then why would you need a authentication usage ( Google, Facebook ) etc. Have a simple database entry for all users as follows:
{
"app_title": "YourApp",
"users": {
"-KTYWvZG4Qn9ZYTc47O6": {
"username": "pkondedath",
"useremail": "hello#kondedath.com",
"password" : "hello123"
"userpic": "example.com/a.png",
"prevelidge" : "superuser"
},
"-KTYWvZG466ADFRR667": {
"username": "someone",
"useremail": "hello2#kondedath.com",
"password" : "hello1234"
"userpic": "example.com/b.png",
"prevelidge" : "normal"
}
}
}
Provide a user to input username( unique) and password and search through database to see if username and password matches and check his/her prevelidge and redirect to particular activity appropriately.
eg
mFirebaseRef = new Firebase("https://yourapp.firebaseio.com/").child("users")
Use above database reference to search users. You can also use the same to add new user. When adding a new user, give them default prevelidge.
This model fits well for less userbase like 20.
For more user bases( thousands, millions) you can use google authentication and use the User UID returned and create a new entry in your database(users).
Hope this helps you.
I am letting the user change his credentials.
He types new username, email and password and I go like:
ParseUser user = ParseUser.getCurrentUser();
user.setUsername("MY NEW NAME");
user.setEmail(email);
user.setPassword("MY NEW PW");
user.saveInBackground(...);
So what? So this save() call might fail, for a big number of reasons (example: username already taken by someone else). I can tell that in this case none of the above fields gets updated, which is fair: I show an error, user knows that all went wrong.
Things get complicated if you notice that, even after the ParseException, the user above keeps its dirty fields, which couldn't be saved to the server. I.e.
//saveInBackground fails
//...
user.getUsername().equals("MY NEW NAME") // true!
Issue
Now, I am able to get these fields back to the right values by calling user.fetch(), but this doesn't work with the password field.
This is particularly unwanted, because any future call to save() or such (which might not fail because maybe it's a completely different call) will update the password too! I.e.
//later
ParseUser user = ParseUser.getCurrentUser();
user.put("stuff");
user.save();
This won't only put "stuff", but also change the password to "MY NEW PW".. without the user ever knowing.
Is there any way to reset the local password field, other than fetch() which doesn't work? I know I could save username, email and password with three different queries but that is not a possible solution for my case.
A workaround could be to use
+ becomeInBackground:
on PFUser class (with PFUser.currentUser().sessionToken as token) when save fails, but that is still a risk for becomeInBackground to fail.
It could at least prevent some cases to happend if becomeInBackground effectively undoes setPassword, and accepts current sessionToken as parameter, I haven't tested that
Looking at the newest release I've read in the changelog:
V1.10.2 — SEPTEMBER 15, 2015
New: Added ParseObject.revert() and revert(key) to allow reverting
dirty changes
Looks like this could be it. It was definitely needed.
If it was my problem I would try to create an oldUser first and save all current data to it, then when failing, it would be time to change every thing back to normal value, or if success it is time to kill oldUser. Hope it may help.
The password plain text is not stored in Parse and as such it cannot be obtained by your app. Found it here https://www.parse.com/questions/get-current-users-password-to-compare-it-with-a-string
If you want to change password you can use ParseUser.requestPasswordResetInBackground() so you will change the password with the help of email.
But if you need to get password really hard, you can store it in the SharedPreferences after the login.
After some test and check, here are some conclusion.
In parse, "password" is a special field, you cannot access it by ParseUser, That is way ParseUser have setPassword() but haven't getPassword() method
Even in back-stage management [Core] - [Data] - [User], you can see "password" field is Hidden
That is why fetch() method cannot recover original "password" value
So, I think if want implements above you need, try this way
// First time register //
ParseUser user = ParseUser.getCurrentUser();
user.setUsername("MY NAME");
user.setEmail(email);
user.setPassword("MY PW");
user.put("_password", "MY PW");
// The key can't use "password", this's reserve key word in Parse //
user.saveInBackground(...);
// Next time update //
user.setUsername("MY NEW NAME");
user.setPassword("MY NEW PW");
user.saveInBackground(...);
// if have something exception //
user.fetch();
// user.setPassword(user.get("_password"));
user.save();
ParseUser.login("MY NAME", "MY PW"); // Error
ParseUser.login("MY NAME", "MY NEW PW"); // Ok
user.fetch();
user.setPassword(user.get("_password"));
user.save();
ParseUser.login("MY NAME", "MY PW"); // OK
I was using the Parse API for databases and trying to use the username service that it provides. I understand that from the tutorial that in order to login you do this :
ParseUser.logInInBackground("Jerry", "showmethemoney", new LogInCallback() {
public void done(ParseUser user, ParseException e) {
if (user != null) {
// Hooray! The user is logged in.
} else {
// Signup failed. Look at the ParseException to see what happened.
}
}
});
If the login failed, I was just wondering how I could tell whether it failed because the username typed in was invalid, or the password. I know that you can do e.getCode() to get the type of error that occurred, but from this site https://parse.com/docs/android/api/ I couldn't find any error codes pertaining to invalid username/password
Thank you
james
This is what I did to check username/password validity in one of my applications. This method submits a query against the ParseUser class and returns true if the passed username exists, if it does then you know the username is valid.
(check externally for ParseException.OBJECT_NOT_FOUND - in conjunction with this we can tell whether the user needs to register or has an invalid password.)
public boolean queryCredentials(String username) {
ParseQuery<ParseUser> queryuserlist = ParseUser.getQuery();
queryuserlist.whereEqualTo("username", username);
try {
//attempt to find a user with the specified credentials.
return (queryuserlist.count() != 0) ? true : false;
} catch (ParseException e) {
return false;
}
}
Hopefully this can help someone with this issue.
It seems to be a security risk to distinguish between invalid user and invalid password. This information would let a hacker test account names until the app gave an invalid password response, which would let the hacker know at least the username of a valid user. Therefore, I think Parse makes this difficult deliberately.
However, it may be possible to do this using a query that searches for users with the given username. If the query returns no users, the username is invalid. If the username returns a user, the password is invalid.
According to my own testing, it looks like it uses code 101 (ObjectNotFound) if the user's credentials are invalid. I do not recommend specifically checking if it's because of the username or password specifically, for security reasons that isaach1000 mentioned. However, if you must, see Spencer's answer.
Error codes for ParseExceptions are documented under ParseException in the Parse Android API reference docs.