xref: /plugin/twofactor/action/login.php (revision 30625b49d43f35ae8dc732acccd71d5a391980ea)
1fca58076SAndreas Gohr<?php
28b7620a8SAndreas Gohr
38b7620a8SAndreas Gohruse dokuwiki\plugin\twofactor\Manager;
48b7620a8SAndreas Gohr
5*30625b49SAndreas Gohr/**
6*30625b49SAndreas Gohr * DokuWiki Plugin twofactor (Action Component)
7*30625b49SAndreas Gohr *
8*30625b49SAndreas Gohr * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
9*30625b49SAndreas Gohr */
10fca58076SAndreas Gohrclass action_plugin_twofactor_login extends DokuWiki_Action_Plugin
11fca58076SAndreas Gohr{
12848a9be0SAndreas Gohr    const TWOFACTOR_COOKIE = '2FA' . DOKU_COOKIE;
13848a9be0SAndreas Gohr
14a386a536SAndreas Gohr    /** @var Manager */
15a386a536SAndreas Gohr    protected $manager;
16fca58076SAndreas Gohr
17a386a536SAndreas Gohr    /**
18a386a536SAndreas Gohr     * Constructor
19a386a536SAndreas Gohr     */
20fca58076SAndreas Gohr    public function __construct()
21fca58076SAndreas Gohr    {
22a386a536SAndreas Gohr        $this->manager = Manager::getInstance();
23fca58076SAndreas Gohr    }
24fca58076SAndreas Gohr
25fca58076SAndreas Gohr    /**
26fca58076SAndreas Gohr     * Registers the event handlers.
27fca58076SAndreas Gohr     */
28fca58076SAndreas Gohr    public function register(Doku_Event_Handler $controller)
29fca58076SAndreas Gohr    {
308b7620a8SAndreas Gohr        if (!(Manager::getInstance())->isReady()) return;
318b7620a8SAndreas Gohr
32a386a536SAndreas Gohr        // check 2fa requirements and either move to profile or login handling
33a386a536SAndreas Gohr        $controller->register_hook(
34a386a536SAndreas Gohr            'ACTION_ACT_PREPROCESS',
35a386a536SAndreas Gohr            'BEFORE',
36a386a536SAndreas Gohr            $this,
37a386a536SAndreas Gohr            'handleActionPreProcess',
38a386a536SAndreas Gohr            null,
39a386a536SAndreas Gohr            -999999
40a386a536SAndreas Gohr        );
41fca58076SAndreas Gohr
42a386a536SAndreas Gohr        // display login form
43a386a536SAndreas Gohr        $controller->register_hook(
44a386a536SAndreas Gohr            'TPL_ACT_UNKNOWN',
45a386a536SAndreas Gohr            'BEFORE',
46a386a536SAndreas Gohr            $this,
47a386a536SAndreas Gohr            'handleLoginDisplay'
48a386a536SAndreas Gohr        );
49a386a536SAndreas Gohr
50a386a536SAndreas Gohr        // FIXME disable user in all non-main screens (media, detail, ajax, ...)
51fca58076SAndreas Gohr    }
52fca58076SAndreas Gohr
53fca58076SAndreas Gohr    /**
54a386a536SAndreas Gohr     * Decide if any 2fa handling needs to be done for the current user
55a386a536SAndreas Gohr     *
56a386a536SAndreas Gohr     * @param Doku_Event $event
57a386a536SAndreas Gohr     */
58a386a536SAndreas Gohr    public function handleActionPreProcess(Doku_Event $event)
59a386a536SAndreas Gohr    {
60a386a536SAndreas Gohr        if (!$this->manager->getUser()) 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'),
69848a9be0SAndreas Gohr                $INPUT->bool('sticky')
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
80a386a536SAndreas Gohr        // authed already, continue
81a386a536SAndreas Gohr        if ($this->isAuthed()) {
82a386a536SAndreas Gohr            return;
83a386a536SAndreas Gohr        }
84a386a536SAndreas Gohr
85a386a536SAndreas Gohr        if (count($this->manager->getUserProviders())) {
86a386a536SAndreas Gohr            // user has already 2fa set up - they need to authenticate before anything else
87a386a536SAndreas Gohr            $event->data = 'twofactor_login';
88a386a536SAndreas Gohr            $event->preventDefault();
89a386a536SAndreas Gohr            $event->stopPropagation();
90a386a536SAndreas Gohr            return;
91a386a536SAndreas Gohr        }
92a386a536SAndreas Gohr
93a386a536SAndreas Gohr        if ($this->manager->isRequired()) {
94a386a536SAndreas Gohr            // 2fa is required - they need to set it up now
95a386a536SAndreas Gohr            // this will be handled by action/profile.php
96a386a536SAndreas Gohr            $event->data = 'twofactor_profile';
97a386a536SAndreas Gohr        }
98a386a536SAndreas Gohr
99a386a536SAndreas Gohr        // all good. proceed
100a386a536SAndreas Gohr    }
101a386a536SAndreas Gohr
102a386a536SAndreas Gohr    /**
103a386a536SAndreas Gohr     * Show a 2fa login screen
104a386a536SAndreas Gohr     *
105a386a536SAndreas Gohr     * @param Doku_Event $event
106a386a536SAndreas Gohr     */
107a386a536SAndreas Gohr    public function handleLoginDisplay(Doku_Event $event)
108a386a536SAndreas Gohr    {
109a386a536SAndreas Gohr        if ($event->data !== 'twofactor_login') return;
110a386a536SAndreas Gohr        $event->preventDefault();
111a386a536SAndreas Gohr        $event->stopPropagation();
112a386a536SAndreas Gohr
113a386a536SAndreas Gohr        global $INPUT;
114a386a536SAndreas Gohr        $providerID = $INPUT->str('2fa_provider');
115a386a536SAndreas Gohr        $providers = $this->manager->getUserProviders();
116a386a536SAndreas Gohr        if (isset($providers[$providerID])) {
117a386a536SAndreas Gohr            $provider = $providers[$providerID];
118a386a536SAndreas Gohr        } else {
119b6119621SAndreas Gohr            $provider = $this->manager->getUserDefaultProvider();
120a386a536SAndreas Gohr        }
121b6119621SAndreas Gohr        // remove current provider from list
122b6119621SAndreas Gohr        unset($providers[$provider->getProviderID()]);
123a386a536SAndreas Gohr
124848a9be0SAndreas Gohr        $form = new dokuwiki\Form\Form(['method' => 'POST']);
125848a9be0SAndreas Gohr        $form->setHiddenField('do', 'twofactor_login');
126a386a536SAndreas Gohr        $form->setHiddenField('2fa_provider', $provider->getProviderID());
127a386a536SAndreas Gohr        $form->addFieldsetOpen($provider->getLabel());
128a386a536SAndreas Gohr        try {
129a386a536SAndreas Gohr            $code = $provider->generateCode();
130a386a536SAndreas Gohr            $info = $provider->transmitMessage($code);
131a386a536SAndreas Gohr            $form->addHTML('<p>' . hsc($info) . '</p>');
132848a9be0SAndreas Gohr            $form->addTextInput('2fa_code', 'Your Code')->val('');
133848a9be0SAndreas Gohr            $form->addCheckbox('sticky', 'Remember this browser'); // reuse same name as login
134a386a536SAndreas Gohr            $form->addButton('2fa', 'Submit')->attr('type', 'submit');
135a386a536SAndreas Gohr        } catch (\Exception $e) {
136a386a536SAndreas Gohr            msg(hsc($e->getMessage()), -1); // FIXME better handling
137a386a536SAndreas Gohr        }
138a386a536SAndreas Gohr        $form->addFieldsetClose();
139a386a536SAndreas Gohr
140a386a536SAndreas Gohr        if (count($providers)) {
141a386a536SAndreas Gohr            $form->addFieldsetOpen('Alternative methods');
142a386a536SAndreas Gohr            foreach ($providers as $prov) {
143a386a536SAndreas Gohr                $link = $prov->getProviderID(); // FIXME build correct links
144a386a536SAndreas Gohr
145a386a536SAndreas Gohr                $form->addHTML($link);
146a386a536SAndreas Gohr            }
147a386a536SAndreas Gohr            $form->addFieldsetClose();
148a386a536SAndreas Gohr        }
149a386a536SAndreas Gohr
150a386a536SAndreas Gohr        echo $form->toHTML();
151a386a536SAndreas Gohr    }
152a386a536SAndreas Gohr
153a386a536SAndreas 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    {
159848a9be0SAndreas Gohr        if (!isset($_COOKIE[self::TWOFACTOR_COOKIE])) return false;
160848a9be0SAndreas Gohr        $data = unserialize(base64_decode($_COOKIE[self::TWOFACTOR_COOKIE]));
161848a9be0SAndreas Gohr        if (!is_array($data)) return false;
162848a9be0SAndreas Gohr        list($providerID, $buid,) = $data;
163848a9be0SAndreas Gohr        if (auth_browseruid() !== $buid) return false;
164848a9be0SAndreas Gohr
165848a9be0SAndreas Gohr        try {
166848a9be0SAndreas Gohr            // ensure it's a still valid provider
167848a9be0SAndreas Gohr            $this->manager->getUserProvider($providerID);
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
194848a9be0SAndreas Gohr        $data = base64_encode(serialize([$providerID, auth_browseruid(), time()]));
195848a9be0SAndreas Gohr        $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
196848a9be0SAndreas Gohr        $time = $sticky ? (time() + 60 * 60 * 24 * 365) : 0; //one year
197848a9be0SAndreas Gohr        setcookie(self::TWOFACTOR_COOKIE, $data, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true);
198a386a536SAndreas Gohr
199a386a536SAndreas Gohr        return true;
200a386a536SAndreas Gohr    }
201fca58076SAndreas Gohr}
202