xref: /plugin/twofactor/action/login.php (revision 30625b49d43f35ae8dc732acccd71d5a391980ea)
1<?php
2
3use dokuwiki\plugin\twofactor\Manager;
4
5/**
6 * DokuWiki Plugin twofactor (Action Component)
7 *
8 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
9 */
10class action_plugin_twofactor_login extends DokuWiki_Action_Plugin
11{
12    const TWOFACTOR_COOKIE = '2FA' . DOKU_COOKIE;
13
14    /** @var Manager */
15    protected $manager;
16
17    /**
18     * Constructor
19     */
20    public function __construct()
21    {
22        $this->manager = Manager::getInstance();
23    }
24
25    /**
26     * Registers the event handlers.
27     */
28    public function register(Doku_Event_Handler $controller)
29    {
30        if (!(Manager::getInstance())->isReady()) return;
31
32        // check 2fa requirements and either move to profile or login handling
33        $controller->register_hook(
34            'ACTION_ACT_PREPROCESS',
35            'BEFORE',
36            $this,
37            'handleActionPreProcess',
38            null,
39            -999999
40        );
41
42        // display login form
43        $controller->register_hook(
44            'TPL_ACT_UNKNOWN',
45            'BEFORE',
46            $this,
47            'handleLoginDisplay'
48        );
49
50        // FIXME disable user in all non-main screens (media, detail, ajax, ...)
51    }
52
53    /**
54     * Decide if any 2fa handling needs to be done for the current user
55     *
56     * @param Doku_Event $event
57     */
58    public function handleActionPreProcess(Doku_Event $event)
59    {
60        if (!$this->manager->getUser()) 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                $INPUT->bool('sticky')
70            )) {
71                $event->data = 'show';
72                return;
73            } else {
74                // show form
75                $event->preventDefault();
76                return;
77            }
78        }
79
80        // authed already, continue
81        if ($this->isAuthed()) {
82            return;
83        }
84
85        if (count($this->manager->getUserProviders())) {
86            // user has already 2fa set up - they need to authenticate before anything else
87            $event->data = 'twofactor_login';
88            $event->preventDefault();
89            $event->stopPropagation();
90            return;
91        }
92
93        if ($this->manager->isRequired()) {
94            // 2fa is required - they need to set it up now
95            // this will be handled by action/profile.php
96            $event->data = 'twofactor_profile';
97        }
98
99        // all good. proceed
100    }
101
102    /**
103     * Show a 2fa login screen
104     *
105     * @param Doku_Event $event
106     */
107    public function handleLoginDisplay(Doku_Event $event)
108    {
109        if ($event->data !== 'twofactor_login') return;
110        $event->preventDefault();
111        $event->stopPropagation();
112
113        global $INPUT;
114        $providerID = $INPUT->str('2fa_provider');
115        $providers = $this->manager->getUserProviders();
116        if (isset($providers[$providerID])) {
117            $provider = $providers[$providerID];
118        } else {
119            $provider = $this->manager->getUserDefaultProvider();
120        }
121        // remove current provider from list
122        unset($providers[$provider->getProviderID()]);
123
124        $form = new dokuwiki\Form\Form(['method' => 'POST']);
125        $form->setHiddenField('do', 'twofactor_login');
126        $form->setHiddenField('2fa_provider', $provider->getProviderID());
127        $form->addFieldsetOpen($provider->getLabel());
128        try {
129            $code = $provider->generateCode();
130            $info = $provider->transmitMessage($code);
131            $form->addHTML('<p>' . hsc($info) . '</p>');
132            $form->addTextInput('2fa_code', 'Your Code')->val('');
133            $form->addCheckbox('sticky', 'Remember this browser'); // reuse same name as login
134            $form->addButton('2fa', 'Submit')->attr('type', 'submit');
135        } catch (\Exception $e) {
136            msg(hsc($e->getMessage()), -1); // FIXME better handling
137        }
138        $form->addFieldsetClose();
139
140        if (count($providers)) {
141            $form->addFieldsetOpen('Alternative methods');
142            foreach ($providers as $prov) {
143                $link = $prov->getProviderID(); // FIXME build correct links
144
145                $form->addHTML($link);
146            }
147            $form->addFieldsetClose();
148        }
149
150        echo $form->toHTML();
151    }
152
153    /**
154     * Has the user already authenticated with the second factor?
155     * @return bool
156     */
157    protected function isAuthed()
158    {
159        if (!isset($_COOKIE[self::TWOFACTOR_COOKIE])) return false;
160        $data = unserialize(base64_decode($_COOKIE[self::TWOFACTOR_COOKIE]));
161        if (!is_array($data)) return false;
162        list($providerID, $buid,) = $data;
163        if (auth_browseruid() !== $buid) return false;
164
165        try {
166            // ensure it's a still valid provider
167            $this->manager->getUserProvider($providerID);
168            return true;
169        } catch (\Exception $e) {
170            return false;
171        }
172    }
173
174    /**
175     * Verify a given code
176     *
177     * @return bool
178     * @throws Exception
179     */
180    protected function verify($code, $providerID, $sticky)
181    {
182        global $conf;
183
184        if (!$code) return false;
185        if (!$providerID) return false;
186        $provider = $this->manager->getUserProvider($providerID);
187        $ok = $provider->checkCode($code);
188        if (!$ok) {
189            msg('code was wrong', -1);
190            return false;
191        }
192
193        // store cookie
194        $data = base64_encode(serialize([$providerID, auth_browseruid(), time()]));
195        $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
196        $time = $sticky ? (time() + 60 * 60 * 24 * 365) : 0; //one year
197        setcookie(self::TWOFACTOR_COOKIE, $data, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true);
198
199        return true;
200    }
201}
202