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