Skip to content

Commit 50503ca

Browse files
committed
Http: Review OAuth 2.0 classes
- Rename `OAuth2GrantType` to `HasGrantType`; access its constants (now with `GRANT_` prefixes) via its implementations; add missing grants, incl. `device_code` for planned device flow support - Remove redundant `OAuth2Flow` (grants and flows are equivalent) - Add `HasResponseType` for values passed to the authorization endpoint, incl. for OpenID Connect flows
1 parent 2f102b3 commit 50503ca

File tree

9 files changed

+136
-156
lines changed

9 files changed

+136
-156
lines changed

phpstan-baseline-7.4.neon

Lines changed: 0 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

phpstan-baseline-8.3.neon

Lines changed: 0 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

phpstan.neon.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ parameters:
4040
paths:
4141
- src/Toolkit/Core/Facade/*
4242
- tests/unit/Toolkit/Core/Facade/*
43+
- tests/fixtures/Toolkit/Http/OAuth2/OAuth2Client/OAuth2TestClient.php
4344
-
4445
identifier: salient.needless.coalesce
4546
paths:
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Salient\Http\OAuth2;
4+
5+
/**
6+
* @api
7+
*/
8+
interface HasGrantType
9+
{
10+
/**
11+
* Authorization code
12+
*
13+
* - \[RFC6749] Section 4.1 ("Authorization Code Grant")
14+
* - \[OpenID.Core] Section 3.1 ("Authentication using the Authorization
15+
* Code Flow")
16+
* - \[OpenID.Core] Section 3.3 ("Authentication using the Hybrid Flow")
17+
*/
18+
public const GRANT_AUTHORIZATION_CODE = 'authorization_code';
19+
20+
/**
21+
* Resource owner password
22+
*
23+
* - \[RFC6749] Section 4.3 ("Resource Owner Password Credentials Grant")
24+
*/
25+
public const GRANT_PASSWORD = 'password';
26+
27+
/**
28+
* Client credentials
29+
*
30+
* - \[RFC6749] Section 4.4 ("Client Credentials Grant")
31+
*/
32+
public const GRANT_CLIENT_CREDENTIALS = 'client_credentials';
33+
34+
/**
35+
* Device code
36+
*
37+
* - \[RFC8628] ("OAuth 2.0 Device Authorization Grant")
38+
*/
39+
public const GRANT_DEVICE_CODE = 'urn:ietf:params:oauth:grant-type:device_code';
40+
41+
/**
42+
* Refresh token
43+
*
44+
* - \[RFC6749] Section 6 ("Refreshing an Access Token")
45+
*/
46+
public const GRANT_REFRESH_TOKEN = 'refresh_token';
47+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Salient\Http\OAuth2;
4+
5+
/**
6+
* @api
7+
*/
8+
interface HasResponseType
9+
{
10+
/**
11+
* Authorization code
12+
*
13+
* - \[RFC6749] Section 4.1 ("Authorization Code Grant")
14+
* - \[OpenID.Core] Section 3.1 ("Authentication using the Authorization
15+
* Code Flow")
16+
*/
17+
public const RESPONSE_CODE = 'code';
18+
19+
/**
20+
* Token
21+
*
22+
* - \[RFC6749] Section 4.2 ("Implicit Grant")
23+
*/
24+
public const RESPONSE_TOKEN = 'token';
25+
26+
/**
27+
* ID token
28+
*
29+
* - \[OpenID.Core] Section 3.2 ("Authentication using the Implicit Flow")
30+
*/
31+
public const RESPONSE_ID_TOKEN = 'id_token';
32+
33+
/**
34+
* ID token + token
35+
*
36+
* - \[OpenID.Core] Section 3.2 ("Authentication using the Implicit Flow")
37+
*/
38+
public const RESPONSE_ID_TOKEN_TOKEN = 'id_token token';
39+
40+
/**
41+
* Authorization code + ID token
42+
*
43+
* - \[OpenID.Core] Section 3.3 ("Authentication using the Hybrid Flow")
44+
*/
45+
public const RESPONSE_CODE_ID_TOKEN = 'code id_token';
46+
47+
/**
48+
* Authorization code + token
49+
*
50+
* - \[OpenID.Core] Section 3.3 ("Authentication using the Hybrid Flow")
51+
*/
52+
public const RESPONSE_CODE_TOKEN = 'code token';
53+
54+
/**
55+
* Authorization code + ID token + token
56+
*
57+
* - \[OpenID.Core] Section 3.3 ("Authentication using the Hybrid Flow")
58+
*/
59+
public const RESPONSE_CODE_ID_TOKEN_TOKEN = 'code id_token token';
60+
}

src/Toolkit/Http/OAuth2/OAuth2Client.php

Lines changed: 24 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
use Salient\Core\Facade\Cache;
1111
use Salient\Core\Facade\Console;
1212
use Salient\Curler\Curler;
13-
use Salient\Http\Message\Response;
1413
use Salient\Http\Server\Server;
1514
use Salient\Http\Server\ServerResponse;
1615
use Salient\Utility\Arr;
@@ -21,98 +20,34 @@
2120
use Throwable;
2221
use UnexpectedValueException;
2322

24-
/**
25-
* A headless OAuth 2.0 client that acquires and validates tokens required for
26-
* access to protected resources
27-
*/
28-
abstract class OAuth2Client
23+
abstract class OAuth2Client implements HasGrantType, HasResponseType
2924
{
3025
private ?Server $Listener;
3126
private AbstractProvider $Provider;
32-
/** @var OAuth2Flow::* */
33-
private int $Flow;
27+
/** @var self::GRANT_* */
28+
private string $Flow;
3429
private string $TokenKey;
3530

3631
/**
37-
* Return an HTTP listener to receive OAuth 2.0 redirects from the provider,
38-
* or null to disable flows that require it
39-
*
40-
* Reference implementation:
41-
*
42-
* ```php
43-
* <?php
44-
* class OAuth2TestClient extends OAuth2Client
45-
* {
46-
* protected function getListener(): ?Server
47-
* {
48-
* $listener = new Server(
49-
* Env::get('app_host', 'localhost'),
50-
* Env::getInt('app_port', 27755),
51-
* );
52-
*
53-
* $proxyHost = Env::getNullable('app_proxy_host', null);
54-
* $proxyPort = Env::getNullableInt('app_proxy_port', null);
55-
*
56-
* if ($proxyHost !== null && $proxyPort !== null) {
57-
* return $listener->withProxy(
58-
* $proxyHost,
59-
* $proxyPort,
60-
* Env::getNullableBool('app_proxy_tls', null),
61-
* Env::get('app_proxy_base_path', ''),
62-
* );
63-
* }
64-
*
65-
* return $listener;
66-
* }
67-
* }
68-
* ```
32+
* Get an in-process HTTP server to receive OAuth 2.0 redirects from the
33+
* provider to the client, or null if flows that require it are disabled
6934
*/
7035
abstract protected function getListener(): ?Server;
7136

7237
/**
73-
* Return an OAuth 2.0 provider to request and validate tokens that
74-
* authorize access to the resource server
75-
*
76-
* Example:
77-
*
78-
* The following provider could be used to authorize access to the Microsoft
79-
* Graph API on behalf of a user or application. `redirectUri` can be
80-
* omitted if support for the Authorization Code flow is not required.
81-
*
82-
* > The only scope required for access to the Microsoft Graph API is
83-
* > `https://graph.microsoft.com/.default`
84-
*
85-
* ```php
86-
* <?php
87-
* class OAuth2TestClient extends OAuth2Client
88-
* {
89-
* protected function getProvider(): GenericProvider
90-
* {
91-
* return new GenericProvider([
92-
* 'clientId' => $this->AppId,
93-
* 'clientSecret' => $this->Secret,
94-
* 'redirectUri' => $this->getRedirectUri(),
95-
* 'urlAuthorize' => sprintf('https://login.microsoftonline.com/%s/oauth2/authorize', $this->TenantId),
96-
* 'urlAccessToken' => sprintf('https://login.microsoftonline.com/%s/oauth2/v2.0/token', $this->TenantId),
97-
* 'urlResourceOwnerDetails' => sprintf('https://login.microsoftonline.com/%s/openid/userinfo', $this->TenantId),
98-
* 'scopes' => ['openid', 'profile', 'email', 'offline_access', 'https://graph.microsoft.com/.default'],
99-
* 'scopeSeparator' => ' ',
100-
* ]);
101-
* }
102-
* }
103-
* ```
38+
* Get an OAuth 2.0 provider for the client
10439
*/
10540
abstract protected function getProvider(): AbstractProvider;
10641

10742
/**
108-
* Return the OAuth 2.0 flow to use
43+
* Get the client's OAuth 2.0 flow
10944
*
110-
* @return OAuth2Flow::*
45+
* @return OAuth2Client::GRANT_*
11146
*/
112-
abstract protected function getFlow(): int;
47+
abstract protected function getFlow(): string;
11348

11449
/**
115-
* Return the URL of the OAuth 2.0 provider's JSON Web Key Set, or null to
50+
* Get the URL of the OAuth 2.0 provider's JSON Web Key Set, or null to
11651
* disable JWT signature validation and decoding
11752
*
11853
* Required for token signature validation. Check the provider's
@@ -124,9 +59,13 @@ abstract protected function getJsonWebKeySetUrl(): ?string;
12459
* Called when an access token is received from the OAuth 2.0 provider
12560
*
12661
* @param array<string,mixed>|null $idToken
127-
* @param OAuth2GrantType::* $grantType
62+
* @param OAuth2Client::GRANT_* $grantType
12863
*/
129-
abstract protected function receiveToken(OAuth2AccessToken $token, ?array $idToken, string $grantType): void;
64+
abstract protected function receiveToken(
65+
OAuth2AccessToken $token,
66+
?array $idToken,
67+
string $grantType
68+
): void;
13069

13170
public function __construct()
13271
{
@@ -215,7 +154,7 @@ final protected function refreshAccessToken(): ?OAuth2AccessToken
215154
return $refreshToken === null
216155
? null
217156
: $this->requestAccessToken(
218-
OAuth2GrantType::REFRESH_TOKEN,
157+
self::GRANT_REFRESH_TOKEN,
219158
['refresh_token' => $refreshToken]
220159
);
221160
}
@@ -251,14 +190,14 @@ final protected function authorize(array $options = []): OAuth2AccessToken
251190
$this->flushTokens();
252191

253192
switch ($this->Flow) {
254-
case OAuth2Flow::CLIENT_CREDENTIALS:
193+
case self::GRANT_CLIENT_CREDENTIALS:
255194
return $this->authorizeWithClientCredentials($options);
256195

257-
case OAuth2Flow::AUTHORIZATION_CODE:
196+
case self::GRANT_AUTHORIZATION_CODE:
258197
return $this->authorizeWithAuthorizationCode($options);
259198

260199
default:
261-
throw new LogicException(sprintf('Invalid OAuth2Flow: %d', $this->Flow));
200+
throw new LogicException(sprintf('Invalid flow: %s', $this->Flow));
262201
}
263202
}
264203

@@ -282,7 +221,7 @@ private function authorizeWithClientCredentials(array $options = []): OAuth2Acce
282221
}
283222

284223
return $this->requestAccessToken(
285-
OAuth2GrantType::CLIENT_CREDENTIALS,
224+
self::GRANT_CLIENT_CREDENTIALS,
286225
$options
287226
);
288227
}
@@ -327,7 +266,7 @@ private function authorizeWithAuthorizationCode(array $options = []): OAuth2Acce
327266
}
328267

329268
return $this->requestAccessToken(
330-
OAuth2GrantType::AUTHORIZATION_CODE,
269+
self::GRANT_AUTHORIZATION_CODE,
331270
['code' => $code],
332271
$options['scope'] ?? null
333272
);
@@ -372,7 +311,7 @@ private function receiveAuthorizationCode(ServerRequestInterface $request): Serv
372311
* Request an access token from the OAuth 2.0 provider, then validate, cache
373312
* and return it
374313
*
375-
* @param string&OAuth2GrantType::* $grantType
314+
* @param self::GRANT_* $grantType
376315
* @param array<string,mixed> $options
377316
* @param mixed $scope
378317
*/
@@ -418,7 +357,7 @@ private function requestAccessToken(
418357
?? $options['scope']
419358
?? $scope);
420359

421-
if (!$scopes && $grantType === OAuth2GrantType::REFRESH_TOKEN) {
360+
if (!$scopes && $grantType === self::GRANT_REFRESH_TOKEN) {
422361
$lastToken = Cache::getInstance()->getInstanceOf($this->TokenKey, OAuth2AccessToken::class);
423362
if ($lastToken) {
424363
$scopes = $lastToken->getScopes();

src/Toolkit/Http/OAuth2/OAuth2Flow.php

Lines changed: 0 additions & 19 deletions
This file was deleted.

src/Toolkit/Http/OAuth2/OAuth2GrantType.php

Lines changed: 0 additions & 13 deletions
This file was deleted.

0 commit comments

Comments
 (0)