xref: /plugin/twofactor/action/login.php (revision 6c996db8dff158c7317a04e1185060c59b19ad7d)
1fca58076SAndreas Gohr<?php
28b7620a8SAndreas Gohr
38b7620a8SAndreas Gohruse dokuwiki\plugin\twofactor\Manager;
4*6c996db8SAndreas 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
15a386a536SAndreas Gohr    /** @var Manager */
16a386a536SAndreas Gohr    protected $manager;
17fca58076SAndreas Gohr
18a386a536SAndreas Gohr    /**
19a386a536SAndreas Gohr     * Constructor
20a386a536SAndreas Gohr     */
21fca58076SAndreas Gohr    public function __construct()
22fca58076SAndreas Gohr    {
23a386a536SAndreas Gohr        $this->manager = Manager::getInstance();
24fca58076SAndreas Gohr    }
25fca58076SAndreas Gohr
26fca58076SAndreas Gohr    /**
27fca58076SAndreas Gohr     * Registers the event handlers.
28fca58076SAndreas Gohr     */
29fca58076SAndreas Gohr    public function register(Doku_Event_Handler $controller)
30fca58076SAndreas Gohr    {
318b7620a8SAndreas Gohr        if (!(Manager::getInstance())->isReady()) return;
328b7620a8SAndreas Gohr
33a386a536SAndreas Gohr        // check 2fa requirements and either move to profile or login handling
34a386a536SAndreas Gohr        $controller->register_hook(
35a386a536SAndreas Gohr            'ACTION_ACT_PREPROCESS',
36a386a536SAndreas Gohr            'BEFORE',
37a386a536SAndreas Gohr            $this,
38a386a536SAndreas Gohr            'handleActionPreProcess',
39a386a536SAndreas Gohr            null,
40a386a536SAndreas Gohr            -999999
41a386a536SAndreas Gohr        );
42fca58076SAndreas Gohr
43a386a536SAndreas Gohr        // display login form
44a386a536SAndreas Gohr        $controller->register_hook(
45a386a536SAndreas Gohr            'TPL_ACT_UNKNOWN',
46a386a536SAndreas Gohr            'BEFORE',
47a386a536SAndreas Gohr            $this,
48a386a536SAndreas Gohr            'handleLoginDisplay'
49a386a536SAndreas Gohr        );
50a386a536SAndreas Gohr
51a386a536SAndreas Gohr        // FIXME disable user in all non-main screens (media, detail, ajax, ...)
52fca58076SAndreas Gohr    }
53fca58076SAndreas Gohr
54fca58076SAndreas Gohr    /**
55a386a536SAndreas Gohr     * Decide if any 2fa handling needs to be done for the current user
56a386a536SAndreas Gohr     *
57a386a536SAndreas Gohr     * @param Doku_Event $event
58a386a536SAndreas Gohr     */
59a386a536SAndreas Gohr    public function handleActionPreProcess(Doku_Event $event)
60a386a536SAndreas Gohr    {
61a386a536SAndreas Gohr        if (!$this->manager->getUser()) return;
62a386a536SAndreas Gohr
63a386a536SAndreas Gohr        global $INPUT;
64a386a536SAndreas Gohr
65a386a536SAndreas Gohr        // already in a 2fa login?
66a386a536SAndreas Gohr        if ($event->data === 'twofactor_login') {
67848a9be0SAndreas Gohr            if ($this->verify(
68848a9be0SAndreas Gohr                $INPUT->str('2fa_code'),
69848a9be0SAndreas Gohr                $INPUT->str('2fa_provider'),
70848a9be0SAndreas Gohr                $INPUT->bool('sticky')
71848a9be0SAndreas Gohr            )) {
72a386a536SAndreas Gohr                $event->data = 'show';
73848a9be0SAndreas Gohr                return;
74a386a536SAndreas Gohr            } else {
75a386a536SAndreas Gohr                // show form
76a386a536SAndreas Gohr                $event->preventDefault();
77a386a536SAndreas Gohr                return;
78a386a536SAndreas Gohr            }
79a386a536SAndreas Gohr        }
80a386a536SAndreas Gohr
81a386a536SAndreas Gohr        // authed already, continue
82a386a536SAndreas Gohr        if ($this->isAuthed()) {
83a386a536SAndreas Gohr            return;
84a386a536SAndreas Gohr        }
85a386a536SAndreas Gohr
86a386a536SAndreas Gohr        if (count($this->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
94a386a536SAndreas Gohr        if ($this->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;
111a386a536SAndreas Gohr        $event->preventDefault();
112a386a536SAndreas Gohr        $event->stopPropagation();
113a386a536SAndreas Gohr
114a386a536SAndreas Gohr        global $INPUT;
115a386a536SAndreas Gohr        $providerID = $INPUT->str('2fa_provider');
116a386a536SAndreas Gohr        $providers = $this->manager->getUserProviders();
117a386a536SAndreas Gohr        if (isset($providers[$providerID])) {
118a386a536SAndreas Gohr            $provider = $providers[$providerID];
119a386a536SAndreas Gohr        } else {
120b6119621SAndreas Gohr            $provider = $this->manager->getUserDefaultProvider();
121a386a536SAndreas Gohr        }
122b6119621SAndreas Gohr        // remove current provider from list
123b6119621SAndreas Gohr        unset($providers[$provider->getProviderID()]);
124a386a536SAndreas Gohr
125848a9be0SAndreas Gohr        $form = new dokuwiki\Form\Form(['method' => 'POST']);
126848a9be0SAndreas Gohr        $form->setHiddenField('do', 'twofactor_login');
127a386a536SAndreas Gohr        $form->setHiddenField('2fa_provider', $provider->getProviderID());
128a386a536SAndreas Gohr        $form->addFieldsetOpen($provider->getLabel());
129a386a536SAndreas Gohr        try {
130a386a536SAndreas Gohr            $code = $provider->generateCode();
131a386a536SAndreas Gohr            $info = $provider->transmitMessage($code);
132a386a536SAndreas Gohr            $form->addHTML('<p>' . hsc($info) . '</p>');
133848a9be0SAndreas Gohr            $form->addTextInput('2fa_code', 'Your Code')->val('');
134848a9be0SAndreas Gohr            $form->addCheckbox('sticky', 'Remember this browser'); // reuse same name as login
135a386a536SAndreas Gohr            $form->addButton('2fa', 'Submit')->attr('type', 'submit');
136a386a536SAndreas Gohr        } catch (\Exception $e) {
137a386a536SAndreas Gohr            msg(hsc($e->getMessage()), -1); // FIXME better handling
138a386a536SAndreas Gohr        }
139a386a536SAndreas Gohr        $form->addFieldsetClose();
140a386a536SAndreas Gohr
141a386a536SAndreas Gohr        if (count($providers)) {
142a386a536SAndreas Gohr            $form->addFieldsetOpen('Alternative methods');
143a386a536SAndreas Gohr            foreach ($providers as $prov) {
144a386a536SAndreas Gohr                $link = $prov->getProviderID(); // FIXME build correct links
145a386a536SAndreas Gohr
146a386a536SAndreas Gohr                $form->addHTML($link);
147a386a536SAndreas Gohr            }
148a386a536SAndreas Gohr            $form->addFieldsetClose();
149a386a536SAndreas Gohr        }
150a386a536SAndreas Gohr
151a386a536SAndreas Gohr        echo $form->toHTML();
152a386a536SAndreas Gohr    }
153a386a536SAndreas Gohr
154a386a536SAndreas Gohr    /**
155a386a536SAndreas Gohr     * Has the user already authenticated with the second factor?
156a386a536SAndreas Gohr     * @return bool
157a386a536SAndreas Gohr     */
158a386a536SAndreas Gohr    protected function isAuthed()
159a386a536SAndreas Gohr    {
160848a9be0SAndreas Gohr        if (!isset($_COOKIE[self::TWOFACTOR_COOKIE])) return false;
161848a9be0SAndreas Gohr        $data = unserialize(base64_decode($_COOKIE[self::TWOFACTOR_COOKIE]));
162848a9be0SAndreas Gohr        if (!is_array($data)) return false;
163*6c996db8SAndreas Gohr        list($providerID, $hash,) = $data;
164848a9be0SAndreas Gohr
165848a9be0SAndreas Gohr        try {
166*6c996db8SAndreas Gohr            $provider = $this->manager->getUserProvider($providerID);
167*6c996db8SAndreas Gohr            if ($this->cookieHash($provider) !== $hash) return false;
168848a9be0SAndreas Gohr            return true;
169848a9be0SAndreas Gohr        } catch (\Exception $e) {
170a386a536SAndreas Gohr            return false;
171a386a536SAndreas Gohr        }
172848a9be0SAndreas Gohr    }
173a386a536SAndreas Gohr
174a386a536SAndreas Gohr    /**
175a386a536SAndreas Gohr     * Verify a given code
176a386a536SAndreas Gohr     *
177a386a536SAndreas Gohr     * @return bool
178a386a536SAndreas Gohr     * @throws Exception
179a386a536SAndreas Gohr     */
180848a9be0SAndreas Gohr    protected function verify($code, $providerID, $sticky)
181a386a536SAndreas Gohr    {
182848a9be0SAndreas Gohr        global $conf;
183848a9be0SAndreas Gohr
184a386a536SAndreas Gohr        if (!$code) return false;
185a386a536SAndreas Gohr        if (!$providerID) return false;
186a386a536SAndreas Gohr        $provider = $this->manager->getUserProvider($providerID);
187a386a536SAndreas Gohr        $ok = $provider->checkCode($code);
188848a9be0SAndreas Gohr        if (!$ok) {
189848a9be0SAndreas Gohr            msg('code was wrong', -1);
190848a9be0SAndreas Gohr            return false;
191848a9be0SAndreas Gohr        }
192a386a536SAndreas Gohr
193848a9be0SAndreas Gohr        // store cookie
194*6c996db8SAndreas Gohr        $hash = $this->cookieHash($provider);
195*6c996db8SAndreas Gohr        $data = base64_encode(serialize([$providerID, $hash, time()]));
196848a9be0SAndreas Gohr        $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
197848a9be0SAndreas Gohr        $time = $sticky ? (time() + 60 * 60 * 24 * 365) : 0; //one year
198848a9be0SAndreas Gohr        setcookie(self::TWOFACTOR_COOKIE, $data, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true);
199a386a536SAndreas Gohr
200a386a536SAndreas Gohr        return true;
201a386a536SAndreas Gohr    }
202*6c996db8SAndreas Gohr
203*6c996db8SAndreas Gohr    /**
204*6c996db8SAndreas Gohr     * Create a hash that validates the cookie
205*6c996db8SAndreas Gohr     *
206*6c996db8SAndreas Gohr     * @param Provider $provider
207*6c996db8SAndreas Gohr     * @return string
208*6c996db8SAndreas Gohr     */
209*6c996db8SAndreas Gohr    protected function cookieHash($provider)
210*6c996db8SAndreas Gohr    {
211*6c996db8SAndreas Gohr        return sha1(join("\n", [
212*6c996db8SAndreas Gohr            $provider->getProviderID(),
213*6c996db8SAndreas Gohr            $this->manager->getUser(),
214*6c996db8SAndreas Gohr            $provider->getSecret(),
215*6c996db8SAndreas Gohr            auth_browseruid(),
216*6c996db8SAndreas Gohr            auth_cookiesalt(false, true),
217*6c996db8SAndreas Gohr        ]));
218*6c996db8SAndreas Gohr    }
219fca58076SAndreas Gohr}
220