first commit
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 by Gabriel Birke <gb@birke-software.de>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Secure "Remember Me"
|
||||
This library implements the best practices for implementing a secure
|
||||
"Remember Me" functionality on web sites. Login information and unique secure
|
||||
tokens are stored in a cookie. If the user visits the site, the login information
|
||||
from the cookie is compared to information stored on the server. If the tokens
|
||||
match, the user is logged in. A user can have login cookies on several
|
||||
computers/browsers.
|
||||
|
||||
This library is heavily inspired by Barry Jaspan's article
|
||||
"[Improved Persistent Login Cookie Best Practice][1]". The library protects
|
||||
against the following attack scenarios:
|
||||
|
||||
- The computer of a user is stolen or compromised, enabling the attacker to log
|
||||
in with the existing "Remember Me" cookie. The user knows this has happened.
|
||||
The user can remotely invalidate all login cookies.
|
||||
- An attacker has obtained the "Remember Me" cookie and has logged in with it.
|
||||
The user does not know this. The next time he tries to log in with the cookie
|
||||
that was stolen, he gets a warning and all login cookies are invalidated.
|
||||
- An attacker has obtained the database of login tokens from the server. The
|
||||
stored tokens are hashed so he can't use them without computational effort
|
||||
(rainbow tables or brute force).
|
||||
|
||||
## Installation
|
||||
|
||||
composer require birke/rememberme
|
||||
|
||||
## Usage example
|
||||
See the `example` directory for an example.
|
||||
|
||||
## Improving security
|
||||
The generated tokens are pseudo-random and the storage classes use the SHA1 algorithm
|
||||
to hash them. If you need better security than that, overwrite the
|
||||
`Authenticator::generateToken` method to generate a truly random token. If you are
|
||||
using PHP >=5.5 you can use the "[password_hash][2]" and "[password_verify][3]" functions.
|
||||
On lower PHP versions you could use the [userland implementations][4] of these functions.
|
||||
|
||||
[1]: http://jaspan.com/improved%5Fpersistent%5Flogin%5Fcookie%5Fbest%5Fpractice
|
||||
[2]: http://www.php.net/manual/en/function.password-hash.php
|
||||
[3]: http://www.php.net/manual/en/function.password-verify.php
|
||||
[4]: https://github.com/ircmaxell/password_compat
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "birke/rememberme",
|
||||
"version": "1.0.5",
|
||||
"description": "Secure \"Remember Me\" functionality",
|
||||
"keywords": [ "cookie", "remember", "security"],
|
||||
"homepage": "https://github.com/gbirke/rememberme",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Gabriel Birke",
|
||||
"email": "gb@birke-software.de"
|
||||
}
|
||||
],
|
||||
"minimum-stability": "stable",
|
||||
"autoload": {
|
||||
"psr-4": {"Birke\\": "src/"}
|
||||
},
|
||||
"require": {
|
||||
"paragonie/random_compat": "^1.1.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "4.*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
/**
|
||||
* This file demonstrates how to use the Rememberme library.
|
||||
*
|
||||
* Some code (autoload, templating) is just simple boilerplate and no shining
|
||||
* example of how to write php applications.
|
||||
*
|
||||
* @author Gabriel Birke
|
||||
*/
|
||||
|
||||
require_once __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
use Birke\Rememberme;
|
||||
|
||||
/**
|
||||
* Helper function for redirecting and destroying the session
|
||||
* @param bool $destroySession
|
||||
* @return void
|
||||
*/
|
||||
function redirect($destroySession=false) {
|
||||
if($destroySession) {
|
||||
session_regenerate_id(true);
|
||||
session_destroy();
|
||||
}
|
||||
header("Location: index.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
// Normally you would store the credentials in a DB
|
||||
$username = "demo";
|
||||
$password = "demo";
|
||||
|
||||
// Initialize RememberMe Library with file storage
|
||||
$storagePath = dirname(__FILE__)."/tokens";
|
||||
if(!is_writable($storagePath) || !is_dir($storagePath)) {
|
||||
die("'$storagePath' does not exist or is not writable by the web server.
|
||||
To run the example, please create the directory and give it the
|
||||
correct permissions.");
|
||||
}
|
||||
$storage = new Rememberme\Storage\File($storagePath);
|
||||
$rememberMe = new Rememberme\Authenticator($storage);
|
||||
|
||||
// First, we initialize the session, to see if we are already logged in
|
||||
session_start();
|
||||
|
||||
if(!empty($_SESSION['username'])) {
|
||||
if(!empty($_GET['logout'])) {
|
||||
$rememberMe->clearCookie($_SESSION['username']);
|
||||
redirect(true);
|
||||
}
|
||||
|
||||
if(!empty($_GET['completelogout'])) {
|
||||
$storage->cleanAllTriplets($_SESSION['username']);
|
||||
redirect(true);
|
||||
}
|
||||
|
||||
// Check, if the Rememberme cookie exists and is still valid.
|
||||
// If not, we log out the current session
|
||||
if(!empty($_COOKIE[$rememberMe->getCookieName()]) && !$rememberMe->cookieIsValid()) {
|
||||
redirect(true);
|
||||
}
|
||||
|
||||
// User is still logged in - show content
|
||||
$content = tpl("user_is_logged_in");
|
||||
}
|
||||
// If we are not logged in, try to log in via Rememberme cookie
|
||||
else {
|
||||
// If we can present the correct tokens from the cookie, we are logged in
|
||||
$loginresult = $rememberMe->login();
|
||||
if($loginresult) {
|
||||
$_SESSION['username'] = $loginresult;
|
||||
// There is a chance that an attacker has stolen the login token, so we store
|
||||
// the fact that the user was logged in via RememberMe (instead of login form)
|
||||
$_SESSION['remembered_by_cookie'] = true;
|
||||
redirect();
|
||||
}
|
||||
else {
|
||||
// If $rememberMe returned false, check if the token was invalid
|
||||
if($rememberMe->loginTokenWasInvalid()) {
|
||||
$content = tpl("cookie_was_stolen");
|
||||
}
|
||||
// $rememberMe returned false because of invalid/missing Rememberme cookie - normal login process
|
||||
else {
|
||||
if(!empty($_POST)) {
|
||||
if($username == $_POST['username'] && $password == $_POST['password']) {
|
||||
session_regenerate_id();
|
||||
$_SESSION['username'] = $username;
|
||||
// If the user wants to be remembered, create Rememberme cookie
|
||||
if(!empty($_POST['rememberme'])) {
|
||||
$rememberMe->createCookie($username);
|
||||
}
|
||||
else {
|
||||
$rememberMe->clearCookie();
|
||||
}
|
||||
redirect();
|
||||
}
|
||||
else {
|
||||
$content = tpl("login", "Invalid credentials");
|
||||
}
|
||||
}
|
||||
else {
|
||||
$content = tpl("login");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// template function for including content, nothing interesting
|
||||
function tpl($template, $msg="") {
|
||||
$fn = __DIR__ . DIRECTORY_SEPARATOR . "templates" . DIRECTORY_SEPARATOR . $template . ".php";
|
||||
if(file_exists($fn)) {
|
||||
ob_start();
|
||||
include $fn;
|
||||
return ob_get_clean();
|
||||
}
|
||||
else {
|
||||
return "Template $fn not found";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/* HTML5 ✰ Boilerplate */
|
||||
|
||||
html, body, div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
abbr, address, cite, code, del, dfn, em, img, ins, kbd, q, samp,
|
||||
small, strong, sub, sup, var, b, i, dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, figcaption, figure, footer, header, hgroup,
|
||||
menu, nav, section, summary, time, mark, audio, video {
|
||||
margin:0;
|
||||
padding:0;
|
||||
border:0;
|
||||
outline:0;
|
||||
font-size:100%;
|
||||
vertical-align:baseline;
|
||||
background:transparent;
|
||||
}
|
||||
article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section {
|
||||
display:block;
|
||||
}
|
||||
nav ul { list-style:none; }
|
||||
blockquote, q { quotes:none; }
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after { content:''; content:none; }
|
||||
a { margin:0; padding:0; font-size:100%; vertical-align:baseline; background:transparent; }
|
||||
ins { background-color:#ff9; color:#000; text-decoration:none; }
|
||||
mark { background-color:#ff9; color:#000; font-style:italic; font-weight:bold; }
|
||||
del { text-decoration: line-through; }
|
||||
abbr[title], dfn[title] { border-bottom:1px dotted; cursor:help; }
|
||||
table { border-collapse:collapse; border-spacing:0; }
|
||||
hr { display:block; height:1px; border:0; border-top:1px solid #ccc; margin:1em 0; padding:0; }
|
||||
input, select { vertical-align:middle; }
|
||||
|
||||
|
||||
body { font:13px/1.231 sans-serif; *font-size:small; }
|
||||
select, input, textarea, button { font:99% sans-serif; }
|
||||
pre, code, kbd, samp { font-family: monospace, sans-serif; }
|
||||
|
||||
body, select, input, textarea { color: #444; }
|
||||
h1,h2,h3,h4,h5,h6 { font-weight: bold; }
|
||||
html { overflow-y: scroll; }
|
||||
|
||||
a:hover, a:active { outline: none; }
|
||||
a, a:active, a:visited { color: #607890; }
|
||||
a:hover { color: #036; }
|
||||
|
||||
ul, ol { margin-left: 1.8em; }
|
||||
ol { list-style-type: decimal; }
|
||||
|
||||
nav ul, nav li { margin: 0; }
|
||||
small { font-size: 85%; }
|
||||
strong, th { font-weight: bold; }
|
||||
td, td img { vertical-align: top; }
|
||||
sub { vertical-align: sub; font-size: smaller; }
|
||||
sup { vertical-align: super; font-size: smaller; }
|
||||
pre { padding: 15px; white-space: pre; white-space: pre-wrap; white-space: pre-line; word-wrap: break-word; }
|
||||
textarea { overflow: auto; }
|
||||
.ie6 legend, .ie7 legend { margin-left: -7px; }
|
||||
input[type="radio"] { vertical-align: text-bottom; }
|
||||
input[type="checkbox"] { vertical-align: bottom; }
|
||||
.ie7 input[type="checkbox"] { vertical-align: baseline; }
|
||||
.ie6 input { vertical-align: text-bottom; }
|
||||
label, input[type=button], input[type=submit], button { cursor: pointer; }
|
||||
button, input, select, textarea { margin: 0; }
|
||||
input:valid, textarea:valid { }
|
||||
input:invalid, textarea:invalid { border-radius: 1px; -moz-box-shadow: 0px 0px 5px red; -webkit-box-shadow: 0px 0px 5px red; box-shadow: 0px 0px 5px red; }
|
||||
.no-boxshadow input:invalid,
|
||||
.no-boxshadow textarea:invalid { background-color: #f0dddd; }
|
||||
|
||||
::-moz-selection{ background: #FF5E99; color:#fff; text-shadow: none; }
|
||||
::selection { background:#FF5E99; color:#fff; text-shadow: none; }
|
||||
a:link { -webkit-tap-highlight-color: #FF5E99; }
|
||||
|
||||
button { width: auto; overflow: visible; }
|
||||
.ie7 img { -ms-interpolation-mode: bicubic; }
|
||||
|
||||
.ir { display: block; text-indent: -999em; overflow: hidden; background-repeat: no-repeat; text-align: left; direction: ltr; }
|
||||
.hidden { display: none; visibility: hidden; }
|
||||
.visuallyhidden { position: absolute !important; clip: rect(1px 1px 1px 1px); clip: rect(1px, 1px, 1px, 1px); }
|
||||
.invisible { visibility: hidden; }
|
||||
.clearfix:before, .clearfix:after { content: "\0020"; display: block; height: 0; visibility: hidden; }
|
||||
.clearfix:after { clear: both; }
|
||||
.clearfix { zoom: 1; }
|
||||
|
||||
|
||||
/* Primary Styles
|
||||
Author:
|
||||
*/
|
||||
|
||||
|
||||
h1 {
|
||||
width:800px;
|
||||
margin:50px auto;
|
||||
text-align:center;
|
||||
font-size:200%;
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
#main {
|
||||
width:800px;
|
||||
margin:50px auto;
|
||||
}
|
||||
|
||||
label {
|
||||
display:inline-block;
|
||||
width:8em;
|
||||
}
|
||||
|
||||
input {
|
||||
margin:5px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom:1em;
|
||||
}
|
||||
|
||||
ol {
|
||||
margin-bottom:1em;
|
||||
padding-left:1.5em;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@media all and (orientation:portrait) {
|
||||
|
||||
}
|
||||
|
||||
@media all and (orientation:landscape) {
|
||||
|
||||
}
|
||||
|
||||
@media screen and (max-device-width: 480px) {
|
||||
|
||||
|
||||
/* html { -webkit-text-size-adjust:none; -ms-text-size-adjust:none; } */
|
||||
}
|
||||
|
||||
@media print {
|
||||
* { background: transparent !important; color: #444 !important; text-shadow: none !important; }
|
||||
a, a:visited { color: #444 !important; text-decoration: underline; }
|
||||
a:after { content: " (" attr(href) ")"; }
|
||||
abbr:after { content: " (" attr(title) ")"; }
|
||||
.ir a:after { content: ""; }
|
||||
pre, blockquote { border: 1px solid #999; page-break-inside: avoid; }
|
||||
thead { display: table-header-group; }
|
||||
tr, img { page-break-inside: avoid; }
|
||||
@page { margin: 0.5cm; }
|
||||
p, h2, h3 { orphans: 3; widows: 3; }
|
||||
h2, h3{ page-break-after: avoid; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
// Include PHP before any content is generated to we can set cookies
|
||||
// Sets the $content variable with the dynamic page content
|
||||
include "./action.php";
|
||||
?>
|
||||
<!doctype html>
|
||||
|
||||
<!--[if lt IE 7 ]> <html lang="en" class="no-js ie6"> <![endif]-->
|
||||
<!--[if IE 7 ]> <html lang="en" class="no-js ie7"> <![endif]-->
|
||||
<!--[if IE 8 ]> <html lang="en" class="no-js ie8"> <![endif]-->
|
||||
<!--[if IE 9 ]> <html lang="en" class="no-js ie9"> <![endif]-->
|
||||
<!--[if (gt IE 9)|!(IE)]><!--> <html lang="en" class="no-js"> <!--<![endif]-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<title>Rememberme PHP library test</title>
|
||||
<meta name="author" content="Gabriel Birke">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="css/style.css?v=2">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="container">
|
||||
<header>
|
||||
<h1>Rememberme PHP library test</h1>
|
||||
</header>
|
||||
|
||||
<div id="main">
|
||||
<?php
|
||||
// Output generated content
|
||||
echo $content;
|
||||
?>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
|
||||
</footer>
|
||||
</div> <!-- end of #container -->
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
<p class="error">Someone else has used your login information to acccess this page!<br>
|
||||
All sessions were logged out. <br>
|
||||
Please log in with your credentials and check your data.</p>
|
||||
<p><a href="index.php">To login form</a></p>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php if(!empty($msg)) echo "<p class='msg'>$msg</p>"; ?>
|
||||
|
||||
<p>This is the demo for logging in with the Rememberme Library. <br>
|
||||
You are seeing this form because you have no active "Remember me" cookie and no
|
||||
credentials stored in the session.
|
||||
</p>
|
||||
<p>Please log in with the username and password <em>demo</em></p>
|
||||
|
||||
<form method="post" action="index.php">
|
||||
<label for="username">User Name:</label> <input type="text" name="username" id="username"> <br>
|
||||
<label for="password">Password:</label> <input type="password" name="password" id="password"><br>
|
||||
<input type="checkbox" id="rememberme" value="1" name="rememberme"> Remember me <br>
|
||||
<input type="submit" value="Log me in">
|
||||
</form>
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<p>You are logged in as <strong><?php echo $_SESSION['username']; ?></strong></p>
|
||||
<p>Your session ID is <strong><?php echo session_id(); ?></strong></p>
|
||||
<?php if(!empty($_COOKIE['PHP_REMEMBERME'])): ?>
|
||||
<p>The remember me cookie is active.
|
||||
Cookie value is <code><?php echo $_COOKIE['PHP_REMEMBERME']; ?></code></p>
|
||||
<?php else: ?>
|
||||
<p>The remember me cookie is not active.</p>
|
||||
<?php endif; ?>
|
||||
<?php if(!empty($_SESSION['remembered_by_cookie'])): ?>
|
||||
<p>You were logged in with the "Remember me" cookie. In a real application
|
||||
you should ask the user for his credentials before allowing him anything
|
||||
"dangerous" like changing the login information, accessing sensitive data
|
||||
or making a payment.</p>
|
||||
<?php endif; ?>
|
||||
<p>If you want to test the warning when a possible identity theft is detected, try the following steps:</p>
|
||||
<ol>
|
||||
<li>Login to this page with a non-Firefox Browser. In the following steps I
|
||||
will call that browser "Chrome" :) <br>
|
||||
Make sure to check the "Remember me" checkbox when logging in.</li>
|
||||
<li>Copy the cookie value from above into the clipboard.</li>
|
||||
<li>Quit Chrome to end the session.</li>
|
||||
<li>Start Firefox and install the <a href="https://addons.mozilla.org/de/firefox/addon/6683/">Firecookie</a> extension if needed.</li>
|
||||
<li>Show this page. If you see this text, log out. You should see the login form.</li>
|
||||
<li>Create the <code>PHP_REMEMBERME</code> cookie with the value you copied.</li>
|
||||
<li>Refresh the page - you are now logged in and should see this text. You have stolen the login credential from Chrome!</li>
|
||||
<li>Start Chrome and try to show this page - you should get a warning instead of the login dialog.</li>
|
||||
<li>Refresh this page in Firefox - You are logged out.</li>
|
||||
</ol>
|
||||
<p><a href="index.php?logout=true">Log out in this browser window.</a></p>
|
||||
<p><a href="index.php?completelogout=true">Log out from <strong>all</strong>
|
||||
sessions in all browser windows where the "Remember me" cookie is active.</a></p>
|
||||
@@ -0,0 +1,7 @@
|
||||
<phpunit bootstrap="test/bootstrap.php">
|
||||
<testsuites>
|
||||
<testsuite name="Rememberme">
|
||||
<directory>test</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
<?php
|
||||
|
||||
namespace Birke\Rememberme;
|
||||
|
||||
class Authenticator
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $cookieName = "PHP_REMEMBERME";
|
||||
|
||||
/**
|
||||
* @var Cookie
|
||||
*/
|
||||
protected $cookie;
|
||||
|
||||
/**
|
||||
* @var Storage\StorageInterface
|
||||
*/
|
||||
protected $storage;
|
||||
|
||||
/**
|
||||
* Number of seconds in the future the cookie and storage will expire (defaults to 1 week)
|
||||
* @var int
|
||||
*/
|
||||
protected $expireTime = 604800;
|
||||
|
||||
/**
|
||||
* If the return from the storage was Birke\Rememberme\Storage\StorageInterface::TRIPLET_INVALID,
|
||||
* this is set to true
|
||||
* @var bool
|
||||
*/
|
||||
protected $lastLoginTokenWasInvalid = false;
|
||||
|
||||
/**
|
||||
* If the login token was invalid, delete all login tokens of this user
|
||||
* @var bool
|
||||
*/
|
||||
protected $cleanStoredTokensOnInvalidResult = true;
|
||||
|
||||
/**
|
||||
* Additional salt to add more entropy when the tokens are stored as hashes.
|
||||
* @var string
|
||||
*/
|
||||
protected $salt = "";
|
||||
|
||||
/**
|
||||
* @param Storage\StorageInterface $storage
|
||||
*/
|
||||
public function __construct(Storage\StorageInterface $storage)
|
||||
{
|
||||
$this->storage = $storage;
|
||||
$this->cookie = new Cookie();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Credentials from cookie. Returns false if login was not successful, credential string if it was successful
|
||||
* @return bool|string
|
||||
*/
|
||||
public function login()
|
||||
{
|
||||
$cookieValues = $this->getCookieValues();
|
||||
|
||||
if (!$cookieValues) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$loginResult = false;
|
||||
|
||||
switch ($this->storage->findTriplet($cookieValues[0], $cookieValues[1] . $this->salt, $cookieValues[2] . $this->salt)) {
|
||||
|
||||
case Storage\StorageInterface::TRIPLET_FOUND:
|
||||
$expire = time() + $this->expireTime;
|
||||
$newToken = $this->createToken();
|
||||
$this->storage->replaceTriplet($cookieValues[0], $newToken . $this->salt, $cookieValues[2] . $this->salt, $expire);
|
||||
$this->cookie->setCookie($this->cookieName, implode("|", array($cookieValues[0], $newToken, $cookieValues[2])), $expire);
|
||||
$loginResult = $cookieValues[0];
|
||||
break;
|
||||
|
||||
case Storage\StorageInterface::TRIPLET_INVALID:
|
||||
$this->cookie->setCookie($this->cookieName, "", time() - $this->expireTime);
|
||||
$this->lastLoginTokenWasInvalid = true;
|
||||
|
||||
if ($this->cleanStoredTokensOnInvalidResult) {
|
||||
$this->storage->cleanAllTriplets($cookieValues[0]);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
return $loginResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function cookieIsValid()
|
||||
{
|
||||
$cookieValues = $this->getCookieValues();
|
||||
|
||||
if (!$cookieValues) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$state = $this->storage->findTriplet($cookieValues[0], $cookieValues[1] . $this->salt, $cookieValues[2] . $this->salt);
|
||||
return $state == Storage\StorageInterface::TRIPLET_FOUND;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $credential
|
||||
* @return $this
|
||||
*/
|
||||
public function createCookie($credential)
|
||||
{
|
||||
$newToken = $this->createToken();
|
||||
$newPersistentToken = $this->createToken();
|
||||
|
||||
$expire = time() + $this->expireTime;
|
||||
|
||||
$this->storage->storeTriplet($credential, $newToken . $this->salt, $newPersistentToken . $this->salt, $expire);
|
||||
$this->cookie->setCookie($this->cookieName, implode("|", array($credential, $newToken, $newPersistentToken)), $expire);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expire the rememberme cookie, unset $_COOKIE[$this->cookieName] value and
|
||||
* remove current login triplet from storage.
|
||||
* @param boolean $clearFromStorage
|
||||
* @return boolean
|
||||
*/
|
||||
public function clearCookie($clearFromStorage = true)
|
||||
{
|
||||
if (empty($_COOKIE[$this->cookieName])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$cookieValues = explode("|", $_COOKIE[$this->cookieName], 3);
|
||||
|
||||
$this->cookie->setCookie($this->cookieName, "", time() - $this->expireTime);
|
||||
|
||||
unset($_COOKIE[$this->cookieName]);
|
||||
|
||||
if (!$clearFromStorage) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (count($cookieValues) < 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->storage->cleanTriplet($cookieValues[0], $cookieValues[2] . $this->salt);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCookieName()
|
||||
{
|
||||
return $this->cookieName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $name
|
||||
* @return $this
|
||||
*/
|
||||
public function setCookieName($name)
|
||||
{
|
||||
$this->cookieName = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Cookie $cookie
|
||||
* @return $this
|
||||
*/
|
||||
public function setCookie(Cookie $cookie)
|
||||
{
|
||||
$this->cookie = $cookie;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function loginTokenWasInvalid()
|
||||
{
|
||||
return $this->lastLoginTokenWasInvalid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Cookie
|
||||
*/
|
||||
public function getCookie()
|
||||
{
|
||||
return $this->cookie;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $state
|
||||
* @return Authenticator
|
||||
*/
|
||||
public function setCleanStoredTokensOnInvalidResult($state)
|
||||
{
|
||||
$this->cleanStoredTokensOnInvalidResult = $state;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getCleanStoredTokensOnInvalidResult()
|
||||
{
|
||||
return $this->cleanStoredTokensOnInvalidResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a pseudo-random token.
|
||||
*
|
||||
* The token is pseudo-random. If you need better security, read from /dev/urandom
|
||||
*/
|
||||
protected function createToken()
|
||||
{
|
||||
return bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function getCookieValues()
|
||||
{
|
||||
// Cookie was not sent with incoming request
|
||||
if (empty($_COOKIE[$this->cookieName])) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$cookieValues = explode("|", $_COOKIE[$this->cookieName], 3);
|
||||
|
||||
if (count($cookieValues) < 3) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return $cookieValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return how many seconds in the future that the cookie will expire
|
||||
* @return int
|
||||
*/
|
||||
public function getExpireTime()
|
||||
{
|
||||
return $this->expireTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $expireTime How many seconds in the future the cookie will expire
|
||||
*
|
||||
* Default is 604800 (1 week)
|
||||
*
|
||||
* @return Authenticator
|
||||
*/
|
||||
public function setExpireTime($expireTime)
|
||||
{
|
||||
$this->expireTime = $expireTime;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSalt()
|
||||
{
|
||||
return $this->salt;
|
||||
}
|
||||
|
||||
/**
|
||||
* The salt is additional information that is added to the tokens to make
|
||||
* them more unqiue and secure. The salt is not stored in the cookie and
|
||||
* should not saved in the storage.
|
||||
*
|
||||
* For example, to bind a token to an IP address use $_SERVER['REMOTE_ADDR'].
|
||||
* To bind a token to the browser (user agent), use $_SERVER['HTTP_USER_AGENT].
|
||||
* You could also use a long random string that is uniqe to your application.
|
||||
* @param string $salt
|
||||
*/
|
||||
public function setSalt($salt)
|
||||
{
|
||||
$this->salt = $salt;
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace Birke\Rememberme;
|
||||
|
||||
/**
|
||||
* Wrapper around setcookie function for better testability
|
||||
*/
|
||||
class Cookie
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $path = "";
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $domain = "";
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $secure = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $httpOnly = true;
|
||||
|
||||
/**
|
||||
* @param $name
|
||||
* @param string $value
|
||||
* @param int $expire
|
||||
* @return bool
|
||||
*/
|
||||
public function setCookie($name, $value = "", $expire = 0)
|
||||
{
|
||||
return setcookie($name, $value, $expire, $this->path, $this->domain, $this->secure, $this->httpOnly);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPath()
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $path
|
||||
*/
|
||||
public function setPath($path)
|
||||
{
|
||||
$this->path = $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDomain()
|
||||
{
|
||||
return $this->domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $domain
|
||||
*/
|
||||
public function setDomain($domain)
|
||||
{
|
||||
$this->domain = $domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getSecure()
|
||||
{
|
||||
return $this->secure;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $secure
|
||||
*/
|
||||
public function setSecure($secure)
|
||||
{
|
||||
$this->secure = $secure;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getHttpOnly()
|
||||
{
|
||||
return $this->httpOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $httponly
|
||||
*/
|
||||
public function setHttpOnly($httponly)
|
||||
{
|
||||
$this->httpOnly = $httponly;
|
||||
}
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace Birke\Rememberme\Storage;
|
||||
|
||||
/**
|
||||
* This abstract class contains properties with getters and setters for all
|
||||
* database storage classes
|
||||
*
|
||||
* @author Gabriel Birke
|
||||
*/
|
||||
abstract class DB implements StorageInterface
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $tableName = "";
|
||||
|
||||
/**
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $credentialColumn = "";
|
||||
|
||||
/**
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $tokenColumn = "";
|
||||
|
||||
/**
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $persistentTokenColumn = "";
|
||||
|
||||
/**
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $expiresColumn = "";
|
||||
|
||||
/**
|
||||
* @param $options
|
||||
*/
|
||||
public function __construct($options)
|
||||
{
|
||||
foreach ($options as $prop => $value) {
|
||||
$setter = "set" . ucfirst($prop);
|
||||
if (method_exists($this, $setter)) {
|
||||
$this->$setter($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTableName()
|
||||
{
|
||||
return $this->tableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $tableName
|
||||
* @return $this
|
||||
*/
|
||||
public function setTableName($tableName)
|
||||
{
|
||||
$this->tableName = $tableName;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCredentialColumn()
|
||||
{
|
||||
return $this->credentialColumn;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $credentialColumn
|
||||
* @return $this
|
||||
*/
|
||||
public function setCredentialColumn($credentialColumn)
|
||||
{
|
||||
$this->credentialColumn = $credentialColumn;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTokenColumn()
|
||||
{
|
||||
return $this->tokenColumn;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $tokenColumn
|
||||
* @return $this
|
||||
*/
|
||||
public function setTokenColumn($tokenColumn)
|
||||
{
|
||||
$this->tokenColumn = $tokenColumn;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPersistentTokenColumn()
|
||||
{
|
||||
return $this->persistentTokenColumn;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $persistentTokenColumn
|
||||
* @return $this
|
||||
*/
|
||||
public function setPersistentTokenColumn($persistentTokenColumn)
|
||||
{
|
||||
$this->persistentTokenColumn = $persistentTokenColumn;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getExpiresColumn()
|
||||
{
|
||||
return $this->expiresColumn;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $expiresColumn
|
||||
* @return $this
|
||||
*/
|
||||
public function setExpiresColumn($expiresColumn)
|
||||
{
|
||||
$this->expiresColumn = $expiresColumn;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Birke\Rememberme\Storage;
|
||||
|
||||
/**
|
||||
* File-Based Storage
|
||||
*/
|
||||
class File implements StorageInterface
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $path = "";
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $suffix = ".txt";
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @param string $suffix
|
||||
*/
|
||||
public function __construct($path = "", $suffix = ".txt")
|
||||
{
|
||||
$this->path = $path;
|
||||
$this->suffix = $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $credential
|
||||
* @param string $token
|
||||
* @param string $persistentToken
|
||||
* @return int
|
||||
*/
|
||||
public function findTriplet($credential, $token, $persistentToken)
|
||||
{
|
||||
// Hash the tokens, because they can contain a salt and can be accessed in the file system
|
||||
$persistentToken = sha1($persistentToken);
|
||||
$token = sha1($token);
|
||||
$fn = $this->getFilename($credential, $persistentToken);
|
||||
|
||||
if (!file_exists($fn)) {
|
||||
return self::TRIPLET_NOT_FOUND;
|
||||
}
|
||||
|
||||
$fileToken = trim(file_get_contents($fn));
|
||||
|
||||
if ($fileToken == $token) {
|
||||
return self::TRIPLET_FOUND;
|
||||
}
|
||||
|
||||
return self::TRIPLET_INVALID;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $credential
|
||||
* @param string $token
|
||||
* @param string $persistentToken
|
||||
* @param int $expire
|
||||
* @return $this
|
||||
*/
|
||||
public function storeTriplet($credential, $token, $persistentToken, $expire = 0)
|
||||
{
|
||||
// Hash the tokens, because they can contain a salt and can be accessed in the file system
|
||||
$persistentToken = sha1($persistentToken);
|
||||
$token = sha1($token);
|
||||
$fn = $this->getFilename($credential, $persistentToken);
|
||||
file_put_contents($fn, $token);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $credential
|
||||
* @param string $persistentToken
|
||||
*/
|
||||
public function cleanTriplet($credential, $persistentToken)
|
||||
{
|
||||
$persistentToken = sha1($persistentToken);
|
||||
$fn = $this->getFilename($credential, $persistentToken);
|
||||
|
||||
if (file_exists($fn)) {
|
||||
unlink($fn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace current token after successful authentication
|
||||
* @param $credential
|
||||
* @param $token
|
||||
* @param $persistentToken
|
||||
* @param int $expire
|
||||
*/
|
||||
public function replaceTriplet($credential, $token, $persistentToken, $expire = 0)
|
||||
{
|
||||
$this->cleanTriplet($credential, $persistentToken);
|
||||
$this->storeTriplet($credential, $token, $persistentToken, $expire);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $credential
|
||||
*/
|
||||
public function cleanAllTriplets($credential)
|
||||
{
|
||||
foreach (glob($this->path . DIRECTORY_SEPARATOR . $credential . ".*" . $this->suffix) as $file) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $credential
|
||||
* @param $persistentToken
|
||||
* @return string
|
||||
*/
|
||||
protected function getFilename($credential, $persistentToken)
|
||||
{
|
||||
return $this->path . DIRECTORY_SEPARATOR . $credential . "." . $persistentToken . $this->suffix;
|
||||
}
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace Birke\Rememberme\Storage;
|
||||
|
||||
/**
|
||||
* Store login tokens in database with PDO class
|
||||
*
|
||||
* @author birke
|
||||
*/
|
||||
class PDO extends DB
|
||||
{
|
||||
/**
|
||||
* @var \PDO
|
||||
*/
|
||||
protected $connection;
|
||||
|
||||
/**
|
||||
* @param mixed $credential
|
||||
* @param string $token
|
||||
* @param string $persistentToken
|
||||
* @return int
|
||||
*/
|
||||
public function findTriplet($credential, $token, $persistentToken)
|
||||
{
|
||||
// We don't store the sha1 as binary values because otherwise we could not use
|
||||
// proper XML test data
|
||||
$sql = "SELECT IF(SHA1(?) = {$this->tokenColumn}, 1, -1) AS token_match " .
|
||||
"FROM {$this->tableName} WHERE {$this->credentialColumn} = ? " .
|
||||
"AND {$this->persistentTokenColumn} = SHA1(?) AND {$this->expiresColumn} > NOW() LIMIT 1";
|
||||
|
||||
$query = $this->connection->prepare($sql);
|
||||
$query->execute(array($token, $credential, $persistentToken));
|
||||
|
||||
$result = $query->fetchColumn();
|
||||
|
||||
if (!$result) {
|
||||
return self::TRIPLET_NOT_FOUND;
|
||||
} elseif ($result == 1) {
|
||||
return self::TRIPLET_FOUND;
|
||||
}
|
||||
|
||||
return self::TRIPLET_INVALID;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $credential
|
||||
* @param string $token
|
||||
* @param string $persistentToken
|
||||
* @param int $expire
|
||||
*/
|
||||
public function storeTriplet($credential, $token, $persistentToken, $expire = 0)
|
||||
{
|
||||
$sql = "INSERT INTO {$this->tableName}({$this->credentialColumn}, " .
|
||||
"{$this->tokenColumn}, {$this->persistentTokenColumn}, " .
|
||||
"{$this->expiresColumn}) VALUES(?, SHA1(?), SHA1(?), ?)";
|
||||
|
||||
$query = $this->connection->prepare($sql);
|
||||
$query->execute(array($credential, $token, $persistentToken, date("Y-m-d H:i:s", $expire)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $credential
|
||||
* @param string $persistentToken
|
||||
*/
|
||||
public function cleanTriplet($credential, $persistentToken)
|
||||
{
|
||||
$sql = "DELETE FROM {$this->tableName} WHERE {$this->credentialColumn} = ? "
|
||||
. "AND {$this->persistentTokenColumn} = SHA1(?)";
|
||||
|
||||
$query = $this->connection->prepare($sql);
|
||||
$query->execute(array($credential, $persistentToken));
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace current token after successful authentication
|
||||
* @param $credential
|
||||
* @param $token
|
||||
* @param $persistentToken
|
||||
* @param int $expire
|
||||
*/
|
||||
public function replaceTriplet($credential, $token, $persistentToken, $expire = 0)
|
||||
{
|
||||
try {
|
||||
$this->connection->beginTransaction();
|
||||
$this->cleanTriplet($credential, $persistentToken);
|
||||
$this->storeTriplet($credential, $token, $persistentToken, $expire);
|
||||
$this->connection->commit();
|
||||
}
|
||||
catch (\PDOException $e) {
|
||||
$this->connection->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $credential
|
||||
*/
|
||||
public function cleanAllTriplets($credential)
|
||||
{
|
||||
$sql = "DELETE FROM {$this->tableName} WHERE {$this->credentialColumn} = ? ";
|
||||
|
||||
$query = $this->connection->prepare($sql);
|
||||
$query->execute(array($credential));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \PDO
|
||||
*/
|
||||
public function getConnection()
|
||||
{
|
||||
return $this->connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PDO $connection
|
||||
*/
|
||||
public function setConnection(\PDO $connection)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+66
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Birke\Rememberme\Storage;
|
||||
|
||||
/**
|
||||
* This interface is for storing the credential/token/persistentToken triplets
|
||||
*
|
||||
* IMPORTANT SECURITY NOTICE: The storage should not store the token values in the clear.
|
||||
* Always use a secure hash function!
|
||||
*/
|
||||
interface StorageInterface
|
||||
{
|
||||
const TRIPLET_FOUND = 1;
|
||||
const TRIPLET_NOT_FOUND = 0;
|
||||
const TRIPLET_INVALID = -1;
|
||||
|
||||
/**
|
||||
* Return Tri-state value constant
|
||||
*
|
||||
* @param mixed $credential Unique credential (user id, email address, user name)
|
||||
* @param string $token One-Time Token
|
||||
* @param string $persistentToken Persistent Token
|
||||
* @return int
|
||||
*/
|
||||
public function findTriplet($credential, $token, $persistentToken);
|
||||
|
||||
/**
|
||||
* Store the new token for the credential and the persistent token.
|
||||
* Create a new storage entry, if the combination of credential and persistent
|
||||
* token does not exist.
|
||||
*
|
||||
* @param mixed $credential
|
||||
* @param string $token
|
||||
* @param string $persistentToken
|
||||
* @param int $expire Timestamp when this triplet will expire (0=no expiry)
|
||||
*/
|
||||
public function storeTriplet($credential, $token, $persistentToken, $expire = 0);
|
||||
|
||||
/**
|
||||
* Replace current token after successful authentication
|
||||
* @param $credential
|
||||
* @param $token
|
||||
* @param $persistentToken
|
||||
* @param int $expire
|
||||
*/
|
||||
public function replaceTriplet($credential, $token, $persistentToken, $expire = 0);
|
||||
|
||||
/**
|
||||
* Remove one triplet of the user from the store
|
||||
*
|
||||
* @abstract
|
||||
* @param mixed $credential
|
||||
* @param string $persistentToken
|
||||
* @return void
|
||||
*/
|
||||
public function cleanTriplet($credential, $persistentToken);
|
||||
|
||||
/**
|
||||
* Remove all triplets of a user, effectively logging him out on all machines
|
||||
*
|
||||
* @abstract
|
||||
* @param $credential
|
||||
* @return void
|
||||
*/
|
||||
public function cleanAllTriplets($credential);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
class CookieTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function testDefaultValues()
|
||||
{
|
||||
$cookie = new \Birke\Rememberme\Cookie();
|
||||
|
||||
$this->assertEquals('', $cookie->getPath());
|
||||
$this->assertEquals('', $cookie->getDomain());
|
||||
$this->assertFalse($cookie->getSecure());
|
||||
$this->assertTrue($cookie->getHttpOnly());
|
||||
}
|
||||
|
||||
public function testSetters()
|
||||
{
|
||||
$cookie = new \Birke\Rememberme\Cookie();
|
||||
|
||||
$cookie->setPath('/test');
|
||||
$this->assertEquals('/test', $cookie->getPath());
|
||||
|
||||
$cookie->setDomain('www.foo.com');
|
||||
$this->assertEquals('www.foo.com', $cookie->getDomain());
|
||||
|
||||
$cookie->setSecure(true);
|
||||
$this->assertTrue($cookie->getSecure());
|
||||
|
||||
$cookie->setHttpOnly(false);
|
||||
$this->assertFalse($cookie->getHttpOnly());
|
||||
}
|
||||
}
|
||||
+352
@@ -0,0 +1,352 @@
|
||||
<?php
|
||||
|
||||
class RemembermeTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
/**
|
||||
* @var Rememberme
|
||||
*/
|
||||
protected $rememberme;
|
||||
|
||||
/**
|
||||
* Default user id, used as credential information to check
|
||||
*/
|
||||
protected $userid = 1;
|
||||
|
||||
protected $validToken = "78b1e6d775cec5260001af137a79dbd5";
|
||||
|
||||
protected $validPersistentToken = "0e0530c1430da76495955eb06eb99d95";
|
||||
|
||||
protected $invalidToken = "7ae7c7caa0c7b880cb247bb281d527de";
|
||||
|
||||
protected $cookie;
|
||||
|
||||
protected $storage;
|
||||
|
||||
function setUp() {
|
||||
$this->storage = $this->getMockBuilder(\Birke\Rememberme\Storage\StorageInterface::class)->getMock();
|
||||
$this->rememberme = new Birke\Rememberme\Authenticator($this->storage);
|
||||
|
||||
$this->cookie = $this->getMockBuilder(\Birke\Rememberme\Cookie::class)->setMethods(['setcookie'])->getMock();
|
||||
|
||||
$this->rememberme->setCookie($this->cookie);
|
||||
|
||||
$_COOKIE = array();
|
||||
}
|
||||
|
||||
/* Basic cases */
|
||||
|
||||
public function testReturnFalseIfNoCookieExists()
|
||||
{
|
||||
$this->assertFalse($this->rememberme->login());
|
||||
}
|
||||
|
||||
public function testReturnFalseIfCookieIsInvalid()
|
||||
{
|
||||
$_COOKIE = array($this->rememberme->getCookieName() => "DUMMY");
|
||||
$this->assertFalse($this->rememberme->login());
|
||||
$_COOKIE = array($this->rememberme->getCookieName() => $this->userid."|a");
|
||||
$this->assertFalse($this->rememberme->login());
|
||||
}
|
||||
|
||||
public function testLoginTriesToFindTripletWithValuesFromCookie() {
|
||||
$_COOKIE[$this->rememberme->getCookieName()] = implode("|", array(
|
||||
$this->userid, $this->validToken, $this->validPersistentToken));
|
||||
$this->storage->expects($this->once())
|
||||
->method("findTriplet")
|
||||
->with($this->equalTo($this->userid), $this->equalTo($this->validToken), $this->equalTo($this->validPersistentToken));
|
||||
$this->rememberme->login();
|
||||
}
|
||||
|
||||
/* Success cases */
|
||||
|
||||
public function testReturnTrueIfTripletIsFound() {
|
||||
$_COOKIE[$this->rememberme->getCookieName()] = implode("|", array(
|
||||
$this->userid, $this->validToken, $this->validPersistentToken));
|
||||
|
||||
$this->storage->expects($this->once())
|
||||
->method("findTriplet")
|
||||
->will($this->returnValue(Birke\Rememberme\Storage\StorageInterface::TRIPLET_FOUND));
|
||||
$this->assertEquals($this->userid, $this->rememberme->login());
|
||||
}
|
||||
|
||||
public function testStoreNewTripletInCookieIfTripletIsFound() {
|
||||
$oldcookieValue = implode("|", array(
|
||||
$this->userid, $this->validToken, $this->validPersistentToken));
|
||||
$_COOKIE[$this->rememberme->getCookieName()] = $oldcookieValue;
|
||||
$this->storage->expects($this->once())
|
||||
->method("findTriplet")
|
||||
->will($this->returnValue(Birke\Rememberme\Storage\StorageInterface::TRIPLET_FOUND));
|
||||
$this->cookie->expects($this->once())
|
||||
->method("setcookie")
|
||||
->with(
|
||||
$this->anything(),
|
||||
$this->logicalAnd(
|
||||
$this->matchesRegularExpression('/^'.$this->userid.'\|[a-f0-9]{32,}\|'.$this->validPersistentToken.'$/'),
|
||||
$this->logicalNot($this->equalTo($oldcookieValue))
|
||||
)
|
||||
);
|
||||
$this->rememberme->login();
|
||||
}
|
||||
|
||||
public function testReplaceTripletInStorageIfTripletIsFound() {
|
||||
$_COOKIE[$this->rememberme->getCookieName()] = implode("|", array(
|
||||
$this->userid, $this->validToken, $this->validPersistentToken));
|
||||
$this->storage->expects($this->once())
|
||||
->method("findTriplet")
|
||||
->will($this->returnValue(Birke\Rememberme\Storage\StorageInterface::TRIPLET_FOUND));
|
||||
$this->storage->expects($this->once())
|
||||
->method("replaceTriplet")
|
||||
->with(
|
||||
$this->equalTo($this->userid),
|
||||
$this->logicalAnd(
|
||||
$this->matchesRegularExpression('/^[a-f0-9]{32,}$/'),
|
||||
$this->logicalNot($this->equalTo($this->validToken))
|
||||
),
|
||||
$this->equalTo($this->validPersistentToken)
|
||||
);
|
||||
$this->rememberme->login();
|
||||
}
|
||||
|
||||
public function testCookieContainsUserIDAndHexTokensIfTripletIsFound()
|
||||
{
|
||||
$_COOKIE[$this->rememberme->getCookieName()] = implode("|", array(
|
||||
$this->userid, $this->validToken, $this->validPersistentToken));
|
||||
$this->storage->expects($this->once())
|
||||
->method("findTriplet")
|
||||
->will($this->returnValue(Birke\Rememberme\Storage\StorageInterface::TRIPLET_FOUND));
|
||||
$this->cookie->expects($this->once())
|
||||
->method("setcookie")
|
||||
->with($this->anything(),
|
||||
$this->matchesRegularExpression('/^'.$this->userid.'\|[a-f0-9]{32,}\|[a-f0-9]{32,}$/')
|
||||
);
|
||||
$this->rememberme->login();
|
||||
}
|
||||
|
||||
public function testCookieContainsNewTokenIfTripletIsFound()
|
||||
{
|
||||
$oldcookieValue = implode("|", array(
|
||||
$this->userid, $this->validToken, $this->validPersistentToken));
|
||||
$_COOKIE[$this->rememberme->getCookieName()] = $oldcookieValue;
|
||||
$this->storage->expects($this->once())
|
||||
->method("findTriplet")
|
||||
->will($this->returnValue(Birke\Rememberme\Storage\StorageInterface::TRIPLET_FOUND));
|
||||
$this->cookie->expects($this->once())
|
||||
->method("setcookie")
|
||||
->with($this->anything(),
|
||||
$this->logicalAnd(
|
||||
$this->matchesRegularExpression('/^'.$this->userid.'\|[a-f0-9]{32,}\|'.$this->validPersistentToken.'$/'),
|
||||
$this->logicalNot($this->equalTo($oldcookieValue))
|
||||
)
|
||||
);
|
||||
$this->rememberme->login();
|
||||
}
|
||||
|
||||
public function testCookieExpiryIsInTheFutureIfTripletIsFound()
|
||||
{
|
||||
$oldcookieValue = implode("|", array(
|
||||
$this->userid, $this->validToken, $this->validPersistentToken));
|
||||
$_COOKIE[$this->rememberme->getCookieName()] = $oldcookieValue;
|
||||
$now = time();
|
||||
$this->storage->expects($this->once())
|
||||
->method("findTriplet")
|
||||
->will($this->returnValue(Birke\Rememberme\Storage\StorageInterface::TRIPLET_FOUND));
|
||||
$this->cookie->expects($this->once())
|
||||
->method("setcookie")
|
||||
->with($this->anything(), $this->anything(), $this->greaterThan($now));
|
||||
$this->rememberme->login();
|
||||
}
|
||||
|
||||
/* Failure Cases */
|
||||
|
||||
public function testFalseIfTripletIsNotFound() {
|
||||
$_COOKIE[$this->rememberme->getCookieName()] = implode("|", array(
|
||||
$this->userid, $this->validToken, $this->validPersistentToken));
|
||||
|
||||
$this->storage->expects($this->once())
|
||||
->method("findTriplet")
|
||||
->will($this->returnValue(Birke\Rememberme\Storage\StorageInterface::TRIPLET_NOT_FOUND));
|
||||
$this->assertFalse($this->rememberme->login());
|
||||
}
|
||||
|
||||
public function testFalseIfTripletIsInvalid() {
|
||||
$_COOKIE[$this->rememberme->getCookieName()] = implode("|", array(
|
||||
$this->userid, $this->invalidToken, $this->validPersistentToken));
|
||||
|
||||
$this->storage->expects($this->once())
|
||||
->method("findTriplet")
|
||||
->will($this->returnValue(Birke\Rememberme\Storage\StorageInterface::TRIPLET_INVALID));
|
||||
$this->assertFalse($this->rememberme->login());
|
||||
}
|
||||
|
||||
public function testCookieIsExpiredIfTripletIsInvalid() {
|
||||
$_COOKIE[$this->rememberme->getCookieName()] = implode("|", array(
|
||||
$this->userid, $this->invalidToken, $this->validPersistentToken));
|
||||
$now = time();
|
||||
$this->storage->expects($this->once())
|
||||
->method("findTriplet")
|
||||
->will($this->returnValue(Birke\Rememberme\Storage\StorageInterface::TRIPLET_INVALID));
|
||||
$this->cookie->expects($this->once())
|
||||
->method("setcookie")
|
||||
->with($this->anything(), $this->anything(), $this->lessThan($now));
|
||||
$this->rememberme->login();
|
||||
}
|
||||
|
||||
public function testAllStoredTokensAreClearedIfTripletIsInvalid() {
|
||||
$_COOKIE[$this->rememberme->getCookieName()] = implode("|", array(
|
||||
$this->userid, $this->invalidToken, $this->validPersistentToken));
|
||||
$this->storage->expects($this->any())
|
||||
->method("findTriplet")
|
||||
->will($this->returnValue(Birke\Rememberme\Storage\StorageInterface::TRIPLET_INVALID));
|
||||
$this->storage->expects($this->once())
|
||||
->method("cleanAllTriplets")
|
||||
->with($this->equalTo($this->userid));
|
||||
$this->rememberme->setCleanStoredTokensOnInvalidResult(true);
|
||||
$this->rememberme->login();
|
||||
$this->rememberme->setCleanStoredTokensOnInvalidResult(false);
|
||||
$this->rememberme->login();
|
||||
}
|
||||
|
||||
public function testInvalidTripletStateIsStored() {
|
||||
$_COOKIE[$this->rememberme->getCookieName()] = implode("|", array(
|
||||
$this->userid, $this->invalidToken, $this->validPersistentToken));
|
||||
|
||||
$this->storage->expects($this->once())
|
||||
->method("findTriplet")
|
||||
->will($this->returnValue(Birke\Rememberme\Storage\StorageInterface::TRIPLET_INVALID));
|
||||
$this->assertFalse($this->rememberme->loginTokenWasInvalid());
|
||||
$this->rememberme->login();
|
||||
$this->assertTrue($this->rememberme->loginTokenWasInvalid());
|
||||
}
|
||||
|
||||
/* Cookie tests */
|
||||
|
||||
public function testCookieNameCanBeSet() {
|
||||
$cookieName = "myCustomName";
|
||||
$this->rememberme->setCookieName($cookieName);
|
||||
$_COOKIE[$cookieName] = implode("|", array($this->userid, $this->validToken, $this->validPersistentToken));
|
||||
$this->storage->expects($this->once())
|
||||
->method("findTriplet")
|
||||
->will($this->returnValue(Birke\Rememberme\Storage\StorageInterface::TRIPLET_FOUND));
|
||||
$this->cookie->expects($this->once())
|
||||
->method("setcookie")
|
||||
->with($this->equalTo($cookieName));
|
||||
$this->assertEquals($this->userid, $this->rememberme->login());
|
||||
}
|
||||
|
||||
public function testCookieIsSetToConfiguredExpiryDate() {
|
||||
$_COOKIE[$this->rememberme->getCookieName()] = implode("|", array(
|
||||
$this->userid, $this->validToken, $this->validPersistentToken));
|
||||
$now = time();
|
||||
$expireTime = 31556926; // 1 year
|
||||
$this->rememberme->setExpireTime($expireTime);
|
||||
$this->storage->expects($this->once())
|
||||
->method("findTriplet")
|
||||
->will($this->returnValue(Birke\Rememberme\Storage\StorageInterface::TRIPLET_FOUND));
|
||||
$this->cookie->expects($this->once())
|
||||
->method("setcookie")
|
||||
->with($this->anything(), $this->anything(), $this->equalTo($now+$expireTime, 10));
|
||||
$this->rememberme->login();
|
||||
}
|
||||
|
||||
/* Salting test */
|
||||
|
||||
public function testSaltIsAddedToTokensOnLogin() {
|
||||
$salt = "Mozilla Firefox 4.0";
|
||||
$_COOKIE[$this->rememberme->getCookieName()] = implode("|", array(
|
||||
$this->userid, $this->validToken, $this->validPersistentToken));
|
||||
$this->storage->expects($this->once())
|
||||
->method("findTriplet")
|
||||
->with($this->equalTo($this->userid), $this->equalTo($this->validToken.$salt), $this->equalTo($this->validPersistentToken.$salt))
|
||||
->will($this->returnValue(Birke\Rememberme\Storage\StorageInterface::TRIPLET_FOUND));
|
||||
$this->storage->expects($this->once())
|
||||
->method("replaceTriplet")
|
||||
->with(
|
||||
$this->equalTo($this->userid),
|
||||
$this->matchesRegularExpression('/^[a-f0-9]{32,}'.preg_quote($salt)."$/"),
|
||||
$this->equalTo($this->validPersistentToken.$salt)
|
||||
);
|
||||
$this->rememberme->setSalt($salt);
|
||||
$this->rememberme->login();
|
||||
}
|
||||
|
||||
public function testSaltIsAddedToTokensOnCookieIsValid() {
|
||||
$salt = "Mozilla Firefox 4.0";
|
||||
$_COOKIE[$this->rememberme->getCookieName()] = implode("|", array(
|
||||
$this->userid, $this->validToken, $this->validPersistentToken));
|
||||
$this->storage->expects($this->once())
|
||||
->method("findTriplet")
|
||||
->with($this->equalTo($this->userid), $this->equalTo($this->validToken.$salt), $this->equalTo($this->validPersistentToken.$salt));
|
||||
$this->rememberme->setSalt($salt);
|
||||
$this->rememberme->cookieIsValid($this->userid);
|
||||
}
|
||||
|
||||
public function testSaltIsAddedToTokensOnCreateCookie() {
|
||||
$salt = "Mozilla Firefox 4.0";
|
||||
$testExpr = '/^[a-f0-9]{32,}'.preg_quote($salt).'$/';
|
||||
$this->storage->expects($this->once())
|
||||
->method("storeTriplet")
|
||||
->with(
|
||||
$this->equalTo($this->userid),
|
||||
$this->matchesRegularExpression($testExpr),
|
||||
$this->matchesRegularExpression($testExpr)
|
||||
);
|
||||
$this->rememberme->setSalt($salt);
|
||||
$this->rememberme->createCookie($this->userid);
|
||||
}
|
||||
|
||||
public function testSaltIsAddedToTokensOnClearCookie() {
|
||||
$salt = "Mozilla Firefox 4.0";
|
||||
$_COOKIE[$this->rememberme->getCookieName()] = implode("|", array(
|
||||
$this->userid, $this->validToken, $this->validPersistentToken));
|
||||
$this->storage->expects($this->once())
|
||||
->method("cleanTriplet")
|
||||
->with(
|
||||
$this->equalTo($this->userid),
|
||||
$this->equalTo($this->validPersistentToken.$salt)
|
||||
);
|
||||
$this->rememberme->setSalt($salt);
|
||||
$this->rememberme->clearCookie(true);
|
||||
}
|
||||
|
||||
/* Other functions */
|
||||
|
||||
public function testCreateCookieCreatesCookieAndStoresTriplets() {
|
||||
$now = time();
|
||||
$this->cookie->expects($this->once())
|
||||
->method("setcookie")
|
||||
->with(
|
||||
$this->equalTo($this->rememberme->getCookieName()),
|
||||
$this->matchesRegularExpression('/^'.$this->userid.'\|[a-f0-9]{32,}\|[a-f0-9]{32,}$/'),
|
||||
$this->greaterThan($now)
|
||||
);
|
||||
$testExpr = '/^[a-f0-9]{32,}$/';
|
||||
$this->storage->expects($this->once())
|
||||
->method("storeTriplet")
|
||||
->with(
|
||||
$this->equalTo($this->userid),
|
||||
$this->matchesRegularExpression($testExpr),
|
||||
$this->matchesRegularExpression($testExpr)
|
||||
);
|
||||
$this->rememberme->createCookie($this->userid);
|
||||
}
|
||||
|
||||
public function testClearCookieExpiresCookieAndDeletesTriplet() {
|
||||
$_COOKIE[$this->rememberme->getCookieName()] = implode("|", array(
|
||||
$this->userid, $this->validToken, $this->validPersistentToken));
|
||||
$now = time();
|
||||
$this->cookie->expects($this->once())
|
||||
->method("setcookie")
|
||||
->with(
|
||||
$this->equalTo($this->rememberme->getCookieName()),
|
||||
$this->anything(),
|
||||
$this->lessThan($now)
|
||||
);
|
||||
$this->storage->expects($this->once())
|
||||
->method("cleanTriplet")
|
||||
->with(
|
||||
$this->equalTo($this->userid),
|
||||
$this->equalTo($this->validPersistentToken)
|
||||
);
|
||||
$this->rememberme->clearCookie(true);
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
/*
|
||||
*/
|
||||
|
||||
require_once dirname(__FILE__).'/../bootstrap.php';
|
||||
require_once "PHPUnit/Extensions/Database/TestCase.php";
|
||||
|
||||
/**
|
||||
* @author birke
|
||||
*/
|
||||
class Rememberme_Storage_PDOTest extends PHPUnit_Extensions_Database_TestCase {
|
||||
|
||||
/**
|
||||
*
|
||||
* @var PDO
|
||||
*/
|
||||
protected $pdo;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var Rememberme_Storage_PDO
|
||||
*/
|
||||
protected $storage;
|
||||
|
||||
protected $userid = 'test';
|
||||
protected $validToken = "78b1e6d775cec5260001af137a79dbd5";
|
||||
protected $validPersistentToken = "0e0530c1430da76495955eb06eb99d95";
|
||||
protected $invalidToken = "7ae7c7caa0c7b880cb247bb281d527de";
|
||||
|
||||
// SHA1 hashes of the tokens
|
||||
protected $validDBToken = 'e0e6d29addce0fbdd0f845799be7d0395ed087c3';
|
||||
protected $validDBPersistentToken = 'd27d330764ef61e99adf5d16f90b95a2a63c209a';
|
||||
protected $invalidDBToken = 'ec15fbc40cdff6a2050a1bcbbc1b2196222f13f4';
|
||||
|
||||
protected $expire = "2012-12-21 21:21:00";
|
||||
protected $expireTS = 1356121260;
|
||||
|
||||
protected function getConnection()
|
||||
{
|
||||
$this->pdo = new PDO('mysql:host=localhost;dbname=test', 'webuser', '');
|
||||
return $this->createDefaultDBConnection($this->pdo, 'test');
|
||||
}
|
||||
|
||||
protected function getDataSet()
|
||||
{
|
||||
return $this->createFlatXMLDataSet(dirname(__FILE__).'/tokens.xml');
|
||||
}
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->storage = new Rememberme_Storage_PDO(array(
|
||||
'connection' => $this->pdo,
|
||||
'tableName' => 'tokens',
|
||||
'credentialColumn' => 'credential',
|
||||
'tokenColumn' => 'token',
|
||||
'persistentTokenColumn' => 'persistent_token',
|
||||
'expiresColumn' => 'expires'
|
||||
));
|
||||
}
|
||||
|
||||
public function testFindTripletReturnsFoundIfDataMatches() {
|
||||
$result = $this->storage->findTriplet($this->userid, $this->validToken, $this->validPersistentToken);
|
||||
$this->assertEquals(Rememberme_Storage_StorageInterface::TRIPLET_FOUND, $result);
|
||||
}
|
||||
|
||||
public function testFindTripletReturnsNotFoundIfNoDataMatches() {
|
||||
$this->pdo->exec("TRUNCATE tokens");
|
||||
$result = $this->storage->findTriplet($this->userid, $this->validToken, $this->validPersistentToken);
|
||||
$this->assertEquals(Rememberme_Storage_StorageInterface::TRIPLET_NOT_FOUND, $result);
|
||||
}
|
||||
|
||||
public function testFindTripletReturnsInvalidTokenIfTokenIsInvalid() {
|
||||
$result = $this->storage->findTriplet($this->userid, $this->invalidToken, $this->validPersistentToken);
|
||||
$this->assertEquals(Rememberme_Storage_StorageInterface::TRIPLET_INVALID, $result);
|
||||
}
|
||||
|
||||
public function testStoreTripletSavesValuesIntoDatabase() {
|
||||
$this->pdo->exec("TRUNCATE tokens");
|
||||
$this->storage->storeTriplet($this->userid, $this->validToken, $this->validPersistentToken, $this->expireTS);
|
||||
$result = $this->pdo->query("SELECT credential,token,persistent_token, expires FROM tokens");
|
||||
$row = $result->fetch(PDO::FETCH_NUM);
|
||||
$this->assertEquals(array($this->userid, $this->validDBToken, $this->validDBPersistentToken, $this->expire), $row);
|
||||
$this->assertFalse($result->fetch());
|
||||
}
|
||||
|
||||
public function testCleanTripletRemovesEntryFromDatabase() {
|
||||
$this->storage->cleanTriplet($this->userid, $this->validPersistentToken);
|
||||
$this->assertEquals(0, $this->pdo->query("SELECT COUNT(*) FROM tokens")->fetchColumn());
|
||||
}
|
||||
|
||||
public function testCleanAllTripletsRemovesAllEntriesWithMatchingCredentialsFromDatabase() {
|
||||
$this->pdo->exec("INSERT INTO tokens VALUES ('{$this->userid}', 'dummy', 'dummy', NOW())");
|
||||
$this->storage->cleanAllTriplets($this->userid);
|
||||
$this->assertEquals(0, $this->pdo->query("SELECT COUNT(*) FROM tokens")->fetchColumn());
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<dataset>
|
||||
<tokens credential="test"
|
||||
token="e0e6d29addce0fbdd0f845799be7d0395ed087c3"
|
||||
persistent_token="d27d330764ef61e99adf5d16f90b95a2a63c209a"
|
||||
expires="2012-12-21 21:21:00"
|
||||
/>
|
||||
</dataset>
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__."/../vendor/autoload.php";
|
||||
Reference in New Issue
Block a user