1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
/* |
6
|
|
|
* The MIT License (MIT) |
7
|
|
|
* |
8
|
|
|
* Copyright (c) 2014-2018 Spomky-Labs |
9
|
|
|
* |
10
|
|
|
* This software may be modified and distributed under the terms |
11
|
|
|
* of the MIT license. See the LICENSE file for details. |
12
|
|
|
*/ |
13
|
|
|
|
14
|
|
|
namespace OAuth2Framework\Component\Server\Core\Client; |
15
|
|
|
|
16
|
|
|
use Jose\Component\Core\JWK; |
17
|
|
|
use Jose\Component\Core\JWKSet; |
18
|
|
|
use OAuth2Framework\Component\Server\Core\Client\Event as ClientEvent; |
19
|
|
|
use OAuth2Framework\Component\Server\Core\DataBag\DataBag; |
20
|
|
|
use OAuth2Framework\Component\Server\Core\Event\Event; |
21
|
|
|
use OAuth2Framework\Component\Server\Core\ResourceOwner\ResourceOwnerId; |
22
|
|
|
use OAuth2Framework\Component\Server\Core\ResourceOwner\ResourceOwner; |
23
|
|
|
use OAuth2Framework\Component\Server\Core\UserAccount\UserAccountId; |
24
|
|
|
use OAuth2Framework\Component\Server\Core\DomainObject; |
25
|
|
|
use SimpleBus\Message\Recorder\ContainsRecordedMessages; |
26
|
|
|
use SimpleBus\Message\Recorder\PrivateMessageRecorderCapabilities; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* Class Client. |
30
|
|
|
* |
31
|
|
|
* This class is used for every client types. |
32
|
|
|
* A client is a resource owner with a set of allowed grant types and can perform requests against |
33
|
|
|
* available endpoints. |
34
|
|
|
*/ |
35
|
|
|
final class Client implements ResourceOwner, ContainsRecordedMessages, DomainObject |
36
|
|
|
{ |
37
|
|
|
use PrivateMessageRecorderCapabilities; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* @var bool |
41
|
|
|
*/ |
42
|
|
|
private $deleted = false; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* @var UserAccountId|null |
46
|
|
|
*/ |
47
|
|
|
private $ownerId = null; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* @var ClientId|null |
51
|
|
|
*/ |
52
|
|
|
private $clientId = null; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* @var DataBag |
56
|
|
|
*/ |
57
|
|
|
protected $parameters; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* Client constructor. |
61
|
|
|
*/ |
62
|
|
|
private function __construct() |
63
|
|
|
{ |
64
|
|
|
$this->parameters = DataBag::create([]); |
65
|
|
|
} |
66
|
|
|
|
67
|
|
|
/** |
68
|
|
|
* {@inheritdoc} |
69
|
|
|
*/ |
70
|
|
|
public static function getSchema(): string |
71
|
|
|
{ |
72
|
|
|
return 'https://oauth2-framework.spomky-labs.com/schemas/model/client/1.0/schema'; |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* @return Client |
77
|
|
|
*/ |
78
|
|
|
public static function createEmpty(): self |
79
|
|
|
{ |
80
|
|
|
return new self(); |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* @param ClientId $clientId |
85
|
|
|
* @param DataBag $parameters |
86
|
|
|
* @param UserAccountId|null $ownerId |
87
|
|
|
* |
88
|
|
|
* @return Client |
89
|
|
|
*/ |
90
|
|
|
public function create(ClientId $clientId, DataBag $parameters, ? UserAccountId $ownerId): self |
91
|
|
|
{ |
92
|
|
|
$clone = clone $this; |
93
|
|
|
$clone->clientId = $clientId; |
94
|
|
|
$clone->parameters = $parameters; |
95
|
|
|
$clone->ownerId = $ownerId; |
96
|
|
|
|
97
|
|
|
$event = ClientEvent\ClientCreatedEvent::create($clone->clientId, $parameters, $ownerId); |
98
|
|
|
$clone->record($event); |
99
|
|
|
|
100
|
|
|
return $clone; |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* @return UserAccountId|null |
105
|
|
|
*/ |
106
|
|
|
public function getOwnerId(): ? UserAccountId |
107
|
|
|
{ |
108
|
|
|
return $this->ownerId; |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* @param UserAccountId $ownerId |
113
|
|
|
* |
114
|
|
|
* @return Client |
115
|
|
|
*/ |
116
|
|
|
public function withOwnerId(UserAccountId $ownerId): self |
117
|
|
|
{ |
118
|
|
|
if ($this->getOwnerId()->getValue() === $ownerId->getValue()) { |
119
|
|
|
return $this; |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
$clone = clone $this; |
123
|
|
|
$clone->ownerId = $ownerId; |
124
|
|
|
$event = ClientEvent\ClientOwnerChangedEvent::create($clone->getPublicId(), $ownerId); |
125
|
|
|
$clone->record($event); |
126
|
|
|
|
127
|
|
|
return $clone; |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
/** |
131
|
|
|
* @param DataBag $parameters |
132
|
|
|
* |
133
|
|
|
* @return Client |
134
|
|
|
*/ |
135
|
|
|
public function withParameters(DataBag $parameters): self |
136
|
|
|
{ |
137
|
|
|
$clone = clone $this; |
138
|
|
|
$clone->parameters = $parameters; |
139
|
|
|
$event = ClientEvent\ClientParametersUpdatedEvent::create($clone->getPublicId(), $parameters); |
140
|
|
|
$clone->record($event); |
141
|
|
|
|
142
|
|
|
return $clone; |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* @return Client |
147
|
|
|
*/ |
148
|
|
|
public function markAsDeleted(): self |
149
|
|
|
{ |
150
|
|
|
$clone = clone $this; |
151
|
|
|
$clone->deleted = true; |
152
|
|
|
$event = ClientEvent\ClientDeletedEvent::create($clone->getPublicId()); |
153
|
|
|
$clone->record($event); |
154
|
|
|
|
155
|
|
|
return $clone; |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
/** |
159
|
|
|
* @return bool |
160
|
|
|
*/ |
161
|
|
|
public function isDeleted(): bool |
162
|
|
|
{ |
163
|
|
|
return $this->deleted; |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
/** |
167
|
|
|
* @param string $grant_type |
168
|
|
|
* |
169
|
|
|
* @return bool |
170
|
|
|
*/ |
171
|
|
|
public function isGrantTypeAllowed(string $grant_type): bool |
172
|
|
|
{ |
173
|
|
|
$grant_types = $this->has('grant_types') ? $this->get('grant_types') : []; |
174
|
|
|
if (!is_array($grant_types)) { |
175
|
|
|
throw new \InvalidArgumentException('The metadata "grant_types" must be an array.'); |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
return in_array($grant_type, $grant_types); |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
/** |
182
|
|
|
* @param string $response_type |
183
|
|
|
* |
184
|
|
|
* @return bool |
185
|
|
|
*/ |
186
|
|
|
public function isResponseTypeAllowed(string $response_type): bool |
187
|
|
|
{ |
188
|
|
|
$response_types = $this->has('response_types') ? $this->get('response_types') : []; |
189
|
|
|
if (!is_array($response_type)) { |
190
|
|
|
throw new \InvalidArgumentException('The metadata "response_types" must be an array.'); |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
return in_array($response_type, $response_types); |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* @param string $token_type |
198
|
|
|
* |
199
|
|
|
* @return bool |
200
|
|
|
*/ |
201
|
|
|
public function isTokenTypeAllowed(string $token_type): bool |
202
|
|
|
{ |
203
|
|
|
if (!$this->has('token_types')) { |
204
|
|
|
return true; |
205
|
|
|
} |
206
|
|
|
$token_types = $this->get('token_types'); |
207
|
|
|
if (!is_array($token_types)) { |
208
|
|
|
throw new \InvalidArgumentException('The metadata "token_types" must be an array.'); |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
return in_array($token_type, $token_types); |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
/** |
215
|
|
|
* @return bool |
216
|
|
|
*/ |
217
|
|
|
public function isPublic(): bool |
218
|
|
|
{ |
219
|
|
|
return 'none' === $this->getTokenEndpointAuthenticationMethod(); |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
/** |
223
|
|
|
* @return string |
224
|
|
|
*/ |
225
|
|
|
public function getTokenEndpointAuthenticationMethod(): string |
226
|
|
|
{ |
227
|
|
|
if ($this->has('token_endpoint_auth_method')) { |
228
|
|
|
return $this->get('token_endpoint_auth_method'); |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
return 'client_secret_basic'; |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
/** |
235
|
|
|
* @return int |
236
|
|
|
*/ |
237
|
|
|
public function getClientCredentialsExpiresAt(): int |
238
|
|
|
{ |
239
|
|
|
if ($this->has('client_secret_expires_at')) { |
240
|
|
|
return $this->get('client_secret_expires_at'); |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
return 0; |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
/** |
247
|
|
|
* @return bool |
248
|
|
|
*/ |
249
|
|
|
public function areClientCredentialsExpired(): bool |
250
|
|
|
{ |
251
|
|
|
if (0 === $this->getClientCredentialsExpiresAt()) { |
252
|
|
|
return false; |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
return time() > $this->getClientCredentialsExpiresAt(); |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
/** |
259
|
|
|
* @return bool |
260
|
|
|
*/ |
261
|
|
|
public function hasPublicKeySet(): bool |
262
|
|
|
{ |
263
|
|
|
return $this->has('jwks') || $this->has('jwks_uri') || $this->has('client_secret'); |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* @return JWKSet |
268
|
|
|
*/ |
269
|
|
|
public function getPublicKeySet(): JWKSet |
270
|
|
|
{ |
271
|
|
|
if (!$this->hasPublicKeySet()) { |
272
|
|
|
throw new \LogicException('The client has no public key set'); |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
$jwkset = null; |
276
|
|
|
if ($this->has('jwks')) { |
277
|
|
|
$jwkset = JWKSet::createFromJson($this->get('jwks')); |
278
|
|
|
} |
279
|
|
|
//FIXME: find a way to allow jku |
280
|
|
|
/*if ($this->has('jwks_uri')) { |
|
|
|
|
281
|
|
|
$jwkset = JKUFactory::createFromJKU($this->get('jwks_uri')); |
282
|
|
|
}*/ |
283
|
|
|
if ($this->has('client_secret')) { |
284
|
|
|
$key = JWK::create([ |
285
|
|
|
'kty' => 'oct', |
286
|
|
|
'use' => 'sig', |
287
|
|
|
'k' => $this->get('client_secret'), |
288
|
|
|
]); |
289
|
|
|
if (null === $jwkset) { |
290
|
|
|
$jwkset = JWKSet::createFromKeys([]); |
291
|
|
|
$jwkset = $jwkset->with($key); |
292
|
|
|
|
293
|
|
|
return $jwkset; |
294
|
|
|
} else { |
295
|
|
|
$jwkset = $jwkset->with($key); |
296
|
|
|
} |
297
|
|
|
} |
298
|
|
|
|
299
|
|
|
return $jwkset; |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
/** |
303
|
|
|
* {@inheritdoc} |
304
|
|
|
*/ |
305
|
|
|
public function getPublicId(): ResourceOwnerId |
306
|
|
|
{ |
307
|
|
|
if (null === $this->clientId) { |
308
|
|
|
throw new \RuntimeException('Client not initialized.'); |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
return $this->clientId; |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
/** |
315
|
|
|
* {@inheritdoc} |
316
|
|
|
*/ |
317
|
|
|
public function has(string $key): bool |
318
|
|
|
{ |
319
|
|
|
return $this->parameters->has($key); |
320
|
|
|
} |
321
|
|
|
|
322
|
|
|
/** |
323
|
|
|
* {@inheritdoc} |
324
|
|
|
*/ |
325
|
|
|
public function get(string $key) |
326
|
|
|
{ |
327
|
|
|
if (!$this->has($key)) { |
328
|
|
|
throw new \InvalidArgumentException(sprintf('Configuration value with key "%s" does not exist.', $key)); |
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
return $this->parameters->get($key); |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
/** |
335
|
|
|
* @return array |
336
|
|
|
*/ |
337
|
|
|
public function all(): array |
338
|
|
|
{ |
339
|
|
|
$all = $this->parameters->all(); |
340
|
|
|
$all['client_id'] = $this->getPublicId()->getValue(); |
341
|
|
|
|
342
|
|
|
return $all; |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
/** |
346
|
|
|
* {@inheritdoc} |
347
|
|
|
*/ |
348
|
|
|
public function jsonSerialize() |
349
|
|
|
{ |
350
|
|
|
$data = [ |
351
|
|
|
'$schema' => $this->getSchema(), |
352
|
|
|
'type' => get_class($this), |
353
|
|
|
'client_id' => $this->getPublicId()->getValue(), |
354
|
|
|
'owner_id' => $this->getOwnerId() ? $this->getOwnerId()->getValue() : null, |
355
|
|
|
'parameters' => (object) $this->all(), |
356
|
|
|
'is_deleted' => $this->isDeleted(), |
357
|
|
|
]; |
358
|
|
|
|
359
|
|
|
return $data; |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
/** |
363
|
|
|
* {@inheritdoc} |
364
|
|
|
*/ |
365
|
|
|
public static function createFromJson(\stdClass $json): DomainObject |
366
|
|
|
{ |
367
|
|
|
$clientId = ClientId::create($json->client_id); |
368
|
|
|
$ownerId = null !== $json->owner_id ? UserAccountId::create($json->owner_id) : null; |
369
|
|
|
$parameters = DataBag::create((array) $json->parameters); |
370
|
|
|
$deleted = $json->is_deleted; |
371
|
|
|
|
372
|
|
|
$client = new self(); |
373
|
|
|
$client->clientId = $clientId; |
374
|
|
|
$client->ownerId = $ownerId; |
375
|
|
|
$client->parameters = $parameters; |
376
|
|
|
$client->deleted = $deleted; |
377
|
|
|
|
378
|
|
|
return $client; |
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
/** |
382
|
|
|
* @param Event $event |
383
|
|
|
* |
384
|
|
|
* @return Client |
385
|
|
|
*/ |
386
|
|
|
public function apply(Event $event): self |
387
|
|
|
{ |
388
|
|
|
$map = $this->getEventMap(); |
389
|
|
|
if (!array_key_exists($event->getType(), $map)) { |
390
|
|
|
throw new \InvalidArgumentException('Unsupported event.'); |
391
|
|
|
} |
392
|
|
|
if (null !== $this->clientId && $this->clientId->getValue() !== $event->getDomainId()->getValue()) { |
393
|
|
|
throw new \InvalidArgumentException('Event not applicable for this client.'); |
394
|
|
|
} |
395
|
|
|
$method = $map[$event->getType()]; |
396
|
|
|
|
397
|
|
|
return $this->$method($event); |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
/** |
401
|
|
|
* @return array |
402
|
|
|
*/ |
403
|
|
|
private function getEventMap(): array |
404
|
|
|
{ |
405
|
|
|
return [ |
406
|
|
|
ClientEvent\ClientCreatedEvent::class => 'applyClientCreatedEvent', |
407
|
|
|
ClientEvent\ClientOwnerChangedEvent::class => 'applyClientOwnerChangedEvent', |
408
|
|
|
ClientEvent\ClientDeletedEvent::class => 'applyClientDeletedEvent', |
409
|
|
|
ClientEvent\ClientParametersUpdatedEvent::class => 'applyClientParametersUpdatedEvent', |
410
|
|
|
]; |
411
|
|
|
} |
412
|
|
|
|
413
|
|
|
/** |
414
|
|
|
* @param ClientEvent\ClientCreatedEvent $event |
415
|
|
|
* |
416
|
|
|
* @return Client |
417
|
|
|
*/ |
418
|
|
|
protected function applyClientCreatedEvent(ClientEvent\ClientCreatedEvent $event): self |
419
|
|
|
{ |
420
|
|
|
$clone = clone $this; |
421
|
|
|
$clone->clientId = $event->getClientId(); |
422
|
|
|
$clone->ownerId = $event->getOwnerId(); |
423
|
|
|
$clone->parameters = $event->getParameters(); |
424
|
|
|
|
425
|
|
|
return $clone; |
426
|
|
|
} |
427
|
|
|
|
428
|
|
|
/** |
429
|
|
|
* @param ClientEvent\ClientOwnerChangedEvent $event |
430
|
|
|
* |
431
|
|
|
* @return Client |
432
|
|
|
*/ |
433
|
|
|
protected function applyClientOwnerChangedEvent(ClientEvent\ClientOwnerChangedEvent $event): self |
434
|
|
|
{ |
435
|
|
|
$clone = clone $this; |
436
|
|
|
$clone->ownerId = $event->getNewOwnerId(); |
437
|
|
|
|
438
|
|
|
return $clone; |
439
|
|
|
} |
440
|
|
|
|
441
|
|
|
/** |
442
|
|
|
* @param ClientEvent\ClientDeletedEvent $event |
443
|
|
|
* |
444
|
|
|
* @return Client |
445
|
|
|
*/ |
446
|
|
|
protected function applyClientDeletedEvent(ClientEvent\ClientDeletedEvent $event): self |
447
|
|
|
{ |
448
|
|
|
$clone = clone $this; |
449
|
|
|
$clone->deleted = true; |
450
|
|
|
|
451
|
|
|
return $clone; |
452
|
|
|
} |
453
|
|
|
|
454
|
|
|
/** |
455
|
|
|
* @param ClientEvent\ClientParametersUpdatedEvent $event |
456
|
|
|
* |
457
|
|
|
* @return Client |
458
|
|
|
*/ |
459
|
|
|
protected function applyClientParametersUpdatedEvent(ClientEvent\ClientParametersUpdatedEvent $event): self |
460
|
|
|
{ |
461
|
|
|
$clone = clone $this; |
462
|
|
|
$clone->parameters = $event->getParameters(); |
463
|
|
|
|
464
|
|
|
return $clone; |
465
|
|
|
} |
466
|
|
|
} |
467
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.