xref: /plugin/twofactor/action/login.php (revision 95ed3afa6e3b878e620177edc9f7972901595a2d)
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
38210d81e3SAndreas Gohr        // disable user in all non-main screens (media, detail, ajax, ...)
39210d81e3SAndreas Gohr        $controller->register_hook(
40210d81e3SAndreas Gohr            'DOKUWIKI_INIT_DONE',
41210d81e3SAndreas Gohr            'BEFORE',
42210d81e3SAndreas Gohr            $this,
43210d81e3SAndreas Gohr            'handleInitDone'
44210d81e3SAndreas 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    {
54c8525a21SAndreas Gohr        if ($event->data === 'resendpwd') {
55c8525a21SAndreas Gohr            // this is completely handled in resendpwd.php
56c8525a21SAndreas Gohr            return;
57c8525a21SAndreas Gohr        }
58c8525a21SAndreas Gohr
595f8f561aSAndreas Gohr        $manager = Manager::getInstance();
605f8f561aSAndreas Gohr        if (!$manager->isReady()) return;
61a386a536SAndreas Gohr
62a386a536SAndreas Gohr        global $INPUT;
63a386a536SAndreas Gohr
64a386a536SAndreas Gohr        // already in a 2fa login?
65a386a536SAndreas Gohr        if ($event->data === 'twofactor_login') {
66848a9be0SAndreas Gohr            if ($this->verify(
67848a9be0SAndreas Gohr                $INPUT->str('2fa_code'),
68848a9be0SAndreas Gohr                $INPUT->str('2fa_provider'),
6903bae0e0SAndreas Gohr                $this->isSticky()
70848a9be0SAndreas Gohr            )) {
71a386a536SAndreas Gohr                $event->data = 'show';
72848a9be0SAndreas Gohr                return;
73a386a536SAndreas Gohr            } else {
74a386a536SAndreas Gohr                // show form
75a386a536SAndreas Gohr                $event->preventDefault();
76a386a536SAndreas Gohr                return;
77a386a536SAndreas Gohr            }
78a386a536SAndreas Gohr        }
79a386a536SAndreas Gohr
80857c5abcSAndreas Gohr        // clear cookie on logout
81857c5abcSAndreas Gohr        if ($event->data === 'logout') {
82857c5abcSAndreas Gohr            $this->deAuth();
83857c5abcSAndreas Gohr            return;
84857c5abcSAndreas Gohr        }
85857c5abcSAndreas Gohr
86a386a536SAndreas Gohr        // authed already, continue
87a386a536SAndreas Gohr        if ($this->isAuthed()) {
88a386a536SAndreas Gohr            return;
89a386a536SAndreas Gohr        }
90a386a536SAndreas Gohr
915f8f561aSAndreas Gohr        if (count($manager->getUserProviders())) {
92a386a536SAndreas Gohr            // user has already 2fa set up - they need to authenticate before anything else
93a386a536SAndreas Gohr            $event->data = 'twofactor_login';
94a386a536SAndreas Gohr            $event->preventDefault();
95a386a536SAndreas Gohr            $event->stopPropagation();
96a386a536SAndreas Gohr            return;
97a386a536SAndreas Gohr        }
98a386a536SAndreas Gohr
995f8f561aSAndreas Gohr        if ($manager->isRequired()) {
100a386a536SAndreas Gohr            // 2fa is required - they need to set it up now
101a386a536SAndreas Gohr            // this will be handled by action/profile.php
102a386a536SAndreas Gohr            $event->data = 'twofactor_profile';
103a386a536SAndreas Gohr        }
104a386a536SAndreas Gohr
105a386a536SAndreas Gohr        // all good. proceed
106a386a536SAndreas Gohr    }
107a386a536SAndreas Gohr
108a386a536SAndreas Gohr    /**
109a386a536SAndreas Gohr     * Show a 2fa login screen
110a386a536SAndreas Gohr     *
111a386a536SAndreas Gohr     * @param Doku_Event $event
112a386a536SAndreas Gohr     */
113a386a536SAndreas Gohr    public function handleLoginDisplay(Doku_Event $event)
114a386a536SAndreas Gohr    {
115a386a536SAndreas Gohr        if ($event->data !== 'twofactor_login') return;
1165f8f561aSAndreas Gohr        $manager = Manager::getInstance();
1175f8f561aSAndreas Gohr        if (!$manager->isReady()) return;
1185f8f561aSAndreas Gohr
119a386a536SAndreas Gohr        $event->preventDefault();
120a386a536SAndreas Gohr        $event->stopPropagation();
121a386a536SAndreas Gohr
122a386a536SAndreas Gohr        global $INPUT;
123a386a536SAndreas Gohr        $providerID = $INPUT->str('2fa_provider');
124a386a536SAndreas Gohr
1250407d282SAndreas Gohr        echo '<div class="plugin_twofactor_login">';
1260407d282SAndreas Gohr        echo inlineSVG(__DIR__ . '/../admin.svg');
127857c5abcSAndreas Gohr        echo $this->locale_xhtml('login');
128c8525a21SAndreas Gohr        echo $manager->getCodeForm($providerID)->toHTML();
1290407d282SAndreas Gohr        echo '</div>';
130a386a536SAndreas Gohr    }
131a386a536SAndreas Gohr
132a386a536SAndreas Gohr    /**
133210d81e3SAndreas Gohr     * Remove user info from non-main entry points while we wait for 2fa
134210d81e3SAndreas Gohr     *
135210d81e3SAndreas Gohr     * @param Doku_Event $event
136210d81e3SAndreas Gohr     */
137210d81e3SAndreas Gohr    public function handleInitDone(Doku_Event $event)
138210d81e3SAndreas Gohr    {
139210d81e3SAndreas Gohr        global $INPUT;
140210d81e3SAndreas Gohr
141210d81e3SAndreas Gohr        if (!(Manager::getInstance())->isReady()) return;
142210d81e3SAndreas Gohr        if (basename($INPUT->server->str('SCRIPT_NAME')) == DOKU_SCRIPT) return;
143210d81e3SAndreas Gohr        if ($this->isAuthed()) return;
144210d81e3SAndreas Gohr
1450d5f8055SAnna Dabrowska        if ($this->getConf('optinout') !== 'mandatory' && empty(Manager::getInstance()->getUserProviders())) return;
1460d5f8055SAnna Dabrowska
147210d81e3SAndreas Gohr        // temporarily remove user info from environment
148210d81e3SAndreas Gohr        $INPUT->server->remove('REMOTE_USER');
149210d81e3SAndreas Gohr        unset($_SESSION[DOKU_COOKIE]['auth']);
150210d81e3SAndreas Gohr        unset($GLOBALS['USERINFO']);
151210d81e3SAndreas Gohr    }
152210d81e3SAndreas Gohr
153210d81e3SAndreas Gohr    /**
154a386a536SAndreas Gohr     * Has the user already authenticated with the second factor?
155a386a536SAndreas Gohr     * @return bool
156a386a536SAndreas Gohr     */
157a386a536SAndreas Gohr    protected function isAuthed()
158a386a536SAndreas Gohr    {
159*95ed3afaSAndreas Gohr        // if we trust the IP, we don't need 2fa and consider the user authed automatically
160*95ed3afaSAndreas Gohr        if (
161*95ed3afaSAndreas Gohr            $this->getConf('trustedIPs') &&
162*95ed3afaSAndreas Gohr            preg_match('/' . $this->getConf('trustedIPs') . '/', clientIP(true))
163*95ed3afaSAndreas Gohr        ) {
164*95ed3afaSAndreas Gohr            return true;
165*95ed3afaSAndreas Gohr        }
166*95ed3afaSAndreas Gohr
167848a9be0SAndreas Gohr        if (!isset($_COOKIE[self::TWOFACTOR_COOKIE])) return false;
168848a9be0SAndreas Gohr        $data = unserialize(base64_decode($_COOKIE[self::TWOFACTOR_COOKIE]));
169848a9be0SAndreas Gohr        if (!is_array($data)) return false;
1706c996db8SAndreas Gohr        list($providerID, $hash,) = $data;
171848a9be0SAndreas Gohr
172848a9be0SAndreas Gohr        try {
1735f8f561aSAndreas Gohr            $provider = (Manager::getInstance())->getUserProvider($providerID);
1746c996db8SAndreas Gohr            if ($this->cookieHash($provider) !== $hash) return false;
175848a9be0SAndreas Gohr            return true;
1765f8f561aSAndreas Gohr        } catch (Exception $ignored) {
177a386a536SAndreas Gohr            return false;
178a386a536SAndreas Gohr        }
179848a9be0SAndreas Gohr    }
180a386a536SAndreas Gohr
181a386a536SAndreas Gohr    /**
18203bae0e0SAndreas Gohr     * Get sticky value from standard cookie
18303bae0e0SAndreas Gohr     *
18403bae0e0SAndreas Gohr     * @return bool
18503bae0e0SAndreas Gohr     */
18603bae0e0SAndreas Gohr    protected function isSticky()
18703bae0e0SAndreas Gohr    {
18803bae0e0SAndreas Gohr        if (!isset($_COOKIE[DOKU_COOKIE])) {
18903bae0e0SAndreas Gohr            return false;
19003bae0e0SAndreas Gohr        }
19103bae0e0SAndreas Gohr        list(, $sticky,) = explode('|', $_COOKIE[DOKU_COOKIE], 3);
19203bae0e0SAndreas Gohr        return (bool)$sticky;
19303bae0e0SAndreas Gohr    }
19403bae0e0SAndreas Gohr
19503bae0e0SAndreas Gohr    /**
196857c5abcSAndreas Gohr     * Deletes the cookie
197857c5abcSAndreas Gohr     *
198857c5abcSAndreas Gohr     * @return void
199857c5abcSAndreas Gohr     */
200857c5abcSAndreas Gohr    protected function deAuth()
201857c5abcSAndreas Gohr    {
202857c5abcSAndreas Gohr        global $conf;
203857c5abcSAndreas Gohr
204857c5abcSAndreas Gohr        $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
205857c5abcSAndreas Gohr        $time = time() - 60 * 60 * 24 * 365; // one year in the past
206857c5abcSAndreas Gohr        setcookie(self::TWOFACTOR_COOKIE, null, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true);
207857c5abcSAndreas Gohr    }
208857c5abcSAndreas Gohr
209857c5abcSAndreas Gohr    /**
210a386a536SAndreas Gohr     * Verify a given code
211a386a536SAndreas Gohr     *
212a386a536SAndreas Gohr     * @return bool
213a386a536SAndreas Gohr     * @throws Exception
214a386a536SAndreas Gohr     */
215848a9be0SAndreas Gohr    protected function verify($code, $providerID, $sticky)
216a386a536SAndreas Gohr    {
217848a9be0SAndreas Gohr        global $conf;
218848a9be0SAndreas Gohr
219c8525a21SAndreas Gohr        $manager = Manager::getInstance();
220c8525a21SAndreas Gohr        if (!$manager->verifyCode($code, $providerID)) return false;
221c8525a21SAndreas Gohr
2225f8f561aSAndreas Gohr        $provider = (Manager::getInstance())->getUserProvider($providerID);
223a386a536SAndreas Gohr
224848a9be0SAndreas Gohr        // store cookie
2256c996db8SAndreas Gohr        $hash = $this->cookieHash($provider);
2266c996db8SAndreas Gohr        $data = base64_encode(serialize([$providerID, $hash, time()]));
227848a9be0SAndreas Gohr        $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
22803bae0e0SAndreas Gohr        $time = $sticky ? (time() + 60 * 60 * 24 * 30 * 3) : 0; //three months on sticky login
229848a9be0SAndreas Gohr        setcookie(self::TWOFACTOR_COOKIE, $data, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true);
230a386a536SAndreas Gohr
231a386a536SAndreas Gohr        return true;
232a386a536SAndreas Gohr    }
2336c996db8SAndreas Gohr
2346c996db8SAndreas Gohr    /**
2356c996db8SAndreas Gohr     * Create a hash that validates the cookie
2366c996db8SAndreas Gohr     *
2376c996db8SAndreas Gohr     * @param Provider $provider
2386c996db8SAndreas Gohr     * @return string
2396c996db8SAndreas Gohr     */
2406c996db8SAndreas Gohr    protected function cookieHash($provider)
2416c996db8SAndreas Gohr    {
2426c996db8SAndreas Gohr        return sha1(join("\n", [
2436c996db8SAndreas Gohr            $provider->getProviderID(),
2445f8f561aSAndreas Gohr            (Manager::getInstance())->getUser(),
2456c996db8SAndreas Gohr            $provider->getSecret(),
24609c2ba1aSalexdraconian            $this->getConf("useinternaluid") ? auth_browseruid() : $_SERVER['HTTP_USER_AGENT'],
2476c996db8SAndreas Gohr            auth_cookiesalt(false, true),
2486c996db8SAndreas Gohr        ]));
2496c996db8SAndreas Gohr    }
250fca58076SAndreas Gohr}
251