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

21 Commits

Author SHA1 Message Date
Marco
27a3990472 Only throttle login attempts that have wrong credentials 2016-07-09 00:58:04 +02:00
Marco
1979799480 Improve cookie handling to fix domain issues and add same-site flag 2016-07-09 00:48:55 +02:00
Marco
fff4a59be5 Include 'delight-im/cookie' as dependency for cookie handling 2016-07-09 00:14:51 +02:00
Marco
122e2b8006 Add missing '+' 2016-06-07 19:04:31 +02:00
Marco
bebd3efce2 Use Composer autoloader for tests as well 2016-06-07 19:01:56 +02:00
Marco
00dc8c3158 Enable both error reporting and assertions in 'tests' 2016-06-07 18:48:50 +02:00
Marco
e074474955 Fix list of dependencies 2016-06-07 18:42:29 +02:00
Marco
44907f3489 Add 'NOTICE' 2016-04-02 22:25:14 +02:00
Marco
ea27e8b7d3 Remove trailing newline in 'LICENSE' 2016-04-02 22:24:51 +02:00
Marco
354b34a724 Add migration guide for switch from v1.x.x to v2.x.x 2016-02-24 01:00:23 +01:00
Marco
3e083f9f17 Update '.editorconfig' to indent with spaces in Markdown 2016-02-24 01:00:08 +01:00
Marco
227c37e2b5 Update MySQL database schema to ensure full Unicode support 2016-02-24 00:59:50 +01:00
Marco
5403270ed2 Always use 'bindValue' instead of 'bindParam' with PDO 2016-02-24 00:59:18 +01:00
Marco
b1fa54efc9 Automatically re-hash passwords over time when necessary 2016-02-22 18:55:41 +01:00
Marco
2f8aaec42a Remove unnecessary code from MySQL data structure file 2016-02-22 18:53:52 +01:00
Marco
e5777f8bf2 Add '.editorconfig' 2016-01-28 16:26:01 +01:00
Marco
4c27a5a185 Update copyright notices 2016-01-28 16:25:45 +01:00
Marco
90fe75c27e Fix referenced namespaces to be absolute or fully qualified 2015-10-30 09:34:32 +01:00
Marco
5a954ca13b Improve usage guide and feature list in documentation 2015-10-29 21:07:09 +01:00
Marco
6ca92ecb11 Reduce visibility of 'throttle(...)' method that wrongly was 'public' 2015-10-29 20:59:39 +01:00
Marco
6ced34789f Update project tagline 2015-10-29 20:18:02 +01:00
13 changed files with 397 additions and 161 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

5
.gitignore vendored
View File

@@ -1 +1,6 @@
# IntelliJ
.idea/
# Composer
vendor/
composer.phar

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` (
`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,
`username` varchar(100) DEFAULT NULL,
`username` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`verified` tinyint(1) unsigned NOT NULL DEFAULT '0',
`registered` int(10) unsigned NOT NULL,
`last_login` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
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` (
`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,
`token` varchar(255) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
`expires` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `selector` (`selector`),
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` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
@@ -39,7 +30,7 @@ CREATE TABLE IF NOT EXISTS `users_remembered` (
PRIMARY KEY (`id`),
UNIQUE KEY `selector` (`selector`),
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` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
@@ -50,18 +41,14 @@ CREATE TABLE IF NOT EXISTS `users_resets` (
PRIMARY KEY (`id`),
UNIQUE KEY `selector` (`selector`),
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` (
`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,
`time_bucket` int(10) unsigned NOT NULL,
`attempts` mediumint(8) unsigned NOT NULL DEFAULT '1',
PRIMARY KEY (`id`),
UNIQUE KEY `action_type_selector_time_bucket` (`action_type`,`selector`,`time_bucket`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
/*!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 */;
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -199,4 +199,3 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

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;
```

5
NOTICE Normal file
View File

@@ -0,0 +1,5 @@
PHP-Auth
Copyright (c) delight.im <info@delight.im>
This product includes software developed by
delight.im (http://www.delight.im/).

135
README.md
View File

@@ -1,6 +1,8 @@
# 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.
@@ -15,8 +17,8 @@ Completely framework-agnostic and database-agnostic.
## Requirements
* PHP 5.5.0+
* PDO
* OpenSSL
* OpenSSL extension
* MySQL 5.5.3+ **or** MariaDB 5.5.23+
## Installation
@@ -41,14 +43,20 @@ Completely framework-agnostic and database-agnostic.
### Create a new instance
```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);
$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 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)
```php
@@ -59,20 +67,22 @@ try {
// we have signed up a new user with the ID `$userId`
}
catch (Delight\Auth\InvalidEmailException $e) {
catch (\Delight\Auth\InvalidEmailException $e) {
// invalid email address
}
catch (Delight\Auth\InvalidPasswordException $e) {
catch (\Delight\Auth\InvalidPasswordException $e) {
// invalid password
}
catch (Delight\Auth\UserAlreadyExistsException $e) {
catch (\Delight\Auth\UserAlreadyExistsException $e) {
// user already exists
}
catch (Delight\Auth\TooManyRequestsException $e) {
catch (\Delight\Auth\TooManyRequestsException $e) {
// 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.:
```php
@@ -89,20 +99,22 @@ try {
// user is logged in
}
catch (Delight\Auth\InvalidEmailException $e) {
catch (\Delight\Auth\InvalidEmailException $e) {
// wrong email address
}
catch (Delight\Auth\InvalidPasswordException $e) {
catch (\Delight\Auth\InvalidPasswordException $e) {
// wrong password
}
catch (Delight\Auth\EmailNotVerifiedException $e) {
catch (\Delight\Auth\EmailNotVerifiedException $e) {
// email not verified
}
catch (Delight\Auth\TooManyRequestsException $e) {
catch (\Delight\Auth\TooManyRequestsException $e) {
// 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
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
}
catch (Delight\Auth\InvalidSelectorTokenPairException $e) {
catch (\Delight\Auth\InvalidSelectorTokenPairException $e) {
// invalid token
}
catch (Delight\Auth\TokenExpiredException $e) {
catch (\Delight\Auth\TokenExpiredException $e) {
// token expired
}
catch (Delight\Auth\TooManyRequestsException $e) {
catch (\Delight\Auth\TooManyRequestsException $e) {
// too many requests
}
```
@@ -134,10 +146,10 @@ try {
// password has been changed
}
catch (Delight\Auth\NotLoggedInException $e) {
catch (\Delight\Auth\NotLoggedInException $e) {
// not logged in
}
catch (Delight\Auth\InvalidPasswordException $e) {
catch (\Delight\Auth\InvalidPasswordException $e) {
// invalid password(s)
}
```
@@ -150,6 +162,81 @@ $auth->logout();
// 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
* registration
@@ -167,7 +254,13 @@ $auth->logout();
* logout
* full and reliable destruction of session
* session management
* protection against session hijacking via cross-site scripting (XSS)
* do *not* permit script-based access to cookies
* restrict cookies to HTTPS to prevent session hijacking via non-secure HTTP
* protection against session fixation attacks
* protection against cross-site request forgery (CSRF)
* works automatically (i.e. no need for CSRF tokens everywhere)
* do *not* use HTTP `GET` requests for "dangerous" operations
* throttling
* per IP address
* per account
@@ -175,6 +268,10 @@ $auth->logout();
* prevents clickjacking
* prevent content sniffing (MIME sniffing)
* 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
@@ -201,7 +298,7 @@ All contributions are welcome! If you wish to contribute, please create an issue
## 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");
you may not use this file except in compliance with the License.

View File

@@ -1,8 +1,10 @@
{
"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": {
"php": ">=5.5.0"
"php": ">=5.5.0",
"ext-openssl": "*",
"delight-im/cookie": "^1.2"
},
"type": "library",
"keywords": [ "auth", "authentication", "login", "security" ],

99
composer.lock generated Normal file
View File

@@ -0,0 +1,99 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "dcde6f2cbd249e6604634f661a6bbcef",
"content-hash": "b15a581c89266dd3f30d1a1b92fbe1f1",
"packages": [
{
"name": "delight-im/cookie",
"version": "v1.2.1",
"source": {
"type": "git",
"url": "https://github.com/delight-im/PHP-Cookie.git",
"reference": "bac433e605a5879aefc4cd7f35329c3ded2213b5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/delight-im/PHP-Cookie/zipball/bac433e605a5879aefc4cd7f35329c3ded2213b5",
"reference": "bac433e605a5879aefc4cd7f35329c3ded2213b5",
"shasum": ""
},
"require": {
"delight-im/http": "^1.1",
"php": ">=5.3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Delight\\Cookie\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"description": "Modern cookie management for PHP",
"homepage": "https://github.com/delight-im/PHP-Cookie",
"keywords": [
"cookie",
"cookies",
"csrf",
"http",
"same-site",
"samesite",
"xss"
],
"time": "2016-07-08 22:01:56"
},
{
"name": "delight-im/http",
"version": "v1.1.0",
"source": {
"type": "git",
"url": "https://github.com/delight-im/PHP-HTTP.git",
"reference": "2ca9001f047c8b4e1b7ca7281823a1a9437850f8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/delight-im/PHP-HTTP/zipball/2ca9001f047c8b4e1b7ca7281823a1a9437850f8",
"reference": "2ca9001f047c8b4e1b7ca7281823a1a9437850f8",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Delight\\Http\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"description": "Hypertext Transfer Protocol (HTTP) utilities for PHP",
"homepage": "https://github.com/delight-im/PHP-HTTP",
"keywords": [
"headers",
"http",
"https"
],
"time": "2016-07-08 21:19:02"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=5.5.0",
"ext-openssl": "*"
},
"platform-dev": []
}

View File

@@ -1,7 +1,7 @@
<?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");
* 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__.'/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 {
const SESSION_FIELD_LOGGED_IN = 'auth_logged_in';
@@ -85,7 +85,7 @@ class Auth {
session_set_cookie_params($params['lifetime'], $params['path'], $params['domain'], $params['secure'], $params['httponly']);
// start the session
@session_start();
@\Delight\Cookie\Session::start();
}
/** Improves the application's security over HTTP(S) by setting specific headers */
@@ -118,7 +118,7 @@ class Auth {
// if both selector and token were found
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->bindParam(':selector', $parts[0], \PDO::PARAM_STR);
$stmt->bindValue(':selector', $parts[0], \PDO::PARAM_STR);
if ($stmt->execute()) {
$rememberData = $stmt->fetch(\PDO::FETCH_ASSOC);
if ($rememberData !== false) {
@@ -168,16 +168,15 @@ class Auth {
}
$username = isset($username) ? trim($username) : null;
$registered = time();
$password = password_hash($password, PASSWORD_DEFAULT);
$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->bindParam(':email', $email, \PDO::PARAM_STR);
$stmt->bindParam(':password', $password, \PDO::PARAM_STR);
$stmt->bindParam(':username', $username, \PDO::PARAM_STR);
$stmt->bindParam(':verified', $verified, \PDO::PARAM_INT);
$stmt->bindParam(':registered', $registered, \PDO::PARAM_INT);
$stmt->bindValue(':email', $email, \PDO::PARAM_STR);
$stmt->bindValue(':password', $password, \PDO::PARAM_STR);
$stmt->bindValue(':username', $username, \PDO::PARAM_STR);
$stmt->bindValue(':verified', $verified, \PDO::PARAM_INT);
$stmt->bindValue(':registered', time(), \PDO::PARAM_INT);
try {
$result = $stmt->execute();
@@ -198,7 +197,7 @@ class Auth {
if ($result) {
// get the ID of the user that we've just created
$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()) {
$newUserId = $stmt->fetchColumn();
@@ -235,10 +234,10 @@ class Auth {
$expires = time() + 3600 * 24;
$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->bindParam(':selector', $selector, \PDO::PARAM_STR);
$stmt->bindParam(':token', $tokenHashed, \PDO::PARAM_STR);
$stmt->bindParam(':expires', $expires, \PDO::PARAM_INT);
$stmt->bindValue(':email', $email, \PDO::PARAM_STR);
$stmt->bindValue(':selector', $selector, \PDO::PARAM_STR);
$stmt->bindValue(':token', $tokenHashed, \PDO::PARAM_STR);
$stmt->bindValue(':expires', $expires, \PDO::PARAM_INT);
if ($stmt->execute()) {
if (isset($emailConfirmationCallback) && is_callable($emailConfirmationCallback)) {
@@ -267,9 +266,6 @@ class Auth {
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function login($email, $password, $remember = false) {
$this->throttle(self::THROTTLE_ACTION_LOGIN);
$this->throttle(self::THROTTLE_ACTION_LOGIN, $email);
$email = isset($email) ? trim($email) : null;
if (empty($email)) {
throw new InvalidEmailException();
@@ -284,11 +280,17 @@ class Auth {
}
$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()) {
$userData = $stmt->fetch(\PDO::FETCH_ASSOC);
if ($userData !== false) {
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) {
$this->onLoginSuccessful($userData['id'], $email, $userData['username'], false);
@@ -303,10 +305,16 @@ class Auth {
}
}
else {
$this->throttle(self::THROTTLE_ACTION_LOGIN);
$this->throttle(self::THROTTLE_ACTION_LOGIN, $email);
throw new InvalidPasswordException();
}
}
else {
$this->throttle(self::THROTTLE_ACTION_LOGIN);
$this->throttle(self::THROTTLE_ACTION_LOGIN, $email);
throw new InvalidEmailException();
}
}
@@ -328,10 +336,10 @@ class Auth {
$expires = time() + 3600 * 24 * 28;
$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->bindParam(':selector', $selector, \PDO::PARAM_STR);
$stmt->bindParam(':token', $tokenHashed, \PDO::PARAM_STR);
$stmt->bindParam(':expires', $expires, \PDO::PARAM_INT);
$stmt->bindValue(':user', $userId, \PDO::PARAM_INT);
$stmt->bindValue(':selector', $selector, \PDO::PARAM_STR);
$stmt->bindValue(':token', $tokenHashed, \PDO::PARAM_STR);
$stmt->bindValue(':expires', $expires, \PDO::PARAM_INT);
if ($stmt->execute()) {
$this->setRememberCookie($selector, $token, $expires);
@@ -351,7 +359,7 @@ class Auth {
*/
private function deleteRememberDirective($userId) {
$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()) {
$this->setRememberCookie(null, null, time() - 3600);
@@ -383,7 +391,18 @@ class Auth {
}
// set the cookie with the selector and token
$result = setcookie(self::COOKIE_NAME_REMEMBER, $content, $expires, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
$cookie = new \Delight\Cookie\Cookie(self::COOKIE_NAME_REMEMBER);
$cookie->setValue($content);
$cookie->setExpiryTime($expires);
if (!empty($params['path'])) {
$cookie->setPath($params['path']);
}
if (!empty($params['domain'])) {
$cookie->setDomain($params['domain']);
}
$cookie->setHttpOnly($params['httponly']);
$cookie->setSecureOnly($params['secure']);
$result = $cookie->save();
if ($result === false) {
throw new HeadersAlreadySentError();
@@ -399,15 +418,13 @@ class Auth {
* @param bool $remembered whether the user was remembered ("remember me") or logged in actively
*/
private function onLoginSuccessful($userId, $email, $username, $remembered) {
$lastLogin = time();
$stmt = $this->db->prepare("UPDATE users SET last_login = :lastLogin WHERE id = :id");
$stmt->bindParam(':lastLogin', $lastLogin, \PDO::PARAM_INT);
$stmt->bindParam(':id', $userId, \PDO::PARAM_INT);
$stmt->bindValue(':lastLogin', time(), \PDO::PARAM_INT);
$stmt->bindValue(':id', $userId, \PDO::PARAM_INT);
$stmt->execute();
// re-generate the session ID to prevent session fixation attacks
session_regenerate_id(true);
\Delight\Cookie\Session::regenerate(true);
// save the user data in the session
$this->setLoggedIn(true);
@@ -453,8 +470,17 @@ class Auth {
// get our cookie settings
$params = $this->createCookieSettings();
// set the cookie with the selector and token
$result = setcookie(session_name(), '', time() - 3600, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
// cause the session cookie to be deleted
$cookie = new \Delight\Cookie\Cookie(session_name());
if (!empty($params['path'])) {
$cookie->setPath($params['path']);
}
if (!empty($params['domain'])) {
$cookie->setDomain($params['domain']);
}
$cookie->setHttpOnly($params['httponly']);
$cookie->setSecureOnly($params['secure']);
$result = $cookie->delete();
if ($result === false) {
throw new HeadersAlreadySentError();
@@ -477,20 +503,18 @@ class Auth {
$this->throttle(self::THROTTLE_ACTION_CONFIRM_EMAIL, $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()) {
$confirmationData = $stmt->fetch(\PDO::FETCH_ASSOC);
if ($confirmationData !== false) {
if (password_verify($token, $confirmationData['token'])) {
if ($confirmationData['expires'] >= time()) {
$verified = 1;
$stmt = $this->db->prepare("UPDATE users SET verified = :verified WHERE email = :email");
$stmt->bindParam(':verified', $verified, \PDO::PARAM_INT);
$stmt->bindParam(':email', $confirmationData['email'], \PDO::PARAM_STR);
$stmt->bindValue(':verified', 1, \PDO::PARAM_INT);
$stmt->bindValue(':email', $confirmationData['email'], \PDO::PARAM_STR);
if ($stmt->execute()) {
$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()) {
return;
}
@@ -543,7 +567,7 @@ class Auth {
$userId = $this->getUserId();
$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()) {
$passwordInDatabase = $stmt->fetchColumn();
if (password_verify($oldPassword, $passwordInDatabase)) {
@@ -574,8 +598,8 @@ class Auth {
$newPassword = password_hash($newPassword, PASSWORD_DEFAULT);
$stmt = $this->db->prepare("UPDATE users SET password = :password WHERE id = :userId");
$stmt->bindParam(':password', $newPassword, \PDO::PARAM_STR);
$stmt->bindParam(':userId', $userId, \PDO::PARAM_INT);
$stmt->bindValue(':password', $newPassword, \PDO::PARAM_STR);
$stmt->bindValue(':userId', $userId, \PDO::PARAM_INT);
$stmt->execute();
}
@@ -745,7 +769,7 @@ class Auth {
* @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
* @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 (isset($customSelector)) {
// use the provided selector for throttling
@@ -761,9 +785,9 @@ class Auth {
$timeBucket = self::getTimeBucket();
$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->bindParam(':selector', $selector, \PDO::PARAM_STR);
$stmt->bindParam(':timeBucket', $timeBucket, \PDO::PARAM_INT);
$stmt->bindValue(':actionType', $actionType, \PDO::PARAM_STR);
$stmt->bindValue(':selector', $selector, \PDO::PARAM_STR);
$stmt->bindValue(':timeBucket', $timeBucket, \PDO::PARAM_INT);
try {
$stmt->execute();
}
@@ -772,9 +796,9 @@ class Auth {
if ($e->getCode() == '23000') {
// 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->bindParam(':actionType', $actionType, \PDO::PARAM_STR);
$stmt->bindParam(':selector', $selector, \PDO::PARAM_STR);
$stmt->bindParam(':timeBucket', $timeBucket, \PDO::PARAM_INT);
$stmt->bindValue(':actionType', $actionType, \PDO::PARAM_STR);
$stmt->bindValue(':selector', $selector, \PDO::PARAM_STR);
$stmt->bindValue(':timeBucket', $timeBucket, \PDO::PARAM_INT);
$stmt->execute();
}
// if we have another error
@@ -785,9 +809,9 @@ class Auth {
}
$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->bindParam(':selector', $selector, \PDO::PARAM_STR);
$stmt->bindParam(':timeBucket', $timeBucket, \PDO::PARAM_INT);
$stmt->bindValue(':actionType', $actionType, \PDO::PARAM_STR);
$stmt->bindValue(':selector', $selector, \PDO::PARAM_STR);
$stmt->bindValue(':timeBucket', $timeBucket, \PDO::PARAM_INT);
if ($stmt->execute()) {
$attempts = $stmt->fetchColumn();
@@ -828,8 +852,6 @@ class Auth {
// get the default cookie settings
$params = session_get_cookie_params();
// optimize the cookie domain
$params['domain'] = self::optimizeCookieDomain($params['domain']);
// check if we want to send cookies via SSL/TLS only
$params['secure'] = $params['secure'] || $this->useHttps;
// check if we want to send cookies via HTTP(S) only
@@ -839,41 +861,6 @@ class Auth {
return $params;
}
/**
* Optimizes the specified cookie domain
*
* @param string $domain the supplied cookie domain
* @return string the optimized cookie domain
*/
private static function optimizeCookieDomain($domain) {
// if no domain has been explicitly provided
if (empty($domain)) {
// use the current hostname as a default
$domain = $_SERVER['SERVER_NAME'];
}
// if the domain name starts with the `www` subdomain
if (substr($domain, 0, 4) === 'www.') {
// strip the subdomain
$domain = substr($domain, 4);
}
// count the dots in the domain name
$numDots = substr_count($domain, '.');
// if there is no dot at all (usually `localhost`) or only a single dot (no subdomain)
if ($numDots < 2) {
// if the domain doesn't already start with a dot
if (substr($domain, 0, 1) !== '.') {
// prepend a dot to allow all subdomains
$domain = '.'.$domain;
}
}
// return the optimized domain name
return $domain;
}
/**
* Creates a random string with the given maximum length
*

View File

@@ -1,7 +1,7 @@
<?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");
* you may not use this file except in compliance with the License.

View File

@@ -1,7 +1,7 @@
<?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");
* you may not use this file except in compliance with the License.

View File

@@ -1,7 +1,7 @@
<?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");
* you may not use this file except in compliance with the License.
@@ -16,16 +16,23 @@
* limitations under the License.
*/
header('Content-type: text/html; charset=utf-8');
// enable error reporting
error_reporting(E_ALL);
ini_set('display_errors', 'stdout');
$db = new PDO('mysql:dbname=php_auth;host=127.0.0.1;charset=utf8', 'root', '');
// enable assertions
ini_set('assert.active', 1);
ini_set('zend.assertions', 1);
ini_set('assert.exception', 1);
header('Content-type: text/html; charset=utf-8');
require __DIR__.'/../vendor/autoload.php';
$db = new PDO('mysql:dbname=php_auth;host=127.0.0.1;charset=utf8mb4', 'root', '');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
require __DIR__.'/../src/Auth.php';
$auth = new Delight\Auth\Auth($db);
$auth = new \Delight\Auth\Auth($db);
$result = processRequestData($auth);
@@ -38,7 +45,7 @@ else {
showGuestUserForm();
}
function processRequestData(Delight\Auth\Auth $auth) {
function processRequestData(\Delight\Auth\Auth $auth) {
if (isset($_POST)) {
if (isset($_POST['action'])) {
if ($_POST['action'] === 'login') {
@@ -47,16 +54,16 @@ function processRequestData(Delight\Auth\Auth $auth) {
return 'ok';
}
catch (Delight\Auth\InvalidEmailException $e) {
catch (\Delight\Auth\InvalidEmailException $e) {
return 'wrong email address';
}
catch (Delight\Auth\InvalidPasswordException $e) {
catch (\Delight\Auth\InvalidPasswordException $e) {
return 'wrong password';
}
catch (Delight\Auth\EmailNotVerifiedException $e) {
catch (\Delight\Auth\EmailNotVerifiedException $e) {
return 'email not verified';
}
catch (Delight\Auth\TooManyRequestsException $e) {
catch (\Delight\Auth\TooManyRequestsException $e) {
return 'too many requests';
}
}
@@ -83,16 +90,16 @@ function processRequestData(Delight\Auth\Auth $auth) {
return $auth->register($_POST['email'], $_POST['password'], $_POST['username'], $callback);
}
catch (Delight\Auth\InvalidEmailException $e) {
catch (\Delight\Auth\InvalidEmailException $e) {
return 'invalid email address';
}
catch (Delight\Auth\InvalidPasswordException $e) {
catch (\Delight\Auth\InvalidPasswordException $e) {
return 'invalid password';
}
catch (Delight\Auth\UserAlreadyExistsException $e) {
catch (\Delight\Auth\UserAlreadyExistsException $e) {
return 'user already exists';
}
catch (Delight\Auth\TooManyRequestsException $e) {
catch (\Delight\Auth\TooManyRequestsException $e) {
return 'too many requests';
}
}
@@ -102,13 +109,13 @@ function processRequestData(Delight\Auth\Auth $auth) {
return 'ok';
}
catch (Delight\Auth\InvalidSelectorTokenPairException $e) {
catch (\Delight\Auth\InvalidSelectorTokenPairException $e) {
return 'invalid token';
}
catch (Delight\Auth\TokenExpiredException $e) {
catch (\Delight\Auth\TokenExpiredException $e) {
return 'token expired';
}
catch (Delight\Auth\TooManyRequestsException $e) {
catch (\Delight\Auth\TooManyRequestsException $e) {
return 'too many requests';
}
}
@@ -118,10 +125,10 @@ function processRequestData(Delight\Auth\Auth $auth) {
return 'ok';
}
catch (Delight\Auth\NotLoggedInException $e) {
catch (\Delight\Auth\NotLoggedInException $e) {
return 'not logged in';
}
catch (Delight\Auth\InvalidPasswordException $e) {
catch (\Delight\Auth\InvalidPasswordException $e) {
return 'invalid password(s)';
}
}
@@ -139,7 +146,7 @@ function processRequestData(Delight\Auth\Auth $auth) {
return null;
}
function showDebugData(Delight\Auth\Auth $auth, $result) {
function showDebugData(\Delight\Auth\Auth $auth, $result) {
echo '<pre>';
echo 'Last operation'."\t\t\t\t";
@@ -171,9 +178,9 @@ function showDebugData(Delight\Auth\Auth $auth, $result) {
echo "\n";
echo 'Auth::createRandomString()'."\t\t";
var_dump(Delight\Auth\Auth::createRandomString());
var_dump(\Delight\Auth\Auth::createRandomString());
echo 'Auth::createUuid()'."\t\t\t";
var_dump(Delight\Auth\Auth::createUuid());
var_dump(\Delight\Auth\Auth::createUuid());
echo '</pre>';
}