Skip to content

Commit

Permalink
Merge pull request #113 from aziraphale/cookie-auth
Browse files Browse the repository at this point in the history
Allow logging in via HTML <form> and cookies rather than HTTP Digest …
  • Loading branch information
erikdubbelboer committed Jun 21, 2017
2 parents ba67928 + a5617b0 commit b1327c0
Show file tree
Hide file tree
Showing 5 changed files with 319 additions and 67 deletions.
116 changes: 116 additions & 0 deletions css/login.css
@@ -0,0 +1,116 @@
/* Styles borrowed from http://getbootstrap.com/examples/signin/ */
h1.logo {
text-align: center;
}
h2 {
font-size: 30px;
}
h2 {
margin-top: 20px;
margin-bottom: 10px;
}
h2 {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-weight: 500;
line-height: 1.1;
color: inherit;
}
.form-signin {
max-width: 330px;
padding: 15px;
margin: 0 auto;
}
.form-signin .form-signin-heading,
.form-signin .form-control {
position: relative;
height: auto;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
padding: 10px;
}
.form-signin .form-control:focus {
z-index: 2;
}
.form-signin input[type="text"] {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
border: 0;
}
.form-control {
display: block;
width: 100%;
height: 34px;
padding: 6px 12px;
font-size: 14px;
line-height: 1.42857143;
color: #555;
background-color: #fff;
background-image: none;
border: 1px solid #ccc;
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
-webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
-o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
}
.btn {
display: inline-block;
padding: 6px 12px;
margin-bottom: 0;
font-size: 14px;
font-weight: 400;
line-height: 1.42857143;
text-align: center;
white-space: nowrap;
vertical-align: middle;
-ms-touch-action: manipulation;
touch-action: manipulation;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-image: none;
border: 1px solid transparent;
border-radius: 4px;
}
.btn-block {
display: block;
width: 100%;
}
.btn-lg {
padding: 10px 16px;
font-size: 18px;
line-height: 1.3333333;
border-radius: 6px;
}
.btn-primary {
color: #fff;
background-color: #337ab7;
border-color: #2e6da4;
}
.invalid-credentials {
border: 1px solid #ff0000;
background-color: rgba(255, 0, 0, 0.2);
color: #880000;
padding: 10px;
border-radius: 5px;
margin: 20px;
}
3 changes: 3 additions & 0 deletions includes/config.sample.inc.php
Expand Up @@ -56,6 +56,9 @@
)
),*/

// Use HTML form/cookie-based auth instead of HTTP Basic/Digest auth
'cookie_auth' => false,


/*'serialization' => array(
'foo*' => array( // Match like KEYS
Expand Down
165 changes: 123 additions & 42 deletions includes/login.inc.php
@@ -1,63 +1,144 @@
<?php

// This fill will perform HTTP digest authentication. This is not the most secure form of authentication so be carefull when using this.
function authHttpDigest()
{
global $config;

$realm = 'phpRedisAdmin';

$realm = 'phpRedisAdmin';
// Using the md5 of the user agent and IP should make it a bit harder to intercept and reuse the responses.
$opaque = md5('phpRedisAdmin'.$_SERVER['HTTP_USER_AGENT'].$_SERVER['REMOTE_ADDR']);

// Using the md5 of the user agent and IP should make it a bit harder to intercept and reuse the responses.
$opaque = md5('phpRedisAdmin'.$_SERVER['HTTP_USER_AGENT'].$_SERVER['REMOTE_ADDR']);

if (!isset($_SERVER['PHP_AUTH_DIGEST']) || empty($_SERVER['PHP_AUTH_DIGEST'])) {
header('HTTP/1.1 401 Unauthorized');
header('WWW-Authenticate: Digest realm="'.$realm.'",qop="auth",nonce="'.uniqid().'",opaque="'.$opaque.'"');
die;
}

if (!isset($_SERVER['PHP_AUTH_DIGEST']) || empty($_SERVER['PHP_AUTH_DIGEST'])) {
header('HTTP/1.1 401 Unauthorized');
header('WWW-Authenticate: Digest realm="'.$realm.'",qop="auth",nonce="'.uniqid().'",opaque="'.$opaque.'"');
die;
}
$needed_parts = array(
'nonce' => 1,
'nc' => 1,
'cnonce' => 1,
'qop' => 1,
'username' => 1,
'uri' => 1,
'response' => 1
);

$needed_parts = array(
'nonce' => 1,
'nc' => 1,
'cnonce' => 1,
'qop' => 1,
'username' => 1,
'uri' => 1,
'response' => 1
);
$data = array();
$keys = implode('|', array_keys($needed_parts));

$data = array();
$keys = implode('|', array_keys($needed_parts));
preg_match_all('/('.$keys.')=(?:([\'"])([^\2]+?)\2|([^\s,]+))/', $_SERVER['PHP_AUTH_DIGEST'], $matches, PREG_SET_ORDER);

preg_match_all('/('.$keys.')=(?:([\'"])([^\2]+?)\2|([^\s,]+))/', $_SERVER['PHP_AUTH_DIGEST'], $matches, PREG_SET_ORDER);
foreach ($matches as $m) {
$data[$m[1]] = $m[3] ? $m[3] : $m[4];
unset($needed_parts[$m[1]]);
}

foreach ($matches as $m) {
$data[$m[1]] = $m[3] ? $m[3] : $m[4];
unset($needed_parts[$m[1]]);
}
if (!empty($needed_parts)) {
header('HTTP/1.1 401 Unauthorized');
header('WWW-Authenticate: Digest realm="'.$realm.'",qop="auth",nonce="'.uniqid().'",opaque="'.$opaque.'"');
die;
}

if (!empty($needed_parts)) {
header('HTTP/1.1 401 Unauthorized');
header('WWW-Authenticate: Digest realm="'.$realm.'",qop="auth",nonce="'.uniqid().'",opaque="'.$opaque.'"');
die;
}
if (!isset($config['login'][$data['username']])) {
header('HTTP/1.1 401 Unauthorized');
header('WWW-Authenticate: Digest realm="'.$realm.'",qop="auth",nonce="'.uniqid().'",opaque="'.$opaque.'"');
die('Invalid username and/or password combination.');
}

if (!isset($config['login'][$data['username']])) {
header('HTTP/1.1 401 Unauthorized');
header('WWW-Authenticate: Digest realm="'.$realm.'",qop="auth",nonce="'.uniqid().'",opaque="'.$opaque.'"');
die('Invalid username and/or password combination.');
}
$login = $config['login'][$data['username']];
$login['name'] = $data['username'];

$login = $config['login'][$data['username']];
$login['name'] = $data['username'];
$password = md5($login['name'].':'.$realm.':'.$login['password']);

$password = md5($login['name'].':'.$realm.':'.$login['password']);
$response = md5($password.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']));

$response = md5($password.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']));
if ($data['response'] != $response) {
header('HTTP/1.1 401 Unauthorized');
header('WWW-Authenticate: Digest realm="'.$realm.'",qop="auth",nonce="'.uniqid().'",opaque="'.$opaque.'"');
die('Invalid username and/or password combination.');
}

return $login;
}

// Perform auth using a standard HTML <form> submission and cookies to save login state
function authCookie()
{
global $config;

$generateCookieHash = function($username) use ($config) {
if (!isset($config['login'][$username])) {
throw new \RuntimeException("Invalid username");
}

// Storing this value client-side so we need to be careful that it
// doesn't reveal anything nor can be guessed.
// Using SHA512 because MD5, SHA1 are both now considered broken
return hash(
'sha512',
implode(':', [
$username,
$_SERVER['HTTP_USER_AGENT'],
$_SERVER['REMOTE_ADDR'],
$config['login'][$username]['password'],
])
);
};

if (!empty($_COOKIE['phpRedisAdminLogin'])) {
// We have a cookie; is it correct?
// Cookie value looks like "username:password-hash"
$cookieVal = explode(':', $_COOKIE['phpRedisAdminLogin']);
if (count($cookieVal) === 2) {
list($username, $cookieHash) = $cookieVal;
if (isset($config['login'][$username])) {
$userData = $config['login'][$username];
$expectedHash = $generateCookieHash($username);

if ($cookieHash === $expectedHash) {
// Correct username & password
return $userData;
}
}
}
}

if (isset($_POST['username'], $_POST['password'])) {
// Login form submitted; correctly?
if ($config['login'][$_POST['username']]) {
$userData = $config['login'][$_POST['username']];
if ($_POST['password'] === $userData['password']) {
// Correct username & password. Set cookie and redirect to home page
$cookieValue = $_POST['username'] . ':' . $generateCookieHash($_POST['username']);
setcookie('phpRedisAdminLogin', $cookieValue);

// This should be an absolute URL, but that's a bit of a pain to generate; this will work
header("Location: index.php");
die();
}
}
}

// If we're here, we don't have a valid login cookie and we don't have a
// valid form submission, so redirect to the login page if we aren't
// already on that page
if (!defined('LOGIN_PAGE')) {
header("Location: login.php");
die();
}

// We must be on the login page without a valid cookie or submission
return null;
}

if ($data['response'] != $response) {
header('HTTP/1.1 401 Unauthorized');
header('WWW-Authenticate: Digest realm="'.$realm.'",qop="auth",nonce="'.uniqid().'",opaque="'.$opaque.'"');
die('Invalid username and/or password combination.');
if (!empty($config['cookie_auth'])) {
$login = authCookie();
} else {
$login = authHttpDigest();
}

?>
43 changes: 43 additions & 0 deletions login.php
@@ -0,0 +1,43 @@
<?php
define('LOGIN_PAGE', true);

require_once 'includes/common.inc.php';

$page['css'][] = 'login';

require 'includes/header.inc.php';

// Layout borrowed from http://getbootstrap.com/examples/signin/
?>

<h1 class="logo">phpRedisAdmin</h1>

<form class="form-signin" method="post" action="login.php">
<h2 class="form-signin-heading">Please log in</h2>

<?php if (isset($_POST['username']) || isset($_POST['password'])): ?>
<div class="invalid-credentials">
<h3>Invalid username/password</h3>
<p>Please try again.</p>
</div>
<?php endif; ?>

<label for="inputUser" class="sr-only">Username</label>
<input type="text" name="username" id="inputUser" class="form-control"
placeholder="Username"
value="<?= isset($_POST['username']) ? $_POST['username'] : '' ?>"
required <?= isset($_POST['username']) ? '' : 'autofocus' ?>>

<label for="inputPassword" class="sr-only">Password</label>
<input type="password" name="password" id="inputPassword" class="form-control"
placeholder="Password"
required <?= isset($_POST['username']) ? 'autofocus' : '' ?>>

<button class="btn btn-lg btn-primary btn-block" type="submit">Log in</button>
</form>

<?php

require 'includes/footer.inc.php';

?>

0 comments on commit b1327c0

Please sign in to comment.