1
0
mirror of https://github.com/delight-im/PHP-Auth.git synced 2025-08-11 10:34:02 +02:00

12 Commits

9 changed files with 237 additions and 109 deletions

13
.editorconfig Normal file
View File

@@ -0,0 +1,13 @@
# editorconfig.org
root = true
[*]
charset = utf-8
indent_style = tab
trim_trailing_whitespace = true
end_of_line = lf
insert_final_newline = true
[*.md]
indent_style = space
indent_size = 4

View File

@@ -1,34 +1,25 @@
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
CREATE TABLE IF NOT EXISTS `users` ( CREATE TABLE IF NOT EXISTS `users` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT, `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`email` varchar(254) NOT NULL, `email` varchar(249) COLLATE utf8mb4_unicode_ci NOT NULL,
`password` varchar(255) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL, `password` varchar(255) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
`username` varchar(100) DEFAULT NULL, `username` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`verified` tinyint(1) unsigned NOT NULL DEFAULT '0', `verified` tinyint(1) unsigned NOT NULL DEFAULT '0',
`registered` int(10) unsigned NOT NULL, `registered` int(10) unsigned NOT NULL,
`last_login` int(10) unsigned DEFAULT NULL, `last_login` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`) UNIQUE KEY `email` (`email`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ; ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `users_confirmations` ( CREATE TABLE IF NOT EXISTS `users_confirmations` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT, `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`email` varchar(254) NOT NULL, `email` varchar(249) COLLATE utf8mb4_unicode_ci NOT NULL,
`selector` varchar(16) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL, `selector` varchar(16) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
`token` varchar(255) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL, `token` varchar(255) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
`expires` int(10) unsigned NOT NULL, `expires` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `selector` (`selector`), UNIQUE KEY `selector` (`selector`),
KEY `email_expires` (`email`,`expires`) KEY `email_expires` (`email`,`expires`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ; ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `users_remembered` ( CREATE TABLE IF NOT EXISTS `users_remembered` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
@@ -39,7 +30,7 @@ CREATE TABLE IF NOT EXISTS `users_remembered` (
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `selector` (`selector`), UNIQUE KEY `selector` (`selector`),
KEY `user` (`user`) KEY `user` (`user`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ; ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `users_resets` ( CREATE TABLE IF NOT EXISTS `users_resets` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
@@ -50,18 +41,14 @@ CREATE TABLE IF NOT EXISTS `users_resets` (
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `selector` (`selector`), UNIQUE KEY `selector` (`selector`),
KEY `user` (`user`) KEY `user` (`user`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ; ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `users_throttling` ( CREATE TABLE IF NOT EXISTS `users_throttling` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`action_type` enum('login','register','confirm_email') NOT NULL, `action_type` enum('login','register','confirm_email') COLLATE utf8mb4_unicode_ci NOT NULL,
`selector` varchar(44) CHARACTER SET latin1 COLLATE latin1_general_cs DEFAULT NULL, `selector` varchar(44) CHARACTER SET latin1 COLLATE latin1_general_cs DEFAULT NULL,
`time_bucket` int(10) unsigned NOT NULL, `time_bucket` int(10) unsigned NOT NULL,
`attempts` mediumint(8) unsigned NOT NULL DEFAULT '1', `attempts` mediumint(8) unsigned NOT NULL DEFAULT '1',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `action_type_selector_time_bucket` (`action_type`,`selector`,`time_bucket`) UNIQUE KEY `action_type_selector_time_bucket` (`action_type`,`selector`,`time_bucket`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ; ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

35
Migration.md Normal file
View File

@@ -0,0 +1,35 @@
# Migration
* `v1.x.x` to `v2.x.x`
* The MySQL schema has been changed from charset `utf8` to charset `utf8mb4` and from collation `utf8_general_ci` to collation `utf8mb4_unicode_ci`. Use the statements below to update the database schema:
```sql
ALTER TABLE `users` CHANGE `email` `email` VARCHAR(249) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;
ALTER TABLE `users_confirmations` CHANGE `email` `email` VARCHAR(249) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;
-- ALTER DATABASE `<DATABASE_NAME>` CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
ALTER TABLE `users` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `users_confirmations` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `users_remembered` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `users_resets` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `users_throttling` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `users` CHANGE `email` `email` VARCHAR(249) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL;
ALTER TABLE `users` CHANGE `username` `username` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL;
ALTER TABLE `users_confirmations` CHANGE `email` `email` VARCHAR(249) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL;
ALTER TABLE `users_throttling` CHANGE `action_type` `action_type` ENUM('login','register','confirm_email') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL;
REPAIR TABLE users;
OPTIMIZE TABLE users;
REPAIR TABLE users_confirmations;
OPTIMIZE TABLE users_confirmations;
REPAIR TABLE users_remembered;
OPTIMIZE TABLE users_remembered;
REPAIR TABLE users_resets;
OPTIMIZE TABLE users_resets;
REPAIR TABLE users_throttling;
OPTIMIZE TABLE users_throttling;
```

126
README.md
View File

@@ -1,6 +1,8 @@
# Auth # Auth
Secure authentication for PHP, once and for all, really simple to use. Authentication for PHP. Simple, lightweight and secure.
Written once, to be used everywhere.
Completely framework-agnostic and database-agnostic. Completely framework-agnostic and database-agnostic.
@@ -41,14 +43,20 @@ Completely framework-agnostic and database-agnostic.
### Create a new instance ### Create a new instance
```php ```php
// $db = new PDO('mysql:dbname=database;host=localhost;charset=utf8', 'username', 'password'); // $db = new PDO('mysql:dbname=database;host=localhost;charset=utf8mb4', 'username', 'password');
// $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$auth = new Delight\Auth\Auth($db); $auth = new \Delight\Auth\Auth($db);
``` ```
If you have an open `PDO` connection already, just re-use it. If you have an open `PDO` connection already, just re-use it.
If you do enforce HTTPS on your site, pass `true` as the second parameter to the constructor. This is optional and the default is `false`.
Only in the very rare case that you need access to your cookies from JavaScript, pass `true` as the third argument to the constructor. This is optional and the default is `false`. There is almost always a *better* solution than enabling this, however.
If your web server is behind a proxy server and `$_SERVER['REMOTE_ADDR']` only contains the proxy's IP address, you must pass the user's real IP address to the constructor in the fourth argument. The default is `null`.
### Sign up a new user (register) ### Sign up a new user (register)
```php ```php
@@ -59,20 +67,22 @@ try {
// we have signed up a new user with the ID `$userId` // we have signed up a new user with the ID `$userId`
} }
catch (Delight\Auth\InvalidEmailException $e) { catch (\Delight\Auth\InvalidEmailException $e) {
// invalid email address // invalid email address
} }
catch (Delight\Auth\InvalidPasswordException $e) { catch (\Delight\Auth\InvalidPasswordException $e) {
// invalid password // invalid password
} }
catch (Delight\Auth\UserAlreadyExistsException $e) { catch (\Delight\Auth\UserAlreadyExistsException $e) {
// user already exists // user already exists
} }
catch (Delight\Auth\TooManyRequestsException $e) { catch (\Delight\Auth\TooManyRequestsException $e) {
// too many requests // too many requests
} }
``` ```
The username in the third parameter is optional. You can pass `null` here if you don't want to manage usernames.
For email verification, you should build an URL with the selector and token and send it to the user, e.g.: For email verification, you should build an URL with the selector and token and send it to the user, e.g.:
```php ```php
@@ -89,20 +99,22 @@ try {
// user is logged in // user is logged in
} }
catch (Delight\Auth\InvalidEmailException $e) { catch (\Delight\Auth\InvalidEmailException $e) {
// wrong email address // wrong email address
} }
catch (Delight\Auth\InvalidPasswordException $e) { catch (\Delight\Auth\InvalidPasswordException $e) {
// wrong password // wrong password
} }
catch (Delight\Auth\EmailNotVerifiedException $e) { catch (\Delight\Auth\EmailNotVerifiedException $e) {
// email not verified // email not verified
} }
catch (Delight\Auth\TooManyRequestsException $e) { catch (\Delight\Auth\TooManyRequestsException $e) {
// too many requests // too many requests
} }
``` ```
The third parameter controls whether the login is persistent with a long-lived cookie. This is known as the "remember me" feature. Set this to `false` to disable the feature. Otherwise, ask the user if they want to enable "remember me". This is usually done with a checkbox in your user interface. Then use their input to decide between `false` and `true` here. This is optional and the default is `false`.
### Perform email verification ### Perform email verification
Extract the selector and token from the URL that the user clicked on in the verification email. Extract the selector and token from the URL that the user clicked on in the verification email.
@@ -113,13 +125,13 @@ try {
// email address has been verified // email address has been verified
} }
catch (Delight\Auth\InvalidSelectorTokenPairException $e) { catch (\Delight\Auth\InvalidSelectorTokenPairException $e) {
// invalid token // invalid token
} }
catch (Delight\Auth\TokenExpiredException $e) { catch (\Delight\Auth\TokenExpiredException $e) {
// token expired // token expired
} }
catch (Delight\Auth\TooManyRequestsException $e) { catch (\Delight\Auth\TooManyRequestsException $e) {
// too many requests // too many requests
} }
``` ```
@@ -134,10 +146,10 @@ try {
// password has been changed // password has been changed
} }
catch (Delight\Auth\NotLoggedInException $e) { catch (\Delight\Auth\NotLoggedInException $e) {
// not logged in // not logged in
} }
catch (Delight\Auth\InvalidPasswordException $e) { catch (\Delight\Auth\InvalidPasswordException $e) {
// invalid password(s) // invalid password(s)
} }
``` ```
@@ -150,6 +162,81 @@ $auth->logout();
// user has been signed out // user has been signed out
``` ```
### Check if the user is signed in
```php
if ($auth->isLoggedIn()) {
// user is signed in
}
else {
// user is *not* signed in yet
}
```
A shorthand/alias for this method is `$auth->check()`.
### Get the user's ID
```php
$id = $auth->getUserId();
```
If the user is not currently signed in, this returns `null`.
A shorthand/alias for this method is `$auth->id()`.
### Get the user's email address
```php
$email = $auth->getEmail();
```
If the user is not currently signed in, this returns `null`.
### Get the user's display name
```php
$email = $auth->getUsername();
```
Remember that usernames are optional and there is only a username if you supplied it during registration.
If the user is not currently signed in, this returns `null`.
### Check if the user was "remembered"
```php
if ($auth->isRemembered()) {
// user did not sign in but was logged in through their long-lived cookie
}
else {
// user signed in manually
}
```
If the user is not currently signed in, this returns `null`.
### Get the user's IP address
```php
$ip = $auth->getIpAddress();
```
### Utilities
#### Create a random string
```php
$length = 24;
$randomStr = \Delight\Auth\Auth::createRandomString($length);
```
#### Create a UUID v4 as per RFC 4122
```php
$uuid = \Delight\Auth\Auth::createUuid();
```
## Features ## Features
* registration * registration
@@ -167,6 +254,7 @@ $auth->logout();
* logout * logout
* full and reliable destruction of session * full and reliable destruction of session
* session management * session management
* protection against session hijacking
* protection against session fixation attacks * protection against session fixation attacks
* throttling * throttling
* per IP address * per IP address
@@ -175,6 +263,10 @@ $auth->logout();
* prevents clickjacking * prevents clickjacking
* prevent content sniffing (MIME sniffing) * prevent content sniffing (MIME sniffing)
* disables caching of potentially sensitive data * disables caching of potentially sensitive data
* miscellaneous
* ready for both IPv4 and IPv6
* works behind proxy servers as well
* privacy-friendly (e.g. does *not* save readable IP addresses)
## Exceptions ## Exceptions
@@ -201,7 +293,7 @@ All contributions are welcome! If you wish to contribute, please create an issue
## License ## License
``` ```
Copyright 2015 delight.im <info@delight.im> Copyright (c) delight.im <info@delight.im>
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@@ -1,6 +1,6 @@
{ {
"name": "delight-im/auth", "name": "delight-im/auth",
"description": "Secure authentication for PHP, once and for all, really simple to use", "description": "Authentication for PHP. Simple, lightweight and secure.",
"require": { "require": {
"php": ">=5.5.0" "php": ">=5.5.0"
}, },

View File

@@ -1,7 +1,7 @@
<?php <?php
/** /*
* Copyright 2015 delight.im <info@delight.im> * Copyright (c) delight.im <info@delight.im>
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -21,7 +21,7 @@ namespace Delight\Auth;
require __DIR__.'/Base64.php'; require __DIR__.'/Base64.php';
require __DIR__.'/Exceptions.php'; require __DIR__.'/Exceptions.php';
/** Secure authentication for PHP, once and for all, really simple to use */ /** Base class that provides all methods, properties and utilities for secure authentication */
class Auth { class Auth {
const SESSION_FIELD_LOGGED_IN = 'auth_logged_in'; const SESSION_FIELD_LOGGED_IN = 'auth_logged_in';
@@ -118,7 +118,7 @@ class Auth {
// if both selector and token were found // if both selector and token were found
if (isset($parts[0]) && isset($parts[1])) { if (isset($parts[0]) && isset($parts[1])) {
$stmt = $this->db->prepare("SELECT a.user, a.token, a.expires, b.email, b.username FROM users_remembered AS a JOIN users AS b ON a.user = b.id WHERE a.selector = :selector"); $stmt = $this->db->prepare("SELECT a.user, a.token, a.expires, b.email, b.username FROM users_remembered AS a JOIN users AS b ON a.user = b.id WHERE a.selector = :selector");
$stmt->bindParam(':selector', $parts[0], \PDO::PARAM_STR); $stmt->bindValue(':selector', $parts[0], \PDO::PARAM_STR);
if ($stmt->execute()) { if ($stmt->execute()) {
$rememberData = $stmt->fetch(\PDO::FETCH_ASSOC); $rememberData = $stmt->fetch(\PDO::FETCH_ASSOC);
if ($rememberData !== false) { if ($rememberData !== false) {
@@ -168,16 +168,15 @@ class Auth {
} }
$username = isset($username) ? trim($username) : null; $username = isset($username) ? trim($username) : null;
$registered = time();
$password = password_hash($password, PASSWORD_DEFAULT); $password = password_hash($password, PASSWORD_DEFAULT);
$verified = isset($emailConfirmationCallback) && is_callable($emailConfirmationCallback) ? 0 : 1; $verified = isset($emailConfirmationCallback) && is_callable($emailConfirmationCallback) ? 0 : 1;
$stmt = $this->db->prepare("INSERT INTO users (email, password, username, verified, registered) VALUES (:email, :password, :username, :verified, :registered)"); $stmt = $this->db->prepare("INSERT INTO users (email, password, username, verified, registered) VALUES (:email, :password, :username, :verified, :registered)");
$stmt->bindParam(':email', $email, \PDO::PARAM_STR); $stmt->bindValue(':email', $email, \PDO::PARAM_STR);
$stmt->bindParam(':password', $password, \PDO::PARAM_STR); $stmt->bindValue(':password', $password, \PDO::PARAM_STR);
$stmt->bindParam(':username', $username, \PDO::PARAM_STR); $stmt->bindValue(':username', $username, \PDO::PARAM_STR);
$stmt->bindParam(':verified', $verified, \PDO::PARAM_INT); $stmt->bindValue(':verified', $verified, \PDO::PARAM_INT);
$stmt->bindParam(':registered', $registered, \PDO::PARAM_INT); $stmt->bindValue(':registered', time(), \PDO::PARAM_INT);
try { try {
$result = $stmt->execute(); $result = $stmt->execute();
@@ -198,7 +197,7 @@ class Auth {
if ($result) { if ($result) {
// get the ID of the user that we've just created // get the ID of the user that we've just created
$stmt = $this->db->prepare("SELECT id FROM users WHERE email = :email"); $stmt = $this->db->prepare("SELECT id FROM users WHERE email = :email");
$stmt->bindParam(':email', $email, \PDO::PARAM_STR); $stmt->bindValue(':email', $email, \PDO::PARAM_STR);
if ($result = $stmt->execute()) { if ($result = $stmt->execute()) {
$newUserId = $stmt->fetchColumn(); $newUserId = $stmt->fetchColumn();
@@ -235,10 +234,10 @@ class Auth {
$expires = time() + 3600 * 24; $expires = time() + 3600 * 24;
$stmt = $this->db->prepare("INSERT INTO users_confirmations (email, selector, token, expires) VALUES (:email, :selector, :token, :expires)"); $stmt = $this->db->prepare("INSERT INTO users_confirmations (email, selector, token, expires) VALUES (:email, :selector, :token, :expires)");
$stmt->bindParam(':email', $email, \PDO::PARAM_STR); $stmt->bindValue(':email', $email, \PDO::PARAM_STR);
$stmt->bindParam(':selector', $selector, \PDO::PARAM_STR); $stmt->bindValue(':selector', $selector, \PDO::PARAM_STR);
$stmt->bindParam(':token', $tokenHashed, \PDO::PARAM_STR); $stmt->bindValue(':token', $tokenHashed, \PDO::PARAM_STR);
$stmt->bindParam(':expires', $expires, \PDO::PARAM_INT); $stmt->bindValue(':expires', $expires, \PDO::PARAM_INT);
if ($stmt->execute()) { if ($stmt->execute()) {
if (isset($emailConfirmationCallback) && is_callable($emailConfirmationCallback)) { if (isset($emailConfirmationCallback) && is_callable($emailConfirmationCallback)) {
@@ -284,11 +283,17 @@ class Auth {
} }
$stmt = $this->db->prepare("SELECT id, password, verified, username FROM users WHERE email = :email"); $stmt = $this->db->prepare("SELECT id, password, verified, username FROM users WHERE email = :email");
$stmt->bindParam(':email', $email, \PDO::PARAM_STR); $stmt->bindValue(':email', $email, \PDO::PARAM_STR);
if ($stmt->execute()) { if ($stmt->execute()) {
$userData = $stmt->fetch(\PDO::FETCH_ASSOC); $userData = $stmt->fetch(\PDO::FETCH_ASSOC);
if ($userData !== false) { if ($userData !== false) {
if (password_verify($password, $userData['password'])) { if (password_verify($password, $userData['password'])) {
// if the password needs to be re-hashed to keep up with improving password cracking techniques
if (password_needs_rehash($userData['password'], PASSWORD_DEFAULT)) {
// create a new hash from the password and update it in the database
$this->updatePassword($userData['id'], $password);
}
if ($userData['verified'] == 1) { if ($userData['verified'] == 1) {
$this->onLoginSuccessful($userData['id'], $email, $userData['username'], false); $this->onLoginSuccessful($userData['id'], $email, $userData['username'], false);
@@ -328,10 +333,10 @@ class Auth {
$expires = time() + 3600 * 24 * 28; $expires = time() + 3600 * 24 * 28;
$stmt = $this->db->prepare("INSERT INTO users_remembered (user, selector, token, expires) VALUES (:user, :selector, :token, :expires)"); $stmt = $this->db->prepare("INSERT INTO users_remembered (user, selector, token, expires) VALUES (:user, :selector, :token, :expires)");
$stmt->bindParam(':user', $userId, \PDO::PARAM_INT); $stmt->bindValue(':user', $userId, \PDO::PARAM_INT);
$stmt->bindParam(':selector', $selector, \PDO::PARAM_STR); $stmt->bindValue(':selector', $selector, \PDO::PARAM_STR);
$stmt->bindParam(':token', $tokenHashed, \PDO::PARAM_STR); $stmt->bindValue(':token', $tokenHashed, \PDO::PARAM_STR);
$stmt->bindParam(':expires', $expires, \PDO::PARAM_INT); $stmt->bindValue(':expires', $expires, \PDO::PARAM_INT);
if ($stmt->execute()) { if ($stmt->execute()) {
$this->setRememberCookie($selector, $token, $expires); $this->setRememberCookie($selector, $token, $expires);
@@ -351,7 +356,7 @@ class Auth {
*/ */
private function deleteRememberDirective($userId) { private function deleteRememberDirective($userId) {
$stmt = $this->db->prepare("DELETE FROM users_remembered WHERE user = :user"); $stmt = $this->db->prepare("DELETE FROM users_remembered WHERE user = :user");
$stmt->bindParam(':user', $userId, \PDO::PARAM_INT); $stmt->bindValue(':user', $userId, \PDO::PARAM_INT);
if ($stmt->execute()) { if ($stmt->execute()) {
$this->setRememberCookie(null, null, time() - 3600); $this->setRememberCookie(null, null, time() - 3600);
@@ -399,11 +404,9 @@ class Auth {
* @param bool $remembered whether the user was remembered ("remember me") or logged in actively * @param bool $remembered whether the user was remembered ("remember me") or logged in actively
*/ */
private function onLoginSuccessful($userId, $email, $username, $remembered) { private function onLoginSuccessful($userId, $email, $username, $remembered) {
$lastLogin = time();
$stmt = $this->db->prepare("UPDATE users SET last_login = :lastLogin WHERE id = :id"); $stmt = $this->db->prepare("UPDATE users SET last_login = :lastLogin WHERE id = :id");
$stmt->bindParam(':lastLogin', $lastLogin, \PDO::PARAM_INT); $stmt->bindValue(':lastLogin', time(), \PDO::PARAM_INT);
$stmt->bindParam(':id', $userId, \PDO::PARAM_INT); $stmt->bindValue(':id', $userId, \PDO::PARAM_INT);
$stmt->execute(); $stmt->execute();
// re-generate the session ID to prevent session fixation attacks // re-generate the session ID to prevent session fixation attacks
@@ -477,20 +480,18 @@ class Auth {
$this->throttle(self::THROTTLE_ACTION_CONFIRM_EMAIL, $selector); $this->throttle(self::THROTTLE_ACTION_CONFIRM_EMAIL, $selector);
$stmt = $this->db->prepare("SELECT id, email, token, expires FROM users_confirmations WHERE selector = :selector"); $stmt = $this->db->prepare("SELECT id, email, token, expires FROM users_confirmations WHERE selector = :selector");
$stmt->bindParam(':selector', $selector, \PDO::PARAM_STR); $stmt->bindValue(':selector', $selector, \PDO::PARAM_STR);
if ($stmt->execute()) { if ($stmt->execute()) {
$confirmationData = $stmt->fetch(\PDO::FETCH_ASSOC); $confirmationData = $stmt->fetch(\PDO::FETCH_ASSOC);
if ($confirmationData !== false) { if ($confirmationData !== false) {
if (password_verify($token, $confirmationData['token'])) { if (password_verify($token, $confirmationData['token'])) {
if ($confirmationData['expires'] >= time()) { if ($confirmationData['expires'] >= time()) {
$verified = 1;
$stmt = $this->db->prepare("UPDATE users SET verified = :verified WHERE email = :email"); $stmt = $this->db->prepare("UPDATE users SET verified = :verified WHERE email = :email");
$stmt->bindParam(':verified', $verified, \PDO::PARAM_INT); $stmt->bindValue(':verified', 1, \PDO::PARAM_INT);
$stmt->bindParam(':email', $confirmationData['email'], \PDO::PARAM_STR); $stmt->bindValue(':email', $confirmationData['email'], \PDO::PARAM_STR);
if ($stmt->execute()) { if ($stmt->execute()) {
$stmt = $this->db->prepare("DELETE FROM users_confirmations WHERE id = :id"); $stmt = $this->db->prepare("DELETE FROM users_confirmations WHERE id = :id");
$stmt->bindParam(':id', $confirmationData['id'], \PDO::PARAM_INT); $stmt->bindValue(':id', $confirmationData['id'], \PDO::PARAM_INT);
if ($stmt->execute()) { if ($stmt->execute()) {
return; return;
} }
@@ -543,7 +544,7 @@ class Auth {
$userId = $this->getUserId(); $userId = $this->getUserId();
$stmt = $this->db->prepare("SELECT password FROM users WHERE id = :userId"); $stmt = $this->db->prepare("SELECT password FROM users WHERE id = :userId");
$stmt->bindParam(':userId', $userId, \PDO::PARAM_INT); $stmt->bindValue(':userId', $userId, \PDO::PARAM_INT);
if ($stmt->execute()) { if ($stmt->execute()) {
$passwordInDatabase = $stmt->fetchColumn(); $passwordInDatabase = $stmt->fetchColumn();
if (password_verify($oldPassword, $passwordInDatabase)) { if (password_verify($oldPassword, $passwordInDatabase)) {
@@ -574,8 +575,8 @@ class Auth {
$newPassword = password_hash($newPassword, PASSWORD_DEFAULT); $newPassword = password_hash($newPassword, PASSWORD_DEFAULT);
$stmt = $this->db->prepare("UPDATE users SET password = :password WHERE id = :userId"); $stmt = $this->db->prepare("UPDATE users SET password = :password WHERE id = :userId");
$stmt->bindParam(':password', $newPassword, \PDO::PARAM_STR); $stmt->bindValue(':password', $newPassword, \PDO::PARAM_STR);
$stmt->bindParam(':userId', $userId, \PDO::PARAM_INT); $stmt->bindValue(':userId', $userId, \PDO::PARAM_INT);
$stmt->execute(); $stmt->execute();
} }
@@ -745,7 +746,7 @@ class Auth {
* @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded * @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
* @throws AuthError if an internal problem occurred (do *not* catch) * @throws AuthError if an internal problem occurred (do *not* catch)
*/ */
public function throttle($actionType, $customSelector = null) { private function throttle($actionType, $customSelector = null) {
// if a custom selector has been provided (e.g. username, user ID or confirmation token) // if a custom selector has been provided (e.g. username, user ID or confirmation token)
if (isset($customSelector)) { if (isset($customSelector)) {
// use the provided selector for throttling // use the provided selector for throttling
@@ -761,9 +762,9 @@ class Auth {
$timeBucket = self::getTimeBucket(); $timeBucket = self::getTimeBucket();
$stmt = $this->db->prepare('INSERT INTO users_throttling (action_type, selector, time_bucket, attempts) VALUES (:actionType, :selector, :timeBucket, 1)'); $stmt = $this->db->prepare('INSERT INTO users_throttling (action_type, selector, time_bucket, attempts) VALUES (:actionType, :selector, :timeBucket, 1)');
$stmt->bindParam(':actionType', $actionType, \PDO::PARAM_STR); $stmt->bindValue(':actionType', $actionType, \PDO::PARAM_STR);
$stmt->bindParam(':selector', $selector, \PDO::PARAM_STR); $stmt->bindValue(':selector', $selector, \PDO::PARAM_STR);
$stmt->bindParam(':timeBucket', $timeBucket, \PDO::PARAM_INT); $stmt->bindValue(':timeBucket', $timeBucket, \PDO::PARAM_INT);
try { try {
$stmt->execute(); $stmt->execute();
} }
@@ -772,9 +773,9 @@ class Auth {
if ($e->getCode() == '23000') { if ($e->getCode() == '23000') {
// update the old entry // update the old entry
$stmt = $this->db->prepare('UPDATE users_throttling SET attempts = attempts+1 WHERE action_type = :actionType AND selector = :selector AND time_bucket = :timeBucket'); $stmt = $this->db->prepare('UPDATE users_throttling SET attempts = attempts+1 WHERE action_type = :actionType AND selector = :selector AND time_bucket = :timeBucket');
$stmt->bindParam(':actionType', $actionType, \PDO::PARAM_STR); $stmt->bindValue(':actionType', $actionType, \PDO::PARAM_STR);
$stmt->bindParam(':selector', $selector, \PDO::PARAM_STR); $stmt->bindValue(':selector', $selector, \PDO::PARAM_STR);
$stmt->bindParam(':timeBucket', $timeBucket, \PDO::PARAM_INT); $stmt->bindValue(':timeBucket', $timeBucket, \PDO::PARAM_INT);
$stmt->execute(); $stmt->execute();
} }
// if we have another error // if we have another error
@@ -785,9 +786,9 @@ class Auth {
} }
$stmt = $this->db->prepare('SELECT attempts FROM users_throttling WHERE action_type = :actionType AND selector = :selector AND time_bucket = :timeBucket'); $stmt = $this->db->prepare('SELECT attempts FROM users_throttling WHERE action_type = :actionType AND selector = :selector AND time_bucket = :timeBucket');
$stmt->bindParam(':actionType', $actionType, \PDO::PARAM_STR); $stmt->bindValue(':actionType', $actionType, \PDO::PARAM_STR);
$stmt->bindParam(':selector', $selector, \PDO::PARAM_STR); $stmt->bindValue(':selector', $selector, \PDO::PARAM_STR);
$stmt->bindParam(':timeBucket', $timeBucket, \PDO::PARAM_INT); $stmt->bindValue(':timeBucket', $timeBucket, \PDO::PARAM_INT);
if ($stmt->execute()) { if ($stmt->execute()) {
$attempts = $stmt->fetchColumn(); $attempts = $stmt->fetchColumn();

View File

@@ -1,7 +1,7 @@
<?php <?php
/** /*
* Copyright 2015 delight.im <info@delight.im> * Copyright (c) delight.im <info@delight.im>
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.

View File

@@ -1,7 +1,7 @@
<?php <?php
/** /*
* Copyright 2015 delight.im <info@delight.im> * Copyright (c) delight.im <info@delight.im>
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.

View File

@@ -1,7 +1,7 @@
<?php <?php
/** /*
* Copyright 2015 delight.im <info@delight.im> * Copyright (c) delight.im <info@delight.im>
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -20,12 +20,12 @@ header('Content-type: text/html; charset=utf-8');
error_reporting(E_ALL); error_reporting(E_ALL);
ini_set('display_errors', 'stdout'); ini_set('display_errors', 'stdout');
$db = new PDO('mysql:dbname=php_auth;host=127.0.0.1;charset=utf8', 'root', ''); $db = new PDO('mysql:dbname=php_auth;host=127.0.0.1;charset=utf8mb4', 'root', '');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
require __DIR__.'/../src/Auth.php'; require __DIR__.'/../src/Auth.php';
$auth = new Delight\Auth\Auth($db); $auth = new \Delight\Auth\Auth($db);
$result = processRequestData($auth); $result = processRequestData($auth);
@@ -38,7 +38,7 @@ else {
showGuestUserForm(); showGuestUserForm();
} }
function processRequestData(Delight\Auth\Auth $auth) { function processRequestData(\Delight\Auth\Auth $auth) {
if (isset($_POST)) { if (isset($_POST)) {
if (isset($_POST['action'])) { if (isset($_POST['action'])) {
if ($_POST['action'] === 'login') { if ($_POST['action'] === 'login') {
@@ -47,16 +47,16 @@ function processRequestData(Delight\Auth\Auth $auth) {
return 'ok'; return 'ok';
} }
catch (Delight\Auth\InvalidEmailException $e) { catch (\Delight\Auth\InvalidEmailException $e) {
return 'wrong email address'; return 'wrong email address';
} }
catch (Delight\Auth\InvalidPasswordException $e) { catch (\Delight\Auth\InvalidPasswordException $e) {
return 'wrong password'; return 'wrong password';
} }
catch (Delight\Auth\EmailNotVerifiedException $e) { catch (\Delight\Auth\EmailNotVerifiedException $e) {
return 'email not verified'; return 'email not verified';
} }
catch (Delight\Auth\TooManyRequestsException $e) { catch (\Delight\Auth\TooManyRequestsException $e) {
return 'too many requests'; return 'too many requests';
} }
} }
@@ -83,16 +83,16 @@ function processRequestData(Delight\Auth\Auth $auth) {
return $auth->register($_POST['email'], $_POST['password'], $_POST['username'], $callback); return $auth->register($_POST['email'], $_POST['password'], $_POST['username'], $callback);
} }
catch (Delight\Auth\InvalidEmailException $e) { catch (\Delight\Auth\InvalidEmailException $e) {
return 'invalid email address'; return 'invalid email address';
} }
catch (Delight\Auth\InvalidPasswordException $e) { catch (\Delight\Auth\InvalidPasswordException $e) {
return 'invalid password'; return 'invalid password';
} }
catch (Delight\Auth\UserAlreadyExistsException $e) { catch (\Delight\Auth\UserAlreadyExistsException $e) {
return 'user already exists'; return 'user already exists';
} }
catch (Delight\Auth\TooManyRequestsException $e) { catch (\Delight\Auth\TooManyRequestsException $e) {
return 'too many requests'; return 'too many requests';
} }
} }
@@ -102,13 +102,13 @@ function processRequestData(Delight\Auth\Auth $auth) {
return 'ok'; return 'ok';
} }
catch (Delight\Auth\InvalidSelectorTokenPairException $e) { catch (\Delight\Auth\InvalidSelectorTokenPairException $e) {
return 'invalid token'; return 'invalid token';
} }
catch (Delight\Auth\TokenExpiredException $e) { catch (\Delight\Auth\TokenExpiredException $e) {
return 'token expired'; return 'token expired';
} }
catch (Delight\Auth\TooManyRequestsException $e) { catch (\Delight\Auth\TooManyRequestsException $e) {
return 'too many requests'; return 'too many requests';
} }
} }
@@ -118,10 +118,10 @@ function processRequestData(Delight\Auth\Auth $auth) {
return 'ok'; return 'ok';
} }
catch (Delight\Auth\NotLoggedInException $e) { catch (\Delight\Auth\NotLoggedInException $e) {
return 'not logged in'; return 'not logged in';
} }
catch (Delight\Auth\InvalidPasswordException $e) { catch (\Delight\Auth\InvalidPasswordException $e) {
return 'invalid password(s)'; return 'invalid password(s)';
} }
} }
@@ -139,7 +139,7 @@ function processRequestData(Delight\Auth\Auth $auth) {
return null; return null;
} }
function showDebugData(Delight\Auth\Auth $auth, $result) { function showDebugData(\Delight\Auth\Auth $auth, $result) {
echo '<pre>'; echo '<pre>';
echo 'Last operation'."\t\t\t\t"; echo 'Last operation'."\t\t\t\t";
@@ -171,9 +171,9 @@ function showDebugData(Delight\Auth\Auth $auth, $result) {
echo "\n"; echo "\n";
echo 'Auth::createRandomString()'."\t\t"; echo 'Auth::createRandomString()'."\t\t";
var_dump(Delight\Auth\Auth::createRandomString()); var_dump(\Delight\Auth\Auth::createRandomString());
echo 'Auth::createUuid()'."\t\t\t"; echo 'Auth::createUuid()'."\t\t\t";
var_dump(Delight\Auth\Auth::createUuid()); var_dump(\Delight\Auth\Auth::createUuid());
echo '</pre>'; echo '</pre>';
} }