Skip to content

Commit 598421c

Browse files
authored
Merge pull request #171 from Sammyjo20/feature/v2-tappable-oauth
Feature | Added request modifier to OAuth2 process
2 parents 3be28bf + 41b994d commit 598421c

File tree

3 files changed

+219
-8
lines changed

3 files changed

+219
-8
lines changed

src/Helpers/OAuth2/OAuthConfig.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
namespace Saloon\Helpers\OAuth2;
66

7+
use Closure;
78
use Saloon\Traits\Makeable;
9+
use Saloon\Contracts\Request;
810
use Saloon\Exceptions\OAuthConfigValidationException;
911

1012
/**
@@ -56,6 +58,13 @@ class OAuthConfig
5658
*/
5759
protected string $userEndpoint = 'user';
5860

61+
/**
62+
* Callable that modifies the OAuth requests
63+
*
64+
* @var \Closure(\Saloon\Contracts\Request): (void)|null
65+
*/
66+
protected ?Closure $requestModifier = null;
67+
5968
/**
6069
* The default scopes that will be applied to every authorization URL.
6170
*
@@ -224,6 +233,40 @@ public function setDefaultScopes(array $defaultScopes): static
224233
return $this;
225234
}
226235

236+
/**
237+
* Set the request modifier callable which can be used to modify the request being sent
238+
*
239+
* @param callable(\Saloon\Contracts\Request): (void) $requestModifier
240+
* @return $this
241+
*/
242+
public function setRequestModifier(callable $requestModifier): static
243+
{
244+
$this->requestModifier = $requestModifier(...);
245+
246+
return $this;
247+
}
248+
249+
/**
250+
* Invoke the OAuth2 config request modifier
251+
*
252+
* @template TRequest of \Saloon\Contracts\Request
253+
*
254+
* @param TRequest $request
255+
* @return TRequest
256+
*/
257+
public function invokeRequestModifier(Request $request): Request
258+
{
259+
$requestModifier = $this->requestModifier;
260+
261+
if (is_null($requestModifier)) {
262+
return $request;
263+
}
264+
265+
$requestModifier($request);
266+
267+
return $request;
268+
}
269+
227270
/**
228271
* Validate the OAuth2 config.
229272
*

src/Traits/OAuth2/AuthorizationCodeGrant.php

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -96,26 +96,37 @@ public function getAuthorizationUrl(array $scopes = [], string $state = null, st
9696
/**
9797
* Get the access token.
9898
*
99+
* @template TRequest of \Saloon\Contracts\Request
100+
*
99101
* @param string $code
100102
* @param string|null $state
101103
* @param string|null $expectedState
102104
* @param bool $returnResponse
105+
* @param callable(TRequest): (void)|null $requestModifier
103106
* @return \Saloon\Contracts\OAuthAuthenticator|\Saloon\Contracts\Response
104107
* @throws \ReflectionException
105108
* @throws \Saloon\Exceptions\InvalidResponseClassException
106109
* @throws \Saloon\Exceptions\InvalidStateException
107110
* @throws \Saloon\Exceptions\OAuthConfigValidationException
108111
* @throws \Saloon\Exceptions\PendingRequestException
109112
*/
110-
public function getAccessToken(string $code, string $state = null, string $expectedState = null, bool $returnResponse = false): OAuthAuthenticator|Response
113+
public function getAccessToken(string $code, string $state = null, string $expectedState = null, bool $returnResponse = false, ?callable $requestModifier = null): OAuthAuthenticator|Response
111114
{
112115
$this->oauthConfig()->validate();
113116

114117
if (! empty($state) && ! empty($expectedState) && $state !== $expectedState) {
115118
throw new InvalidStateException;
116119
}
117120

118-
$response = $this->send(new GetAccessTokenRequest($code, $this->oauthConfig()));
121+
$request = new GetAccessTokenRequest($code, $this->oauthConfig());
122+
123+
$request = $this->oauthConfig()->invokeRequestModifier($request);
124+
125+
if (is_callable($requestModifier)) {
126+
$requestModifier($request);
127+
}
128+
129+
$response = $this->send($request);
119130

120131
if ($returnResponse === true) {
121132
return $response;
@@ -129,15 +140,18 @@ public function getAccessToken(string $code, string $state = null, string $expec
129140
/**
130141
* Refresh the access token.
131142
*
143+
* @template TRequest of \Saloon\Contracts\Request
144+
*
132145
* @param \Saloon\Contracts\OAuthAuthenticator|string $refreshToken
133146
* @param bool $returnResponse
147+
* @param callable(TRequest): (void)|null $requestModifier
134148
* @return \Saloon\Contracts\OAuthAuthenticator|\Saloon\Contracts\Response
135149
* @throws \ReflectionException
136150
* @throws \Saloon\Exceptions\InvalidResponseClassException
137151
* @throws \Saloon\Exceptions\OAuthConfigValidationException
138152
* @throws \Saloon\Exceptions\PendingRequestException
139153
*/
140-
public function refreshAccessToken(OAuthAuthenticator|string $refreshToken, bool $returnResponse = false): OAuthAuthenticator|Response
154+
public function refreshAccessToken(OAuthAuthenticator|string $refreshToken, bool $returnResponse = false, ?callable $requestModifier = null): OAuthAuthenticator|Response
141155
{
142156
$this->oauthConfig()->validate();
143157

@@ -149,7 +163,15 @@ public function refreshAccessToken(OAuthAuthenticator|string $refreshToken, bool
149163
$refreshToken = $refreshToken->getRefreshToken();
150164
}
151165

152-
$response = $this->send(new GetRefreshTokenRequest($this->oauthConfig(), $refreshToken));
166+
$request = new GetRefreshTokenRequest($this->oauthConfig(), $refreshToken);
167+
168+
$request = $this->oauthConfig()->invokeRequestModifier($request);
169+
170+
if (is_callable($requestModifier)) {
171+
$requestModifier($request);
172+
}
173+
174+
$response = $this->send($request);
153175

154176
if ($returnResponse === true) {
155177
return $response;
@@ -194,17 +216,26 @@ protected function createOAuthAuthenticator(string $accessToken, string $refresh
194216
/**
195217
* Get the authenticated user.
196218
*
219+
* @template TRequest of \Saloon\Contracts\Request
220+
*
197221
* @param \Saloon\Contracts\OAuthAuthenticator $oauthAuthenticator
222+
* @param callable(TRequest): (void)|null $requestModifier
198223
* @return \Saloon\Contracts\Response
199224
* @throws \ReflectionException
200225
* @throws \Saloon\Exceptions\InvalidResponseClassException
201226
* @throws \Saloon\Exceptions\PendingRequestException
202227
*/
203-
public function getUser(OAuthAuthenticator $oauthAuthenticator): Response
228+
public function getUser(OAuthAuthenticator $oauthAuthenticator, ?callable $requestModifier = null): Response
204229
{
205-
return $this->send(
206-
GetUserRequest::make($this->oauthConfig())->authenticate($oauthAuthenticator)
207-
);
230+
$request = GetUserRequest::make($this->oauthConfig())->authenticate($oauthAuthenticator);
231+
232+
if (is_callable($requestModifier)) {
233+
$requestModifier($request);
234+
}
235+
236+
$request = $this->oauthConfig()->invokeRequestModifier($request);
237+
238+
return $this->send($request);
208239
}
209240

210241
/**

tests/Feature/Oauth2/AuthCodeFlowConnectorTest.php

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55
use Saloon\Helpers\Str;
66
use Saloon\Helpers\Date;
77
use Saloon\Http\Response;
8+
use Saloon\Contracts\Request;
89
use Saloon\Http\Faking\MockClient;
910
use Saloon\Http\Faking\MockResponse;
11+
use Saloon\Http\OAuth2\GetUserRequest;
1012
use Saloon\Exceptions\InvalidStateException;
13+
use Saloon\Http\OAuth2\GetAccessTokenRequest;
1114
use Saloon\Http\Auth\AccessTokenAuthenticator;
15+
use Saloon\Http\OAuth2\GetRefreshTokenRequest;
1216
use Saloon\Tests\Fixtures\Connectors\OAuth2Connector;
1317
use Saloon\Tests\Fixtures\Authenticators\CustomOAuthAuthenticator;
1418
use Saloon\Tests\Fixtures\Connectors\CustomResponseOAuth2Connector;
@@ -73,6 +77,29 @@
7377
expect($authenticator->getExpiresAt())->toBeInstanceOf(DateTimeImmutable::class);
7478
});
7579

80+
test('you can tap into the access token request and modify it', function () {
81+
$mockClient = new MockClient([
82+
MockResponse::make(['access_token' => 'access', 'refresh_token' => 'refresh', 'expires_in' => 3600], 200),
83+
]);
84+
85+
$connector = new OAuth2Connector;
86+
87+
$connector->withMockClient($mockClient);
88+
89+
$authenticator = $connector->getAccessToken('code', requestModifier: function (Request $request) {
90+
$request->query()->add('yee', 'haw');
91+
});
92+
93+
expect($authenticator)->toBeInstanceOf(AccessTokenAuthenticator::class);
94+
expect($authenticator->getAccessToken())->toEqual('access');
95+
expect($authenticator->getRefreshToken())->toEqual('refresh');
96+
expect($authenticator->getExpiresAt())->toBeInstanceOf(DateTimeImmutable::class);
97+
98+
$mockClient->assertSentCount(1);
99+
100+
expect($mockClient->getLastPendingRequest()->query()->all())->toEqual(['yee' => 'haw']);
101+
});
102+
76103
test('you can request the original response instead of the authenticator on the create tokens method', function () {
77104
$mockClient = new MockClient([
78105
MockResponse::make(['access_token' => 'access', 'refresh_token' => 'refresh', 'expires_in' => 3600]),
@@ -116,6 +143,31 @@
116143
expect($newAuthenticator->getExpiresAt())->toBeInstanceOf(DateTimeImmutable::class);
117144
});
118145

146+
test('you can tap into the refresh token request', function () {
147+
$mockClient = new MockClient([
148+
MockResponse::make(['access_token' => 'access-new', 'refresh_token' => 'refresh-new', 'expires_in' => 3600]),
149+
]);
150+
151+
$connector = new OAuth2Connector;
152+
153+
$connector->withMockClient($mockClient);
154+
155+
$authenticator = new AccessTokenAuthenticator('access', 'refresh', Date::now()->addSeconds(3600)->toDateTime());
156+
157+
$newAuthenticator = $connector->refreshAccessToken($authenticator, requestModifier: function (Request $request) {
158+
$request->query()->add('yee', 'haw');
159+
});
160+
161+
expect($newAuthenticator)->toBeInstanceOf(AccessTokenAuthenticator::class);
162+
expect($newAuthenticator->getAccessToken())->toEqual('access-new');
163+
expect($newAuthenticator->getRefreshToken())->toEqual('refresh-new');
164+
expect($newAuthenticator->getExpiresAt())->toBeInstanceOf(DateTimeImmutable::class);
165+
166+
$mockClient->assertSentCount(1);
167+
168+
expect($mockClient->getLastPendingRequest()->query()->all())->toEqual(['yee' => 'haw']);
169+
});
170+
119171
test('the refreshAccessToken method throws an exception if you provide it an authenticator that is not refreshable', function () {
120172
$mockClient = new MockClient([
121173
MockResponse::make(['access_token' => 'access-new', 'refresh_token' => 'refresh-new', 'expires_in' => 3600]),
@@ -173,6 +225,33 @@
173225
]);
174226
});
175227

228+
test('you can tap into the the user request', function () {
229+
$mockClient = new MockClient([
230+
MockResponse::make(['user' => 'Sam']),
231+
]);
232+
233+
$connector = new OAuth2Connector;
234+
$connector->withMockClient($mockClient);
235+
236+
$accessToken = new AccessTokenAuthenticator('access', 'refresh', Date::now()->addSeconds(3600)->toDateTime());
237+
238+
$response = $connector->getUser($accessToken, function (Request $request) {
239+
$request->query()->add('yee', 'haw');
240+
});
241+
242+
expect($response)->toBeInstanceOf(Response::class);
243+
244+
$pendingRequest = $response->getPendingRequest();
245+
246+
expect($pendingRequest->query()->all())->toEqual(['yee' => 'haw']);
247+
248+
expect($pendingRequest->headers()->all())->toEqual([
249+
'Accept' => 'application/json',
250+
'Authorization' => 'Bearer access',
251+
'Content-Type' => 'application/x-www-form-urlencoded',
252+
]);
253+
});
254+
176255
test('you can customize the oauth authenticator', function () {
177256
$mockClient = new MockClient([
178257
MockResponse::make(['access_token' => 'access-new', 'refresh_token' => 'refresh-new', 'expires_in' => 3600]),
@@ -186,3 +265,61 @@
186265
expect($authenticator)->toBeInstanceOf(CustomOAuthAuthenticator::class);
187266
expect($authenticator->getGreeting())->toEqual('Howdy!');
188267
});
268+
269+
test('you can register a global request modifier that is called on every step of the OAuth2 process', function () {
270+
$mockClient = new MockClient([
271+
GetAccessTokenRequest::class => MockResponse::make(['access_token' => 'access', 'refresh_token' => 'refresh', 'expires_in' => 3600], 200),
272+
GetRefreshTokenRequest::class => MockResponse::make(['access_token' => 'access-new', 'refresh_token' => 'refresh-new', 'expires_in' => 3600]),
273+
GetUserRequest::class => MockResponse::make(['user' => 'Sam']),
274+
]);
275+
276+
$connector = new OAuth2Connector;
277+
$requests = [];
278+
279+
$connector->oauthConfig()->setRequestModifier(function (Request $request) use (&$requests) {
280+
$requests[] = $request::class;
281+
282+
match ($request::class) {
283+
GetAccessTokenRequest::class => $request->query()->add('request', 'access'),
284+
GetRefreshTokenRequest::class => $request->query()->add('request', 'refresh'),
285+
GetUserRequest::class => $request->query()->add('request', 'user'),
286+
};
287+
});
288+
289+
$connector->withMockClient($mockClient);
290+
291+
$authenticator = $connector->getAccessToken('code');
292+
293+
expect($authenticator)->toBeInstanceOf(AccessTokenAuthenticator::class);
294+
expect($authenticator->getAccessToken())->toEqual('access');
295+
expect($authenticator->getRefreshToken())->toEqual('refresh');
296+
expect($authenticator->getExpiresAt())->toBeInstanceOf(DateTimeImmutable::class);
297+
expect($mockClient->getLastPendingRequest()->query()->all())->toEqual(['request' => 'access']);
298+
299+
$newAuthenticator = $connector->refreshAccessToken($authenticator);
300+
301+
expect($newAuthenticator)->toBeInstanceOf(AccessTokenAuthenticator::class);
302+
expect($newAuthenticator->getAccessToken())->toEqual('access-new');
303+
expect($newAuthenticator->getRefreshToken())->toEqual('refresh-new');
304+
expect($newAuthenticator->getExpiresAt())->toBeInstanceOf(DateTimeImmutable::class);
305+
expect($mockClient->getLastPendingRequest()->query()->all())->toEqual(['request' => 'refresh']);
306+
307+
$response = $connector->getUser($newAuthenticator);
308+
309+
expect($response)->toBeInstanceOf(Response::class);
310+
expect($mockClient->getLastPendingRequest()->query()->all())->toEqual(['request' => 'user']);
311+
312+
$pendingRequest = $response->getPendingRequest();
313+
314+
expect($pendingRequest->headers()->all())->toEqual([
315+
'Accept' => 'application/json',
316+
'Authorization' => 'Bearer access-new',
317+
'Content-Type' => 'application/x-www-form-urlencoded',
318+
]);
319+
320+
expect($requests)->toEqual([
321+
GetAccessTokenRequest::class,
322+
GetRefreshTokenRequest::class,
323+
GetUserRequest::class,
324+
]);
325+
});

0 commit comments

Comments
 (0)