
Come in! Login without username and password
Who's guilty?
One of the often arisen tasks when developing web-projects is to let a user into the site without entering a login and password, while authorizing him.
Here are some examples of such situations:
- Link to account activation by a newly registered user.
- Password recovery link.
- Invitation (return) to the site of a user who has not visited for a long time.
In each of these cases, we need to create a key for the user and add it to the URL sent in the letter.
Typically, this key should be:
- unique
- URL safe;
- difficult (impossible) forged;
- reasonable length.
I saw a lot of solutions (and I myself resorted to them before) based on adding a service field or a whole table to the database, in which the generated keys were placed and some additional information that allows you to authorize the user when he comes to the site with this key. Usually this key is the result of some hash function. For example: sha1 ($ userId. "Secret_key". Time ());
Very often, such a decision comes first. In fact, his place is at the very end.
I offer you a solution that allows you to do with "little blood" - do not require work with the database.
What to do?
In addition to the above key properties, other restrictions are often imposed on it. From them, in fact, have to dance.
In the simplest cases, you can get by with constructions of the form: sha1 ($ userId. "Secret_key"); or sha1 ($ userId. "secret_key". "confirm_code");
If a user comes to the site with such a key at a URL like example.com/users/%user_id%?t=%key% , we can easily check it.
The disadvantages of this approach:
- for each user the key will always be the same;
- such a key will always be valid; it cannot be limited in time;
- You must explicitly pass the user id to the URL.
The disadvantages are significant and therefore, in most cases, such a solution will not work.
But this is not a reason to store data in the database. They can be stored in the key. Store safely - in encrypted form.
In php, encryption functions are implemented in the Mcrypt library, which can be easily installed from pecl or your OS repositories.
You yourself can implement your favorite algorithm. This is not important - the main thing is that we have functions that allow you to encrypt and decrypt arbitrary text.
The idea is simple. We put the necessary data in a line, encrypt it with our secret key, bring it to a URL-safe view and insert it as a key in the link that the user will come to us with.
It must be remembered that the more data we want to put in the key, the longer the key will be, because this is no longer a fixed-length hash.
For my solution, I highlighted such data for storage in the key:
- user id (4 bytes);
- key creation time (4 bytes);
- key validity time (4 bytes);
- mode (1 byte) - a variable with which we can share the keys (password recovery / registration confirmation / just an invitation);
- Random number (1 byte) - add uniqueness;
- Checksum (4 bytes).
Looking ahead, I will show an example of the resulting key: 67147328f43d69f7784770a2d9c84b181a8c .
What you will keep in the key depends on your task. Here I showed that a key of acceptable length can easily fit 18 bytes of information.
Let's move on to implementation. I implemented this solution in one class with two static methods. The keys self :: $ key and self :: $ iv are, for security reasons, better stored separately from the algorithm. (string) AuthToken :: create ($ id, $ expire = 0, $ mode = 0) Creates a key. (mixed) AuthToken :: check ($ tokenHex, $ mode = null) Checks the validity of the key. If successful, returns the user ID, otherwise, logical FALSE.
Copy Source | Copy HTML
class AuthToken {
private static $key = "секретный ключ";
private static $iv = "должен быть каждый раз случайным, но для данного решения подойдёт просто секретный";
private static function int2char($int) {
$char = "";
$hex = sprintf("%08x", $int);
for ($i = 0; $i < 4; $i++) {
$char .= chr(hexdec(substr($hex, $i * 2, 2)));
}
return $char;
}
private static function char2int($char) {
$int = 0;
$hex = "";
for ($i = 0; $i < 4; $i++) {
$hex .= sprintf("%02x", ord($char{$i}));
}
$int = hexdec($hex);
return $int;
}
public static function create($id, $expire = 0, $mode = 0) {
$id = intval($id);
$expire = intval($expire);
$mode = intval($mode);
if ($id < 0 || $expire < 0 || $mode < 0) {
return null;
}
$info = array();
$info["id"] = $id;
$info["time"] = time();
$info["expire"] = $expire;
$info["mode"] = $mode;
$info["rnd"] = ceil(mt_rand( 0, 255));
$info["sum"] = $info["time"] - $info["expire"] - $info["mode"] - $info["rnd"] - $info["id"];
$info = self::int2char($info["id"]) . self::int2char($info["time"]) . self::int2char($info["expire"]) . chr($info["mode"]) . chr($info["rnd"]) . self::int2char($info["sum"]);
$token = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, md5(self::$key), $info, MCRYPT_MODE_OFB, md5(self::$iv));
$tokenHex = "";
$tokenLength = strlen($token);
for ($i = 0; $i < $tokenLength; $i++) {
$tokenHex .= sprintf("%02x", ord($token{$i}));
}
return $tokenHex;
}
public static function check($tokenHex, $mode = null) {
$token = "";
$tokenHexLength = strlen($tokenHex) / 2;
for ($i = 0; $i < $tokenHexLength; $i++) {
$token .= chr(hexdec(substr($tokenHex, $i * 2, 2)));
}
$info = mcrypt_decrypt(MCRYPT_RIJNDAEL_256, md5(self::$key), $token, MCRYPT_MODE_OFB, md5(self::$iv));
if (strlen($info) == 18) {
$info = array("id" => self::char2int(substr($info, 0, 4)), "time" => self::char2int(substr($info, 4, 4)), "expire" => self::char2int(substr($info, 8, 4)), "mode" => ord($info{12}), "rnd" => ord($info{13}), "sum" => self::char2int(substr($info, 14, 4)));
if ($info["sum"] == $info["time"] - $info["expire"] - $info["mode"] - $info["rnd"] - $info["id"]) {
if ($info["expire"] > 0) {
if ($info["expire"] + $info["time"] < time()) {
return false;
}
}
if ($info["mode"] > 0) {
if ($mode !== null) {
if ($info["mode"] != $mode) {
return false;
}
}
}
return $info["id"];
} else {
return false;
}
} else {
return false;
}
}
}
?>
If you do not specify $ expire and / or $ mode, they will not be taken into account when checking the key. If you do not specify $ mode during verification, it will also not be taken into account.
If all else fails?
Such a solution will not work if you need a one-time key. But this can be solved with small crutches. For example, setting the check flag of such a key in memchache in combination with a limited duration of the key will give the desired result in most cases.
Well, then - either a fantasy or a database.