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