xref: /plugin/twofactor/action/login.php (revision a01d09a89e31f5aece2ef0d49fbc9704ef99a198)
1<?php
2
3use dokuwiki\plugin\twofactor\Manager;
4use dokuwiki\plugin\twofactor\OtpField;
5use dokuwiki\plugin\twofactor\Provider;
6
7/**
8 * DokuWiki Plugin twofactor (Action Component)
9 *
10 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
11 */
12class action_plugin_twofactor_login extends DokuWiki_Action_Plugin
13{
14    const TWOFACTOR_COOKIE = '2FA' . DOKU_COOKIE;
15
16    /**
17     * Registers the event handlers.
18     */
19    public function register(Doku_Event_Handler $controller)
20    {
21        // check 2fa requirements and either move to profile or login handling
22        $controller->register_hook(
23            'ACTION_ACT_PREPROCESS',
24            'BEFORE',
25            $this,
26            'handleActionPreProcess',
27            null,
28            Manager::EVENT_PRIORITY
29        );
30
31        // display login form
32        $controller->register_hook(
33            'TPL_ACT_UNKNOWN',
34            'BEFORE',
35            $this,
36            'handleLoginDisplay'
37        );
38
39        // disable user in all non-main screens (media, detail, ajax, ...)
40        $controller->register_hook(
41            'DOKUWIKI_INIT_DONE',
42            'BEFORE',
43            $this,
44            'handleInitDone'
45        );
46    }
47
48    /**
49     * Decide if any 2fa handling needs to be done for the current user
50     *
51     * @param Doku_Event $event
52     */
53    public function handleActionPreProcess(Doku_Event $event)
54    {
55        $manager = Manager::getInstance();
56        if (!$manager->isReady()) return;
57
58        global $INPUT;
59
60        // already in a 2fa login?
61        if ($event->data === 'twofactor_login') {
62            if ($this->verify(
63                $INPUT->str('2fa_code'),
64                $INPUT->str('2fa_provider'),
65                $INPUT->bool('sticky')
66            )) {
67                $event->data = 'show';
68                return;
69            } else {
70                // show form
71                $event->preventDefault();
72                return;
73            }
74        }
75
76        // clear cookie on logout
77        if ($event->data === 'logout') {
78            $this->deAuth();
79            return;
80        }
81
82        // authed already, continue
83        if ($this->isAuthed()) {
84            return;
85        }
86
87        if (count($manager->getUserProviders())) {
88            // user has already 2fa set up - they need to authenticate before anything else
89            $event->data = 'twofactor_login';
90            $event->preventDefault();
91            $event->stopPropagation();
92            return;
93        }
94
95        if ($manager->isRequired()) {
96            // 2fa is required - they need to set it up now
97            // this will be handled by action/profile.php
98            $event->data = 'twofactor_profile';
99        }
100
101        // all good. proceed
102    }
103
104    /**
105     * Show a 2fa login screen
106     *
107     * @param Doku_Event $event
108     */
109    public function handleLoginDisplay(Doku_Event $event)
110    {
111        if ($event->data !== 'twofactor_login') return;
112        $manager = Manager::getInstance();
113        if (!$manager->isReady()) return;
114
115        $event->preventDefault();
116        $event->stopPropagation();
117
118        global $INPUT;
119        global $ID;
120
121        $providerID = $INPUT->str('2fa_provider');
122        $providers = $manager->getUserProviders();
123        $provider = $providers[$providerID] ?? $manager->getUserDefaultProvider();
124        // remove current provider from list
125        unset($providers[$provider->getProviderID()]);
126
127        echo '<div class="plugin_twofactor_login">';
128        echo inlineSVG(__DIR__ . '/../admin.svg');
129        echo $this->locale_xhtml('login');
130        $form = new dokuwiki\Form\Form(['method' => 'POST']);
131        $form->setHiddenField('do', 'twofactor_login');
132        $form->setHiddenField('2fa_provider', $provider->getProviderID());
133        $form->addFieldsetOpen($provider->getLabel());
134        try {
135            $code = $provider->generateCode();
136            $info = $provider->transmitMessage($code);
137            $form->addHTML('<p>' . hsc($info) . '</p>');
138            $form->addElement(new OtpField('2fa_code'));
139            $form->addCheckbox('sticky', 'Remember this browser'); // FIXME localize
140            $form->addTagOpen('div')->addClass('buttons');
141            $form->addButton('2fa', $this->getLang('btn_confirm'))->attr('type', 'submit');
142            $form->addTagClose('div');
143        } catch (Exception $e) {
144            msg(hsc($e->getMessage()), -1); // FIXME better handling
145        }
146        $form->addFieldsetClose();
147
148        if (count($providers)) {
149            $form->addFieldsetOpen('Alternative methods')->addClass('list');
150            $form->addTagOpen('ul');
151            foreach ($providers as $prov) {
152                $url = wl($ID, [
153                    'do' => 'twofactor_login',
154                    '2fa_provider' => $prov->getProviderID(),
155                ]);
156                $form->addHTML(
157                    '<li><div class="li"><a href="' . $url . '">' . hsc($prov->getLabel()) . '</a></div></li>'
158                );
159            }
160
161            $form->addTagClose('ul');
162            $form->addFieldsetClose();
163        }
164
165        echo $form->toHTML();
166        echo '</div>';
167    }
168
169    /**
170     * Remove user info from non-main entry points while we wait for 2fa
171     *
172     * @param Doku_Event $event
173     */
174    public function handleInitDone(Doku_Event $event)
175    {
176        global $INPUT;
177
178        if (!(Manager::getInstance())->isReady()) return;
179        if (basename($INPUT->server->str('SCRIPT_NAME')) == DOKU_SCRIPT) return;
180        if ($this->isAuthed()) return;
181
182        // temporarily remove user info from environment
183        $INPUT->server->remove('REMOTE_USER');
184        unset($_SESSION[DOKU_COOKIE]['auth']);
185        unset($GLOBALS['USERINFO']);
186    }
187
188    /**
189     * Has the user already authenticated with the second factor?
190     * @return bool
191     */
192    protected function isAuthed()
193    {
194        if (!isset($_COOKIE[self::TWOFACTOR_COOKIE])) return false;
195        $data = unserialize(base64_decode($_COOKIE[self::TWOFACTOR_COOKIE]));
196        if (!is_array($data)) return false;
197        list($providerID, $hash,) = $data;
198
199        try {
200            $provider = (Manager::getInstance())->getUserProvider($providerID);
201            if ($this->cookieHash($provider) !== $hash) return false;
202            return true;
203        } catch (Exception $ignored) {
204            return false;
205        }
206    }
207
208    /**
209     * Deletes the cookie
210     *
211     * @return void
212     */
213    protected function deAuth()
214    {
215        global $conf;
216
217        $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
218        $time = time() - 60 * 60 * 24 * 365; // one year in the past
219        setcookie(self::TWOFACTOR_COOKIE, null, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true);
220    }
221
222    /**
223     * Verify a given code
224     *
225     * @return bool
226     * @throws Exception
227     */
228    protected function verify($code, $providerID, $sticky)
229    {
230        global $conf;
231
232        if (!$code) return false;
233        if (!$providerID) return false;
234        $provider = (Manager::getInstance())->getUserProvider($providerID);
235        $ok = $provider->checkCode($code);
236        if (!$ok) return false;
237
238        // store cookie
239        $hash = $this->cookieHash($provider);
240        $data = base64_encode(serialize([$providerID, $hash, time()]));
241        $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
242        $time = $sticky ? (time() + 60 * 60 * 24 * 365) : 0; //one year
243        setcookie(self::TWOFACTOR_COOKIE, $data, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true);
244
245        return true;
246    }
247
248    /**
249     * Create a hash that validates the cookie
250     *
251     * @param Provider $provider
252     * @return string
253     */
254    protected function cookieHash($provider)
255    {
256        return sha1(join("\n", [
257            $provider->getProviderID(),
258            (Manager::getInstance())->getUser(),
259            $provider->getSecret(),
260            auth_browseruid(),
261            auth_cookiesalt(false, true),
262        ]));
263    }
264}
265