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        if ($event->data === 'resendpwd') {
55            // this is completely handled in resendpwd.php
56            return;
57        }
58
59        $manager = Manager::getInstance();
60        if (!$manager->isReady()) return;
61
62        global $INPUT;
63
64        // already in a 2fa login?
65        if ($event->data === 'twofactor_login') {
66            if ($this->verify(
67                $INPUT->str('2fa_code'),
68                $INPUT->str('2fa_provider'),
69                $this->isSticky()
70            )) {
71                $event->data = 'show';
72                return;
73            } else {
74                // show form
75                $event->preventDefault();
76                return;
77            }
78        }
79
80        // clear cookie on logout
81        if ($event->data === 'logout') {
82            $this->deAuth();
83            return;
84        }
85
86        // authed already, continue
87        if ($this->isAuthed()) {
88            return;
89        }
90
91        if (count($manager->getUserProviders())) {
92            // user has already 2fa set up - they need to authenticate before anything else
93            $event->data = 'twofactor_login';
94            $event->preventDefault();
95            $event->stopPropagation();
96            return;
97        }
98
99        if ($manager->isRequired()) {
100            // 2fa is required - they need to set it up now
101            // this will be handled by action/profile.php
102            $event->data = 'twofactor_profile';
103        }
104
105        // all good. proceed
106    }
107
108    /**
109     * Show a 2fa login screen
110     *
111     * @param Doku_Event $event
112     */
113    public function handleLoginDisplay(Doku_Event $event)
114    {
115        if ($event->data !== 'twofactor_login') return;
116        $manager = Manager::getInstance();
117        if (!$manager->isReady()) return;
118
119        $event->preventDefault();
120        $event->stopPropagation();
121
122        global $INPUT;
123        $providerID = $INPUT->str('2fa_provider');
124
125        echo '<div class="plugin_twofactor_login">';
126        echo inlineSVG(__DIR__ . '/../admin.svg');
127        echo $this->locale_xhtml('login');
128        echo $manager->getCodeForm($providerID)->toHTML();
129        echo '</div>';
130    }
131
132    /**
133     * Remove user info from non-main entry points while we wait for 2fa
134     *
135     * @param Doku_Event $event
136     */
137    public function handleInitDone(Doku_Event $event)
138    {
139        global $INPUT;
140
141        if (!(Manager::getInstance())->isReady()) return;
142        if (basename($INPUT->server->str('SCRIPT_NAME')) == DOKU_SCRIPT) return;
143        if ($this->isAuthed()) return;
144
145        if ($this->getConf('optinout') !== 'mandatory' && empty(Manager::getInstance()->getUserProviders())) return;
146
147        // temporarily remove user info from environment
148        $INPUT->server->remove('REMOTE_USER');
149        unset($_SESSION[DOKU_COOKIE]['auth']);
150        unset($GLOBALS['USERINFO']);
151    }
152
153    /**
154     * Has the user already authenticated with the second factor?
155     * @return bool
156     */
157    protected function isAuthed()
158    {
159        // if we trust the IP, we don't need 2fa and consider the user authed automatically
160        if (
161            $this->getConf('trustedIPs') &&
162            preg_match('/' . $this->getConf('trustedIPs') . '/', clientIP(true))
163        ) {
164            return true;
165        }
166
167        if (!isset($_COOKIE[self::TWOFACTOR_COOKIE])) return false;
168        $data = unserialize(base64_decode($_COOKIE[self::TWOFACTOR_COOKIE]));
169        if (!is_array($data)) return false;
170        list($providerID, $hash,) = $data;
171
172        try {
173            $provider = (Manager::getInstance())->getUserProvider($providerID);
174            if ($this->cookieHash($provider) !== $hash) return false;
175            return true;
176        } catch (Exception $ignored) {
177            return false;
178        }
179    }
180
181    /**
182     * Get sticky value from standard cookie
183     *
184     * @return bool
185     */
186    protected function isSticky()
187    {
188        if (!isset($_COOKIE[DOKU_COOKIE])) {
189            return false;
190        }
191        list(, $sticky,) = explode('|', $_COOKIE[DOKU_COOKIE], 3);
192        return (bool)$sticky;
193    }
194
195    /**
196     * Deletes the cookie
197     *
198     * @return void
199     */
200    protected function deAuth()
201    {
202        global $conf;
203
204        $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
205        $time = time() - 60 * 60 * 24 * 365; // one year in the past
206        setcookie(self::TWOFACTOR_COOKIE, null, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true);
207    }
208
209    /**
210     * Verify a given code
211     *
212     * @return bool
213     * @throws Exception
214     */
215    protected function verify($code, $providerID, $sticky)
216    {
217        global $conf;
218
219        $manager = Manager::getInstance();
220        if (!$manager->verifyCode($code, $providerID)) return false;
221
222        $provider = (Manager::getInstance())->getUserProvider($providerID);
223
224        // store cookie
225        $hash = $this->cookieHash($provider);
226        $data = base64_encode(serialize([$providerID, $hash, time()]));
227        $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
228        $time = $sticky ? (time() + 60 * 60 * 24 * 30 * 3) : 0; //three months on sticky login
229        setcookie(self::TWOFACTOR_COOKIE, $data, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true);
230
231        return true;
232    }
233
234    /**
235     * Create a hash that validates the cookie
236     *
237     * @param Provider $provider
238     * @return string
239     */
240    protected function cookieHash($provider)
241    {
242        return sha1(join("\n", [
243            $provider->getProviderID(),
244            (Manager::getInstance())->getUser(),
245            $provider->getSecret(),
246            $this->getConf("useinternaluid") ? auth_browseruid() : $_SERVER['HTTP_USER_AGENT'],
247            auth_cookiesalt(false, true),
248        ]));
249    }
250}
251