1
0
mirror of https://github.com/delight-im/PHP-Auth.git synced 2025-07-09 10:43:32 +02:00

Initial commit

This commit is contained in:
Marco
2015-10-20 14:26:38 +02:00
commit 4b1df6e291
9 changed files with 1737 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.idea/

67
Database/MySQL.sql Normal file
View File

@ -0,0 +1,67 @@
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,
`password` varchar(255) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
`username` varchar(100) 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 ;
CREATE TABLE IF NOT EXISTS `users_confirmations` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`email` varchar(254) 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 ;
CREATE TABLE IF NOT EXISTS `users_remembered` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`user` int(10) unsigned NOT NULL,
`selector` varchar(24) 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 `user` (`user`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
CREATE TABLE IF NOT EXISTS `users_resets` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`user` int(10) unsigned NOT NULL,
`selector` varchar(24) 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 `user` (`user`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
CREATE TABLE IF NOT EXISTS `users_throttling` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`action_type` enum('login','register','confirm_email') 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 */;

202
LICENSE Normal file
View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.

213
README.md Normal file
View File

@ -0,0 +1,213 @@
# Auth
Secure authentication for PHP, once and for all, really simple to use.
Completely framework-agnostic and database-agnostic.
## Why do I need this?
* There are [tons](http://www.troyhunt.com/2011/01/whos-who-of-bad-password-practices.html) [of](http://www.jeremytunnell.com/posts/swab-password-policies-and-two-factor-authentication-a-comedy-of-errors) [websites](http://badpasswordpolicies.tumblr.com/) with weak authentication systems. Don't build such a site.
* Re-implementing a new authentication system for every PHP project is *not* a good idea.
* Building your own authentication classes piece by piece, and copying it to every project, is *not* recommended, either.
* A secure authentication system with an easy-to-use API should be thoroughly designed and planned.
* Peer-review for your critical infrastructure is *a must*.
## Requirements
* PHP 5.5.0+
* PDO
* OpenSSL
## Installation
* Set up the PHP library
* Install via [Composer](https://getcomposer.org/) (recommended)
`$ composer require delight-im/auth`
* or
* Install manually (*not* recommended)
* Copy the contents of the [`src`](src) directory to your project
* Include the files in your code via `require` or `require_once`
* Set up a database and create the required tables
* [MySQL](Database/MySQL.sql)
## Usage
### Create a new instance
```php
// $db = new PDO('mysql:dbname=database;host=localhost;charset=utf8', 'username', 'password');
// $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$auth = new Delight\Auth\Auth($db);
```
If you have an open `PDO` connection already, just re-use it.
### Sign up a new user (register)
```php
try {
$userId = $auth->register($_POST['email'], $_POST['password'], $_POST['username'], function ($selector, $token) {
// send `$selector` and `$token` to the user (e.g. via email)
});
// we have signed up a new user with the ID `$userId`
}
catch (Delight\Auth\InvalidEmailException $e) {
// invalid email address
}
catch (Delight\Auth\InvalidPasswordException $e) {
// invalid password
}
catch (Delight\Auth\UserAlreadyExistsException $e) {
// user already exists
}
catch (Delight\Auth\TooManyRequestsException $e) {
// too many requests
}
```
For email verification, you should build an URL with the selector and token and send it to the user, e.g.:
```php
$url = 'https://www.example.com/verify_email?selector='.urlencode($selector).'&token='.urlencode($token);
```
If you don't want to perform email verification, just omit the last parameter to `register(...)`. The new user will be active immediately, then.
### Sign in an existing user (login)
```php
try {
$auth->login($_POST['email'], $_POST['password'], ($_POST['remember'] == 1));
// user is logged in
}
catch (Delight\Auth\InvalidEmailException $e) {
// wrong email address
}
catch (Delight\Auth\InvalidPasswordException $e) {
// wrong password
}
catch (Delight\Auth\EmailNotVerifiedException $e) {
// email not verified
}
catch (Delight\Auth\TooManyRequestsException $e) {
// too many requests
}
```
### Perform email verification
Extract the selector and token from the URL that the user clicked on in the verification email.
```php
try {
$auth->confirmEmail($_GET['selector'], $_GET['token']);
// email address has been verified
}
catch (Delight\Auth\InvalidSelectorTokenPairException $e) {
// invalid token
}
catch (Delight\Auth\TokenExpiredException $e) {
// token expired
}
catch (Delight\Auth\TooManyRequestsException $e) {
// too many requests
}
```
### Change the current user's password
If a user is currently logged in, they may change their password.
```php
try {
$auth->changePassword($_POST['oldPassword'], $_POST['newPassword']);
// password has been changed
}
catch (Delight\Auth\NotLoggedInException $e) {
// not logged in
}
catch (Delight\Auth\InvalidPasswordException $e) {
// invalid password(s)
}
```
### Logout
```php
$auth->logout();
// user has been signed out
```
## Features
* registration
* secure password storage using the bcrypt algorithm
* email verification through message with confirmation link
* assurance of unique email addresses
* customizable password requirements and enforcement
* optional usernames with customizable restrictions
* login
* keeping the user logged in for a long time via secure long-lived token ("remember me")
* account management
* change password
* tracking the time of sign up and last login
* check if user has been logged in via "remember me" cookie
* logout
* full and reliable destruction of session
* session management
* protection against session fixation attacks
* throttling
* per IP address
* per account
* enhanced HTTP security
* prevents clickjacking
* prevent content sniffing (MIME sniffing)
* disables caching of potentially sensitive data
## Exceptions
This library throws two types of exceptions to indicate problems:
* `AuthException` and its subclasses are thrown whenever a method does not complete successfully. You should *always* catch these exceptions as they carry the normal error responses that you must react to.
* `AuthError` and its subclasses are thrown whenever there is an internal problem or the library has not been installed correctly. You should *not* catch these exceptions.
## General advice
* Both serving the authentication pages (e.g. login and registration) and submitting the data entered by the user should only be done over TLS (HTTPS).
* You should enforce a minimum length for passwords, e.g. 10 characters, but *no* maximum length. Moreover, you should not restrict the set of allowed characters.
* Whenever a user was remembered ("remember me") and did not log in by entering their password, you should require re-authentication for critical features.
* Encourage users to use pass*phrases*, i.e. combinations of words or even full sentences, instead of single pass*words*.
* Do not prevent users' password managers from working correctly. Thus please use the standard form fields only and do not prevent copy and paste.
* Before executing sensitive account operations (e.g. changing a user's email address, deleting a user's account), you should always require re-authentication, i.e. require the user to sign in once more.
* You should not offer an online password reset feature ("forgot password") for high-security applications.
* For high-security applications, you should not use email addresses as identifiers. Instead, choose identifiers that are specific to the application and secret, e.g. an internal customer number.
## Contributing
All contributions are welcome! If you wish to contribute, please create an issue first so that your feature, problem or question can be discussed.
## License
```
Copyright 2015 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.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
```

16
composer.json Normal file
View File

@ -0,0 +1,16 @@
{
"name": "delight-im/auth",
"description": "Secure authentication for PHP, once and for all, really simple to use",
"require": {
"php": ">=5.5.0"
},
"type": "library",
"keywords": [ "auth", "authentication", "login", "security" ],
"homepage": "https://github.com/delight-im/PHP-Auth",
"license": "Apache-2.0",
"autoload": {
"psr-4": {
"Delight\\Auth\\": "src/"
}
}
}

914
src/Auth.php Normal file
View File

@ -0,0 +1,914 @@
<?php
/**
* Copyright 2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* 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.
*/
namespace Delight\Auth;
require __DIR__.'/Base64.php';
require __DIR__.'/Exceptions.php';
/** Secure authentication for PHP, once and for all, really simple to use */
class Auth {
const SESSION_FIELD_LOGGED_IN = 'auth_logged_in';
const SESSION_FIELD_USER_ID = 'auth_user_id';
const SESSION_FIELD_EMAIL = 'auth_email';
const SESSION_FIELD_USERNAME = 'auth_username';
const SESSION_FIELD_REMEMBERED = 'auth_remembered';
const COOKIE_CONTENT_SEPARATOR = '~';
const COOKIE_NAME_REMEMBER = 'auth_remember';
const IP_ADDRESS_HASH_ALGORITHM = 'sha256';
const THROTTLE_ACTION_LOGIN = 'login';
const THROTTLE_ACTION_REGISTER = 'register';
const THROTTLE_ACTION_CONFIRM_EMAIL = 'confirm_email';
const THROTTLE_HTTP_RESPONSE_CODE = 429;
/** @var \PDO the database connection that will be used */
private $db;
/** @var boolean whether HTTPS (TLS/SSL) will be used (recommended) */
private $useHttps;
/** @var boolean whether cookies should be accessible via client-side scripts (*not* recommended) */
private $allowCookiesScriptAccess;
/** @var string the user's current IP address */
private $ipAddress;
/** @var int the number of actions allowed (in throttling) per time bucket */
private $throttlingActionsPerTimeBucket;
/** @var int the size of the time buckets (used for throttling) in seconds */
private $throttlingTimeBucketSize;
/**
* @param \PDO $databaseConnection the database connection that will be used
* @param bool $useHttps whether HTTPS (TLS/SSL) will be used (recommended)
* @param bool $allowCookiesScriptAccess whether cookies should be accessible via client-side scripts (*not* recommended)
* @param string $ipAddress the IP address that should be used instead of the default setting (if any), e.g. when behind a proxy
*/
public function __construct(\PDO $databaseConnection, $useHttps = false, $allowCookiesScriptAccess = false, $ipAddress = null) {
$this->db = $databaseConnection;
$this->useHttps = $useHttps;
$this->allowCookiesScriptAccess = $allowCookiesScriptAccess;
$this->ipAddress = empty($ipAddress) ? $_SERVER['REMOTE_ADDR'] : $ipAddress;
$this->throttlingActionsPerTimeBucket = 20;
$this->throttlingTimeBucketSize = 3600;
$this->initSession();
$this->enhanceHttpSecurity();
$this->processRememberDirective();
}
/** Initializes the session and sets the correct configuration */
private function initSession() {
// use cookies to store session IDs
ini_set('session.use_cookies', 1);
// use cookies only (do not send session IDs in URLs)
ini_set('session.use_only_cookies', 1);
// do not send session IDs in URLs
ini_set('session.use_trans_sid', 0);
// get our cookie settings
$params = $this->createCookieSettings();
// define our new cookie settings
session_set_cookie_params($params['lifetime'], $params['path'], $params['domain'], $params['secure'], $params['httponly']);
// start the session
@session_start();
}
/** Improves the application's security over HTTP(S) by setting specific headers */
private function enhanceHttpSecurity() {
// remove exposure of PHP version (at least where possible)
header_remove('X-Powered-By');
// if the user is signed in
if ($this->isLoggedIn()) {
// prevent clickjacking
header('X-Frame-Options: sameorigin');
// prevent content sniffing (MIME sniffing)
header('X-Content-Type-Options: nosniff');
// disable caching of potentially sensitive data
header('Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0', true);
header('Expires: Thu, 19 Nov 1981 00:00:00 GMT', true);
header('Pragma: no-cache', true);
}
}
/** Checks if there is a "remember me" directive set and handles the automatic login (if appropriate) */
private function processRememberDirective() {
// if the user is not signed in yet
if (!$this->isLoggedIn()) {
// if a remember cookie is set
if (isset($_COOKIE[self::COOKIE_NAME_REMEMBER])) {
// split the cookie's content into selector and token
$parts = explode(self::COOKIE_CONTENT_SEPARATOR, $_COOKIE[self::COOKIE_NAME_REMEMBER], 2);
// 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);
if ($stmt->execute()) {
$rememberData = $stmt->fetch(\PDO::FETCH_ASSOC);
if ($rememberData !== false) {
if ($rememberData['expires'] >= time()) {
if (password_verify($parts[1], $rememberData['token'])) {
$this->onLoginSuccessful($rememberData['user'], $rememberData['email'], $rememberData['username'], true);
}
}
}
}
}
}
}
}
/**
* Attempts to sign up a user
*
* If you want accounts to be activated by default, pass `null` as the fourth argument
*
* If you want to perform email verification, pass `function ($selector, $token) {}` as the fourth argument
*
* @param string $email the email address to register
* @param string $password the password for the new account
* @param string|null $username (optional) the username that will be displayed
* @param callable|null $emailConfirmationCallback (optional) the function that sends the confirmation email
* @return int the ID of the user that has been created (if any)
* @throws InvalidEmailException if the email address was invalid
* @throws InvalidPasswordException if the password was invalid
* @throws UserAlreadyExistsException if a user with the specified email address already exists
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function register($email, $password, $username = null, callable $emailConfirmationCallback = null) {
$this->throttle(self::THROTTLE_ACTION_REGISTER);
$email = isset($email) ? trim($email) : null;
if (empty($email)) {
throw new InvalidEmailException();
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidEmailException();
}
$password = isset($password) ? trim($password) : null;
if (empty($password)) {
throw new InvalidPasswordException();
}
$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);
try {
$result = $stmt->execute();
}
catch (\PDOException $e) {
// if we have a duplicate entry
if ($e->getCode() == '23000') {
throw new UserAlreadyExistsException();
}
// if we have another error
else {
// throw an exception
throw new DatabaseError(null, null, $e);
}
}
// if creating the new user was successful
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);
if ($result = $stmt->execute()) {
$newUserId = $stmt->fetchColumn();
}
else {
$newUserId = null;
}
if ($verified === 1) {
return $newUserId;
}
else {
$this->createConfirmationRequest($email, $emailConfirmationCallback);
return $newUserId;
}
}
else {
throw new DatabaseError();
}
}
/**
* Creates a request for email confirmation
*
* @param string $email the email address to verify
* @param callable $emailConfirmationCallback the function that sends the confirmation email
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
private function createConfirmationRequest($email, callable $emailConfirmationCallback) {
$selector = self::createRandomString(16);
$token = self::createRandomString(16);
$tokenHashed = password_hash($token, PASSWORD_DEFAULT);
$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);
if ($stmt->execute()) {
if (isset($emailConfirmationCallback) && is_callable($emailConfirmationCallback)) {
$emailConfirmationCallback($selector, $token);
}
else {
throw new MissingCallbackError();
}
return;
}
else {
throw new DatabaseError();
}
}
/**
* Attempts to sign in a user
*
* @param string $email the user's email address
* @param string $password the user's password
* @param bool $remember whether to keep the user logged in ("remember me") or not
* @throws InvalidEmailException if the email address was invalid or could not be found
* @throws InvalidPasswordException if the password was invalid or didn't match the email address
* @throws EmailNotVerifiedException if the email address has not been verified yet via confirmation email
* @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();
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidEmailException();
}
$password = isset($password) ? trim($password) : null;
if (empty($password)) {
throw new InvalidPasswordException();
}
$stmt = $this->db->prepare("SELECT id, password, verified, username FROM users WHERE email = :email");
$stmt->bindParam(':email', $email, \PDO::PARAM_STR);
if ($stmt->execute()) {
$userData = $stmt->fetch(\PDO::FETCH_ASSOC);
if ($userData !== false) {
if (password_verify($password, $userData['password'])) {
if ($userData['verified'] == 1) {
$this->onLoginSuccessful($userData['id'], $email, $userData['username'], false);
if ($remember) {
$this->createRememberDirective($userData['id']);
}
return;
}
else {
throw new EmailNotVerifiedException();
}
}
else {
throw new InvalidPasswordException();
}
}
else {
throw new InvalidEmailException();
}
}
else {
throw new DatabaseError();
}
}
/**
* Creates a new directive keeping the user logged in ("remember me")
*
* @param int $userId the user ID to keep signed in
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
private function createRememberDirective($userId) {
$selector = self::createRandomString(24);
$token = self::createRandomString(24);
$tokenHashed = password_hash($token, PASSWORD_DEFAULT);
$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);
if ($stmt->execute()) {
$this->setRememberCookie($selector, $token, $expires);
return;
}
else {
throw new DatabaseError();
}
}
/**
* Clears an existing directive that keeps the user logged in ("remember me")
*
* @param int $userId the user ID that shouldn't be kept signed in anymore
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
private function deleteRememberDirective($userId) {
$stmt = $this->db->prepare("DELETE FROM users_remembered WHERE user = :user");
$stmt->bindParam(':user', $userId, \PDO::PARAM_INT);
if ($stmt->execute()) {
$this->setRememberCookie(null, null, time() - 3600);
return;
}
else {
throw new DatabaseError();
}
}
/**
* Sets or updates the cookie that manages the "remember me" token
*
* @param string $selector the selector from the selector/token pair
* @param string $token the token from the selector/token pair
* @param int $expires timestamp (in seconds) when the token expires
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
private function setRememberCookie($selector, $token, $expires) {
// get our cookie settings
$params = $this->createCookieSettings();
if (isset($selector) && isset($token)) {
$content = $selector . self::COOKIE_CONTENT_SEPARATOR . $token;
}
else {
$content = '';
}
// set the cookie with the selector and token
$result = setcookie(self::COOKIE_NAME_REMEMBER, $content, $expires, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
if ($result === false) {
throw new HeadersAlreadySentError();
}
}
/**
* Called when the user has successfully logged in (via standard login or "remember me")
*
* @param int $userId the ID of the user who has just logged in
* @param string $email the email address of the user who has just logged in
* @param string $username the username (if any)
* @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->execute();
// re-generate the session ID to prevent session fixation attacks
session_regenerate_id(true);
// save the user data in the session
$this->setLoggedIn(true);
$this->setUserId($userId);
$this->setEmail($email);
$this->setUsername($username);
$this->setRemembered($remembered);
}
/**
* Logs out the user and destroys all session data
*
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function logout() {
// if the user has been signed in
if ($this->isLoggedIn()) {
// get the user's ID
$userId = $this->getUserId();
// if a user ID was set
if (isset($userId)) {
// delete any existing remember directives
$this->deleteRememberDirective($userId);
}
}
// unset the session variables
$_SESSION = array();
// delete the cookie
$this->deleteSessionCookie();
// destroy the session
session_destroy();
}
/**
* Deletes the session cookie on the client
*
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
private function deleteSessionCookie() {
// 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']);
if ($result === false) {
throw new HeadersAlreadySentError();
}
}
/**
* Confirms an email address and activates the account by supplying the correct selector/token pair
*
* The selector/token pair must have been generated previously by registering a new account
*
* @param string $selector the selector from the selector/token pair
* @param string $token the token from the selector/token pair
* @throws InvalidSelectorTokenPairException if either the selector or the token was not correct
* @throws TokenExpiredException if the token has already expired
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function confirmEmail($selector, $token) {
$this->throttle(self::THROTTLE_ACTION_CONFIRM_EMAIL);
$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);
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);
if ($stmt->execute()) {
$stmt = $this->db->prepare("DELETE FROM users_confirmations WHERE id = :id");
$stmt->bindParam(':id', $confirmationData['id'], \PDO::PARAM_INT);
if ($stmt->execute()) {
return;
}
else {
throw new DatabaseError();
}
}
else {
throw new DatabaseError();
}
}
else {
throw new TokenExpiredException();
}
}
else {
throw new InvalidSelectorTokenPairException();
}
}
else {
throw new InvalidSelectorTokenPairException();
}
}
else {
throw new DatabaseError();
}
}
/**
* Changes the (currently logged-in) user's password
*
* @param string $oldPassword the old password to verify account ownership
* @param string $newPassword the new password that should be used
* @throws NotLoggedInException if the user is not currently logged in
* @throws InvalidPasswordException if either the old password was wrong or the new password was invalid
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function changePassword($oldPassword, $newPassword) {
if ($this->isLoggedIn()) {
$oldPassword = isset($oldPassword) ? trim($oldPassword) : null;
if (empty($oldPassword)) {
throw new InvalidPasswordException();
}
$newPassword = isset($newPassword) ? trim($newPassword) : null;
if (empty($newPassword)) {
throw new InvalidPasswordException();
}
$userId = $this->getUserId();
$stmt = $this->db->prepare("SELECT password FROM users WHERE id = :userId");
$stmt->bindParam(':userId', $userId, \PDO::PARAM_INT);
if ($stmt->execute()) {
$passwordInDatabase = $stmt->fetchColumn();
if (password_verify($oldPassword, $passwordInDatabase)) {
$this->updatePassword($userId, $newPassword);
return;
}
else {
throw new InvalidPasswordException();
}
}
else {
throw new DatabaseError();
}
}
else {
throw new NotLoggedInException();
}
}
/**
* Updates the given user's password by setting it to the new specified password
*
* @param int $userId the ID of the user whose password should be updated
* @param string $newPassword the new password
*/
private function updatePassword($userId, $newPassword) {
$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->execute();
}
/**
* Sets whether the user is currently logged in and updates the session
*
* @param bool $loggedIn whether the user is logged in or not
*/
private function setLoggedIn($loggedIn) {
$_SESSION[self::SESSION_FIELD_LOGGED_IN] = $loggedIn;
}
/**
* Returns whether the user is currently logged in by reading from the session
*
* @return boolean whether the user is logged in or not
*/
public function isLoggedIn() {
return isset($_SESSION) && isset($_SESSION[self::SESSION_FIELD_LOGGED_IN]) && $_SESSION[self::SESSION_FIELD_LOGGED_IN] === true;
}
/**
* Shorthand/alias for ´isLoggedIn()´
*
* @return boolean
*/
public function check() {
return $this->isLoggedIn();
}
/**
* Sets the currently signed-in user's ID and updates the session
*
* @param int $userId the user's ID
*/
private function setUserId($userId) {
$_SESSION[self::SESSION_FIELD_USER_ID] = intval($userId);
}
/**
* Returns the currently signed-in user's ID by reading from the session
*
* @return int the user ID
*/
public function getUserId() {
if (isset($_SESSION) && isset($_SESSION[self::SESSION_FIELD_USER_ID])) {
return $_SESSION[self::SESSION_FIELD_USER_ID];
}
else {
return null;
}
}
/**
* Shorthand/alias for `getUserId()`
*
* @return int
*/
public function id() {
return $this->getUserId();
}
/**
* Sets the currently signed-in user's email address and updates the session
*
* @param string $email the email address
*/
private function setEmail($email) {
$_SESSION[self::SESSION_FIELD_EMAIL] = $email;
}
/**
* Returns the currently signed-in user's email address by reading from the session
*
* @return string the email address
*/
public function getEmail() {
if (isset($_SESSION) && isset($_SESSION[self::SESSION_FIELD_EMAIL])) {
return $_SESSION[self::SESSION_FIELD_EMAIL];
}
else {
return null;
}
}
/**
* Sets the currently signed-in user's display name and updates the session
*
* @param string $username the display name
*/
private function setUsername($username) {
$_SESSION[self::SESSION_FIELD_USERNAME] = $username;
}
/**
* Returns the currently signed-in user's display name by reading from the session
*
* @return string the display name
*/
public function getUsername() {
if (isset($_SESSION) && isset($_SESSION[self::SESSION_FIELD_USERNAME])) {
return $_SESSION[self::SESSION_FIELD_USERNAME];
}
else {
return null;
}
}
/**
* Sets whether the currently signed-in user has been remembered by a long-lived cookie
*
* @param bool $remembered whether the user was remembered
*/
private function setRemembered($remembered) {
$_SESSION[self::SESSION_FIELD_REMEMBERED] = $remembered;
}
/**
* Returns whether the currently signed-in user has been remembered by a long-lived cookie
*
* @return bool whether they have been remembered
*/
public function isRemembered() {
if (isset($_SESSION) && isset($_SESSION[self::SESSION_FIELD_REMEMBERED])) {
return $_SESSION[self::SESSION_FIELD_REMEMBERED];
}
else {
return null;
}
}
/**
* Hashes the supplied data
*
* @param mixed $data the data to hash
* @return string the hash in Base64-encoded format
*/
private static function hash($data) {
$hashRaw = hash(self::IP_ADDRESS_HASH_ALGORITHM, $data, true);
return base64_encode($hashRaw);
}
/**
* Returns the user's current IP address
*
* @return string the IP address (IPv4 or IPv6)
*/
public function getIpAddress() {
return $this->ipAddress;
}
/**
* Returns the current time bucket that is used for throttling purposes
*
* @return int the time bucket
*/
private function getTimeBucket() {
return (int) (time() / $this->throttlingTimeBucketSize);
}
/**
* Throttles the specified action for the user to protect against too many requests
*
* @param string $actionType one of the `THROTTLE_ACTION_*` constants
* @param mixed|null $customSelector a custom selector to use for throttling (if any), otherwise the IP address will be used
* @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) {
// if a custom selector has been provided (e.g. username, user ID or confirmation token)
if (isset($customSelector)) {
// use the provided selector for throttling
$selector = self::hash($customSelector);
}
// if no custom selector was provided
else {
// throttle by the user's IP address
$selector = self::hash($this->getIpAddress());
}
// get the time bucket that we do the throttling for
$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);
try {
$stmt->execute();
}
catch (\PDOException $e) {
// if we have a duplicate entry
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->execute();
}
// if we have another error
else {
// throw an exception
throw new DatabaseError(null, null, $e);
}
}
$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);
if ($stmt->execute()) {
$attempts = $stmt->fetchColumn();
if ($attempts !== false) {
// if the number of attempts has acceeded our accepted limit
if ($attempts > $this->throttlingActionsPerTimeBucket) {
// send a HTTP status code that indicates active throttling
http_response_code(self::THROTTLE_HTTP_RESPONSE_CODE);
// tell the client when they should try again
@header('Retry-After: '.$this->throttlingTimeBucketSize);
// throw an exception
throw new TooManyRequestsException();
}
}
}
}
/**
* Customizes the throttling options
*
* @param int $actionsPerTimeBucket the number of allowed attempts/requests per time bucket
* @param int $timeBucketSize the size of the time buckets in seconds
*/
public function setThrottlingOptions($actionsPerTimeBucket, $timeBucketSize) {
$this->throttlingActionsPerTimeBucket = intval($actionsPerTimeBucket);
if (isset($timeBucketSize)) {
$this->throttlingTimeBucketSize = intval($timeBucketSize);
}
}
/**
* Creates the cookie settings that will be used to create and update cookies on the client
*
* @return array the cookie settings
*/
private function createCookieSettings() {
// 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
$params['httponly'] = $params['httponly'] || !$this->allowCookiesScriptAccess;
// return the modified settings
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
*
* With the default parameter, the output should contain at least as much randomness as a UUID
*
* @param int $maxLength the maximum length of the output string (integer multiple of 4)
* @return string the new random string
*/
public static function createRandomString($maxLength = 24) {
// calculate how many bytes of randomness we need for the specified string length
$bytes = floor(intval($maxLength) / 4) * 3;
// get random data
$data = openssl_random_pseudo_bytes($bytes);
// return the Base64-encoded result
return Base64::encode($data, true);
}
/**
* Creates a UUID v4 as per RFC 4122
*
* The UUID contains 128 bits of data (where 122 are random), i.e. 36 characters
*
* @return string the UUID
* @author Jack @ Stack Overflow
*/
public static function createUuid() {
$data = openssl_random_pseudo_bytes(16);
// set the version to 0100
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
// set bits 6-7 to 10
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
}

44
src/Base64.php Normal file
View File

@ -0,0 +1,44 @@
<?php
/**
* Copyright 2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* 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.
*/
namespace Delight\Auth;
class Base64 {
const SPECIAL_CHARS_ORIGINAL = '+/=';
const SPECIAL_CHARS_SAFE = '._-';
public static function encode($data, $safeChars = false) {
$result = base64_encode($data);
if ($safeChars) {
$result = strtr($result, self::SPECIAL_CHARS_ORIGINAL, self::SPECIAL_CHARS_SAFE);
}
return $result;
}
public static function decode($data) {
$data = strtr($data, self::SPECIAL_CHARS_SAFE, self::SPECIAL_CHARS_ORIGINAL);
$result = base64_decode($data, true);
return $result;
}
}

45
src/Exceptions.php Normal file
View File

@ -0,0 +1,45 @@
<?php
/**
* Copyright 2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* 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.
*/
namespace Delight\Auth;
class AuthException extends \Exception {}
class InvalidEmailException extends AuthException {}
class InvalidPasswordException extends AuthException {}
class EmailNotVerifiedException extends AuthException {}
class UserAlreadyExistsException extends AuthException {}
class NotLoggedInException extends AuthException {}
class InvalidSelectorTokenPairException extends AuthException {}
class TokenExpiredException extends AuthException {}
class TooManyRequestsException extends AuthException {}
class AuthError extends \Exception {}
class DatabaseError extends AuthError {}
class MissingCallbackError extends AuthError {}
class HeadersAlreadySentError extends AuthError {}

235
tests/index.php Normal file
View File

@ -0,0 +1,235 @@
<?php
/**
* Copyright 2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* 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.
*/
header('Content-type: text/html; charset=utf-8');
error_reporting(E_ALL);
ini_set('display_errors', 'stdout');
$db = new PDO('mysql:dbname=php_auth;host=127.0.0.1;charset=utf8', 'root', '');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
require __DIR__.'/../src/Auth.php';
$auth = new Delight\Auth\Auth($db);
$result = processRequestData($auth);
showDebugData($auth, $result);
if ($auth->check()) {
showAuthenticatedUserForm();
}
else {
showGuestUserForm();
}
function processRequestData(Delight\Auth\Auth $auth) {
if (isset($_POST)) {
if (isset($_POST['action'])) {
if ($_POST['action'] === 'login') {
try {
$auth->login($_POST['email'], $_POST['password'], ($_POST['remember'] == 1));
return 'ok';
}
catch (Delight\Auth\InvalidEmailException $e) {
return 'wrong email address';
}
catch (Delight\Auth\InvalidPasswordException $e) {
return 'wrong password';
}
catch (Delight\Auth\EmailNotVerifiedException $e) {
return 'email not verified';
}
catch (Delight\Auth\TooManyRequestsException $e) {
return 'too many requests';
}
}
else if ($_POST['action'] === 'register') {
try {
if ($_POST['require_verification'] == 1) {
$callback = function ($selector, $token) {
echo '<pre>';
echo 'Email confirmation';
echo "\n";
echo ' > Selector';
echo "\t\t\t\t";
echo htmlspecialchars($selector);
echo "\n";
echo ' > Token';
echo "\t\t\t\t";
echo htmlspecialchars($token);
echo '</pre>';
};
}
else {
$callback = null;
}
return $auth->register($_POST['email'], $_POST['password'], $_POST['username'], $callback);
}
catch (Delight\Auth\InvalidEmailException $e) {
return 'invalid email address';
}
catch (Delight\Auth\InvalidPasswordException $e) {
return 'invalid password';
}
catch (Delight\Auth\UserAlreadyExistsException $e) {
return 'user already exists';
}
catch (Delight\Auth\TooManyRequestsException $e) {
return 'too many requests';
}
}
else if ($_POST['action'] === 'confirmEmail') {
try {
$auth->confirmEmail($_POST['selector'], $_POST['token']);
return 'ok';
}
catch (Delight\Auth\InvalidSelectorTokenPairException $e) {
return 'invalid token';
}
catch (Delight\Auth\TokenExpiredException $e) {
return 'token expired';
}
catch (Delight\Auth\TooManyRequestsException $e) {
return 'too many requests';
}
}
else if ($_POST['action'] === 'changePassword') {
try {
$auth->changePassword($_POST['oldPassword'], $_POST['newPassword']);
return 'ok';
}
catch (Delight\Auth\NotLoggedInException $e) {
return 'not logged in';
}
catch (Delight\Auth\InvalidPasswordException $e) {
return 'invalid password(s)';
}
}
else if ($_POST['action'] === 'logout') {
$auth->logout();
return 'ok';
}
else {
throw new Exception('Unexpected action: '.$_POST['action']);
}
}
}
return null;
}
function showDebugData(Delight\Auth\Auth $auth, $result) {
echo '<pre>';
echo 'Last operation'."\t\t\t\t";
var_dump($result);
echo 'Session ID'."\t\t\t\t";
var_dump(session_id());
echo "\n";
echo '$auth->isLoggedIn()'."\t\t\t";
var_dump($auth->isLoggedIn());
echo '$auth->check()'."\t\t\t\t";
var_dump($auth->check());
echo "\n";
echo '$auth->getUserId()'."\t\t\t";
var_dump($auth->getUserId());
echo '$auth->id()'."\t\t\t\t";
var_dump($auth->id());
echo "\n";
echo '$auth->getEmail()'."\t\t\t";
var_dump($auth->getEmail());
echo '$auth->getUsername()'."\t\t\t";
var_dump($auth->getUsername());
echo '$auth->isRemembered()'."\t\t\t";
var_dump($auth->isRemembered());
echo '$auth->getIpAddress()'."\t\t\t";
var_dump($auth->getIpAddress());
echo "\n";
echo 'Auth::createRandomString()'."\t\t";
var_dump(Delight\Auth\Auth::createRandomString());
echo 'Auth::createUuid()'."\t\t\t";
var_dump(Delight\Auth\Auth::createUuid());
echo '</pre>';
}
function showGeneralForm() {
echo '<form action="" method="get" accept-charset="utf-8">';
echo '<button type="submit">Refresh</button>';
echo '</form>';
}
function showAuthenticatedUserForm() {
showGeneralForm();
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="changePassword" />';
echo '<input type="text" name="oldPassword" placeholder="Old password" /> ';
echo '<input type="text" name="newPassword" placeholder="New password" /> ';
echo '<button type="submit">Change password</button>';
echo '</form>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="logout" />';
echo '<button type="submit">Logout</button>';
echo '</form>';
}
function showGuestUserForm() {
showGeneralForm();
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="login" />';
echo '<input type="text" name="email" placeholder="Email" /> ';
echo '<input type="text" name="password" placeholder="Password" /> ';
echo '<select name="remember" size="1">';
echo '<option value="0">Remember? — No</option>';
echo '<option value="1">Remember? — Yes</option>';
echo '</select> ';
echo '<button type="submit">Login</button>';
echo '</form>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="register" />';
echo '<input type="text" name="email" placeholder="Email" /> ';
echo '<input type="text" name="password" placeholder="Password" /> ';
echo '<input type="text" name="username" placeholder="Username (optional)" /> ';
echo '<select name="require_verification" size="1">';
echo '<option value="0">Require email confirmation? — No</option>';
echo '<option value="1">Require email confirmation? — Yes</option>';
echo '</select> ';
echo '<button type="submit">Register</button>';
echo '</form>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="confirmEmail" />';
echo '<input type="text" name="selector" placeholder="Selector" /> ';
echo '<input type="text" name="token" placeholder="Token" /> ';
echo '<button type="submit">Confirm email</button>';
echo '</form>';
}