xref: /plugin/twofactor/action/login.php (revision 210d81e3e8f5a67013ba3c56e6087ed164fb0a39)
1fca58076SAndreas Gohr<?php
28b7620a8SAndreas Gohr
38b7620a8SAndreas Gohruse dokuwiki\plugin\twofactor\Manager;
46c996db8SAndreas Gohruse dokuwiki\plugin\twofactor\Provider;
58b7620a8SAndreas Gohr
630625b49SAndreas Gohr/**
730625b49SAndreas Gohr * DokuWiki Plugin twofactor (Action Component)
830625b49SAndreas Gohr *
930625b49SAndreas Gohr * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
1030625b49SAndreas Gohr */
11fca58076SAndreas Gohrclass action_plugin_twofactor_login extends DokuWiki_Action_Plugin
12fca58076SAndreas Gohr{
13848a9be0SAndreas Gohr    const TWOFACTOR_COOKIE = '2FA' . DOKU_COOKIE;
14848a9be0SAndreas Gohr
15fca58076SAndreas Gohr    /**
16fca58076SAndreas Gohr     * Registers the event handlers.
17fca58076SAndreas Gohr     */
18fca58076SAndreas Gohr    public function register(Doku_Event_Handler $controller)
19fca58076SAndreas Gohr    {
20a386a536SAndreas Gohr        // check 2fa requirements and either move to profile or login handling
21a386a536SAndreas Gohr        $controller->register_hook(
22a386a536SAndreas Gohr            'ACTION_ACT_PREPROCESS',
23a386a536SAndreas Gohr            'BEFORE',
24a386a536SAndreas Gohr            $this,
25a386a536SAndreas Gohr            'handleActionPreProcess',
26a386a536SAndreas Gohr            null,
275f8f561aSAndreas Gohr            Manager::EVENT_PRIORITY
28a386a536SAndreas Gohr        );
29fca58076SAndreas Gohr
30a386a536SAndreas Gohr        // display login form
31a386a536SAndreas Gohr        $controller->register_hook(
32a386a536SAndreas Gohr            'TPL_ACT_UNKNOWN',
33a386a536SAndreas Gohr            'BEFORE',
34a386a536SAndreas Gohr            $this,
35a386a536SAndreas Gohr            'handleLoginDisplay'
36a386a536SAndreas Gohr        );
37a386a536SAndreas Gohr
38*210d81e3SAndreas Gohr        // disable user in all non-main screens (media, detail, ajax, ...)
39*210d81e3SAndreas Gohr        $controller->register_hook(
40*210d81e3SAndreas Gohr            'DOKUWIKI_INIT_DONE',
41*210d81e3SAndreas Gohr            'BEFORE',
42*210d81e3SAndreas Gohr            $this,
43*210d81e3SAndreas Gohr            'handleInitDone'
44*210d81e3SAndreas Gohr        );
45fca58076SAndreas Gohr    }
46fca58076SAndreas Gohr
47fca58076SAndreas Gohr    /**
48a386a536SAndreas Gohr     * Decide if any 2fa handling needs to be done for the current user
49a386a536SAndreas Gohr     *
50a386a536SAndreas Gohr     * @param Doku_Event $event
51a386a536SAndreas Gohr     */
52a386a536SAndreas Gohr    public function handleActionPreProcess(Doku_Event $event)
53a386a536SAndreas Gohr    {
545f8f561aSAndreas Gohr        $manager = Manager::getInstance();
555f8f561aSAndreas Gohr        if (!$manager->isReady()) return;
56a386a536SAndreas Gohr
57a386a536SAndreas Gohr        global $INPUT;
58a386a536SAndreas Gohr
59a386a536SAndreas Gohr        // already in a 2fa login?
60a386a536SAndreas Gohr        if ($event->data === 'twofactor_login') {
61848a9be0SAndreas Gohr            if ($this->verify(
62848a9be0SAndreas Gohr                $INPUT->str('2fa_code'),
63848a9be0SAndreas Gohr                $INPUT->str('2fa_provider'),
64848a9be0SAndreas Gohr                $INPUT->bool('sticky')
65848a9be0SAndreas Gohr            )) {
66a386a536SAndreas Gohr                $event->data = 'show';
67848a9be0SAndreas Gohr                return;
68a386a536SAndreas Gohr            } else {
69a386a536SAndreas Gohr                // show form
70a386a536SAndreas Gohr                $event->preventDefault();
71a386a536SAndreas Gohr                return;
72a386a536SAndreas Gohr            }
73a386a536SAndreas Gohr        }
74a386a536SAndreas Gohr
75857c5abcSAndreas Gohr        // clear cookie on logout
76857c5abcSAndreas Gohr        if ($event->data === 'logout') {
77857c5abcSAndreas Gohr            $this->deAuth();
78857c5abcSAndreas Gohr            return;
79857c5abcSAndreas Gohr        }
80857c5abcSAndreas Gohr
81a386a536SAndreas Gohr        // authed already, continue
82a386a536SAndreas Gohr        if ($this->isAuthed()) {
83a386a536SAndreas Gohr            return;
84a386a536SAndreas Gohr        }
85a386a536SAndreas Gohr
865f8f561aSAndreas Gohr        if (count($manager->getUserProviders())) {
87a386a536SAndreas Gohr            // user has already 2fa set up - they need to authenticate before anything else
88a386a536SAndreas Gohr            $event->data = 'twofactor_login';
89a386a536SAndreas Gohr            $event->preventDefault();
90a386a536SAndreas Gohr            $event->stopPropagation();
91a386a536SAndreas Gohr            return;
92a386a536SAndreas Gohr        }
93a386a536SAndreas Gohr
945f8f561aSAndreas Gohr        if ($manager->isRequired()) {
95a386a536SAndreas Gohr            // 2fa is required - they need to set it up now
96a386a536SAndreas Gohr            // this will be handled by action/profile.php
97a386a536SAndreas Gohr            $event->data = 'twofactor_profile';
98a386a536SAndreas Gohr        }
99a386a536SAndreas Gohr
100a386a536SAndreas Gohr        // all good. proceed
101a386a536SAndreas Gohr    }
102a386a536SAndreas Gohr
103a386a536SAndreas Gohr    /**
104a386a536SAndreas Gohr     * Show a 2fa login screen
105a386a536SAndreas Gohr     *
106a386a536SAndreas Gohr     * @param Doku_Event $event
107a386a536SAndreas Gohr     */
108a386a536SAndreas Gohr    public function handleLoginDisplay(Doku_Event $event)
109a386a536SAndreas Gohr    {
110a386a536SAndreas Gohr        if ($event->data !== 'twofactor_login') return;
1115f8f561aSAndreas Gohr        $manager = Manager::getInstance();
1125f8f561aSAndreas Gohr        if (!$manager->isReady()) return;
1135f8f561aSAndreas Gohr
114a386a536SAndreas Gohr        $event->preventDefault();
115a386a536SAndreas Gohr        $event->stopPropagation();
116a386a536SAndreas Gohr
117a386a536SAndreas Gohr        global $INPUT;
118c9e42a8dSAndreas Gohr        global $ID;
119c9e42a8dSAndreas Gohr
120a386a536SAndreas Gohr        $providerID = $INPUT->str('2fa_provider');
1215f8f561aSAndreas Gohr        $providers = $manager->getUserProviders();
1225f8f561aSAndreas Gohr        $provider = $providers[$providerID] ?? $manager->getUserDefaultProvider();
123b6119621SAndreas Gohr        // remove current provider from list
124b6119621SAndreas Gohr        unset($providers[$provider->getProviderID()]);
125a386a536SAndreas Gohr
126857c5abcSAndreas Gohr        echo $this->locale_xhtml('login');
127848a9be0SAndreas Gohr        $form = new dokuwiki\Form\Form(['method' => 'POST']);
128848a9be0SAndreas Gohr        $form->setHiddenField('do', 'twofactor_login');
129a386a536SAndreas Gohr        $form->setHiddenField('2fa_provider', $provider->getProviderID());
130a386a536SAndreas Gohr        $form->addFieldsetOpen($provider->getLabel());
131a386a536SAndreas Gohr        try {
132a386a536SAndreas Gohr            $code = $provider->generateCode();
133a386a536SAndreas Gohr            $info = $provider->transmitMessage($code);
134a386a536SAndreas Gohr            $form->addHTML('<p>' . hsc($info) . '</p>');
135848a9be0SAndreas Gohr            $form->addTextInput('2fa_code', 'Your Code')->val('');
136848a9be0SAndreas Gohr            $form->addCheckbox('sticky', 'Remember this browser'); // reuse same name as login
137a386a536SAndreas Gohr            $form->addButton('2fa', 'Submit')->attr('type', 'submit');
1385f8f561aSAndreas Gohr        } catch (Exception $e) {
139a386a536SAndreas Gohr            msg(hsc($e->getMessage()), -1); // FIXME better handling
140a386a536SAndreas Gohr        }
141a386a536SAndreas Gohr        $form->addFieldsetClose();
142a386a536SAndreas Gohr
143a386a536SAndreas Gohr        if (count($providers)) {
144a386a536SAndreas Gohr            $form->addFieldsetOpen('Alternative methods');
145a386a536SAndreas Gohr            foreach ($providers as $prov) {
146c9e42a8dSAndreas Gohr                $url = wl($ID, [
147c9e42a8dSAndreas Gohr                    'do' => 'twofactor_login',
148c9e42a8dSAndreas Gohr                    '2fa_provider' => $prov->getProviderID(),
149c9e42a8dSAndreas Gohr                ]);
150c9e42a8dSAndreas Gohr                $form->addHTML('< href="' . $url . '">' . hsc($prov->getLabel()) . '</a>');
151a386a536SAndreas Gohr            }
152a386a536SAndreas Gohr            $form->addFieldsetClose();
153a386a536SAndreas Gohr        }
154a386a536SAndreas Gohr
155a386a536SAndreas Gohr        echo $form->toHTML();
156a386a536SAndreas Gohr    }
157a386a536SAndreas Gohr
158a386a536SAndreas Gohr    /**
159*210d81e3SAndreas Gohr     * Remove user info from non-main entry points while we wait for 2fa
160*210d81e3SAndreas Gohr     *
161*210d81e3SAndreas Gohr     * @param Doku_Event $event
162*210d81e3SAndreas Gohr     */
163*210d81e3SAndreas Gohr    public function handleInitDone(Doku_Event $event)
164*210d81e3SAndreas Gohr    {
165*210d81e3SAndreas Gohr        global $INPUT;
166*210d81e3SAndreas Gohr
167*210d81e3SAndreas Gohr        if (!(Manager::getInstance())->isReady()) return;
168*210d81e3SAndreas Gohr        if (basename($INPUT->server->str('SCRIPT_NAME')) == DOKU_SCRIPT) return;
169*210d81e3SAndreas Gohr        if ($this->isAuthed()) return;
170*210d81e3SAndreas Gohr
171*210d81e3SAndreas Gohr        // temporarily remove user info from environment
172*210d81e3SAndreas Gohr        $INPUT->server->remove('REMOTE_USER');
173*210d81e3SAndreas Gohr        unset($_SESSION[DOKU_COOKIE]['auth']);
174*210d81e3SAndreas Gohr        unset($GLOBALS['USERINFO']);
175*210d81e3SAndreas Gohr    }
176*210d81e3SAndreas Gohr
177*210d81e3SAndreas Gohr    /**
178a386a536SAndreas Gohr     * Has the user already authenticated with the second factor?
179a386a536SAndreas Gohr     * @return bool
180a386a536SAndreas Gohr     */
181a386a536SAndreas Gohr    protected function isAuthed()
182a386a536SAndreas Gohr    {
183848a9be0SAndreas Gohr        if (!isset($_COOKIE[self::TWOFACTOR_COOKIE])) return false;
184848a9be0SAndreas Gohr        $data = unserialize(base64_decode($_COOKIE[self::TWOFACTOR_COOKIE]));
185848a9be0SAndreas Gohr        if (!is_array($data)) return false;
1866c996db8SAndreas Gohr        list($providerID, $hash,) = $data;
187848a9be0SAndreas Gohr
188848a9be0SAndreas Gohr        try {
1895f8f561aSAndreas Gohr            $provider = (Manager::getInstance())->getUserProvider($providerID);
1906c996db8SAndreas Gohr            if ($this->cookieHash($provider) !== $hash) return false;
191848a9be0SAndreas Gohr            return true;
1925f8f561aSAndreas Gohr        } catch (Exception $ignored) {
193a386a536SAndreas Gohr            return false;
194a386a536SAndreas Gohr        }
195848a9be0SAndreas Gohr    }
196a386a536SAndreas Gohr
197a386a536SAndreas Gohr    /**
198857c5abcSAndreas Gohr     * Deletes the cookie
199857c5abcSAndreas Gohr     *
200857c5abcSAndreas Gohr     * @return void
201857c5abcSAndreas Gohr     */
202857c5abcSAndreas Gohr    protected function deAuth()
203857c5abcSAndreas Gohr    {
204857c5abcSAndreas Gohr        global $conf;
205857c5abcSAndreas Gohr
206857c5abcSAndreas Gohr        $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
207857c5abcSAndreas Gohr        $time = time() - 60 * 60 * 24 * 365; // one year in the past
208857c5abcSAndreas Gohr        setcookie(self::TWOFACTOR_COOKIE, null, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true);
209857c5abcSAndreas Gohr    }
210857c5abcSAndreas Gohr
211857c5abcSAndreas Gohr    /**
212a386a536SAndreas Gohr     * Verify a given code
213a386a536SAndreas Gohr     *
214a386a536SAndreas Gohr     * @return bool
215a386a536SAndreas Gohr     * @throws Exception
216a386a536SAndreas Gohr     */
217848a9be0SAndreas Gohr    protected function verify($code, $providerID, $sticky)
218a386a536SAndreas Gohr    {
219848a9be0SAndreas Gohr        global $conf;
220848a9be0SAndreas Gohr
221a386a536SAndreas Gohr        if (!$code) return false;
222a386a536SAndreas Gohr        if (!$providerID) return false;
2235f8f561aSAndreas Gohr        $provider = (Manager::getInstance())->getUserProvider($providerID);
224a386a536SAndreas Gohr        $ok = $provider->checkCode($code);
225848a9be0SAndreas Gohr        if (!$ok) {
226848a9be0SAndreas Gohr            msg('code was wrong', -1);
227848a9be0SAndreas Gohr            return false;
228848a9be0SAndreas Gohr        }
229a386a536SAndreas Gohr
230848a9be0SAndreas Gohr        // store cookie
2316c996db8SAndreas Gohr        $hash = $this->cookieHash($provider);
2326c996db8SAndreas Gohr        $data = base64_encode(serialize([$providerID, $hash, time()]));
233848a9be0SAndreas Gohr        $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
234848a9be0SAndreas Gohr        $time = $sticky ? (time() + 60 * 60 * 24 * 365) : 0; //one year
235848a9be0SAndreas Gohr        setcookie(self::TWOFACTOR_COOKIE, $data, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true);
236a386a536SAndreas Gohr
237a386a536SAndreas Gohr        return true;
238a386a536SAndreas Gohr    }
2396c996db8SAndreas Gohr
2406c996db8SAndreas Gohr    /**
2416c996db8SAndreas Gohr     * Create a hash that validates the cookie
2426c996db8SAndreas Gohr     *
2436c996db8SAndreas Gohr     * @param Provider $provider
2446c996db8SAndreas Gohr     * @return string
2456c996db8SAndreas Gohr     */
2466c996db8SAndreas Gohr    protected function cookieHash($provider)
2476c996db8SAndreas Gohr    {
2486c996db8SAndreas Gohr        return sha1(join("\n", [
2496c996db8SAndreas Gohr            $provider->getProviderID(),
2505f8f561aSAndreas Gohr            (Manager::getInstance())->getUser(),
2516c996db8SAndreas Gohr            $provider->getSecret(),
2526c996db8SAndreas Gohr            auth_browseruid(),
2536c996db8SAndreas Gohr            auth_cookiesalt(false, true),
2546c996db8SAndreas Gohr        ]));
2556c996db8SAndreas Gohr    }
256fca58076SAndreas Gohr}
257