mirror of
https://github.com/delight-im/PHP-Auth.git
synced 2025-07-09 18:46:21 +02:00
Initial commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.idea/
|
67
Database/MySQL.sql
Normal file
67
Database/MySQL.sql
Normal 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
202
LICENSE
Normal 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
213
README.md
Normal 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
16
composer.json
Normal 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
914
src/Auth.php
Normal 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
44
src/Base64.php
Normal 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
45
src/Exceptions.php
Normal 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
235
tests/index.php
Normal 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>';
|
||||||
|
}
|
Reference in New Issue
Block a user