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