I finally managed to finish my User Management part of my web site. This post will summarize a few things about how I did it. You might learn something from that, and maybe find an inspiration for your own problems.
Why do you want users?
For the type of my site, user management is not something you see often, because it’s mostly not necessary. As I wrote in the registration section, you gain nothing by registration, you only see a few more pages (see Features).
I implemented this because I wanted to develop some management pages, e.g. maintain my gallery, without using PHPMyAdmin every time. Since only me (and possibly other people later) should be able to do that, I needed a way to distinguish users.
First I thought about just restricting access via .htaccess and letting me authenticate by HTTP Basic Authentication. This would be technically fine, but I wanted something better, something more fine-grained. I then came to the conclusion that implementing full-blown user management would be the best.
That way, I can implement access level (see below), that I can assign to pages. If the user does not meet this access level, he is not able to see this page. Additionally, I can show content in pages to only a limited user group. If I would go for the .htaccess-solution, I’d had to put this on a separate page.
Why did I reinvent the wheel? Because this site is mostly developing experience for me, I did not want to use a fit-to-use solution.
Think about that for a moment: what features does a page with user management usually have?
I did the same and came to the following set of features:
Let the user register himself in your system. Check if his inputs match your policies (see below), if he does not exist already, and if it seems sane. If all is ok, write that to your database.
Check the user’s credentials, and assign him all the features the system needs
Let the user see what’s stored in his database file (only “viewable” data, not passwords (even hashed ones) or foreign keys and stuff)
I had a look at some other Profie pages. It seems like this is one of the pages that is really specific to your site, it should fit the site’s other design.
Let him edit his personal information
Edit credentials (password and/or e-mail)
Let him edit his password or his e-mail address. These information is part of the credentials, since:
- He can log in with his password
- He may reset his password by his e-mail address (not implemented yet)
Because of that, he has to enter his existing password first.
Let the user log off your system
I currently don’t have:
Delete own user
implementing this is a political thing. Do you allow users to delete them selves, or does this need to be confirmed/done by the administrator?
I don’t have a clever plan to do that yet. I currently want to send an e-mail containing a hash of the current time and some stuff that you can’t guess, and store that in the user’s entity. Then, send an e-mail containing that hash. If he clicks on it, I delete that hash and mark his e-mail as confirmed.
This should be done every time the address changes.
In cases of two pages (one to read information, one to work on that information) like changing a profile. I went for the following naming convention
- change-credentials (read)
- changing-credentials (work)
You can observe that on the addresses in your address bar (oh, really?).
As you see, I made this list in a sort of subsequent order. To tell the truth, I implemented this piece by piece in the following order:
- Add my user manually
- Show profile
- Change credentials
- Change profile
You read this far? You deserve a present, especially since it’s Christmas time. I’ll show you some markups I made on paper, about some of these features. If you don’t mind seeing a really bad handwriting (teachers said the Egyptians would have been proud of me) in german, go on:
As said, I wanted to separate people. I do that based on access levels. On the web, someone suggested implementing a full ACL and then putting them together to predefined roles. I didn’t want to take it that far (yet?), so I went for the following solution:
I currently have 5 access levels. To be fair, I only distinguish between Unregistered, Registered and Administrator for now.
I called them “Everyone” in another markup, but went for this term. If you’re not logged in, you’re actually assigned the ID of the user “Unregistered”
People that went through the registration process. I already know their nickname and their e-mail
Verified (by myself)
These are people who I know they are. This will be a manual process, and basically means that this is someone who doesn’t have multiple accounts and doesn’t spam, if he could write posts or something. I don’t have plans for this group yet though.
Trusted (people who don’t abuse their permissions)
People that I trust and know, that they don’t mess up my site. These may be granted access to beta apps, where I have concerns that they may break my site if abused (like file upload). Again, this is a manual process.
Currently, this is only me (I don’t want to sound selfish). These may be people who I really trust and know that they have the technical level to do this job too.
I assign these levels one of 5 access levels. At the top of my pages, I set a needed access level for this site, then check if the user meets this.
This is one of the most important things when implementing user management. There are various examples of big companies failing just as big, when their user databases were compromised.
Let me take you back to language class. Listen and repeat:
I will always only store hashed and salted passwords. If I need to verify it, I verify the user’s current input with the stored hashed and salted password.
With PHP 5.5, this got a LOT easier. For more information, read this great blog post about password_hash and password_verify. Yep, these are the only two functions you need when dealing with passwords.
No “where do I store the salt” or “what exactly do I hash” or anything else. Supplement the clear text password to password_hash, store that. Read the user’s password and verify it with the stored password_verify. Nothing more, plain simple, needs 1 password column.
Salted password hashing is no rocket science anymore, so please USE it.
Never trust input
That. is. adorable.
There may be people with JS turned off, so they may send you whatever they want. They are even part of the good guys, who send you garbage.
You have to think that a real attacker will send you garbage, even though his browser may not. He can even just construct a HTTP-POST statement himself, without any browser. So even if you made sure that he does not enter
'; DROP TABLE `user`;
I use a sort of pattern. I define $return as “false”, then go into a lot of checks. If every single one of them is correct, $return is “true”. At the end of the function, it returns $return.
As you can see, it’s best to define only the positive cases, because there are so many negative ones you might not think of.
I simply do not want automated bots registering at my page. I know that there are real people you can pay to solve CAPTCHAs, but those are in minority.
Instead of letting you enter scrambled text, I went for the easiest solution, that is both accessible and easy to use, while not already being automatically solved.
All you need to enter is a simple multiplication of two factors, 1-10.
I did this by letting PHP generate these two products. I then define a CAPTCHA input field, where the user should enter the product. I then send the factors along, in a hidden field.
Why am I telling you this? I know that someone may be reading this, and he may adopt his spam software to my CAPTCHA.
- He will never adapt his software to this really small site
- If he does, he just spent some time solving 1 site, while leaving bigger sites protected. Win-win for everybody, isn’t it?
Sorry, I can’t explain you what an SQL injection is, in a nutshell. To really understand this, you have to search yourself. It basically is about escaping from your SQL query strings.
To overcome this problem, I use prepared PDO statements. According to my own experience (and StackOverflow expertise), these are far more secure than your usual mysql_real_escape. Additionally, they are applicable to multiple SQL providers.
This is different from security, since security says what’s clearly not allowed, policy says what’s not allowed in our case.
You have to think about what you allow on your site. For me, these are currently two things:
- Nicknames contain A-Z and numbers
- Passwords must be at least 8 characters long
These are really not the best, especially my nickname policy, but it is a starting point.
Why re-invent the wheel in every case? I used one external service that does what I need perfectly, without implementing this myself, because I don’t really care for this part.
I get the avatar you see on the profile page from Gravatar. If a user (identified by his e-mail address) does ot exist there, you get a little identicon (an image based on the hash of your e-mail address).
That way, I always have an avatar ready, when there should be one, instead of a question mark 🙂
I hope my post helped you along the path of implementing this yourself. As always, these are my opinions, based on what suit me best. Your mileage may vary, you may even find flaws in my model. I hope that it helped you somehow!