-
Notifications
You must be signed in to change notification settings - Fork 0
/
login.php
336 lines (251 loc) · 10.1 KB
/
login.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
<?php
require_once('include/csp.php');
require_once('include/inisets.php');
require_once('include/secrets.php');
require_once('include/initializeDb.php');
/* #1 Cold session, no incoming data, no data to retrieve, clear all cookies, and show defaults on page */
/* #2 Credentials entered, authentication fails, redisplay blank form, along with error message */
/* #3 Credentials entered, authentication succeeds, setup session, redirect to org.php */
/* TODO: This code may be vulnerable to XSS
and probably a bunch of other attacks
Needs serious security review */
try
{
buildCsrfToken(); /* We need this built in most cases, so go ahead and build it */
if (isset($_POST["password"])) /* credentials were submitted */
{
checkCsrfToken();
if (validatePostData())
{
if (authenticateCredentials())
{
/* flow #3 */
redirectToList();
}
else
{
/* set the auth error message */
$auth_fail_msg = "Those credentials are not valid. Please try again.";
}
}
}
if ((!isset($_POST["password"])) || (isset($auth_fail_msg)))
{
/* Flow #1, or #2, display blank form */
clearSession();
if (isset($_GET["errmsg"]))
{
switch ($_GET["errmsg"])
{
/* TODO: make these codes more organized. Right now they are just random numbers and
there's not much rhyme or reason as to what means what */
case "SUCCESSFULLY_LOGGED_OFF" : $auth_fail_msg = SUCCESSFULLY_LOGGED_OFF;
break;
case "LOGGED_OFF_INACTIVITY" : $auth_fail_msg = LOGGED_OFF_INACTIVITY;
break;
case "USER_NOT_LOGGED_IN_ERROR" : $auth_fail_msg = USER_NOT_LOGGED_IN_ERROR;
break;
default : $auth_fail_msg = "An unknown error occurred. Please attempt to log on again";
}
}
}
}
catch (Exception $e)
{
$auth_fail_msg = $e->getMessage();
}
/* end of global section, now fall through to HTML */
function buildCsrfToken()
{
/* for csrf protection, the nonce will be formed from a hash of several variables
that make up the session, concatenated, but should be stable between requests,
along with an expiration date and some random salt (defined in secrets.php),
which makes the hash impossible to predict for anyone who does not know the salt */
global $csrf_nonce, $csrf_salt, $csrf_expdate;
$csrf_expdate = new DateTime(NULL, new DateTimeZone("UTC"));
$csrf_expdate->add(new DateInterval("PT30M")); /* CSRF token expires in 30 minutes */
$token = $_SERVER['SERVER_SIGNATURE'] . $_SERVER['SCRIPT_FILENAME'] . $csrf_expdate->format('U') . $csrf_salt;
//echo "<!-- DEBUG build token = $token -->\n";
$csrf_nonce = hash("sha256", $token);
}
function checkCsrfToken()
{
global $csrf_salt;
if (!array_key_exists("csrf_expdate", $_POST) || !array_key_exists("nonce", $_POST))
{
error_log("POST parameters missing in login.php. Possible tampering detected.");
throw new Exception("An unknown error occurred (1). Please attempt to authenticate again.");
exit(); /* this should not be run, but just in case, we do not want to continue */
}
$token = $_SERVER['SERVER_SIGNATURE'] . $_SERVER['SCRIPT_FILENAME'] . $_POST['csrf_expdate'] . $csrf_salt;
//echo "<!-- DEBUG Check Token = $token -->\n";
if (hash("sha256", $token) != $_POST["nonce"])
{
error_log("csrf token mismatch in login.php. Possible tampering detected.");
throw new Exception("An unknown error occurred (2). Please attempt to authenticate again.");
exit(); /* this should not be run, but just in case, we do not want to continue */
}
$dateint = filter_var($_POST["csrf_expdate"], FILTER_VALIDATE_INT);
if ($dateint == FALSE)
{
error_log("expdate does not follow proper format in login.php. Possible tampering detected.");
throw new Exception("An unknown error occurred (3). Please attempt to authenticate again.");
exit(); /* this should not be run, but just in case, we do not want to continue */
}
$expdate = new DateTime("@" . $dateint, new DateTimeZone("UTC")); /* specify the @ sign to denote passing in a Unix TS integer */
$today = new DateTime(NULL, new DateTimeZone("UTC"));
if ($expdate < $today)
{
error_log("CSRF token expired in login.php");
throw new Exception("An unknown error occurred (4). Please attempt to authenticate again.");
exit(); /* this should not be run, but just in case, we do not want to continue */
}
}
function validatePostData()
{
global $email;
$email = strtolower($_POST["email"]);
return filter_var($email, FILTER_VALIDATE_EMAIL);
}
function authenticateCredentials()
{
global $dbh, $email, $user_id, $pwhash;
initializeDb(); /* I want this outside the try since it has its own exception handler, which I want bubbled up */
try
{
$stmt = $dbh->prepare("CALL selectLoginInfo(:email);" );
$stmt->bindParam(':email', $email);
$stmt->execute();
if ($stmt->errorCode() != "00000")
{
$erinf = $stmt->errorInfo();
error_log("SELECT failed: " . $stmt->errorCode() . " " . $erinf[2]);
throw new Exception("An unknown error was encountered (8). Please attempt to reauthenticate.");
exit();
}
/* retrieve all rows and columns at once, this should be a pretty small dataset, so should not be a problem */
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (count($results) > 0)
{
$pwhash = $results[0]["pwhash"];
if (password_verify($_POST["password"], $pwhash))
{
//$orgid = $row["orgid"];
//$email = $row["email_verified"];
$orgs = array_column($results, "orgid");
//echo "<!-- ";
//var_dump($orgs);
//echo " -->\n";
session_start();
$user_id = $results[0]["user_id"];
$_SESSION["my_user_id"] = $user_id;
$_SESSION["orgids"] = $orgs;
$_SESSION["admin_user_ind"] = $results[0]["admin_user_ind"];
return TRUE;
}
}
return FALSE;
}
catch (PDOException $e)
{
error_log("Database error during query: " . $e->getMessage());
throw new Exception("An unknown error was encountered (7). Please attempt to reauthenticate.");
exit();
}
catch(Exception $e)
{
error_log("Error during database query: " . $e->getMessage());
/* We most likely got here from the SQL error above, so just bubble up the exception */
throw new Exception("An unknown error was encountered (8). Please attempt to reauthenticate.");
exit();
}
}
function clearSession()
{
/* The following was copied from php.net */
/* The goal is to clear all cookies when the login page is shown
which mitigates against some session hijacking and fixation threats */
// Initialize the session.
// If you are using session_name("something"), don't forget it now!
session_start();
// Unset all of the session variables. (server side)
$_SESSION = array();
// If it's desired to kill the session, also delete the session cookie.
// Note: This will destroy the session, and not just the session data!
if (ini_get("session.use_cookies"))
{
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params["path"], $params["domain"],
$params["secure"], $params["httponly"]
);
}
// Finally, destroy the session.
session_destroy();
}
function redirectToOrg($orgid)
{
header("Location: org.php?orgid=$orgid");
}
function redirectToList()
{
global $user_id;
if ($_SESSION["admin_user_ind"] == TRUE) /* admins go to user list page, they probably don't want to work on orgs */
{
header("Location: userList.php");
}
else /* regular users get the organization list by default */
{
header("Location: orgList.php?user_id=$user_id");
}
}
?>
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<title><?php echo $site_brand; ?> - Organization</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet prefetch"
href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
crossorigin="anonymous">
<link rel="stylesheet" href="css/style.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js"
integrity="sha384-nrOSfDHtoPMzJHjVTdCopGqIqeYETSXhZDFyniQ8ZHcVy08QesyHcnOUpMpqnmWq"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
crossorigin="anonymous"></script>
<script src="js/login.js"></script>
</head>
<body>
<?php
$highlight_tab = "LOGIN";
require('include/unauth_nav_bar.php'); ?>
<div class="container-fluid">
<center>
<div class="page-header">
<h2>Organization Login</h2>
</div>
</center>
<form method="POST" action="login.php" id="login_form" autocomplete="off" >
<input type="hidden" id="nonce" name="nonce" value="<?php echo $csrf_nonce; ?>" />
<input type="hidden" id="csrf_expdate" name="csrf_expdate" value="<?php echo $csrf_expdate->format('U'); ?>" />
<div class="form-group">
<label for="email">Email address:</label>
<input class="form-control" type="email" id="email" maxlength="255" name="email" value="" />
</div> <!-- form-group -->
<div class="form-group">
<label for="password">Password:</label>
<input class="form-control" type="password" id="password" maxlength="128" name="password" value="" />
</div> <!-- form-group -->
<div class="alert alert-danger" <?php if (!isset($auth_fail_msg)) echo "hidden='true'"; ?> id="auth_fail_msg" >
<?php if (isset($auth_fail_msg)) echo $auth_fail_msg; ?>
</div>
<button type="submit" class="btn btn-default btn-lg" id="submit">Submit</button>
</form>
<?php require('include/footer.php'); ?>
</div> <!-- Container fluid -->
</body>
</html>