xref: /plugin/twofactor/action/login.php (revision 09c2ba1ad8cae17a8638e08ccfd0236b58c5d412)
1fca58076SAndreas Gohr<?php
28b7620a8SAndreas Gohr
38b7620a8SAndreas Gohruse dokuwiki\plugin\twofactor\Manager;
4a01d09a8SAndreas Gohruse dokuwiki\plugin\twofactor\OtpField;
56c996db8SAndreas Gohruse dokuwiki\plugin\twofactor\Provider;
68b7620a8SAndreas Gohr
730625b49SAndreas Gohr/**
830625b49SAndreas Gohr * DokuWiki Plugin twofactor (Action Component)
930625b49SAndreas Gohr *
1030625b49SAndreas Gohr * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
1130625b49SAndreas Gohr */
12fca58076SAndreas Gohrclass action_plugin_twofactor_login extends DokuWiki_Action_Plugin
13fca58076SAndreas Gohr{
14848a9be0SAndreas Gohr    const TWOFACTOR_COOKIE = '2FA' . DOKU_COOKIE;
15848a9be0SAndreas Gohr
16fca58076SAndreas Gohr    /**
17fca58076SAndreas Gohr     * Registers the event handlers.
18fca58076SAndreas Gohr     */
19fca58076SAndreas Gohr    public function register(Doku_Event_Handler $controller)
20fca58076SAndreas Gohr    {
21a386a536SAndreas Gohr        // check 2fa requirements and either move to profile or login handling
22a386a536SAndreas Gohr        $controller->register_hook(
23a386a536SAndreas Gohr            'ACTION_ACT_PREPROCESS',
24a386a536SAndreas Gohr            'BEFORE',
25a386a536SAndreas Gohr            $this,
26a386a536SAndreas Gohr            'handleActionPreProcess',
27a386a536SAndreas Gohr            null,
285f8f561aSAndreas Gohr            Manager::EVENT_PRIORITY
29a386a536SAndreas Gohr        );
30fca58076SAndreas Gohr
31a386a536SAndreas Gohr        // display login form
32a386a536SAndreas Gohr        $controller->register_hook(
33a386a536SAndreas Gohr            'TPL_ACT_UNKNOWN',
34a386a536SAndreas Gohr            'BEFORE',
35a386a536SAndreas Gohr            $this,
36a386a536SAndreas Gohr            'handleLoginDisplay'
37a386a536SAndreas Gohr        );
38a386a536SAndreas Gohr
39210d81e3SAndreas Gohr        // disable user in all non-main screens (media, detail, ajax, ...)
40210d81e3SAndreas Gohr        $controller->register_hook(
41210d81e3SAndreas Gohr            'DOKUWIKI_INIT_DONE',
42210d81e3SAndreas Gohr            'BEFORE',
43210d81e3SAndreas Gohr            $this,
44210d81e3SAndreas Gohr            'handleInitDone'
45210d81e3SAndreas Gohr        );
46fca58076SAndreas Gohr    }
47fca58076SAndreas Gohr
48fca58076SAndreas Gohr    /**
49a386a536SAndreas Gohr     * Decide if any 2fa handling needs to be done for the current user
50a386a536SAndreas Gohr     *
51a386a536SAndreas Gohr     * @param Doku_Event $event
52a386a536SAndreas Gohr     */
53a386a536SAndreas Gohr    public function handleActionPreProcess(Doku_Event $event)
54a386a536SAndreas Gohr    {
555f8f561aSAndreas Gohr        $manager = Manager::getInstance();
565f8f561aSAndreas Gohr        if (!$manager->isReady()) return;
57a386a536SAndreas Gohr
58a386a536SAndreas Gohr        global $INPUT;
59a386a536SAndreas Gohr
60a386a536SAndreas Gohr        // already in a 2fa login?
61a386a536SAndreas Gohr        if ($event->data === 'twofactor_login') {
62848a9be0SAndreas Gohr            if ($this->verify(
63848a9be0SAndreas Gohr                $INPUT->str('2fa_code'),
64848a9be0SAndreas Gohr                $INPUT->str('2fa_provider'),
6503bae0e0SAndreas Gohr                $this->isSticky()
66848a9be0SAndreas Gohr            )) {
67a386a536SAndreas Gohr                $event->data = 'show';
68848a9be0SAndreas Gohr                return;
69a386a536SAndreas Gohr            } else {
70a386a536SAndreas Gohr                // show form
71a386a536SAndreas Gohr                $event->preventDefault();
72a386a536SAndreas Gohr                return;
73a386a536SAndreas Gohr            }
74a386a536SAndreas Gohr        }
75a386a536SAndreas Gohr
76857c5abcSAndreas Gohr        // clear cookie on logout
77857c5abcSAndreas Gohr        if ($event->data === 'logout') {
78857c5abcSAndreas Gohr            $this->deAuth();
79857c5abcSAndreas Gohr            return;
80857c5abcSAndreas Gohr        }
81857c5abcSAndreas Gohr
82a386a536SAndreas Gohr        // authed already, continue
83a386a536SAndreas Gohr        if ($this->isAuthed()) {
84a386a536SAndreas Gohr            return;
85a386a536SAndreas Gohr        }
86a386a536SAndreas Gohr
875f8f561aSAndreas Gohr        if (count($manager->getUserProviders())) {
88a386a536SAndreas Gohr            // user has already 2fa set up - they need to authenticate before anything else
89a386a536SAndreas Gohr            $event->data = 'twofactor_login';
90a386a536SAndreas Gohr            $event->preventDefault();
91a386a536SAndreas Gohr            $event->stopPropagation();
92a386a536SAndreas Gohr            return;
93a386a536SAndreas Gohr        }
94a386a536SAndreas Gohr
955f8f561aSAndreas Gohr        if ($manager->isRequired()) {
96a386a536SAndreas Gohr            // 2fa is required - they need to set it up now
97a386a536SAndreas Gohr            // this will be handled by action/profile.php
98a386a536SAndreas Gohr            $event->data = 'twofactor_profile';
99a386a536SAndreas Gohr        }
100a386a536SAndreas Gohr
101a386a536SAndreas Gohr        // all good. proceed
102a386a536SAndreas Gohr    }
103a386a536SAndreas Gohr
104a386a536SAndreas Gohr    /**
105a386a536SAndreas Gohr     * Show a 2fa login screen
106a386a536SAndreas Gohr     *
107a386a536SAndreas Gohr     * @param Doku_Event $event
108a386a536SAndreas Gohr     */
109a386a536SAndreas Gohr    public function handleLoginDisplay(Doku_Event $event)
110a386a536SAndreas Gohr    {
111a386a536SAndreas Gohr        if ($event->data !== 'twofactor_login') return;
1125f8f561aSAndreas Gohr        $manager = Manager::getInstance();
1135f8f561aSAndreas Gohr        if (!$manager->isReady()) return;
1145f8f561aSAndreas Gohr
115a386a536SAndreas Gohr        $event->preventDefault();
116a386a536SAndreas Gohr        $event->stopPropagation();
117a386a536SAndreas Gohr
118a386a536SAndreas Gohr        global $INPUT;
119c9e42a8dSAndreas Gohr        global $ID;
120c9e42a8dSAndreas Gohr
121a386a536SAndreas Gohr        $providerID = $INPUT->str('2fa_provider');
1225f8f561aSAndreas Gohr        $providers = $manager->getUserProviders();
1235f8f561aSAndreas Gohr        $provider = $providers[$providerID] ?? $manager->getUserDefaultProvider();
124b6119621SAndreas Gohr        // remove current provider from list
125b6119621SAndreas Gohr        unset($providers[$provider->getProviderID()]);
126a386a536SAndreas Gohr
1270407d282SAndreas Gohr        echo '<div class="plugin_twofactor_login">';
1280407d282SAndreas Gohr        echo inlineSVG(__DIR__ . '/../admin.svg');
129857c5abcSAndreas Gohr        echo $this->locale_xhtml('login');
130848a9be0SAndreas Gohr        $form = new dokuwiki\Form\Form(['method' => 'POST']);
131848a9be0SAndreas Gohr        $form->setHiddenField('do', 'twofactor_login');
132a386a536SAndreas Gohr        $form->setHiddenField('2fa_provider', $provider->getProviderID());
133a386a536SAndreas Gohr        $form->addFieldsetOpen($provider->getLabel());
134a386a536SAndreas Gohr        try {
135a386a536SAndreas Gohr            $code = $provider->generateCode();
136a386a536SAndreas Gohr            $info = $provider->transmitMessage($code);
137a386a536SAndreas Gohr            $form->addHTML('<p>' . hsc($info) . '</p>');
138a01d09a8SAndreas Gohr            $form->addElement(new OtpField('2fa_code'));
1390407d282SAndreas Gohr            $form->addTagOpen('div')->addClass('buttons');
140a01d09a8SAndreas Gohr            $form->addButton('2fa', $this->getLang('btn_confirm'))->attr('type', 'submit');
1410407d282SAndreas Gohr            $form->addTagClose('div');
1425f8f561aSAndreas Gohr        } catch (Exception $e) {
143a386a536SAndreas Gohr            msg(hsc($e->getMessage()), -1); // FIXME better handling
144a386a536SAndreas Gohr        }
145a386a536SAndreas Gohr        $form->addFieldsetClose();
146a386a536SAndreas Gohr
147a386a536SAndreas Gohr        if (count($providers)) {
14878279978SAndreas Gohr            $form->addFieldsetOpen('Alternative methods')->addClass('list');
14978279978SAndreas Gohr            $form->addTagOpen('ul');
150a386a536SAndreas Gohr            foreach ($providers as $prov) {
151c9e42a8dSAndreas Gohr                $url = wl($ID, [
152c9e42a8dSAndreas Gohr                    'do' => 'twofactor_login',
153c9e42a8dSAndreas Gohr                    '2fa_provider' => $prov->getProviderID(),
154c9e42a8dSAndreas Gohr                ]);
15578279978SAndreas Gohr                $form->addHTML(
15678279978SAndreas Gohr                    '<li><div class="li"><a href="' . $url . '">' . hsc($prov->getLabel()) . '</a></div></li>'
15778279978SAndreas Gohr                );
158a386a536SAndreas Gohr            }
15978279978SAndreas Gohr
16078279978SAndreas Gohr            $form->addTagClose('ul');
161a386a536SAndreas Gohr            $form->addFieldsetClose();
162a386a536SAndreas Gohr        }
163a386a536SAndreas Gohr
164a386a536SAndreas Gohr        echo $form->toHTML();
1650407d282SAndreas Gohr        echo '</div>';
166a386a536SAndreas Gohr    }
167a386a536SAndreas Gohr
168a386a536SAndreas Gohr    /**
169210d81e3SAndreas Gohr     * Remove user info from non-main entry points while we wait for 2fa
170210d81e3SAndreas Gohr     *
171210d81e3SAndreas Gohr     * @param Doku_Event $event
172210d81e3SAndreas Gohr     */
173210d81e3SAndreas Gohr    public function handleInitDone(Doku_Event $event)
174210d81e3SAndreas Gohr    {
175210d81e3SAndreas Gohr        global $INPUT;
176210d81e3SAndreas Gohr
177210d81e3SAndreas Gohr        if (!(Manager::getInstance())->isReady()) return;
178210d81e3SAndreas Gohr        if (basename($INPUT->server->str('SCRIPT_NAME')) == DOKU_SCRIPT) return;
179210d81e3SAndreas Gohr        if ($this->isAuthed()) return;
180210d81e3SAndreas Gohr
1810d5f8055SAnna Dabrowska        if ($this->getConf('optinout') !== 'mandatory' && empty(Manager::getInstance()->getUserProviders())) return;
1820d5f8055SAnna Dabrowska
183210d81e3SAndreas Gohr        // temporarily remove user info from environment
184210d81e3SAndreas Gohr        $INPUT->server->remove('REMOTE_USER');
185210d81e3SAndreas Gohr        unset($_SESSION[DOKU_COOKIE]['auth']);
186210d81e3SAndreas Gohr        unset($GLOBALS['USERINFO']);
187210d81e3SAndreas Gohr    }
188210d81e3SAndreas Gohr
189210d81e3SAndreas Gohr    /**
190a386a536SAndreas Gohr     * Has the user already authenticated with the second factor?
191a386a536SAndreas Gohr     * @return bool
192a386a536SAndreas Gohr     */
193a386a536SAndreas Gohr    protected function isAuthed()
194a386a536SAndreas Gohr    {
195848a9be0SAndreas Gohr        if (!isset($_COOKIE[self::TWOFACTOR_COOKIE])) return false;
196848a9be0SAndreas Gohr        $data = unserialize(base64_decode($_COOKIE[self::TWOFACTOR_COOKIE]));
197848a9be0SAndreas Gohr        if (!is_array($data)) return false;
1986c996db8SAndreas Gohr        list($providerID, $hash,) = $data;
199848a9be0SAndreas Gohr
200848a9be0SAndreas Gohr        try {
2015f8f561aSAndreas Gohr            $provider = (Manager::getInstance())->getUserProvider($providerID);
2026c996db8SAndreas Gohr            if ($this->cookieHash($provider) !== $hash) return false;
203848a9be0SAndreas Gohr            return true;
2045f8f561aSAndreas Gohr        } catch (Exception $ignored) {
205a386a536SAndreas Gohr            return false;
206a386a536SAndreas Gohr        }
207848a9be0SAndreas Gohr    }
208a386a536SAndreas Gohr
209a386a536SAndreas Gohr    /**
21003bae0e0SAndreas Gohr     * Get sticky value from standard cookie
21103bae0e0SAndreas Gohr     *
21203bae0e0SAndreas Gohr     * @return bool
21303bae0e0SAndreas Gohr     */
21403bae0e0SAndreas Gohr    protected function isSticky()
21503bae0e0SAndreas Gohr    {
21603bae0e0SAndreas Gohr        if (!isset($_COOKIE[DOKU_COOKIE])) {
21703bae0e0SAndreas Gohr            return false;
21803bae0e0SAndreas Gohr        }
21903bae0e0SAndreas Gohr        list(, $sticky,) = explode('|', $_COOKIE[DOKU_COOKIE], 3);
22003bae0e0SAndreas Gohr        return (bool)$sticky;
22103bae0e0SAndreas Gohr    }
22203bae0e0SAndreas Gohr
22303bae0e0SAndreas Gohr    /**
224857c5abcSAndreas Gohr     * Deletes the cookie
225857c5abcSAndreas Gohr     *
226857c5abcSAndreas Gohr     * @return void
227857c5abcSAndreas Gohr     */
228857c5abcSAndreas Gohr    protected function deAuth()
229857c5abcSAndreas Gohr    {
230857c5abcSAndreas Gohr        global $conf;
231857c5abcSAndreas Gohr
232857c5abcSAndreas Gohr        $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
233857c5abcSAndreas Gohr        $time = time() - 60 * 60 * 24 * 365; // one year in the past
234857c5abcSAndreas Gohr        setcookie(self::TWOFACTOR_COOKIE, null, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true);
235857c5abcSAndreas Gohr    }
236857c5abcSAndreas Gohr
237857c5abcSAndreas Gohr    /**
238a386a536SAndreas Gohr     * Verify a given code
239a386a536SAndreas Gohr     *
240a386a536SAndreas Gohr     * @return bool
241a386a536SAndreas Gohr     * @throws Exception
242a386a536SAndreas Gohr     */
243848a9be0SAndreas Gohr    protected function verify($code, $providerID, $sticky)
244a386a536SAndreas Gohr    {
245848a9be0SAndreas Gohr        global $conf;
246848a9be0SAndreas Gohr
247a386a536SAndreas Gohr        if (!$code) return false;
248a386a536SAndreas Gohr        if (!$providerID) return false;
2495f8f561aSAndreas Gohr        $provider = (Manager::getInstance())->getUserProvider($providerID);
250a386a536SAndreas Gohr        $ok = $provider->checkCode($code);
25116ed3964SAndreas Gohr        if (!$ok) return false;
252a386a536SAndreas Gohr
253848a9be0SAndreas Gohr        // store cookie
2546c996db8SAndreas Gohr        $hash = $this->cookieHash($provider);
2556c996db8SAndreas Gohr        $data = base64_encode(serialize([$providerID, $hash, time()]));
256848a9be0SAndreas Gohr        $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
25703bae0e0SAndreas Gohr        $time = $sticky ? (time() + 60 * 60 * 24 * 30 * 3) : 0; //three months on sticky login
258848a9be0SAndreas Gohr        setcookie(self::TWOFACTOR_COOKIE, $data, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true);
259a386a536SAndreas Gohr
260a386a536SAndreas Gohr        return true;
261a386a536SAndreas Gohr    }
2626c996db8SAndreas Gohr
2636c996db8SAndreas Gohr    /**
2646c996db8SAndreas Gohr     * Create a hash that validates the cookie
2656c996db8SAndreas Gohr     *
2666c996db8SAndreas Gohr     * @param Provider $provider
2676c996db8SAndreas Gohr     * @return string
2686c996db8SAndreas Gohr     */
2696c996db8SAndreas Gohr    protected function cookieHash($provider)
2706c996db8SAndreas Gohr    {
2716c996db8SAndreas Gohr        return sha1(join("\n", [
2726c996db8SAndreas Gohr            $provider->getProviderID(),
2735f8f561aSAndreas Gohr            (Manager::getInstance())->getUser(),
2746c996db8SAndreas Gohr            $provider->getSecret(),
275*09c2ba1aSalexdraconian            $this->getConf("useinternaluid") ? auth_browseruid() : $_SERVER['HTTP_USER_AGENT'],
2766c996db8SAndreas Gohr            auth_cookiesalt(false, true),
2776c996db8SAndreas Gohr        ]));
2786c996db8SAndreas Gohr    }
279fca58076SAndreas Gohr}
280