-
Notifications
You must be signed in to change notification settings - Fork 80
/
Adapter.php
395 lines (348 loc) · 11.1 KB
/
Adapter.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
<?php
namespace dokuwiki\plugin\oauth;
use dokuwiki\Extension\EventHandler;
use dokuwiki\Extension\Event;
use dokuwiki\Extension\ActionPlugin;
use OAuth\Common\Consumer\Credentials;
use OAuth\Common\Http\Exception\TokenResponseException;
use OAuth\Common\Storage\Exception\TokenNotFoundException;
use OAuth\Common\Storage\Session as SessionStorage;
use OAuth\OAuth1\Service\AbstractService as Abstract1Service;
use OAuth\OAuth1\Token\TokenInterface;
use OAuth\OAuth2\Service\AbstractService as Abstract2Service;
use OAuth\OAuth2\Service\Exception\InvalidAuthorizationStateException;
use OAuth\OAuth2\Service\Exception\MissingRefreshTokenException;
use OAuth\ServiceFactory;
/**
* Base class to implement a Backend Service for the oAuth Plugin
*/
abstract class Adapter extends ActionPlugin
{
/**
* @var Abstract2Service|Abstract1Service
* @see getOAuthService() use this to ensure it's intialized
*/
protected $oAuth;
// region internal methods
/**
* Auto register this plugin with the oAuth authentication plugin
*
* @inheritDoc
*/
public function register(EventHandler $controller)
{
$controller->register_hook('PLUGIN_OAUTH_BACKEND_REGISTER', 'AFTER', $this, 'handleRegister');
}
/**
* Auto register this plugin with the oAuth authentication plugin
*/
public function handleRegister(Event $event, $param)
{
$event->data[$this->getServiceID()] = $this;
}
/**
* Initialize the oAuth service
*
* @param string $storageId user based storage key (if available, yet)
* @throws \OAuth\Common\Exception\Exception
*/
public function initOAuthService($storageId = '')
{
/** @var \helper_plugin_oauth $hlp */
$hlp = plugin_load('helper', 'oauth');
$credentials = new Credentials(
$this->getKey(),
$this->getSecret(),
$hlp->redirectURI()
);
$serviceFactory = new ServiceFactory();
$serviceFactory->setHttpClient(new HTTPClient());
$servicename = $this->getServiceID();
$serviceclass = $this->registerServiceClass();
if ($serviceclass) {
$serviceFactory->registerService($servicename, $serviceclass);
}
if ($storageId) {
$storage = new Storage($storageId);
} else {
$storage = new SessionStorage();
}
$this->oAuth = $serviceFactory->createService(
$servicename,
$credentials,
$storage,
$this->getScopes()
);
if ($this->oAuth === null) {
throw new Exception('Failed to initialize Service ' . $this->getLabel());
}
}
/**
* @return Abstract2Service|Abstract1Service
* @throws Exception
*/
public function getOAuthService()
{
if ($this->oAuth === null) throw new Exception('OAuth Service not properly initialized');
return $this->oAuth;
}
/**
* Once a user has been authenticated, the current token storage needs to be made permanent
*
* @param string $storageId
* @throws Exception
* @throws TokenNotFoundException
*/
public function upgradeStorage($storageId)
{
$oauth = $this->getOAuthService();
$service = $oauth->service();
$oldStorage = $oauth->getStorage();
$newStorage = new Storage($storageId);
if ($oldStorage->hasAccessToken($service)) {
$newStorage->storeAccessToken($service, $oldStorage->retrieveAccessToken($service));
}
if ($oldStorage->hasAuthorizationState($service)) {
$newStorage->storeAuthorizationState($service, $oldStorage->retrieveAuthorizationState($service));
}
// fixme invalidate current oauth object? reinitialize it?
}
/**
* Refresh a possibly outdated access token
*
* Does nothing when the current token is still good to use
*
* @return void
* @throws MissingRefreshTokenException
* @throws TokenNotFoundException
* @throws TokenResponseException
* @throws Exception
*/
public function refreshOutdatedToken()
{
$oauth = $this->getOAuthService();
if (!$oauth->getStorage()->hasAccessToken($oauth->service())) {
// no token to refresh
return;
}
$token = $oauth->getStorage()->retrieveAccessToken($oauth->service());
if (
$token->getEndOfLife() < 0 ||
$token->getEndOfLife() - time() > 3600
) {
// token is still good
return;
}
$refreshToken = $token->getRefreshToken();
$token = $oauth->refreshAccessToken($token);
// If the IDP did not provide a new refresh token, store the old one
if (!$token->getRefreshToken()) {
$token->setRefreshToken($refreshToken);
$oauth->getStorage()->storeAccessToken($oauth->service(), $token);
}
}
/**
* Redirects to the service for requesting access
*
* This is the first step of oAuth authentication
*
* This implementation tries to abstract away differences between oAuth1 and oAuth2,
* but might need to be overwritten for specific services
*
* @throws TokenResponseException
* @throws \Exception
*/
public function login()
{
$oauth = $this->getOAuthService();
// store Farmer animal in oAuth state parameter
/** @var \helper_plugin_farmer $farmer */
$farmer = plugin_load('helper', 'farmer');
$parameters = [];
if ($farmer && $animal = $farmer->getAnimal()) {
$parameters['state'] = urlencode(base64_encode(json_encode(
[
'animal' => $animal,
'state' => md5(random_int(0, mt_getrandmax())),
]
)));
$oauth->getStorage()->storeAuthorizationState($oauth->service(), $parameters['state']);
}
if (is_a($oauth, Abstract1Service::class)) { /* oAuth1 handling */
// extra request needed for oauth1 to request a request token
$token = $oauth->requestRequestToken();
$parameters['oauth_token'] = $token->getRequestToken();
}
$url = $oauth->getAuthorizationUri($parameters);
send_redirect($url);
}
/**
* Request access token
*
* This is the second step of oAuth authentication
*
* This implementation tries to abstract away differences between oAuth1 and oAuth2,
* but might need to be overwritten for specific services
*
* Thrown exceptions indicate a non-successful login because of some error, appropriate messages
* should be shown to the user. A return of false with no exceptions indicates that there was no
* oauth data at all. This can probably be silently ignored.
*
* @return bool true if authentication was successful
* @throws \OAuth\Common\Exception\Exception
* @throws InvalidAuthorizationStateException
*/
public function checkToken()
{
global $INPUT;
$oauth = $this->getOAuthService();
if (is_a($oauth, Abstract2Service::class)) {
if (!$INPUT->get->has('code')) return false;
$state = $INPUT->get->str('state', null);
$accessToken = $oauth->requestAccessToken($INPUT->get->str('code'), $state);
} else {
if (!$INPUT->get->has('oauth_token')) return false;
/** @var TokenInterface $token */
$token = $oauth->getStorage()->retrieveAccessToken($this->getServiceID());
$accessToken = $oauth->requestAccessToken(
$INPUT->get->str('oauth_token'),
$INPUT->get->str('oauth_verifier'),
$token->getRequestTokenSecret()
);
}
if (
$accessToken->getEndOfLife() !== $accessToken::EOL_NEVER_EXPIRES &&
!$accessToken->getRefreshToken()
) {
msg('Service did not provide a Refresh Token. You will be logged out when the session expires.');
}
return true;
}
/**
* Return the Service Login Button
*
* @return string
*/
public function loginButton()
{
global $ID;
$attr = buildAttributes([
'href' => wl($ID, ['oauthlogin' => $this->getServiceID()], false, '&'),
'class' => 'plugin_oauth_' . $this->getServiceID(),
'style' => 'background-color: ' . $this->getColor(),
]);
return '<a ' . $attr . '>' . $this->getSvgLogo() . '<span>' . $this->getLabel() . '</span></a> ';
}
// endregion
// region overridable methods
/**
* Called on logout
*
* If there are required procedures for the service, you can implement them by overriding this.
*
* @return void
*/
public function logout()
{
}
/**
* Retrieve the user's data via API
*
* The returned array needs to contain at least 'email', 'name', 'user' and optionally 'grps'
*
* Use the request() method of the oauth object to talk to the API
*
* @return array
* @throws Exception
* @see getOAuthService()
*/
abstract public function getUser();
/**
* Return the scopes to request
*
* This should return the minimal scopes needed for accessing the user's data
*
* @return string[]
*/
public function getScopes()
{
return [];
}
/**
* Return the user friendly name of the service
*
* Defaults to ServiceID. You may want to override this.
*
* @return string
*/
public function getLabel()
{
return ucfirst($this->getServiceID());
}
/**
* Return the internal name of the Service
*
* Defaults to the plugin name (without oauth prefix). This has to match the Service class name in
* the appropriate lusitantian oauth Service namespace
*
* @return string
*/
public function getServiceID()
{
$name = $this->getPluginName();
if (substr($name, 0, 5) === 'oauth') {
$name = substr($name, 5);
}
return $name;
}
/**
* Register a new Service
*
* @return string A fully qualified class name to register as new Service for your ServiceID
*/
public function registerServiceClass()
{
return null;
}
/**
* Return the button color to use
*
* @return string
*/
public function getColor()
{
return '#999';
}
/**
* Return the SVG of the logo for this service
*
* Defaults to a logo.svg in the plugin directory
*
* @return string
*/
public function getSvgLogo()
{
$logo = DOKU_PLUGIN . $this->getPluginName() . '/logo.svg';
if (file_exists($logo)) return inlineSVG($logo);
return '';
}
/**
* The oauth key
*
* @return string
*/
public function getKey()
{
return $this->getConf('key');
}
/**
* The oauth secret
*
* @return string
*/
public function getSecret()
{
return $this->getConf('secret');
}
// endregion
}