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