xref: /plugin/twofactor/action/login.php (revision 857c5abc48e153778e4e448b1ae040ae418fff29)
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        // FIXME disable user in all non-main screens (media, detail, ajax, ...)
39    }
40
41    /**
42     * Decide if any 2fa handling needs to be done for the current user
43     *
44     * @param Doku_Event $event
45     */
46    public function handleActionPreProcess(Doku_Event $event)
47    {
48        $manager = Manager::getInstance();
49        if (!$manager->isReady()) return;
50
51        global $INPUT;
52
53        // already in a 2fa login?
54        if ($event->data === 'twofactor_login') {
55            if ($this->verify(
56                $INPUT->str('2fa_code'),
57                $INPUT->str('2fa_provider'),
58                $INPUT->bool('sticky')
59            )) {
60                $event->data = 'show';
61                return;
62            } else {
63                // show form
64                $event->preventDefault();
65                return;
66            }
67        }
68
69        // clear cookie on logout
70        if ($event->data === 'logout') {
71            $this->deAuth();
72            return;
73        }
74
75        // authed already, continue
76        if ($this->isAuthed()) {
77            return;
78        }
79
80        if (count($manager->getUserProviders())) {
81            // user has already 2fa set up - they need to authenticate before anything else
82            $event->data = 'twofactor_login';
83            $event->preventDefault();
84            $event->stopPropagation();
85            return;
86        }
87
88        if ($manager->isRequired()) {
89            // 2fa is required - they need to set it up now
90            // this will be handled by action/profile.php
91            $event->data = 'twofactor_profile';
92        }
93
94        // all good. proceed
95    }
96
97    /**
98     * Show a 2fa login screen
99     *
100     * @param Doku_Event $event
101     */
102    public function handleLoginDisplay(Doku_Event $event)
103    {
104        if ($event->data !== 'twofactor_login') return;
105        $manager = Manager::getInstance();
106        if (!$manager->isReady()) return;
107
108        $event->preventDefault();
109        $event->stopPropagation();
110
111        global $INPUT;
112        global $ID;
113
114        $providerID = $INPUT->str('2fa_provider');
115        $providers = $manager->getUserProviders();
116        $provider = $providers[$providerID] ?? $manager->getUserDefaultProvider();
117        // remove current provider from list
118        unset($providers[$provider->getProviderID()]);
119
120        echo $this->locale_xhtml('login');
121        $form = new dokuwiki\Form\Form(['method' => 'POST']);
122        $form->setHiddenField('do', 'twofactor_login');
123        $form->setHiddenField('2fa_provider', $provider->getProviderID());
124        $form->addFieldsetOpen($provider->getLabel());
125        try {
126            $code = $provider->generateCode();
127            $info = $provider->transmitMessage($code);
128            $form->addHTML('<p>' . hsc($info) . '</p>');
129            $form->addTextInput('2fa_code', 'Your Code')->val('');
130            $form->addCheckbox('sticky', 'Remember this browser'); // reuse same name as login
131            $form->addButton('2fa', 'Submit')->attr('type', 'submit');
132        } catch (Exception $e) {
133            msg(hsc($e->getMessage()), -1); // FIXME better handling
134        }
135        $form->addFieldsetClose();
136
137        if (count($providers)) {
138            $form->addFieldsetOpen('Alternative methods');
139            foreach ($providers as $prov) {
140                $url = wl($ID, [
141                    'do' => 'twofactor_login',
142                    '2fa_provider' => $prov->getProviderID(),
143                ]);
144                $form->addHTML('< href="' . $url . '">' . hsc($prov->getLabel()) . '</a>');
145            }
146            $form->addFieldsetClose();
147        }
148
149        echo $form->toHTML();
150    }
151
152    /**
153     * Has the user already authenticated with the second factor?
154     * @return bool
155     */
156    protected function isAuthed()
157    {
158        if (!isset($_COOKIE[self::TWOFACTOR_COOKIE])) return false;
159        $data = unserialize(base64_decode($_COOKIE[self::TWOFACTOR_COOKIE]));
160        if (!is_array($data)) return false;
161        list($providerID, $hash,) = $data;
162
163        try {
164            $provider = (Manager::getInstance())->getUserProvider($providerID);
165            if ($this->cookieHash($provider) !== $hash) return false;
166            return true;
167        } catch (Exception $ignored) {
168            return false;
169        }
170    }
171
172    /**
173     * Deletes the cookie
174     *
175     * @return void
176     */
177    protected function deAuth()
178    {
179        global $conf;
180
181        $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
182        $time = time() - 60 * 60 * 24 * 365; // one year in the past
183        setcookie(self::TWOFACTOR_COOKIE, null, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true);
184    }
185
186    /**
187     * Verify a given code
188     *
189     * @return bool
190     * @throws Exception
191     */
192    protected function verify($code, $providerID, $sticky)
193    {
194        global $conf;
195
196        if (!$code) return false;
197        if (!$providerID) return false;
198        $provider = (Manager::getInstance())->getUserProvider($providerID);
199        $ok = $provider->checkCode($code);
200        if (!$ok) {
201            msg('code was wrong', -1);
202            return false;
203        }
204
205        // store cookie
206        $hash = $this->cookieHash($provider);
207        $data = base64_encode(serialize([$providerID, $hash, time()]));
208        $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
209        $time = $sticky ? (time() + 60 * 60 * 24 * 365) : 0; //one year
210        setcookie(self::TWOFACTOR_COOKIE, $data, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true);
211
212        return true;
213    }
214
215    /**
216     * Create a hash that validates the cookie
217     *
218     * @param Provider $provider
219     * @return string
220     */
221    protected function cookieHash($provider)
222    {
223        return sha1(join("\n", [
224            $provider->getProviderID(),
225            (Manager::getInstance())->getUser(),
226            $provider->getSecret(),
227            auth_browseruid(),
228            auth_cookiesalt(false, true),
229        ]));
230    }
231}
232