1<?php
2
3namespace dokuwiki\plugin\twofactor;
4
5use dokuwiki\Extension\ActionPlugin;
6use dokuwiki\Extension\AuthPlugin;
7use dokuwiki\Form\Form;
8use dokuwiki\Utf8\PhpString;
9
10/**
11 * Baseclass for all second factor providers
12 */
13abstract class Provider extends ActionPlugin
14{
15    /** @var Settings */
16    protected $settings;
17
18    /** @var string */
19    protected $providerID;
20
21    /** @inheritdoc */
22    public function register(\Doku_Event_Handler $controller)
23    {
24        $controller->register_hook(
25            'PLUGIN_TWOFACTOR_PROVIDER_REGISTER',
26            'AFTER',
27            $this,
28            'registerSelf',
29            null,
30            Manager::EVENT_PRIORITY - 1 // providers first
31        );
32    }
33
34    /**
35     * Register this class as a twofactor provider
36     *
37     * @param \Doku_Event $event
38     * @return void
39     */
40    public function registerSelf(\Doku_Event $event)
41    {
42        $event->data[$this->getProviderID()] = $this;
43    }
44
45    /**
46     * Initializes the provider for the given user
47     * @param string $user Current user
48     */
49    public function init($user)
50    {
51        $this->settings = new Settings($this->getProviderID(), $user);
52    }
53
54    // region Introspection methods
55
56    /**
57     * The user data for the current user
58     * @return array (user=>'login', name=>'full name', mail=>'user@example.com', grps=>['group1', 'group2',...])
59     */
60    public function getUserData()
61    {
62        /** @var AuthPlugin $auth */
63        global $auth;
64        $user = $this->settings->getUser();
65        $userdata = $auth->getUserData($user);
66        if (!$userdata) throw new \RuntimeException('2fa: Failed to get user details from auth backend');
67        $userdata['user'] = $user;
68        return $userdata;
69    }
70
71    /**
72     * The ID of this provider
73     *
74     * @return string
75     */
76    public function getProviderID()
77    {
78        if (!$this->providerID) {
79            $this->providerID = $this->getPluginName();
80        }
81
82        return $this->providerID;
83    }
84
85    /**
86     * Pretty Label for this provider
87     *
88     * @return string
89     */
90    public function getLabel()
91    {
92        return PhpString::ucfirst($this->providerID);
93    }
94
95    // endregion
96    // region Configuration methods
97
98    /**
99     * Clear all settings
100     */
101    public function reset()
102    {
103        $this->settings->purge();
104    }
105
106    /**
107     * Has this provider been fully configured and verified by the user and thus can be used
108     * for authentication?
109     *
110     * @return bool
111     */
112    abstract public function isConfigured();
113
114    /**
115     * Render the configuration form
116     *
117     * This method should add the needed form elements to (re)configure the provider.
118     * The contents of the form may change depending on the current settings.
119     *
120     * No submit button should be added - this is handled by the main plugin.
121     *
122     * @param Form $form The initial form to add elements to
123     * @return Form
124     */
125    abstract public function renderProfileForm(Form $form);
126
127    /**
128     * Handle any input data
129     *
130     * @return void
131     */
132    abstract public function handleProfileForm();
133
134    // endregion
135    // region OTP methods
136
137    /**
138     * Create and store a new secret for this provider
139     *
140     * @return string the new secret
141     * @throws \Exception when no suitable random source is available
142     */
143    public function initSecret()
144    {
145        $ga = new GoogleAuthenticator();
146        $secret = $ga->createSecret();
147
148        $this->settings->set('secret', $secret);
149        return $secret;
150    }
151
152    /**
153     * Get the secret for this provider
154     *
155     * @return string
156     */
157    public function getSecret()
158    {
159        return $this->settings->get('secret');
160    }
161
162    /**
163     * Generate an auth code
164     *
165     * @return string
166     * @throws \Exception when no code can be created
167     */
168    public function generateCode()
169    {
170        $secret = $this->settings->get('secret');
171        if (!$secret) throw new \Exception('No secret for provider ' . $this->getProviderID());
172
173        $ga = new GoogleAuthenticator();
174        return $ga->getCode($secret);
175    }
176
177    /**
178     * Check the given code
179     *
180     * @param string $code
181     * @param bool $usermessage should a message about the failed code be shown to the user?
182     * @return bool
183     * @throws \RuntimeException when no code can be created
184     */
185    public function checkCode($code, $usermessage = true)
186    {
187        $secret = $this->settings->get('secret');
188        if (!$secret) throw new \RuntimeException('No secret for provider ' . $this->getProviderID());
189
190        $ga = new GoogleAuthenticator();
191        $ok = $ga->verifyCode($secret, $code, $this->getTolerance());
192        if (!$ok && $usermessage) {
193            msg((Manager::getInstance())->getLang('codefail'), -1);
194        }
195        return $ok;
196    }
197
198    /**
199     * The tolerance to be used when verifying codes
200     *
201     * This is the allowed time drift in 30 second units (8 means 4 minutes before or after)
202     * Different providers may want to use different tolerances by overriding this method.
203     *
204     * @return int
205     */
206    public function getTolerance()
207    {
208        return 2;
209    }
210
211    /**
212     * Transmits the code to the user
213     *
214     * If a provider does not transmit anything (eg. TOTP) simply
215     * return the message.
216     *
217     * @param string $code The code to transmit
218     * @return string Informational message for the user
219     * @throw \Exception when the message can't be sent
220     */
221    abstract public function transmitMessage($code);
222
223    // endregion
224}
225