Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #113 from aziraphale/cookie-auth
Allow logging in via HTML <form> and cookies rather than HTTP Digest …
- Loading branch information
Showing
5 changed files
with
319 additions
and
67 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} | ||
|
||
?> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'; | ||
|
||
?> |
Oops, something went wrong.