xref: /plugin/twofactor/Provider.php (revision c8525a2117e724dfae4ab9910e06d5d95c6ff6ea)
1fca58076SAndreas Gohr<?php
2fca58076SAndreas Gohr
3fca58076SAndreas Gohrnamespace dokuwiki\plugin\twofactor;
4fca58076SAndreas Gohr
55f8f561aSAndreas Gohruse dokuwiki\Extension\ActionPlugin;
6*c8525a21SAndreas Gohruse dokuwiki\Extension\AuthPlugin;
7a635cb20SAndreas Gohruse dokuwiki\Form\Form;
8a635cb20SAndreas Gohruse dokuwiki\Utf8\PhpString;
9fca58076SAndreas Gohr
10fca58076SAndreas Gohr/**
11fca58076SAndreas Gohr * Baseclass for all second factor providers
12fca58076SAndreas Gohr */
135f8f561aSAndreas Gohrabstract class Provider extends ActionPlugin
14fca58076SAndreas Gohr{
15a635cb20SAndreas Gohr    /** @var Settings */
16a635cb20SAndreas Gohr    protected $settings;
17fca58076SAndreas Gohr
18a635cb20SAndreas Gohr    /** @var string */
19a635cb20SAndreas Gohr    protected $providerID;
20a635cb20SAndreas Gohr
215f8f561aSAndreas Gohr    /** @inheritdoc */
225f8f561aSAndreas Gohr    public function register(\Doku_Event_Handler $controller)
235f8f561aSAndreas Gohr    {
245f8f561aSAndreas Gohr        $controller->register_hook(
255f8f561aSAndreas Gohr            'PLUGIN_TWOFACTOR_PROVIDER_REGISTER',
265f8f561aSAndreas Gohr            'AFTER',
275f8f561aSAndreas Gohr            $this,
285f8f561aSAndreas Gohr            'registerSelf',
295f8f561aSAndreas Gohr            null,
305f8f561aSAndreas Gohr            Manager::EVENT_PRIORITY - 1 // providers first
315f8f561aSAndreas Gohr        );
325f8f561aSAndreas Gohr    }
335f8f561aSAndreas Gohr
345f8f561aSAndreas Gohr    /**
355f8f561aSAndreas Gohr     * Register this class as a twofactor provider
365f8f561aSAndreas Gohr     *
375f8f561aSAndreas Gohr     * @param \Doku_Event $event
385f8f561aSAndreas Gohr     * @return void
395f8f561aSAndreas Gohr     */
405f8f561aSAndreas Gohr    public function registerSelf(\Doku_Event $event)
415f8f561aSAndreas Gohr    {
425f8f561aSAndreas Gohr        $event->data[$this->getProviderID()] = $this;
435f8f561aSAndreas Gohr    }
445f8f561aSAndreas Gohr
45a635cb20SAndreas Gohr    /**
46a635cb20SAndreas Gohr     * Initializes the provider for the given user
47a635cb20SAndreas Gohr     * @param string $user Current user
48a635cb20SAndreas Gohr     */
495f8f561aSAndreas Gohr    public function init($user)
50a635cb20SAndreas Gohr    {
515f8f561aSAndreas Gohr        $this->settings = new Settings($this->getProviderID(), $user);
52a635cb20SAndreas Gohr    }
53a635cb20SAndreas Gohr
5430625b49SAndreas Gohr    // region Introspection methods
5530625b49SAndreas Gohr
56a635cb20SAndreas Gohr    /**
57*c8525a21SAndreas Gohr     * The user data for the current user
58*c8525a21SAndreas Gohr     * @return array (user=>'login', name=>'full name', mail=>'user@example.com', grps=>['group1', 'group2',...])
59*c8525a21SAndreas Gohr     */
60*c8525a21SAndreas Gohr    public function getUserData()
61*c8525a21SAndreas Gohr    {
62*c8525a21SAndreas Gohr        /** @var AuthPlugin $auth */
63*c8525a21SAndreas Gohr        global $auth;
64*c8525a21SAndreas Gohr        $user = $this->settings->getUser();
65*c8525a21SAndreas Gohr        $userdata = $auth->getUserData($user);
66*c8525a21SAndreas Gohr        if (!$userdata) throw new \RuntimeException('2fa: Failed to get user details from auth backend');
67*c8525a21SAndreas Gohr        $userdata['user'] = $user;
68*c8525a21SAndreas Gohr        return $userdata;
69*c8525a21SAndreas Gohr    }
70*c8525a21SAndreas Gohr
71*c8525a21SAndreas Gohr    /**
72a635cb20SAndreas Gohr     * The ID of this provider
73a635cb20SAndreas Gohr     *
74a635cb20SAndreas Gohr     * @return string
75a635cb20SAndreas Gohr     */
76a635cb20SAndreas Gohr    public function getProviderID()
77a635cb20SAndreas Gohr    {
785f8f561aSAndreas Gohr        if (!$this->providerID) {
795f8f561aSAndreas Gohr            $this->providerID = $this->getPluginName();
805f8f561aSAndreas Gohr        }
815f8f561aSAndreas Gohr
82a635cb20SAndreas Gohr        return $this->providerID;
83a635cb20SAndreas Gohr    }
84a635cb20SAndreas Gohr
85a635cb20SAndreas Gohr    /**
86a635cb20SAndreas Gohr     * Pretty Label for this provider
87a635cb20SAndreas Gohr     *
88a635cb20SAndreas Gohr     * @return string
89a635cb20SAndreas Gohr     */
90a635cb20SAndreas Gohr    public function getLabel()
91a635cb20SAndreas Gohr    {
92a635cb20SAndreas Gohr        return PhpString::ucfirst($this->providerID);
93a635cb20SAndreas Gohr    }
94a635cb20SAndreas Gohr
9530625b49SAndreas Gohr    // endregion
9630625b49SAndreas Gohr    // region Configuration methods
9730625b49SAndreas Gohr
98a635cb20SAndreas Gohr    /**
99a635cb20SAndreas Gohr     * Clear all settings
100a635cb20SAndreas Gohr     */
101a635cb20SAndreas Gohr    public function reset()
102a635cb20SAndreas Gohr    {
103a635cb20SAndreas Gohr        $this->settings->purge();
104a635cb20SAndreas Gohr    }
105a635cb20SAndreas Gohr
106a635cb20SAndreas Gohr    /**
107a386a536SAndreas Gohr     * Has this provider been fully configured and verified by the user and thus can be used
108a635cb20SAndreas Gohr     * for authentication?
109a635cb20SAndreas Gohr     *
110a635cb20SAndreas Gohr     * @return bool
111a635cb20SAndreas Gohr     */
112a635cb20SAndreas Gohr    abstract public function isConfigured();
113a635cb20SAndreas Gohr
114a635cb20SAndreas Gohr    /**
115a635cb20SAndreas Gohr     * Render the configuration form
116a635cb20SAndreas Gohr     *
117a635cb20SAndreas Gohr     * This method should add the needed form elements to (re)configure the provider.
118a635cb20SAndreas Gohr     * The contents of the form may change depending on the current settings.
119a635cb20SAndreas Gohr     *
120a635cb20SAndreas Gohr     * No submit button should be added - this is handled by the main plugin.
121a635cb20SAndreas Gohr     *
122a635cb20SAndreas Gohr     * @param Form $form The initial form to add elements to
123a635cb20SAndreas Gohr     * @return Form
124a635cb20SAndreas Gohr     */
125a635cb20SAndreas Gohr    abstract public function renderProfileForm(Form $form);
126a635cb20SAndreas Gohr
127a635cb20SAndreas Gohr    /**
128a635cb20SAndreas Gohr     * Handle any input data
129a635cb20SAndreas Gohr     *
130a635cb20SAndreas Gohr     * @return void
131a635cb20SAndreas Gohr     */
132a635cb20SAndreas Gohr    abstract public function handleProfileForm();
133a635cb20SAndreas Gohr
13430625b49SAndreas Gohr    // endregion
135a635cb20SAndreas Gohr    // region OTP methods
136a635cb20SAndreas Gohr
137a635cb20SAndreas Gohr    /**
138a635cb20SAndreas Gohr     * Create and store a new secret for this provider
139a635cb20SAndreas Gohr     *
140a635cb20SAndreas Gohr     * @return string the new secret
141a635cb20SAndreas Gohr     * @throws \Exception when no suitable random source is available
142a635cb20SAndreas Gohr     */
143a635cb20SAndreas Gohr    public function initSecret()
144a635cb20SAndreas Gohr    {
145a635cb20SAndreas Gohr        $ga = new GoogleAuthenticator();
146a635cb20SAndreas Gohr        $secret = $ga->createSecret();
147a635cb20SAndreas Gohr
148a635cb20SAndreas Gohr        $this->settings->set('secret', $secret);
149a635cb20SAndreas Gohr        return $secret;
150a635cb20SAndreas Gohr    }
151a635cb20SAndreas Gohr
152a635cb20SAndreas Gohr    /**
1536c996db8SAndreas Gohr     * Get the secret for this provider
1546c996db8SAndreas Gohr     *
1556c996db8SAndreas Gohr     * @return string
1566c996db8SAndreas Gohr     */
1576c996db8SAndreas Gohr    public function getSecret()
1586c996db8SAndreas Gohr    {
1596c996db8SAndreas Gohr        return $this->settings->get('secret');
1606c996db8SAndreas Gohr    }
1616c996db8SAndreas Gohr
1626c996db8SAndreas Gohr    /**
163a635cb20SAndreas Gohr     * Generate an auth code
164a635cb20SAndreas Gohr     *
165a635cb20SAndreas Gohr     * @return string
166a635cb20SAndreas Gohr     * @throws \Exception when no code can be created
167a635cb20SAndreas Gohr     */
168a635cb20SAndreas Gohr    public function generateCode()
169a635cb20SAndreas Gohr    {
170a635cb20SAndreas Gohr        $secret = $this->settings->get('secret');
171a635cb20SAndreas Gohr        if (!$secret) throw new \Exception('No secret for provider ' . $this->getProviderID());
172a635cb20SAndreas Gohr
173a635cb20SAndreas Gohr        $ga = new GoogleAuthenticator();
174a635cb20SAndreas Gohr        return $ga->getCode($secret);
175a635cb20SAndreas Gohr    }
176a635cb20SAndreas Gohr
177a635cb20SAndreas Gohr    /**
178a635cb20SAndreas Gohr     * Check the given code
179a635cb20SAndreas Gohr     *
180a635cb20SAndreas Gohr     * @param string $code
18116ed3964SAndreas Gohr     * @param bool $usermessage should a message about the failed code be shown to the user?
182a386a536SAndreas Gohr     * @return bool
18316ed3964SAndreas Gohr     * @throws \RuntimeException when no code can be created
184a635cb20SAndreas Gohr     */
18516ed3964SAndreas Gohr    public function checkCode($code, $usermessage = true)
186a635cb20SAndreas Gohr    {
187a635cb20SAndreas Gohr        $secret = $this->settings->get('secret');
18816ed3964SAndreas Gohr        if (!$secret) throw new \RuntimeException('No secret for provider ' . $this->getProviderID());
189a635cb20SAndreas Gohr
190a635cb20SAndreas Gohr        $ga = new GoogleAuthenticator();
19116ed3964SAndreas Gohr        $ok = $ga->verifyCode($secret, $code, $this->getTolerance());
19216ed3964SAndreas Gohr        if (!$ok && $usermessage) {
19316ed3964SAndreas Gohr            msg((Manager::getInstance())->getLang('codefail'), -1);
19416ed3964SAndreas Gohr        }
19516ed3964SAndreas Gohr        return $ok;
1962fadf188SAndreas Gohr    }
1972fadf188SAndreas Gohr
1982fadf188SAndreas Gohr    /**
1992fadf188SAndreas Gohr     * The tolerance to be used when verifying codes
2002fadf188SAndreas Gohr     *
2012fadf188SAndreas Gohr     * This is the allowed time drift in 30 second units (8 means 4 minutes before or after)
2022fadf188SAndreas Gohr     * Different providers may want to use different tolerances by overriding this method.
2032fadf188SAndreas Gohr     *
2042fadf188SAndreas Gohr     * @return int
2052fadf188SAndreas Gohr     */
2062fadf188SAndreas Gohr    public function getTolerance()
2072fadf188SAndreas Gohr    {
2082fadf188SAndreas Gohr        return 2;
209a635cb20SAndreas Gohr    }
210a635cb20SAndreas Gohr
211a635cb20SAndreas Gohr    /**
21230625b49SAndreas Gohr     * Transmits the code to the user
21330625b49SAndreas Gohr     *
21430625b49SAndreas Gohr     * If a provider does not transmit anything (eg. TOTP) simply
21530625b49SAndreas Gohr     * return the message.
21630625b49SAndreas Gohr     *
21730625b49SAndreas Gohr     * @param string $code The code to transmit
21830625b49SAndreas Gohr     * @return string Informational message for the user
21930625b49SAndreas Gohr     * @throw \Exception when the message can't be sent
220a635cb20SAndreas Gohr     */
22130625b49SAndreas Gohr    abstract public function transmitMessage($code);
222a635cb20SAndreas Gohr
223a635cb20SAndreas Gohr    // endregion
224fca58076SAndreas Gohr}
225