Skip to content

Commit d47722a

Browse files
committed
Start of implementing 2fa
1 parent f973496 commit d47722a

File tree

4 files changed

+208
-2
lines changed

4 files changed

+208
-2
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE `sys_user` ADD `otp_enabled` SET('n', 'y','v') NOT NULL DEFAULT 'n' AFTER `lost_password_reqtime`, ADD `otp_type` SET('email') NOT NULL DEFAULT 'email' AFTER `otp_enabled`, ADD `otp_data` VARCHAR(255) NULL AFTER `otp_type`, ADD `otp_recovery` VARCHAR(64) NULL AFTER `otp_data`, ADD `otp_attempts` TINYINT NOT NULL DEFAULT '0' AFTER `otp_recovery`;

install/sql/ispconfig3.sql

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1820,6 +1820,11 @@ CREATE TABLE `sys_user` (
18201820
`lost_password_function` tinyint(1) NOT NULL default '1',
18211821
`lost_password_hash` VARCHAR(50) NOT NULL default '',
18221822
`lost_password_reqtime` DATETIME NULL default NULL,
1823+
`otp_enabled` set('n','y','v') NOT NULL DEFAULT 'n',
1824+
`otp_type` set('email') NOT NULL DEFAULT 'email',
1825+
`otp_data` varchar(255) DEFAULT NULL,
1826+
`otp_recovery` varchar(64) DEFAULT NULL,
1827+
`otp_attempts` tinyint(4) NOT NULL DEFAULT 0,
18231828
PRIMARY KEY (`userid`)
18241829
) DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
18251830

interface/web/login/index.php

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,8 +279,27 @@
279279
echo 'LOGIN_REDIRECT:'.$_SESSION['s']['module']['startpage'];
280280
exit;
281281
} else {
282-
header('Location: ../index.php');
283-
die();
282+
283+
//* Do 2FA authentication
284+
if($user['otp_enabled'] != 'n') {
285+
286+
//* Save session in pending state and destroy original session
287+
$_SESSION['s_pending'] = $_SESSION['s'];
288+
unset($_SESSION['s']);
289+
290+
//* Create OTP session
291+
$_SESSION['otp']['session_attempts'] = 0;
292+
$_SESSION['otp']['type'] = $user['otp_type'];
293+
$_SESSION['otp']['data'] = $user['otp_data'];
294+
$_SESSION['otp']['recovery'] = $user['otp_recovery'];
295+
296+
//* Redirect to otp script
297+
header('Location: otp.php');
298+
die();
299+
} else {
300+
header('Location: ../index.php');
301+
die();
302+
}
284303
}
285304
}
286305
} else {

interface/web/login/otp.php

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
<?php
2+
3+
/*
4+
Copyright (c) 2021, Till Brehm, ISPConfig UG
5+
All rights reserved.
6+
7+
Redistribution and use in source and binary forms, with or without modification,
8+
are permitted provided that the following conditions are met:
9+
10+
* Redistributions of source code must retain the above copyright notice,
11+
this list of conditions and the following disclaimer.
12+
* Redistributions in binary form must reproduce the above copyright notice,
13+
this list of conditions and the following disclaimer in the documentation
14+
and/or other materials provided with the distribution.
15+
* Neither the name of ISPConfig nor the names of its contributors
16+
may be used to endorse or promote products derived from this software without
17+
specific prior written permission.
18+
19+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
22+
IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
23+
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
24+
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
26+
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
28+
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
31+
require_once '../../lib/config.inc.php';
32+
require_once '../../lib/app.inc.php';
33+
34+
//* Check if we have an active users ession
35+
if($_SESSION['s']['user']['active'] == 1) {
36+
header('Location: /index.php');
37+
die();
38+
}
39+
40+
//* If we don't have a 2fa session go back to login page
41+
if(!isset($_SESSION['otp'])) {
42+
header('Location: index.php');
43+
die();
44+
}
45+
46+
//* Variables and settings
47+
$error = '';
48+
$msg = '';
49+
$max_session_code_retry = 3;
50+
$max_global_code_retry = 10;
51+
52+
53+
//* CSRF Check if we got POST data
54+
if(count($_POST) >= 1) {
55+
$app->auth->csrf_token_check();
56+
}
57+
58+
//* Handle recovery code
59+
if(isset($_POST['code']) && strlen($_POST['code']) == 32 && $_SESSION['otp']['recovery'])) {
60+
//* TODO Recovery code handling
61+
62+
$user = $app->db->queryOneRecord('SELECT otp_attempts FROM sys_user WHERE userid = ?',$_SESSION['s_pending']['user']['userid']);
63+
64+
//* We allow one more try to enter recovery code
65+
if($user['otp_attempts'] > $max_global_code_retry + 1) {
66+
67+
}
68+
69+
70+
die('Handle recovery code');
71+
}
72+
73+
74+
//* Begin 2fa via Email
75+
if($_SESSION['otp']['type'] == 'email') {
76+
77+
//* Email 2fa handler settings
78+
$max_code_resend = 3;
79+
$max_time = 600; // time in seconds until the code gets invalidated
80+
$code_length = 6;
81+
82+
if(isset($_POST['code']) && strlen($_POST['code']) == $code_length && isset($_SESSION['otp']['code'])) {
83+
84+
if(strlen($_SESSION['otp']['code']) != $code_length) die(); // wrong code lenght, this should never happen
85+
86+
$user = $app->db->queryOneRecord('SELECT otp_attempts FROM sys_user WHERE userid = ?',$_SESSION['s_pending']['user']['userid']);
87+
88+
//* Check if we reached limits
89+
if($_SESSION['otp']['sent'] > $max_code_resend
90+
|| $_SESSION['otp']['session_attempts'] > $max_session_code_retry
91+
|| $user['otp_attempts'] > $max_global_code_retry
92+
|| time() > $_SESSION['otp']['starttime'] + $max_time) {
93+
unset($_SESSION['otp']);
94+
unset($_SESSION['s_pending']);
95+
$app->error('2FA failed','index.php');
96+
}
97+
98+
//* 2fa success
99+
if($_POST['code'] == $_SESSION['otp']['code']) {
100+
$_SESSION['s'] = $_SESSION['s_pending'];
101+
unset($_SESSION['s_pending']);
102+
unset($_SESSION['otp']);
103+
header('Location: ../index.php');
104+
die();
105+
} else {
106+
//* 2fa wrong code
107+
$_SESSION['otp']['session_attempts']++;
108+
$app->db->query()
109+
}
110+
}
111+
112+
//* set code
113+
if(!isset($_SESSION['otp']['code']) || empty($_SESSION['otp']['code'])) {
114+
// TODO Code generator
115+
$_SESSION['otp']['code'] = 123456;
116+
$_SESSION['otp']['starttime'] = time();
117+
}
118+
119+
//* Send code via email
120+
if(!isset($_SESSION['otp']['sent']) || $_GET['action'] == 'resend') {
121+
122+
//* Ensure that code is not sent too often
123+
if(isset($_SESSION['otp']['sent']) && $_SESSION['otp']['sent'] > $max_code_resend) {
124+
$app->error('Code resend limit reached','index.php');
125+
}
126+
127+
$app->uses('functions');
128+
129+
//* send email
130+
$email_to = $_SESSION['otp']['data'];
131+
$subject = 'ISPConfig Login authentication';
132+
$text = '';
133+
$from = 'root@localhost';
134+
135+
$app->functions->mail($email_to, $subject, $text, $from);
136+
137+
//* increase sent counter
138+
if(!isset($_SESSION['otp']['sent'])) {
139+
$_SESSION['otp']['sent'] = 1;
140+
} else {
141+
$_SESSION['otp']['sent']++;
142+
}
143+
144+
}
145+
146+
//* Show form to enter email code
147+
148+
149+
150+
} else {
151+
//* unsupported 2fa type
152+
$app->error('Code resend limit reached','index.php');
153+
}
154+
155+
156+
157+
158+
159+
//* Load templating system and lang file
160+
$app->uses('tpl');
161+
$app->tpl->newTemplate('main_login.tpl.htm');
162+
$app->tpl->setInclude('content_tpl', 'templates/otp.htm');
163+
164+
165+
//* SET csrf token
166+
$csrf_token = $app->auth->csrf_token_get('language_edit');
167+
$app->tpl->setVar('_csrf_id',$csrf_token['csrf_id']);
168+
$app->tpl->setVar('_csrf_key',$csrf_token['csrf_key']);
169+
170+
171+
$app->load_language_file('web/login/lib/lang/'.$conf["language"].'.lng');
172+
173+
174+
175+
176+
177+
$app->tpl_defaults();
178+
$app->tpl->pparse();
179+
180+
181+
?>

0 commit comments

Comments
 (0)