xref: /plugin/twofactor/action/login.php (revision fca58076a31917642cc917f0d19e4f3f0a82d9e9)
1*fca58076SAndreas Gohr<?php
2*fca58076SAndreas Gohr// must be run within Dokuwiki
3*fca58076SAndreas Gohrif (!defined('DOKU_INC')) die();
4*fca58076SAndreas Gohr/**
5*fca58076SAndreas Gohr * Two Factor Action Plugin
6*fca58076SAndreas Gohr *
7*fca58076SAndreas Gohr * @author Mike Wilmes mwilmes@avc.edu
8*fca58076SAndreas Gohr * Big thanks to Daniel Popp and his Google 2FA code (authgoogle2fa) as a
9*fca58076SAndreas Gohr * starting reference.
10*fca58076SAndreas Gohr *
11*fca58076SAndreas Gohr * Overview:
12*fca58076SAndreas Gohr * The plugin provides for two opportunities to perform two factor
13*fca58076SAndreas Gohr * authentication. The first is on the main login page, via a code provided by
14*fca58076SAndreas Gohr * an external authenticator. The second is at a separate prompt after the
15*fca58076SAndreas Gohr * initial login. By default, all modules will process from the second login,
16*fca58076SAndreas Gohr * but a module can subscribe to accepting a password from the main login when
17*fca58076SAndreas Gohr * it makes sense, because the user has access to the code in advance.
18*fca58076SAndreas Gohr *
19*fca58076SAndreas Gohr * If a user only has configured modules that provide for login at the main
20*fca58076SAndreas Gohr * screen, the code will only be accepted at the main login screen for
21*fca58076SAndreas Gohr * security purposes.
22*fca58076SAndreas Gohr *
23*fca58076SAndreas Gohr * Modules will be called to render their configuration forms on the profile
24*fca58076SAndreas Gohr * page and to verify a user's submitted code. If any module accepts the
25*fca58076SAndreas Gohr * submitted code, then the user is granted access.
26*fca58076SAndreas Gohr *
27*fca58076SAndreas Gohr * Each module may be used to transmit a message to the user that their
28*fca58076SAndreas Gohr * account has been logged into. One module may be used as the default
29*fca58076SAndreas Gohr * transmit option. These options are handled by the parent module.
30*fca58076SAndreas Gohr */
31*fca58076SAndreas Gohr
32*fca58076SAndreas Gohr// Create a definition for a 2FA cookie.
33*fca58076SAndreas Gohrdefine('TWOFACTOR_COOKIE', '2FA' . DOKU_COOKIE);
34*fca58076SAndreas Gohr
35*fca58076SAndreas Gohrclass action_plugin_twofactor_login extends DokuWiki_Action_Plugin
36*fca58076SAndreas Gohr{
37*fca58076SAndreas Gohr    public $success = false;
38*fca58076SAndreas Gohr    private $attribute = null;
39*fca58076SAndreas Gohr    private $tokenMods = null;
40*fca58076SAndreas Gohr    private $otpMods = null;
41*fca58076SAndreas Gohr    private $setTime = false;
42*fca58076SAndreas Gohr
43*fca58076SAndreas Gohr    public function __construct()
44*fca58076SAndreas Gohr    {
45*fca58076SAndreas Gohr        $this->loadConfig();
46*fca58076SAndreas Gohr        // Load the attribute helper if GA is active or not requiring use of email to send the OTP.
47*fca58076SAndreas Gohr
48*fca58076SAndreas Gohr        $this->attribute = $this->loadHelper('attribute',
49*fca58076SAndreas Gohr            'TwoFactor depends on the Attribute plugin, but the Attribute plugin is not installed!');
50*fca58076SAndreas Gohr        // Now figure out what modules to load and load them.
51*fca58076SAndreas Gohr        $available = Twofactor_Auth_Module::_listModules();
52*fca58076SAndreas Gohr        $allmodules = Twofactor_Auth_Module::_loadModules($available);
53*fca58076SAndreas Gohr        $failed = array_diff($available, array_keys($allmodules));
54*fca58076SAndreas Gohr        if (count($failed) > 0) {
55*fca58076SAndreas Gohr            msg('At least one loaded module did not have a properly named class.' . ' ' . implode(', ', $failed), -1);
56*fca58076SAndreas Gohr        }
57*fca58076SAndreas Gohr        $this->modules = array_filter($allmodules, function ($obj) {
58*fca58076SAndreas Gohr            return $obj->getConf('enable') == 1;
59*fca58076SAndreas Gohr        });
60*fca58076SAndreas Gohr        // Sanity check.
61*fca58076SAndreas Gohr        $this->success = (!$requireAttribute || ($this->attribute && $this->attribute->success)) && count($this->modules) > 0;
62*fca58076SAndreas Gohr    }
63*fca58076SAndreas Gohr
64*fca58076SAndreas Gohr    /**
65*fca58076SAndreas Gohr     * Registers the event handlers.
66*fca58076SAndreas Gohr     */
67*fca58076SAndreas Gohr    public function register(Doku_Event_Handler $controller)
68*fca58076SAndreas Gohr    {
69*fca58076SAndreas Gohr        if (!$this->success) return;
70*fca58076SAndreas Gohr
71*fca58076SAndreas Gohr        $firstlogin = false;
72*fca58076SAndreas Gohr        foreach ($this->modules as $mod) {
73*fca58076SAndreas Gohr            $firstlogin |= $mod->canAuthLogin();
74*fca58076SAndreas Gohr        }
75*fca58076SAndreas Gohr        if ($firstlogin) {
76*fca58076SAndreas Gohr            $controller->register_hook('HTML_LOGINFORM_OUTPUT', 'BEFORE', $this, 'twofactor_login_form');
77*fca58076SAndreas Gohr        }
78*fca58076SAndreas Gohr
79*fca58076SAndreas Gohr
80*fca58076SAndreas Gohr        // Manage action flow around the twofactor authentication requirements.
81*fca58076SAndreas Gohr        $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'twofactor_action_process_handler',
82*fca58076SAndreas Gohr            null, -999999);
83*fca58076SAndreas Gohr        // Handle the twofactor login and profile actions.
84*fca58076SAndreas Gohr        $controller->register_hook('TPL_ACT_UNKNOWN', 'BEFORE', $this, 'twofactor_handle_unknown_action');
85*fca58076SAndreas Gohr        $controller->register_hook('TPL_ACTION_GET', 'BEFORE', $this, 'twofactor_get_unknown_action');
86*fca58076SAndreas Gohr
87*fca58076SAndreas Gohr        // If the user supplies a token code at login, checks it before logging the user in.
88*fca58076SAndreas Gohr        $controller->register_hook('AUTH_LOGIN_CHECK', 'BEFORE', $this, 'twofactor_before_auth_check', null,
89*fca58076SAndreas Gohr            -999999);
90*fca58076SAndreas Gohr        // Atempts to process the second login if the user hasn't done so already.
91*fca58076SAndreas Gohr        $controller->register_hook('AUTH_LOGIN_CHECK', 'AFTER', $this, 'twofactor_after_auth_check');
92*fca58076SAndreas Gohr        $this->log('register: Session: ' . print_r($_SESSION, true), self::LOGGING_DEBUGPLUS);
93*fca58076SAndreas Gohr
94*fca58076SAndreas Gohr    }
95*fca58076SAndreas Gohr
96*fca58076SAndreas Gohr
97*fca58076SAndreas Gohr    /**
98*fca58076SAndreas Gohr     * Handles the login form rendering.
99*fca58076SAndreas Gohr     */
100*fca58076SAndreas Gohr    public function twofactor_login_form(&$event, $param)
101*fca58076SAndreas Gohr    {
102*fca58076SAndreas Gohr        $this->log('twofactor_login_form: start', self::LOGGING_DEBUG);
103*fca58076SAndreas Gohr        $twofa_form = form_makeTextField('otp', '', $this->getLang('twofactor_login'), '', 'block',
104*fca58076SAndreas Gohr            array('size' => '50', 'autocomplete' => 'off'));
105*fca58076SAndreas Gohr        $pos = $event->data->findElementByAttribute('name', 'p');
106*fca58076SAndreas Gohr        $event->data->insertElement($pos + 1, $twofa_form);
107*fca58076SAndreas Gohr    }
108*fca58076SAndreas Gohr
109*fca58076SAndreas Gohr
110*fca58076SAndreas Gohr
111*fca58076SAndreas Gohr    /**
112*fca58076SAndreas Gohr     * Action process redirector.  If logging out, processes the logout
113*fca58076SAndreas Gohr     * function.  If visiting the profile, sets a flag to confirm that the
114*fca58076SAndreas Gohr     * profile is being viewed in order to enable OTP attribute updates.
115*fca58076SAndreas Gohr     */
116*fca58076SAndreas Gohr    public function twofactor_action_process_handler(&$event, $param)
117*fca58076SAndreas Gohr    {
118*fca58076SAndreas Gohr        global $USERINFO, $ID, $INFO, $INPUT;
119*fca58076SAndreas Gohr        $this->log('twofactor_action_process_handler: start ' . $event->data, self::LOGGING_DEBUG);
120*fca58076SAndreas Gohr        // Handle logout.
121*fca58076SAndreas Gohr        if ($event->data == 'logout') {
122*fca58076SAndreas Gohr            $this->_logout();
123*fca58076SAndreas Gohr            return;
124*fca58076SAndreas Gohr        }
125*fca58076SAndreas Gohr        // Handle main login.
126*fca58076SAndreas Gohr        if ($event->data == 'login') {
127*fca58076SAndreas Gohr            // To support loglog or any other module that hooks login checking for success,
128*fca58076SAndreas Gohr            // Confirm that the user is logged in.  If not, then redirect to twofactor_login
129*fca58076SAndreas Gohr            // and fail the login.
130*fca58076SAndreas Gohr            if ($USERINFO && !$this->get_clearance()) {
131*fca58076SAndreas Gohr                // Hijack this event.  We need to resend it after 2FA is done.
132*fca58076SAndreas Gohr                $event->stopPropagation();
133*fca58076SAndreas Gohr                // Send loglog an event to show the user logged in but needs OTP code.
134*fca58076SAndreas Gohr                $log = array('message' => 'logged in, ' . $this->getLang('requires_otp'), 'user' => $user);
135*fca58076SAndreas Gohr                trigger_event('PLUGIN_LOGLOG_LOG', $log);
136*fca58076SAndreas Gohr            }
137*fca58076SAndreas Gohr            return;
138*fca58076SAndreas Gohr        }
139*fca58076SAndreas Gohr
140*fca58076SAndreas Gohr        // Check to see if we are heading to the twofactor login.
141*fca58076SAndreas Gohr        if ($event->data == 'twofactor_login') {
142*fca58076SAndreas Gohr            // Check if we already have clearance- just in case.
143*fca58076SAndreas Gohr            if ($this->get_clearance()) {
144*fca58076SAndreas Gohr                // Okay, this continues on with normal processing.
145*fca58076SAndreas Gohr                return;
146*fca58076SAndreas Gohr            }
147*fca58076SAndreas Gohr            // We will be handling this action's permissions here.
148*fca58076SAndreas Gohr            $event->preventDefault();
149*fca58076SAndreas Gohr            $event->stopPropagation();
150*fca58076SAndreas Gohr            // If not logged into the main auth plugin then send there.
151*fca58076SAndreas Gohr            if (!$USERINFO) {
152*fca58076SAndreas Gohr                $event->result = false;
153*fca58076SAndreas Gohr                send_redirect(wl($ID, array('do' => 'login'), true, '&'));
154*fca58076SAndreas Gohr                return;
155*fca58076SAndreas Gohr            }
156*fca58076SAndreas Gohr            if (count($this->otpMods) == 0) {
157*fca58076SAndreas Gohr                $this->log('No available otp modules.', self::LOGGING_DEBUG);
158*fca58076SAndreas Gohr                // There is no way to handle this login.
159*fca58076SAndreas Gohr                msg($this->getLang('mustusetoken'), -1);
160*fca58076SAndreas Gohr                $event->result = false;
161*fca58076SAndreas Gohr                send_redirect(wl($ID, array('do' => 'logout'), true, '&'));
162*fca58076SAndreas Gohr                return;
163*fca58076SAndreas Gohr            }
164*fca58076SAndreas Gohr            // Otherwise handle the action.
165*fca58076SAndreas Gohr            $act = $this->_process_otp($event, $param);
166*fca58076SAndreas Gohr            $event->result = true;
167*fca58076SAndreas Gohr            if ($act) {
168*fca58076SAndreas Gohr                send_redirect(wl($ID, array('do' => $act), true, '&'));
169*fca58076SAndreas Gohr            }
170*fca58076SAndreas Gohr            return;
171*fca58076SAndreas Gohr        }
172*fca58076SAndreas Gohr
173*fca58076SAndreas Gohr        // Is the user logged into the wiki?
174*fca58076SAndreas Gohr        if (!$USERINFO) {
175*fca58076SAndreas Gohr            // If not logged in, then do nothing.
176*fca58076SAndreas Gohr            return;
177*fca58076SAndreas Gohr        }
178*fca58076SAndreas Gohr
179*fca58076SAndreas Gohr        // See if this user has any OTP methods configured.
180*fca58076SAndreas Gohr        $available = count($this->tokenMods) + count($this->otpMods) > 0;
181*fca58076SAndreas Gohr        // Check if this user needs to login with 2FA.
182*fca58076SAndreas Gohr        // Wiki mandatory is on if user is logged in and config is mandatory
183*fca58076SAndreas Gohr        $mandatory = $this->getConf("optinout") == 'mandatory' && $INPUT->server->str('REMOTE_USER', '');
184*fca58076SAndreas Gohr        // User is NOT OPTED OUT if the optin setting is undefined and the wiki config is optout.
185*fca58076SAndreas Gohr        $not_opted_out = $this->attribute->get("twofactor", "state") == '' && $this->getConf("optinout") == 'optout';
186*fca58076SAndreas Gohr        // The user must login if wiki mandatory is on or if the user is logged in and user is opt in.
187*fca58076SAndreas Gohr        $must_login = $mandatory || ($this->attribute->get("twofactor",
188*fca58076SAndreas Gohr                    "state") == 'in' && $INPUT->server->str('REMOTE_USER', ''));
189*fca58076SAndreas Gohr        $has_clearance = $this->get_clearance() === true;
190*fca58076SAndreas Gohr        $this->log('twofactor_action_process_handler: USERINFO: ' . print_r($USERINFO, true), self::LOGGING_DEBUGPLUS);
191*fca58076SAndreas Gohr
192*fca58076SAndreas Gohr        // Possible combination skipped- not logged in and 2FA is not requred for user {optout conf or (no selection and optin conf)}.
193*fca58076SAndreas Gohr
194*fca58076SAndreas Gohr        // Check to see if updating twofactor is required.
195*fca58076SAndreas Gohr        // This happens if the wiki is mandatory, the user has not opted out of an opt-out wiki, or if the user has opted in, and if there are no available mods for use.
196*fca58076SAndreas Gohr        // The user cannot have available mods without setting them up, and cannot unless the wiki is mandatory or the user has opted in.
197*fca58076SAndreas Gohr        if (($must_login || $not_opted_out) && !$available) {
198*fca58076SAndreas Gohr            // If the user has not been granted access at this point, do so or they will get booted after setting up 2FA.
199*fca58076SAndreas Gohr            if (!$has_clearance) {
200*fca58076SAndreas Gohr                $this->_grant_clearance();
201*fca58076SAndreas Gohr            }
202*fca58076SAndreas Gohr            // We need to go to the twofactor profile.
203*fca58076SAndreas Gohr            // If we were setup properly, we would not be here in the code.
204*fca58076SAndreas Gohr            $event->preventDefault();
205*fca58076SAndreas Gohr            $event->stopPropagation();
206*fca58076SAndreas Gohr            $event->result = false;
207*fca58076SAndreas Gohr            // Send loglog an event to show the user aborted 2FA.
208*fca58076SAndreas Gohr            $log = array('message' => 'logged in, ' . $this->getLang('2fa_mandatory'), 'user' => $user);
209*fca58076SAndreas Gohr            trigger_event('PLUGIN_LOGLOG_LOG', $log);
210*fca58076SAndreas Gohr            send_redirect(wl($ID, array('do' => 'twofactor_profile'), true, '&'));
211*fca58076SAndreas Gohr            return;
212*fca58076SAndreas Gohr        }
213*fca58076SAndreas Gohr
214*fca58076SAndreas Gohr        // Now validate login before proceeding.
215*fca58076SAndreas Gohr        if (!$has_clearance) {
216*fca58076SAndreas Gohr            if ($must_login) {
217*fca58076SAndreas Gohr                if (!in_array($event->data, array('login', 'twofactor_login'))) {
218*fca58076SAndreas Gohr                    // If not logged in then force to the login page.
219*fca58076SAndreas Gohr                    $event->preventDefault();
220*fca58076SAndreas Gohr                    $event->stopPropagation();
221*fca58076SAndreas Gohr                    $event->result = false;
222*fca58076SAndreas Gohr                    // If there are OTP generators, then use them.
223*fca58076SAndreas Gohr                    send_redirect(wl($ID, array('do' => 'twofactor_login'), true, '&'));
224*fca58076SAndreas Gohr                    return;
225*fca58076SAndreas Gohr                }
226*fca58076SAndreas Gohr                // Otherwise go to where we are told.
227*fca58076SAndreas Gohr                return;
228*fca58076SAndreas Gohr            }
229*fca58076SAndreas Gohr            // The user is not set with 2FA and is not required to.
230*fca58076SAndreas Gohr            // Grant clearance and continue.
231*fca58076SAndreas Gohr            $this->_grant_clearance();
232*fca58076SAndreas Gohr        }
233*fca58076SAndreas Gohr        // Otherwise everything is good!
234*fca58076SAndreas Gohr        return;
235*fca58076SAndreas Gohr    }
236*fca58076SAndreas Gohr
237*fca58076SAndreas Gohr
238*fca58076SAndreas Gohr    public function twofactor_handle_unknown_action(Doku_Event $event, $param)
239*fca58076SAndreas Gohr    {
240*fca58076SAndreas Gohr        if ($event->data == 'twofactor_login') {
241*fca58076SAndreas Gohr            $event->preventDefault();
242*fca58076SAndreas Gohr            $event->stopPropagation();
243*fca58076SAndreas Gohr            $event->result = $this->twofactor_otp_login($event, $param);
244*fca58076SAndreas Gohr            return;
245*fca58076SAndreas Gohr        }
246*fca58076SAndreas Gohr    }
247*fca58076SAndreas Gohr
248*fca58076SAndreas Gohr    public function twofactor_get_unknown_action(&$event, $param)
249*fca58076SAndreas Gohr    {
250*fca58076SAndreas Gohr        $this->log('start: twofactor_before_auth_check', self::LOGGING_DEBUG);
251*fca58076SAndreas Gohr        switch ($event->data['type']) {
252*fca58076SAndreas Gohr            case 'twofactor_profile':
253*fca58076SAndreas Gohr                $event->data['params'] = array('do' => 'twofactor_profile');
254*fca58076SAndreas Gohr                // Inject text into $lang.
255*fca58076SAndreas Gohr                global $lang;
256*fca58076SAndreas Gohr                $lang['btn_twofactor_profile'] = $this->getLang('btn_twofactor_profile');
257*fca58076SAndreas Gohr                $event->preventDefault();
258*fca58076SAndreas Gohr                $event->stopPropagation();
259*fca58076SAndreas Gohr                $event->result = false;
260*fca58076SAndreas Gohr                break;
261*fca58076SAndreas Gohr        }
262*fca58076SAndreas Gohr    }
263*fca58076SAndreas Gohr
264*fca58076SAndreas Gohr    /**
265*fca58076SAndreas Gohr     * Logout this session from two factor authentication.  Purge any existing
266*fca58076SAndreas Gohr     * OTP from the user's attributes.
267*fca58076SAndreas Gohr     */
268*fca58076SAndreas Gohr    private function _logout()
269*fca58076SAndreas Gohr    {
270*fca58076SAndreas Gohr        global $conf, $INPUT;
271*fca58076SAndreas Gohr        $this->log('_logout: start', self::LOGGING_DEBUG);
272*fca58076SAndreas Gohr        $this->log(print_r(array($_SESSION, $_COOKIE), true), self::LOGGING_DEBUGPLUS);
273*fca58076SAndreas Gohr        // No need to do this as long as no Cookie or session for login is present!
274*fca58076SAndreas Gohr        if (empty($_SESSION[DOKU_COOKIE]['twofactor_clearance']) && empty($_COOKIE[TWOFACTOR_COOKIE])) {
275*fca58076SAndreas Gohr            $this->log('_logout: quitting, no cookies', self::LOGGING_DEBUG);
276*fca58076SAndreas Gohr            return;
277*fca58076SAndreas Gohr        }
278*fca58076SAndreas Gohr        // Audit log.
279*fca58076SAndreas Gohr        $this->log("2FA Logout: " . $INPUT->server->str('REMOTE_USER', $_REQUEST['r']), self::LOGGING_AUDIT);
280*fca58076SAndreas Gohr        if ($this->attribute) {
281*fca58076SAndreas Gohr            // Purge outstanding OTPs.
282*fca58076SAndreas Gohr            $this->attribute->del("twofactor", "otp");
283*fca58076SAndreas Gohr            // Purge cookie and session ID relation.
284*fca58076SAndreas Gohr            $key = $_COOKIE[TWOFACTOR_COOKIE];
285*fca58076SAndreas Gohr            if (!empty($key) && substr($key, 0, 3) != 'id.') {
286*fca58076SAndreas Gohr                $id = $this->attribute->del("twofactor", $key);
287*fca58076SAndreas Gohr            }
288*fca58076SAndreas Gohr            // Wipe out 2FA cookie.
289*fca58076SAndreas Gohr            $this->log('del cookies: ' . TWOFACTOR_COOKIE . ' ' . print_r(headers_sent(), true),
290*fca58076SAndreas Gohr                self::LOGGING_DEBUGPLUS);
291*fca58076SAndreas Gohr            $cookie = '';
292*fca58076SAndreas Gohr            $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
293*fca58076SAndreas Gohr            $time = time() - 600000; //many seconds ago
294*fca58076SAndreas Gohr            setcookie(TWOFACTOR_COOKIE, $cookie, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true);
295*fca58076SAndreas Gohr            unset($_COOKIE[TWOFACTOR_COOKIE]);
296*fca58076SAndreas Gohr            // Just in case, unset the setTime flag so attributes will be saved again.
297*fca58076SAndreas Gohr            $this->setTime = false;
298*fca58076SAndreas Gohr        }
299*fca58076SAndreas Gohr        // Before we get here, the session is closed. Reopen it to logout the user.
300*fca58076SAndreas Gohr        if (!headers_sent()) {
301*fca58076SAndreas Gohr            $session = session_status() != PHP_SESSION_NONE;
302*fca58076SAndreas Gohr            if (!$session) {
303*fca58076SAndreas Gohr                session_start();
304*fca58076SAndreas Gohr            }
305*fca58076SAndreas Gohr            $_SESSION[DOKU_COOKIE]['twofactor_clearance'] = false;
306*fca58076SAndreas Gohr            unset($_SESSION[DOKU_COOKIE]['twofactor_clearance']);
307*fca58076SAndreas Gohr            if (!$session) {
308*fca58076SAndreas Gohr                session_write_close();
309*fca58076SAndreas Gohr            }
310*fca58076SAndreas Gohr        } else {
311*fca58076SAndreas Gohr            msg("Error! You have not been logged off!!!", -1);
312*fca58076SAndreas Gohr        }
313*fca58076SAndreas Gohr    }
314*fca58076SAndreas Gohr
315*fca58076SAndreas Gohr    /**
316*fca58076SAndreas Gohr     * See if the current session has passed two factor authentication.
317*fca58076SAndreas Gohr     * @return bool - true if the session as successfully passed two factor
318*fca58076SAndreas Gohr     *      authentication.
319*fca58076SAndreas Gohr     */
320*fca58076SAndreas Gohr    public function get_clearance($user = null)
321*fca58076SAndreas Gohr    {
322*fca58076SAndreas Gohr        global $INPUT;
323*fca58076SAndreas Gohr        $this->log("get_clearance: start", self::LOGGING_DEBUG);
324*fca58076SAndreas Gohr        $this->log("User:" . $INPUT->server->str('REMOTE_USER', null), self::LOGGING_DEBUGPLUS);
325*fca58076SAndreas Gohr        # Get and correct the refresh expiry.
326*fca58076SAndreas Gohr        # At least 5 min, at most 1440 min (1 day).
327*fca58076SAndreas Gohr        $refreshexpiry = min(max($this->getConf('refreshexpiry'), 5), 1400) * 60;
328*fca58076SAndreas Gohr        # First check if we have a key.  No key === no login.
329*fca58076SAndreas Gohr        $key = $_COOKIE[TWOFACTOR_COOKIE];
330*fca58076SAndreas Gohr        if (empty($key)) {
331*fca58076SAndreas Gohr            $this->log("get_clearance: No cookie.", self::LOGGING_DEBUGPLUS);
332*fca58076SAndreas Gohr            return false;
333*fca58076SAndreas Gohr        }
334*fca58076SAndreas Gohr        # If the key is not valid, logout.
335*fca58076SAndreas Gohr        if (substr($key, 0, 3) != 'id.') {
336*fca58076SAndreas Gohr            $this->log("get_clearance: BAD cookie.", self::LOGGING_DEBUGPLUS);
337*fca58076SAndreas Gohr            // Purge the login data just in case.
338*fca58076SAndreas Gohr            $this->_logout();
339*fca58076SAndreas Gohr            return false;
340*fca58076SAndreas Gohr        }
341*fca58076SAndreas Gohr        # Load the expiry value from session.
342*fca58076SAndreas Gohr        $expiry = $_SESSION[DOKU_COOKIE]['twofactor_clearance'];
343*fca58076SAndreas Gohr        # Check if this time is valid.
344*fca58076SAndreas Gohr        $clearance = (!empty($expiry) && $expiry + $refreshexpiry > time());
345*fca58076SAndreas Gohr        if (!$clearance) {
346*fca58076SAndreas Gohr            # First use this time to purge the old IDs from attribute.
347*fca58076SAndreas Gohr            foreach (array_filter($this->attribute->enumerateAttributes("twofactor", $user), function ($key) {
348*fca58076SAndreas Gohr                substr($key, 0, 3) == 'id.';
349*fca58076SAndreas Gohr            }) as $attr) {
350*fca58076SAndreas Gohr                if ($this->attribute->get("twofactor", $attr, $user) + $refreshexpiry < time()) {
351*fca58076SAndreas Gohr                    $this->attribute->del("twofactor", $attr, $user);
352*fca58076SAndreas Gohr                }
353*fca58076SAndreas Gohr            }
354*fca58076SAndreas Gohr            # Check if this key still exists.
355*fca58076SAndreas Gohr            $clearance = $this->attribute->exists("twofactor", $key, $user);
356*fca58076SAndreas Gohr            if ($clearance) {
357*fca58076SAndreas Gohr                $this->log("get_clearance: 2FA revived by cookie. Expiry: " . print_r($expiry,
358*fca58076SAndreas Gohr                        true) . " Session: " . print_r($_SESSION, true), self::LOGGING_DEBUGPLUS);
359*fca58076SAndreas Gohr            }
360*fca58076SAndreas Gohr        }
361*fca58076SAndreas Gohr        if ($clearance && !$this->setTime) {
362*fca58076SAndreas Gohr            $session = session_status() != PHP_SESSION_NONE;
363*fca58076SAndreas Gohr            if (!$session) {
364*fca58076SAndreas Gohr                session_start();
365*fca58076SAndreas Gohr            }
366*fca58076SAndreas Gohr            $_SESSION[DOKU_COOKIE]['twofactor_clearance'] = time();
367*fca58076SAndreas Gohr            if (!$session) {
368*fca58076SAndreas Gohr                session_write_close();
369*fca58076SAndreas Gohr            }
370*fca58076SAndreas Gohr            $this->attribute->set("twofactor", $key, $_SESSION[DOKU_COOKIE]['twofactor_clearance'], $user);
371*fca58076SAndreas Gohr            // Set this flag to stop future updates.
372*fca58076SAndreas Gohr            $this->setTime = true;
373*fca58076SAndreas Gohr            $this->log("get_clearance: Session reset. Session: " . print_r($_SESSION, true), self::LOGGING_DEBUGPLUS);
374*fca58076SAndreas Gohr        } elseif (!$clearance) {
375*fca58076SAndreas Gohr            // Otherwise logout.
376*fca58076SAndreas Gohr            $this->_logout();
377*fca58076SAndreas Gohr        }
378*fca58076SAndreas Gohr        return $clearance;
379*fca58076SAndreas Gohr    }
380*fca58076SAndreas Gohr
381*fca58076SAndreas Gohr    /**
382*fca58076SAndreas Gohr     * Flags this session as having passed two factor authentication.
383*fca58076SAndreas Gohr     * @return bool - returns true on successfully granting two factor clearance.
384*fca58076SAndreas Gohr     */
385*fca58076SAndreas Gohr    private function _grant_clearance($user = null)
386*fca58076SAndreas Gohr    {
387*fca58076SAndreas Gohr        global $conf, $INPUT;
388*fca58076SAndreas Gohr        $this->log("_grant_clearance: start", self::LOGGING_DEBUG);
389*fca58076SAndreas Gohr        $this->log('2FA Login: ' . $INPUT->server->str("REMOTE_USER", $user), self::LOGGING_AUDIT);
390*fca58076SAndreas Gohr        if ($INPUT->server->str("REMOTE_USER", $user) == 1) {
391*fca58076SAndreas Gohr            $this->log("_grant_clearance: start", self::LOGGING_DEBUGPLUS);
392*fca58076SAndreas Gohr        }
393*fca58076SAndreas Gohr        // Purge the otp code as a security measure.
394*fca58076SAndreas Gohr        $this->attribute->del("twofactor", "otp", $user);
395*fca58076SAndreas Gohr        if (!headers_sent()) {
396*fca58076SAndreas Gohr            $session = session_status() != PHP_SESSION_NONE;
397*fca58076SAndreas Gohr            if (!$session) {
398*fca58076SAndreas Gohr                session_start();
399*fca58076SAndreas Gohr            }
400*fca58076SAndreas Gohr            $_SESSION[DOKU_COOKIE]['twofactor_clearance'] = time();
401*fca58076SAndreas Gohr            // Set the notify flag if set or required by wiki.
402*fca58076SAndreas Gohr            $this->log('_grant_clearance: conf:' . $this->getConf('loginnotice') . ' user:' . ($this->attribute->get("twofactor",
403*fca58076SAndreas Gohr                    "loginnotice", $user) === true ? 'true' : 'false'), self::LOGGING_DEBUG);
404*fca58076SAndreas Gohr            $send_wanted = $this->getConf('loginnotice') == 'always' || ($this->getConf('loginnotice') == 'user' && $this->attribute->get("twofactor",
405*fca58076SAndreas Gohr                        "loginnotice", $user) == true);
406*fca58076SAndreas Gohr            if ($send_wanted) {
407*fca58076SAndreas Gohr                $_SESSION[DOKU_COOKIE]['twofactor_notify'] = true;
408*fca58076SAndreas Gohr            }
409*fca58076SAndreas Gohr            if (!$session) {
410*fca58076SAndreas Gohr                session_write_close();
411*fca58076SAndreas Gohr            }
412*fca58076SAndreas Gohr        } else {
413*fca58076SAndreas Gohr            msg("Error! You have not been logged in!!!", -1);
414*fca58076SAndreas Gohr        }
415*fca58076SAndreas Gohr        // Creating a cookie in case the session purges.
416*fca58076SAndreas Gohr        $key = 'id.' . session_id();
417*fca58076SAndreas Gohr        // Storing a timeout value.
418*fca58076SAndreas Gohr        $this->attribute->set("twofactor", $key, $_SESSION[DOKU_COOKIE]['twofactor_clearance'], $user);
419*fca58076SAndreas Gohr        // Set the 2FA cookie.
420*fca58076SAndreas Gohr        $this->log('_grant_clearance: new cookies: ' . TWOFACTOR_COOKIE . ' ' . print_r(headers_sent(), true),
421*fca58076SAndreas Gohr            self::LOGGING_DEBUGPLUS);
422*fca58076SAndreas Gohr        $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
423*fca58076SAndreas Gohr        $time = time() + 60 * 60 * 24 * 365; //one year
424*fca58076SAndreas Gohr        setcookie(TWOFACTOR_COOKIE, $key, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true);
425*fca58076SAndreas Gohr        $_COOKIE[TWOFACTOR_COOKIE] = $key;
426*fca58076SAndreas Gohr        return !empty($_SESSION[DOKU_COOKIE]['twofactor_clearance']);
427*fca58076SAndreas Gohr    }
428*fca58076SAndreas Gohr
429*fca58076SAndreas Gohr    /**
430*fca58076SAndreas Gohr     * Sends emails notifying user of successfult 2FA login.
431*fca58076SAndreas Gohr     * @return mixed - returns true on successfully sending notification to all
432*fca58076SAndreas Gohr     *     modules, false if no notifications were sent, or a number indicating
433*fca58076SAndreas Gohr     *     the number of modules that suceeded.
434*fca58076SAndreas Gohr     */
435*fca58076SAndreas Gohr    private function _send_login_notification()
436*fca58076SAndreas Gohr    {
437*fca58076SAndreas Gohr        $this->log("_send_login_notification: start", self::LOGGING_DEBUG);
438*fca58076SAndreas Gohr        // Send login notification.
439*fca58076SAndreas Gohr        $module = $this->attribute->exists("twofactor", "defaultmod") ? $this->attribute->get("twofactor",
440*fca58076SAndreas Gohr            "defaultmod") : null;
441*fca58076SAndreas Gohr        $subject = $this->getConf('loginsubject');
442*fca58076SAndreas Gohr        $time = date(DATE_RFC2822);
443*fca58076SAndreas Gohr        $message = str_replace('$time', $time, $this->getConf('logincontent'));
444*fca58076SAndreas Gohr        $result = $this->_send_message($subject, $message, $module);
445*fca58076SAndreas Gohr        return $result;
446*fca58076SAndreas Gohr    }
447*fca58076SAndreas Gohr
448*fca58076SAndreas Gohr    /**
449*fca58076SAndreas Gohr     * Handles the authentication check. Screens Google Authenticator OTP, if available.
450*fca58076SAndreas Gohr     * NOTE: NOT LOGGED IN YET. Attribute requires user name.
451*fca58076SAndreas Gohr     */
452*fca58076SAndreas Gohr    function twofactor_before_auth_check(&$event, $param)
453*fca58076SAndreas Gohr    {
454*fca58076SAndreas Gohr        global $ACT, $INPUT;
455*fca58076SAndreas Gohr        $this->log("twofactor_before_auth_check: start $ACT", self::LOGGING_DEBUG);
456*fca58076SAndreas Gohr        $this->log("twofactor_before_auth_check: Cookie: " . print_r($_COOKIE, true), self::LOGGING_DEBUGPLUS);
457*fca58076SAndreas Gohr        // Only operate if this is a login.
458*fca58076SAndreas Gohr        if ($ACT !== 'login') {
459*fca58076SAndreas Gohr            return;
460*fca58076SAndreas Gohr        }
461*fca58076SAndreas Gohr        // If there is no supplied username, then there is nothing to check at this time.
462*fca58076SAndreas Gohr        if (!$event->data['user']) {
463*fca58076SAndreas Gohr            return;
464*fca58076SAndreas Gohr        }
465*fca58076SAndreas Gohr        $user = $INPUT->server->str('REMOTE_USER', $event->data['user']);
466*fca58076SAndreas Gohr        // Set helper variables here.
467*fca58076SAndreas Gohr        $this->_setHelperVariables($user);
468*fca58076SAndreas Gohr        // If the user still has clearance, then we can skip this.
469*fca58076SAndreas Gohr        if ($this->get_clearance($user)) {
470*fca58076SAndreas Gohr            return;
471*fca58076SAndreas Gohr        }
472*fca58076SAndreas Gohr        // Allow the user to try to use login tokens, even if the account cannot use them.
473*fca58076SAndreas Gohr        $otp = $INPUT->str('otp', '');
474*fca58076SAndreas Gohr        if ($otp !== '') {
475*fca58076SAndreas Gohr            // Check for any modules that support OTP at login and are ready for use.
476*fca58076SAndreas Gohr            foreach ($this->tokenMods as $mod) {
477*fca58076SAndreas Gohr                $result = $mod->processLogin($otp, $user);
478*fca58076SAndreas Gohr                if ($result) {
479*fca58076SAndreas Gohr                    // The OTP code was valid.
480*fca58076SAndreas Gohr                    $this->_grant_clearance($user);
481*fca58076SAndreas Gohr                    // Send loglog an event to show the user logged in using a token.
482*fca58076SAndreas Gohr                    $log = array('message' => 'logged in ' . $this->getLang('token_ok'), 'user' => $user);
483*fca58076SAndreas Gohr                    trigger_event('PLUGIN_LOGLOG_LOG', $log);
484*fca58076SAndreas Gohr                    return;
485*fca58076SAndreas Gohr                }
486*fca58076SAndreas Gohr            }
487*fca58076SAndreas Gohr            global $lang;
488*fca58076SAndreas Gohr            msg($lang['badlogin'], -1);
489*fca58076SAndreas Gohr            $event->preventDefault();
490*fca58076SAndreas Gohr            $event->result = false;
491*fca58076SAndreas Gohr            // Send loglog an event to show the failure
492*fca58076SAndreas Gohr            if (count($this->tokenMods) == 0) {
493*fca58076SAndreas Gohr                $log = array('message' => 'failed ' . $this->getLang('no_tokens'), 'user' => $user);
494*fca58076SAndreas Gohr            } else {
495*fca58076SAndreas Gohr                $log = array('message' => 'failed ' . $this->getLang('token_mismatch'), 'user' => $user);
496*fca58076SAndreas Gohr            }
497*fca58076SAndreas Gohr            trigger_event('PLUGIN_LOGLOG_LOG', $log);
498*fca58076SAndreas Gohr            return;
499*fca58076SAndreas Gohr        }
500*fca58076SAndreas Gohr        // No GA OTP was supplied.
501*fca58076SAndreas Gohr        // If the user has no modules available, then grant access.
502*fca58076SAndreas Gohr        // The action preprocessing will send the user to the profile if needed.
503*fca58076SAndreas Gohr        $available = count($this->tokenMods) + count($this->otpMods) > 0;
504*fca58076SAndreas Gohr        $this->log('twofactor_before_auth_check: Tokens:' . count($this->tokenMods) . ' Codes:' . count($this->otpMods) . " Available:" . (int)$available,
505*fca58076SAndreas Gohr            self::LOGGING_DEBUGPLUS);
506*fca58076SAndreas Gohr        if (!$available) {
507*fca58076SAndreas Gohr            // The user could not authenticate if they wanted to.
508*fca58076SAndreas Gohr            // Set this so they don't get auth prompted while setting up 2FA.
509*fca58076SAndreas Gohr            $this->_grant_clearance($user);
510*fca58076SAndreas Gohr            return;
511*fca58076SAndreas Gohr        }
512*fca58076SAndreas Gohr        // At this point, the user has a working module.
513*fca58076SAndreas Gohr        // If the only working module is for a token, then fail.
514*fca58076SAndreas Gohr        if (count($this->otpMods) == 0) {
515*fca58076SAndreas Gohr            msg($this->getLang('mustusetoken'), -1);
516*fca58076SAndreas Gohr            $event->preventDefault();
517*fca58076SAndreas Gohr            return;
518*fca58076SAndreas Gohr        }
519*fca58076SAndreas Gohr        // The user is logged in to auth, but not into twofactor.
520*fca58076SAndreas Gohr        // The redirection handler will send the user to the twofactor login.
521*fca58076SAndreas Gohr        return;
522*fca58076SAndreas Gohr    }
523*fca58076SAndreas Gohr
524*fca58076SAndreas Gohr    /**
525*fca58076SAndreas Gohr     * @param $event
526*fca58076SAndreas Gohr     * @param $param
527*fca58076SAndreas Gohr     */
528*fca58076SAndreas Gohr    function twofactor_after_auth_check(&$event, $param)
529*fca58076SAndreas Gohr    {
530*fca58076SAndreas Gohr        global $ACT;
531*fca58076SAndreas Gohr        global $INPUT;
532*fca58076SAndreas Gohr        $this->log("twofactor_after_auth_check: start", self::LOGGING_DEBUG);
533*fca58076SAndreas Gohr        // Check if the action was login.
534*fca58076SAndreas Gohr        if ($ACT == 'login') {
535*fca58076SAndreas Gohr            // If there *was* no one logged in, then purge 2FA tokens.
536*fca58076SAndreas Gohr            if ($INPUT->server->str('REMOTE_USER', '') == '') {
537*fca58076SAndreas Gohr                $this->_logout();
538*fca58076SAndreas Gohr                // If someone *just* logged in, then fire off a log.
539*fca58076SAndreas Gohr                if ($event->data['user']) {
540*fca58076SAndreas Gohr                    // Send loglog an event to show the user logged in but needs OTP code.
541*fca58076SAndreas Gohr                    $log = array(
542*fca58076SAndreas Gohr                        'message' => 'logged in, ' . $this->getLang('requires_otp'),
543*fca58076SAndreas Gohr                        'user' => $event->data['user'],
544*fca58076SAndreas Gohr                    );
545*fca58076SAndreas Gohr                    trigger_event('PLUGIN_LOGLOG_LOG', $log);
546*fca58076SAndreas Gohr                }
547*fca58076SAndreas Gohr                return;
548*fca58076SAndreas Gohr            }
549*fca58076SAndreas Gohr        }
550*fca58076SAndreas Gohr        // Update helper variables here since we are logged in.
551*fca58076SAndreas Gohr        $this->_setHelperVariables();
552*fca58076SAndreas Gohr        // If set, then send login notification and clear flag.
553*fca58076SAndreas Gohr        if ($_SESSION[DOKU_COOKIE]['twofactor_notify'] == true) {
554*fca58076SAndreas Gohr            // Set the clear flag if no messages can be sent or if the result is not false.
555*fca58076SAndreas Gohr            $clear = count($this_ > otpMods) > 0 || $this->_send_login_notification() !== false;
556*fca58076SAndreas Gohr            if ($clear) {
557*fca58076SAndreas Gohr                unset($_SESSION[DOKU_COOKIE]['twofactor_notify']);
558*fca58076SAndreas Gohr            }
559*fca58076SAndreas Gohr        }
560*fca58076SAndreas Gohr        return;
561*fca58076SAndreas Gohr    }
562*fca58076SAndreas Gohr
563*fca58076SAndreas Gohr    /* Returns action to take. */
564*fca58076SAndreas Gohr    private function _process_otp(&$event, $param)
565*fca58076SAndreas Gohr    {
566*fca58076SAndreas Gohr        global $ACT, $ID, $INPUT;
567*fca58076SAndreas Gohr        $this->log("_process_otp: start", self::LOGGING_DEBUG);
568*fca58076SAndreas Gohr        // Get the logged in user.
569*fca58076SAndreas Gohr        $user = $INPUT->server->str('REMOTE_USER');
570*fca58076SAndreas Gohr        // See if the user is quitting OTP.  We don't call it logoff because we don't want the user to think they are logged in!
571*fca58076SAndreas Gohr        // This has to be checked before the template is started.
572*fca58076SAndreas Gohr        if ($INPUT->has('otpquit')) {
573*fca58076SAndreas Gohr            // Send loglog an event to show the user aborted 2FA.
574*fca58076SAndreas Gohr            $log = array('message' => 'logged off, ' . $this->getLang('quit_otp'), 'user' => $user);
575*fca58076SAndreas Gohr            trigger_event('PLUGIN_LOGLOG_LOG', $log);
576*fca58076SAndreas Gohr            // Redirect to logout.
577*fca58076SAndreas Gohr            return 'logout';
578*fca58076SAndreas Gohr        }
579*fca58076SAndreas Gohr        // Check if the user asked to generate and resend the OTP.
580*fca58076SAndreas Gohr        if ($INPUT->has('resend')) {
581*fca58076SAndreas Gohr            if ($INPUT->has('useall')) {
582*fca58076SAndreas Gohr                $defaultMod = null;
583*fca58076SAndreas Gohr            } else {
584*fca58076SAndreas Gohr                $defaultMod = $this->attribute->exists("twofactor", "defaultmod") ? $this->attribute->get("twofactor",
585*fca58076SAndreas Gohr                    "defaultmod") : null;
586*fca58076SAndreas Gohr            }
587*fca58076SAndreas Gohr            // At this point, try to send the OTP.
588*fca58076SAndreas Gohr            $mod = array_key_exists($defaultMod, $this->otpMods) ? $this->otpMods[$defaultMod] : null;
589*fca58076SAndreas Gohr            $this->_send_otp($mod);
590*fca58076SAndreas Gohr            return;
591*fca58076SAndreas Gohr        }
592*fca58076SAndreas Gohr        // If a OTP has been submitted by the user, then verify the OTP.
593*fca58076SAndreas Gohr        // If verified, then grant clearance and continue normally.
594*fca58076SAndreas Gohr        $otp = $INPUT->str('otpcode');
595*fca58076SAndreas Gohr        if ($otp) {
596*fca58076SAndreas Gohr            foreach ($this->otpMods as $mod) {
597*fca58076SAndreas Gohr                $result = $mod->processLogin($otp);
598*fca58076SAndreas Gohr                if ($result) {
599*fca58076SAndreas Gohr                    // The OTP code was valid.
600*fca58076SAndreas Gohr                    $this->_grant_clearance();
601*fca58076SAndreas Gohr                    // Send loglog an event to show the user passed 2FA.
602*fca58076SAndreas Gohr                    $log = array('message' => 'logged in ' . $this->getLang('otp_ok'), 'user' => $user);
603*fca58076SAndreas Gohr                    trigger_event('PLUGIN_LOGLOG_LOG', $log);
604*fca58076SAndreas Gohr                    /*
605*fca58076SAndreas Gohr                    // This bypasses sending any further events to other modules for the login we stole earlier.
606*fca58076SAndreas Gohr                    return 'show';
607*fca58076SAndreas Gohr                    */
608*fca58076SAndreas Gohr                    // This will trigger the login events again.  However, this is to ensure
609*fca58076SAndreas Gohr                    // that other modules work correctly because we hijacked this event earlier.
610*fca58076SAndreas Gohr                    return 'login';
611*fca58076SAndreas Gohr                }
612*fca58076SAndreas Gohr            }
613*fca58076SAndreas Gohr            // Send loglog an event to show the user entered the wrong OTP code.
614*fca58076SAndreas Gohr            $log = array('message' => 'failed OTP login, ' . $this->getLang('otp_mismatch'), 'user' => $user);
615*fca58076SAndreas Gohr            trigger_event('PLUGIN_LOGLOG_LOG', $log);
616*fca58076SAndreas Gohr            msg($this->getLang('twofactor_invalidotp'), -1);
617*fca58076SAndreas Gohr        }
618*fca58076SAndreas Gohr        return;
619*fca58076SAndreas Gohr    }
620*fca58076SAndreas Gohr
621*fca58076SAndreas Gohr    /**
622*fca58076SAndreas Gohr     * Process any updates to two factor settings.
623*fca58076SAndreas Gohr     */
624*fca58076SAndreas Gohr    private function _process_changes(&$event, $param)
625*fca58076SAndreas Gohr    {
626*fca58076SAndreas Gohr        // If the plugin is disabled, then exit.
627*fca58076SAndreas Gohr        $this->log("_process_changes: start", self::LOGGING_DEBUG);
628*fca58076SAndreas Gohr        $changed = false;
629*fca58076SAndreas Gohr        global $INPUT, $USERINFO, $conf, $auth, $lang, $ACT;
630*fca58076SAndreas Gohr        if (!$INPUT->has('save')) {
631*fca58076SAndreas Gohr            return;
632*fca58076SAndreas Gohr        }
633*fca58076SAndreas Gohr        // In needed, verify password.
634*fca58076SAndreas Gohr        if ($conf['profileconfirm']) {
635*fca58076SAndreas Gohr            if (!$auth->checkPass($INPUT->server->str('REMOTE_USER'), $INPUT->post->str('oldpass'))) {
636*fca58076SAndreas Gohr                msg($lang['badpassconfirm'], -1);
637*fca58076SAndreas Gohr                return;
638*fca58076SAndreas Gohr            }
639*fca58076SAndreas Gohr        }
640*fca58076SAndreas Gohr        // Process opt in/out.
641*fca58076SAndreas Gohr        if ($this->getConf("optinout") != 'mandatory') {
642*fca58076SAndreas Gohr            $oldoptinout = $this->attribute->get("twofactor", "state");
643*fca58076SAndreas Gohr            $optinout = $INPUT->bool('optinout', false) ? 'in' : 'out';
644*fca58076SAndreas Gohr            if ($oldoptinout != $optinout) {
645*fca58076SAndreas Gohr                $this->attribute->set("twofactor", "state", $optinout);
646*fca58076SAndreas Gohr                $changed = true;
647*fca58076SAndreas Gohr            }
648*fca58076SAndreas Gohr        }
649*fca58076SAndreas Gohr        // Process notifications.
650*fca58076SAndreas Gohr        if ($this->getConf("loginnotice") == 'user') {
651*fca58076SAndreas Gohr            $oldloginnotice = $this->attribute->get("twofactor", "loginnotice");
652*fca58076SAndreas Gohr            $loginnotice = $INPUT->bool('loginnotice', false);
653*fca58076SAndreas Gohr            if ($oldloginnotice != $loginnotice) {
654*fca58076SAndreas Gohr                $this->attribute->set("twofactor", "loginnotice", $loginnotice);
655*fca58076SAndreas Gohr                $changed = true;
656*fca58076SAndreas Gohr            }
657*fca58076SAndreas Gohr        }
658*fca58076SAndreas Gohr        // Process default module.
659*fca58076SAndreas Gohr        $defaultmodule = $INPUT->str('default_module', '');
660*fca58076SAndreas Gohr        if ($defaultmodule) {
661*fca58076SAndreas Gohr            if ($defaultmodule === $this->getLang('useallotp')) {
662*fca58076SAndreas Gohr                // Set to use ALL OTP channels.
663*fca58076SAndreas Gohr                $this->attribute->set("twofactor", "defaultmod", null);
664*fca58076SAndreas Gohr                $changed = true;
665*fca58076SAndreas Gohr            } else {
666*fca58076SAndreas Gohr                $useableMods = array();
667*fca58076SAndreas Gohr                foreach ($this->modules as $name => $mod) {
668*fca58076SAndreas Gohr                    if (!$mod->canAuthLogin() && $mod->canUse()) {
669*fca58076SAndreas Gohr                        $useableMods[$mod->getLang("name")] = $mod;
670*fca58076SAndreas Gohr                    }
671*fca58076SAndreas Gohr                }
672*fca58076SAndreas Gohr                if (array_key_exists($defaultmodule, $useableMods)) {
673*fca58076SAndreas Gohr                    $this->attribute->set("twofactor", "defaultmod", $defaultmodule);
674*fca58076SAndreas Gohr                    $changed = true;
675*fca58076SAndreas Gohr                }
676*fca58076SAndreas Gohr            }
677*fca58076SAndreas Gohr        }
678*fca58076SAndreas Gohr        // Update module settings.
679*fca58076SAndreas Gohr        $sendotp = null;
680*fca58076SAndreas Gohr        foreach ($this->modules as $name => $mod) {
681*fca58076SAndreas Gohr            $this->log('_process_changes: processing ' . get_class($mod) . '::processProfileForm()',
682*fca58076SAndreas Gohr                self::LOGGING_DEBUG);
683*fca58076SAndreas Gohr            $result = $mod->processProfileForm();
684*fca58076SAndreas Gohr            $this->log('_process_changes: processing ' . get_class($mod) . '::processProfileForm() == ' . $result,
685*fca58076SAndreas Gohr                self::LOGGING_DEBUGPLUS);
686*fca58076SAndreas Gohr            // false:change failed  'failed':OTP failed  null: no change made
687*fca58076SAndreas Gohr            $changed |= $result !== false && $result !== 'failed' && $result !== null;
688*fca58076SAndreas Gohr            switch ((string)$result) {
689*fca58076SAndreas Gohr                case 'verified':
690*fca58076SAndreas Gohr                    // Remove used OTP.
691*fca58076SAndreas Gohr                    $this->attribute->del("twofactor", "otp");
692*fca58076SAndreas Gohr                    msg($mod->getLang('passedsetup'), 1);
693*fca58076SAndreas Gohr                    // Reset helper variables.
694*fca58076SAndreas Gohr                    $this->_setHelperVariables();
695*fca58076SAndreas Gohr                    $this->log("2FA Added: " . $INPUT->server->str('REMOTE_USER', '') . ' ' . get_class($mod),
696*fca58076SAndreas Gohr                        self::LOGGING_AUDIT);
697*fca58076SAndreas Gohr                    break;
698*fca58076SAndreas Gohr                case 'failed':
699*fca58076SAndreas Gohr                    msg($mod->getLang('failedsetup'), -1);
700*fca58076SAndreas Gohr                    break;
701*fca58076SAndreas Gohr                case 'otp':
702*fca58076SAndreas Gohr                    if (!$sendotp) {
703*fca58076SAndreas Gohr                        $sendotp = $mod;
704*fca58076SAndreas Gohr                    }
705*fca58076SAndreas Gohr                    break;
706*fca58076SAndreas Gohr                case 'deleted':
707*fca58076SAndreas Gohr                    $this->log("2FA Removed: " . $INPUT->server->str('REMOTE_USER', '') . ' ' . get_class($mod),
708*fca58076SAndreas Gohr                        self::LOGGING_AUDIT);
709*fca58076SAndreas Gohr                    // Reset helper variables.
710*fca58076SAndreas Gohr                    $this->_setHelperVariables();
711*fca58076SAndreas Gohr                    break;
712*fca58076SAndreas Gohr            }
713*fca58076SAndreas Gohr        }
714*fca58076SAndreas Gohr        // Send OTP if requested.
715*fca58076SAndreas Gohr        if (is_object($sendotp)) {
716*fca58076SAndreas Gohr            // Force the message since it will fail the canUse function.
717*fca58076SAndreas Gohr            if ($this->_send_otp($sendotp, true)) {
718*fca58076SAndreas Gohr                msg($sendotp->getLang('needsetup'), 1);
719*fca58076SAndreas Gohr            } else {
720*fca58076SAndreas Gohr                msg("Could not send message using " . get_class($sendotp), -1);
721*fca58076SAndreas Gohr            }
722*fca58076SAndreas Gohr        }
723*fca58076SAndreas Gohr        // Update change status if changed.
724*fca58076SAndreas Gohr        if ($changed) {
725*fca58076SAndreas Gohr            // If there were any changes, update the available tokens accordingly.
726*fca58076SAndreas Gohr            $this->_setHelperVariables();
727*fca58076SAndreas Gohr            msg($this->getLang('updated'), 1);
728*fca58076SAndreas Gohr        }
729*fca58076SAndreas Gohr        return true;
730*fca58076SAndreas Gohr    }
731*fca58076SAndreas Gohr
732*fca58076SAndreas Gohr    /**
733*fca58076SAndreas Gohr     * Handles the email and text OTP options.
734*fca58076SAndreas Gohr     * NOTE: The user will be technically logged in at this point.  This module will rewrite the
735*fca58076SAndreas Gohr     * page with the prompt for the OTP until validated or the user logs out.
736*fca58076SAndreas Gohr     */
737*fca58076SAndreas Gohr    function twofactor_otp_login(&$event, $param)
738*fca58076SAndreas Gohr    {
739*fca58076SAndreas Gohr        $this->log("twofactor_otp_login: start", self::LOGGING_DEBUG);
740*fca58076SAndreas Gohr        // Skip this if not logged in or already two factor authenticated.
741*fca58076SAndreas Gohr        // Ensure the OTP exists and is still valid. If we need to, send a OTP.
742*fca58076SAndreas Gohr        $otpQuery = $this->get_otp_code();
743*fca58076SAndreas Gohr        if ($otpQuery == false) {
744*fca58076SAndreas Gohr            $useableMods = array();
745*fca58076SAndreas Gohr            foreach ($this->modules as $name => $mod) {
746*fca58076SAndreas Gohr                if (!$mod->canAuthLogin() && $mod->canUse()) {
747*fca58076SAndreas Gohr                    $useableMods[$mod->getLang("name")] = $mod;
748*fca58076SAndreas Gohr                }
749*fca58076SAndreas Gohr            }
750*fca58076SAndreas Gohr            $defaultMod = $this->attribute->exists("twofactor", "defaultmod") ? $this->attribute->get("twofactor",
751*fca58076SAndreas Gohr                "defaultmod") : null;
752*fca58076SAndreas Gohr            $mod = array_key_exists($defaultMod, $useableMods) ? $useableMods[$defaultMod] : null;
753*fca58076SAndreas Gohr            $this->_send_otp($mod);
754*fca58076SAndreas Gohr        }
755*fca58076SAndreas Gohr        // Generate the form to login.
756*fca58076SAndreas Gohr        // If we are here, then only provide options to accept the OTP or to logout.
757*fca58076SAndreas Gohr        global $lang;
758*fca58076SAndreas Gohr        $form = new Doku_Form(array('id' => 'otp_setup'));
759*fca58076SAndreas Gohr        $form->startFieldset($this->getLang('twofactor_otplogin'));
760*fca58076SAndreas Gohr        $form->addElement(form_makeTextField('otpcode', '', $this->getLang('twofactor_otplogin'), '', 'block',
761*fca58076SAndreas Gohr            array('size' => '50', 'autocomplete' => 'off')));
762*fca58076SAndreas Gohr        $form->addElement(form_makeButton('submit', '', $this->getLang('btn_login')));
763*fca58076SAndreas Gohr        $form->addElement(form_makeTag('br'));
764*fca58076SAndreas Gohr        $form->addElement(form_makeCheckboxField('useall', '1', $this->getLang('twofactor_useallmods'), '', 'block'));
765*fca58076SAndreas Gohr        $form->addElement(form_makeTag('br'));
766*fca58076SAndreas Gohr        $form->addElement(form_makeButton('submit', '', $this->getLang('btn_resend'), array('name' => 'resend')));
767*fca58076SAndreas Gohr        $form->addElement(form_makeButton('submit', '', $this->getLang('btn_quit'), array('name' => 'otpquit')));
768*fca58076SAndreas Gohr        $form->endFieldset();
769*fca58076SAndreas Gohr        echo '<div class="centeralign">' . NL . $form->getForm() . '</div>' . NL;
770*fca58076SAndreas Gohr    }
771*fca58076SAndreas Gohr
772*fca58076SAndreas Gohr    /**
773*fca58076SAndreas Gohr     * Sends a message using configured modules.
774*fca58076SAndreas Gohr     * If $module is set to a specific instance, that instance will be used to
775*fca58076SAndreas Gohr     * send the message. If not supplied or null, then all configured modules
776*fca58076SAndreas Gohr     * will be used to send the message. $module can also be an array of
777*fca58076SAndreas Gohr     * selected modules.
778*fca58076SAndreas Gohr     * If $force is true, then will try to send the message even if the module
779*fca58076SAndreas Gohr     * has not been validated.
780*fca58076SAndreas Gohr     * @return array(array, mixed) - The first item in the array is an array
781*fca58076SAndreas Gohr     *     of all modules that the message was successfully sent by.  The
782*fca58076SAndreas Gohr     *     second item is true if successfull to all attempted tramsmission
783*fca58076SAndreas Gohr     *     modules, false if all failed, and a number of how many successes
784*fca58076SAndreas Gohr     *     if only some modules failed.
785*fca58076SAndreas Gohr     */
786*fca58076SAndreas Gohr    private function _send_message($subject, $message, $module = null, $force = false)
787*fca58076SAndreas Gohr    {
788*fca58076SAndreas Gohr        global $INPUT;
789*fca58076SAndreas Gohr        $this->log("_send_message: start", self::LOGGING_DEBUG);
790*fca58076SAndreas Gohr        if ($module === null) {
791*fca58076SAndreas Gohr            $module = $this->otpMods;
792*fca58076SAndreas Gohr        }
793*fca58076SAndreas Gohr        if (!is_array($module)) {
794*fca58076SAndreas Gohr            $module = array($module);
795*fca58076SAndreas Gohr        }
796*fca58076SAndreas Gohr        if (count($module) >= 1) {
797*fca58076SAndreas Gohr            $modulekeys = array_keys($module);
798*fca58076SAndreas Gohr            $modulekey = $modulekeys[0];
799*fca58076SAndreas Gohr            $modname = get_class($module[$modulekey]);
800*fca58076SAndreas Gohr        } else {
801*fca58076SAndreas Gohr            $modname = null;
802*fca58076SAndreas Gohr        }
803*fca58076SAndreas Gohr        // Attempt to deliver messages.
804*fca58076SAndreas Gohr        $user = $INPUT->server->str('REMOTE_USER', '*unknown*');
805*fca58076SAndreas Gohr        $success = 0;
806*fca58076SAndreas Gohr        $modname = array();
807*fca58076SAndreas Gohr        foreach ($module as $mod) {
808*fca58076SAndreas Gohr            if ($mod->canTransmitMessage()) {
809*fca58076SAndreas Gohr                $worked = $mod->transmitMessage($subject, $message, $force);
810*fca58076SAndreas Gohr                if ($worked) {
811*fca58076SAndreas Gohr                    $success += 1;
812*fca58076SAndreas Gohr                    $modname[] = get_class($mod);
813*fca58076SAndreas Gohr                }
814*fca58076SAndreas Gohr                $this->log("Message " . ($worked ? '' : 'not ') . "sent to $user via " . get_class($mod),
815*fca58076SAndreas Gohr                    self::LOGGING_AUDITPLUS);
816*fca58076SAndreas Gohr            }
817*fca58076SAndreas Gohr        }
818*fca58076SAndreas Gohr        return array($modname, $success == 0 ? false : ($success == count($module) ? true : $success));
819*fca58076SAndreas Gohr    }
820*fca58076SAndreas Gohr
821*fca58076SAndreas Gohr    /**
822*fca58076SAndreas Gohr     * Transmits a One-Time Password (OTP) using configured modules.
823*fca58076SAndreas Gohr     * If $module is set to a specific instance, that instance will be used to
824*fca58076SAndreas Gohr     * send the OTP. If not supplied or null, then all configured modules will
825*fca58076SAndreas Gohr     * be used to send the OTP. $module can also be an array of selected
826*fca58076SAndreas Gohr     * modules.
827*fca58076SAndreas Gohr     * If $force is true, then will try to send the message even if the module
828*fca58076SAndreas Gohr     * has not been validated.
829*fca58076SAndreas Gohr     * @return mixed - true if successfull to all attempted tramsmission
830*fca58076SAndreas Gohr     *     modules, false if all failed, and a number of how many successes
831*fca58076SAndreas Gohr     *     if only some modules failed.
832*fca58076SAndreas Gohr     */
833*fca58076SAndreas Gohr    private function _send_otp($module = null, $force = false)
834*fca58076SAndreas Gohr    {
835*fca58076SAndreas Gohr        $this->log("_send_otp: start", self::LOGGING_DEBUG);
836*fca58076SAndreas Gohr        // Generate the OTP code.
837*fca58076SAndreas Gohr        $characters = '0123456789';
838*fca58076SAndreas Gohr        $otp = '';
839*fca58076SAndreas Gohr        for ($index = 0; $index < $this->getConf('otplength'); ++$index) {
840*fca58076SAndreas Gohr            $otp .= $characters[rand(0, strlen($characters) - 1)];
841*fca58076SAndreas Gohr        }
842*fca58076SAndreas Gohr        // Create the subject.
843*fca58076SAndreas Gohr        $subject = $this->getConf('otpsubject');
844*fca58076SAndreas Gohr        // Create the message.
845*fca58076SAndreas Gohr        $message = str_replace('$otp', $otp, $this->getConf('otpcontent'));
846*fca58076SAndreas Gohr        // Attempt to deliver the message.
847*fca58076SAndreas Gohr        list($modname, $result) = $this->_send_message($subject, $message, $module, $force);
848*fca58076SAndreas Gohr        // If partially successful, store the OTP code and the timestamp the OTP expires at.
849*fca58076SAndreas Gohr        if ($result) {
850*fca58076SAndreas Gohr            $otpData = array($otp, time() + $this->getConf('sentexpiry') * 60, $modname);
851*fca58076SAndreas Gohr            if (!$this->attribute->set("twofactor", "otp", $otpData)) {
852*fca58076SAndreas Gohr                msg("Unable to record OTP for later use.", -1);
853*fca58076SAndreas Gohr            }
854*fca58076SAndreas Gohr        }
855*fca58076SAndreas Gohr        return $result;
856*fca58076SAndreas Gohr    }
857*fca58076SAndreas Gohr
858*fca58076SAndreas Gohr    /**
859*fca58076SAndreas Gohr     * Returns the OTP code sent to the user, if it has not expired.
860*fca58076SAndreas Gohr     * @return mixed - false if there is no unexpired OTP, otherwise
861*fca58076SAndreas Gohr     *     array of the OTP and the modules that successfully sent it.
862*fca58076SAndreas Gohr     */
863*fca58076SAndreas Gohr    public function get_otp_code()
864*fca58076SAndreas Gohr    {
865*fca58076SAndreas Gohr        $this->log("get_otp_code: start", self::LOGGING_DEBUG);
866*fca58076SAndreas Gohr        $otpQuery = $this->attribute->get("twofactor", "otp", $success);
867*fca58076SAndreas Gohr        if (!$success) {
868*fca58076SAndreas Gohr            return false;
869*fca58076SAndreas Gohr        }
870*fca58076SAndreas Gohr        list($otp, $expiry, $modname) = $otpQuery;
871*fca58076SAndreas Gohr        if (time() > $expiry) {
872*fca58076SAndreas Gohr            $this->attribute->del("twofactor", "otp");
873*fca58076SAndreas Gohr            return false;
874*fca58076SAndreas Gohr        }
875*fca58076SAndreas Gohr        return array($otp, $modname);
876*fca58076SAndreas Gohr    }
877*fca58076SAndreas Gohr
878*fca58076SAndreas Gohr    private function _setHelperVariables($user = null)
879*fca58076SAndreas Gohr    {
880*fca58076SAndreas Gohr        $this->log("_setHelperVariables: start", self::LOGGING_DEBUGPLUS);
881*fca58076SAndreas Gohr        $tokenMods = array();
882*fca58076SAndreas Gohr        $otpMods = array();
883*fca58076SAndreas Gohr        $state = $this->attribute->get("twofactor", "state");
884*fca58076SAndreas Gohr        $optinout = $this->getConf("optinout");
885*fca58076SAndreas Gohr        $enabled = $optinout == 'mandatory' || ($state == '' ? $optinout == 'optin' : $state == 'in');
886*fca58076SAndreas Gohr        $this->log("_setHelperVariables: " . print_r(array($optinout, $state, $enabled), true), self::LOGGING_DEBUG);
887*fca58076SAndreas Gohr        // Skip if not enabled for user
888*fca58076SAndreas Gohr        if ($enabled) {
889*fca58076SAndreas Gohr            // List all working token modules (GA, RSA, etc.).
890*fca58076SAndreas Gohr            foreach ($this->modules as $name => $mod) {
891*fca58076SAndreas Gohr                if ($mod->canAuthLogin() && $mod->canUse($user)) {
892*fca58076SAndreas Gohr                    $this->log('Can use ' . get_class($mod) . ' for tokens', self::LOGGING_DEBUG);
893*fca58076SAndreas Gohr                    $tokenMods[$mod->getLang("name")] = $mod;
894*fca58076SAndreas Gohr                } else {
895*fca58076SAndreas Gohr                    $this->log('Can NOT use ' . get_class($mod) . ' for tokens', self::LOGGING_DEBUG);
896*fca58076SAndreas Gohr                }
897*fca58076SAndreas Gohr            }
898*fca58076SAndreas Gohr            // List all working OTP modules (SMS, Twilio, etc.).
899*fca58076SAndreas Gohr            foreach ($this->modules as $name => $mod) {
900*fca58076SAndreas Gohr                if (!$mod->canAuthLogin() && $mod->canUse($user)) {
901*fca58076SAndreas Gohr                    $this->log('Can use ' . get_class($mod) . ' for otp', self::LOGGING_DEBUG);
902*fca58076SAndreas Gohr                    $otpMods[$mod->getLang("name")] = $mod;
903*fca58076SAndreas Gohr                } else {
904*fca58076SAndreas Gohr                    $this->log('Can NOT use ' . get_class($mod) . ' for otp', self::LOGGING_DEBUG);
905*fca58076SAndreas Gohr                }
906*fca58076SAndreas Gohr            }
907*fca58076SAndreas Gohr        }
908*fca58076SAndreas Gohr        $this->tokenMods = $tokenMods;
909*fca58076SAndreas Gohr        $this->otpMods = $otpMods;
910*fca58076SAndreas Gohr    }
911*fca58076SAndreas Gohr
912*fca58076SAndreas Gohr    const LOGGING_AUDIT = 1;     // Audit records 2FA login and logout activity.
913*fca58076SAndreas Gohr    const LOGGING_AUDITPLUS = 2; // Audit+ also records sending of notifications.
914*fca58076SAndreas Gohr    const LOGGING_DEBUG = 3;     // Debug provides detailed workflow data.
915*fca58076SAndreas Gohr    const LOGGING_DEBUGPLUS = 4; // Debug+ also includes variables passed to and from functions.
916*fca58076SAndreas Gohr
917*fca58076SAndreas Gohr    public function log($message, $level = 1)
918*fca58076SAndreas Gohr    {
919*fca58076SAndreas Gohr        // If the log level requested is below audit or greater than what is permitted in the configuration, then exit.
920*fca58076SAndreas Gohr        if ($level < self::LOGGING_AUDIT || $level > $this->getConf('logging_level')) {
921*fca58076SAndreas Gohr            return;
922*fca58076SAndreas Gohr        }
923*fca58076SAndreas Gohr        global $conf;
924*fca58076SAndreas Gohr        // Always purge line containing "[pass]".
925*fca58076SAndreas Gohr        $message = implode("\n", array_filter(explode("\n", $message), function ($x) {
926*fca58076SAndreas Gohr            return !strstr($x, '[pass]');
927*fca58076SAndreas Gohr        }));
928*fca58076SAndreas Gohr        // If DEBUGPLUS, then append the trace log.
929*fca58076SAndreas Gohr        if ($level == self::LOGGING_DEBUGPLUS) {
930*fca58076SAndreas Gohr            $e = new Exception();
931*fca58076SAndreas Gohr            $message .= "\n" . print_r(str_replace(DOKU_REL, '', $e->getTraceAsString()), true);
932*fca58076SAndreas Gohr        }
933*fca58076SAndreas Gohr        $logfile = $this->getConf('logging_path');
934*fca58076SAndreas Gohr        $logfile = substr($logfile, 0, 1) == '/' ? $logfile : DOKU_INC . $conf['savedir'] . '/' . $logfile;
935*fca58076SAndreas Gohr        io_lock($logfile);
936*fca58076SAndreas Gohr        #open for append logfile
937*fca58076SAndreas Gohr        $handle = @fopen($logfile, 'at');
938*fca58076SAndreas Gohr        if ($handle) {
939*fca58076SAndreas Gohr            $date = date(DATE_RFC2822);
940*fca58076SAndreas Gohr            $IP = $_SERVER["REMOTE_ADDR"];
941*fca58076SAndreas Gohr            $id = session_id();
942*fca58076SAndreas Gohr            fwrite($handle, "$date,$id,$IP,$level,\"$message\"\n");
943*fca58076SAndreas Gohr            fclose($handle);
944*fca58076SAndreas Gohr        }
945*fca58076SAndreas Gohr        #write "date level message"
946*fca58076SAndreas Gohr        io_unlock($logfile);
947*fca58076SAndreas Gohr    }
948*fca58076SAndreas Gohr}
949