xref: /plugin/oauth/Adapter.php (revision eae50416f9f9034fe944627716b2567b617dfac2)
1<?php
2
3namespace dokuwiki\plugin\oauth;
4
5use dokuwiki\Extension\ActionPlugin;
6use OAuth\Common\Consumer\Credentials;
7use OAuth\Common\Http\Exception\TokenResponseException;
8use OAuth\OAuth1\Service\AbstractService as Abstract1Service;
9use OAuth\OAuth1\Token\TokenInterface;
10use OAuth\OAuth2\Service\AbstractService as Abstract2Service;
11use OAuth\OAuth2\Service\Exception\InvalidAuthorizationStateException;
12use OAuth\ServiceFactory;
13
14/**
15 * Base class to implement a Backend Service for the oAuth Plugin
16 */
17abstract class Adapter extends ActionPlugin
18{
19    /**
20     * @var Abstract2Service|Abstract1Service
21     * @see getOAuthService() use this to ensure it's intialized
22     */
23    protected $oAuth;
24
25    // region internal methods
26
27    /**
28     * Auto register this plugin with the oAuth authentication plugin
29     *
30     * @inheritDoc
31     */
32    public function register(\Doku_Event_Handler $controller)
33    {
34        $controller->register_hook('PLUGIN_OAUTH_BACKEND_REGISTER', 'AFTER', $this, 'handleRegister');
35    }
36
37    /**
38     * Auto register this plugin with the oAuth authentication plugin
39     */
40    public function handleRegister(\Doku_Event $event, $param)
41    {
42        $event->data[$this->getServiceID()] = $this;
43    }
44
45    /**
46     * Initialize the oAuth service
47     *
48     * @param string $guid UIID for the user to authenticate
49     * @throws \OAuth\Common\Exception\Exception
50     */
51    public function initOAuthService($guid)
52    {
53        /** @var \helper_plugin_oauth $hlp */
54        $hlp = plugin_load('helper', 'oauth');
55
56        $credentials = new Credentials(
57            $this->getKey(),
58            $this->getSecret(),
59            $hlp->redirectURI()
60        );
61
62        $serviceFactory = new ServiceFactory();
63        $serviceFactory->setHttpClient(new HTTPClient());
64        $servicename = $this->getServiceID();
65        $serviceclass = $this->registerServiceClass();
66        if ($serviceclass) {
67            $serviceFactory->registerService($servicename, $serviceclass);
68        }
69
70        $this->oAuth = $serviceFactory->createService(
71            $servicename,
72            $credentials,
73            new Storage($guid),
74            $this->getScopes()
75        );
76
77        if ($this->oAuth === null) {
78            throw new Exception('Failed to initialize Service ' . $this->getLabel());
79        }
80    }
81
82    /**
83     * @return Abstract2Service|Abstract1Service
84     * @throws Exception
85     */
86    public function getOAuthService()
87    {
88        if ($this->oAuth === null) throw new Exception('OAuth Service not properly initialized');
89        return $this->oAuth;
90    }
91
92    /**
93     * Redirects to the service for requesting access
94     *
95     * This is the first step of oAuth authentication
96     *
97     * This implementation tries to abstract away differences between oAuth1 and oAuth2,
98     * but might need to be overwritten for specific services
99     *
100     * @throws TokenResponseException
101     * @throws Exception
102     */
103    public function login()
104    {
105        $oauth = $this->getOAuthService();
106
107        // store Farmer animal in oAuth state parameter
108        /** @var \helper_plugin_farmer $farmer */
109        $farmer = plugin_load('helper', 'farmer');
110        $parameters = [];
111        if ($farmer && $animal = $farmer->getAnimal()) {
112            $parameters['state'] = urlencode(base64_encode(json_encode(
113                [
114                    'animal' => $animal,
115                    'state' => md5(rand()),
116                ]
117            )));
118            $oauth->getStorage()->storeAuthorizationState($oauth->service(), $parameters['state']);
119        }
120
121        if (is_a($oauth, Abstract1Service::class)) { /* oAuth1 handling */
122            // extra request needed for oauth1 to request a request token
123            $token = $oauth->requestRequestToken();
124            $parameters['oauth_token'] = $token->getRequestToken();
125        }
126        $url = $oauth->getAuthorizationUri($parameters);
127
128        send_redirect($url);
129    }
130
131    /**
132     * Request access token
133     *
134     * This is the second step of oAuth authentication
135     *
136     * This implementation tries to abstract away differences between oAuth1 and oAuth2,
137     * but might need to be overwritten for specific services
138     *
139     * Thrown exceptions indicate a non-successful login because of some error, appropriate messages
140     * should be shown to the user. A return of false with no exceptions indicates that there was no
141     * oauth data at all. This can probably be silently ignored.
142     *
143     * @return bool true if authentication was successful
144     * @throws \OAuth\Common\Exception\Exception
145     * @throws InvalidAuthorizationStateException
146     */
147    public function checkToken()
148    {
149        global $INPUT;
150
151        $oauth = $this->getOAuthService();
152
153        if (is_a($oauth, Abstract2Service::class)) {
154            /** @var Abstract2Service $oauth */
155            if (!$INPUT->get->has('code')) return false;
156            $state = $INPUT->get->str('state', null);
157            $accessToken = $oauth->requestAccessToken($INPUT->get->str('code'), $state);
158        } else {
159            /** @var Abstract1Service $oauth */
160            if (!$INPUT->get->has('oauth_token')) return false;
161            /** @var TokenInterface $token */
162            $token = $oauth->getStorage()->retrieveAccessToken($this->getServiceID());
163            $accessToken = $oauth->requestAccessToken(
164                $INPUT->get->str('oauth_token'),
165                $INPUT->get->str('oauth_verifier'),
166                $token->getRequestTokenSecret()
167            );
168        }
169
170        if (
171            $accessToken->getEndOfLife() !== $accessToken::EOL_NEVER_EXPIRES &&
172            !$accessToken->getRefreshToken()) {
173            msg('Service did not provide a Refresh Token. You will be logged out when the session expires.');
174        }
175
176        return true;
177    }
178
179    /**
180     * Return the Service Login Button
181     *
182     * @return string
183     */
184    public function loginButton()
185    {
186        global $ID;
187
188        $attr = buildAttributes([
189            'href' => wl($ID, array('oauthlogin' => $this->getServiceID()), false, '&'),
190            'class' => 'plugin_oauth_' . $this->getServiceID(),
191            'style' => 'background-color: ' . $this->getColor(),
192        ]);
193
194        return '<a ' . $attr . '>' . $this->getSvgLogo() . '<span>' . $this->getLabel() . '</span></a> ';
195    }
196    // endregion
197
198    // region overridable methods
199
200    /**
201     * Retrieve the user's data via API
202     *
203     * The returned array needs to contain at least 'email', 'name', 'user' and optionally 'grps'
204     *
205     * Use the request() method of the oauth object to talk to the API
206     *
207     * @return array
208     * @throws Exception
209     * @see getOAuthService()
210     */
211    abstract public function getUser();
212
213    /**
214     * Return the scopes to request
215     *
216     * This should return the minimal scopes needed for accessing the user's data
217     *
218     * @return string[]
219     */
220    public function getScopes()
221    {
222        return [];
223    }
224
225    /**
226     * Return the user friendly name of the service
227     *
228     * Defaults to ServiceID. You may want to override this.
229     *
230     * @return string
231     */
232    public function getLabel()
233    {
234        return ucfirst($this->getServiceID());
235    }
236
237    /**
238     * Return the internal name of the Service
239     *
240     * Defaults to the plugin name (without oauth prefix). This has to match the Service class name in
241     * the appropriate lusitantian oauth Service namespace
242     *
243     * @return string
244     */
245    public function getServiceID()
246    {
247        $name = $this->getPluginName();
248        if (substr($name, 0, 5) === 'oauth') {
249            $name = substr($name, 5);
250        }
251
252        return $name;
253    }
254
255    /**
256     * Register a new Service
257     *
258     * @return string A fully qualified class name to register as new Service for your ServiceID
259     */
260    public function registerServiceClass()
261    {
262        return null;
263    }
264
265    /**
266     * Return the button color to use
267     *
268     * @return string
269     */
270    public function getColor()
271    {
272        return '#999';
273    }
274
275    /**
276     * Return the SVG of the logo for this service
277     *
278     * Defaults to a logo.svg in the plugin directory
279     *
280     * @return string
281     */
282    public function getSvgLogo()
283    {
284        $logo = DOKU_PLUGIN . $this->getPluginName() . '/logo.svg';
285        if (file_exists($logo)) return inlineSVG($logo);
286        return '';
287    }
288
289    /**
290     * The oauth key
291     *
292     * @return string
293     */
294    public function getKey()
295    {
296        return $this->getConf('key');
297    }
298
299    /**
300     * The oauth secret
301     *
302     * @return string
303     */
304    public function getSecret()
305    {
306        return $this->getConf('secret');
307    }
308
309    // endregion
310}
311