xref: /plugin/oauth/Adapter.php (revision a9ed389088d3d78c0f9ad1f5315d8707f36b61b7)
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            $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            $oauth->requestAccessToken(
164                $INPUT->get->str('oauth_token'),
165                $INPUT->get->str('oauth_verifier'),
166                $token->getRequestTokenSecret()
167            );
168        }
169        return true;
170    }
171
172    /**
173     * Return the Service Login Button
174     *
175     * @return string
176     */
177    public function loginButton()
178    {
179        global $ID;
180
181        $attr = buildAttributes([
182            'href' => wl($ID, array('oauthlogin' => $this->getServiceID()), false, '&'),
183            'class' => 'plugin_oauth_' . $this->getServiceID(),
184            'style' => 'background-color: ' . $this->getColor(),
185        ]);
186
187        return '<a ' . $attr . '>' . $this->getSvgLogo() . '<span>' . $this->getLabel() . '</span></a> ';
188    }
189    // endregion
190
191    // region overridable methods
192
193    /**
194     * Retrieve the user's data via API
195     *
196     * The returned array needs to contain at least 'email', 'name', 'user' and optionally 'grps'
197     *
198     * Use the request() method of the oauth object to talk to the API
199     *
200     * @return array
201     * @throws Exception
202     * @see getOAuthService()
203     */
204    abstract public function getUser();
205
206    /**
207     * Return the scopes to request
208     *
209     * This should return the minimal scopes needed for accessing the user's data
210     *
211     * @return string[]
212     */
213    public function getScopes()
214    {
215        return [];
216    }
217
218    /**
219     * Return the user friendly name of the service
220     *
221     * Defaults to ServiceID. You may want to override this.
222     *
223     * @return string
224     */
225    public function getLabel()
226    {
227        return ucfirst($this->getServiceID());
228    }
229
230    /**
231     * Return the internal name of the Service
232     *
233     * Defaults to the plugin name (without oauth prefix). This has to match the Service class name in
234     * the appropriate lusitantian oauth Service namespace
235     *
236     * @return string
237     */
238    public function getServiceID()
239    {
240        $name = $this->getPluginName();
241        if (substr($name, 0, 5) === 'oauth') {
242            $name = substr($name, 5);
243        }
244
245        return $name;
246    }
247
248    /**
249     * Register a new Service
250     *
251     * @return string A fully qualified class name to register as new Service for your ServiceID
252     */
253    public function registerServiceClass()
254    {
255        return null;
256    }
257
258    /**
259     * Return the button color to use
260     *
261     * @return string
262     */
263    public function getColor()
264    {
265        return '#999';
266    }
267
268    /**
269     * Return the SVG of the logo for this service
270     *
271     * Defaults to a logo.svg in the plugin directory
272     *
273     * @return string
274     */
275    public function getSvgLogo()
276    {
277        $logo = DOKU_PLUGIN . $this->getPluginName() . '/logo.svg';
278        if (file_exists($logo)) return inlineSVG($logo);
279        return '';
280    }
281
282    /**
283     * The oauth key
284     *
285     * @return string
286     */
287    public function getKey()
288    {
289        return $this->getConf('key');
290    }
291
292    /**
293     * The oauth secret
294     *
295     * @return string
296     */
297    public function getSecret()
298    {
299        return $this->getConf('secret');
300    }
301
302    // endregion
303}
304