Skip to content

Commit 404b345

Browse files
committed
Add the ability to retrieve current client
This commit adds the ability to retrieve the current client from the request, just like the current user is retrieved. I chose to do this in the TokenGuard class since this class uses the same logic to retrieve the current active user. Usage could be as follows: $client = $tokenGuard->client($request); One of the biggest reasons for adding this is that there isn't any way at the moment to retrieve the current client from the request. By adding this, users can use the client to further perform authorization actions or check the creator of the client and subsequently limit resources bases on either one of those two. This is especially helpful for client credentials grant requests where you simply don't have an active user. This way you can still limit resources if you want based on either the client or its creator. This commit is fully BC. No methods have been renamed, only added. The ones that have been modified still behave in the same way as before but only have some parts extracted to other methods so their code could be re-used. This solves the following long outstanding issue: #143
1 parent c316d8e commit 404b345

File tree

2 files changed

+199
-39
lines changed

2 files changed

+199
-39
lines changed

src/Guards/TokenGuard.php

Lines changed: 87 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,29 @@ public function user(Request $request)
9292
}
9393
}
9494

95+
/**
96+
* Get the client for the incoming request.
97+
*
98+
* @param \Illuminate\Http\Request $request
99+
* @return mixed
100+
*/
101+
public function client(Request $request)
102+
{
103+
if ($request->bearerToken()) {
104+
if (! $psr = $this->getPsrRequestViaBearerToken($request)) {
105+
return;
106+
}
107+
108+
return $this->clients->findActive(
109+
$psr->getAttribute('oauth_client_id')
110+
);
111+
} elseif ($request->cookie(Passport::cookie())) {
112+
if ($token = $this->getTokenViaCookie($request)) {
113+
return $this->clients->findActive($token['aud']);
114+
}
115+
}
116+
}
117+
95118
/**
96119
* Authenticate the incoming request via the Bearer token.
97120
*
@@ -100,44 +123,57 @@ public function user(Request $request)
100123
*/
101124
protected function authenticateViaBearerToken($request)
102125
{
103-
// First, we will convert the Symfony request to a PSR-7 implementation which will
104-
// be compatible with the base OAuth2 library. The Symfony bridge can perform a
105-
// conversion for us to a Zend Diactoros implementation of the PSR-7 request.
106-
$psr = (new DiactorosFactory)->createRequest($request);
126+
if (! $psr = $this->getPsrRequestViaBearerToken($request)) {
127+
return;
128+
}
107129

108-
try {
109-
$psr = $this->server->validateAuthenticatedRequest($psr);
130+
// If the access token is valid we will retrieve the user according to the user ID
131+
// associated with the token. We will use the provider implementation which may
132+
// be used to retrieve users from Eloquent. Next, we'll be ready to continue.
133+
$user = $this->provider->retrieveById(
134+
$psr->getAttribute('oauth_user_id')
135+
);
110136

111-
// If the access token is valid we will retrieve the user according to the user ID
112-
// associated with the token. We will use the provider implementation which may
113-
// be used to retrieve users from Eloquent. Next, we'll be ready to continue.
114-
$user = $this->provider->retrieveById(
115-
$psr->getAttribute('oauth_user_id')
116-
);
137+
if (! $user) {
138+
return;
139+
}
117140

118-
if (! $user) {
119-
return;
120-
}
141+
// Next, we will assign a token instance to this user which the developers may use
142+
// to determine if the token has a given scope, etc. This will be useful during
143+
// authorization such as within the developer's Laravel model policy classes.
144+
$token = $this->tokens->find(
145+
$psr->getAttribute('oauth_access_token_id')
146+
);
121147

122-
// Next, we will assign a token instance to this user which the developers may use
123-
// to determine if the token has a given scope, etc. This will be useful during
124-
// authorization such as within the developer's Laravel model policy classes.
125-
$token = $this->tokens->find(
126-
$psr->getAttribute('oauth_access_token_id')
127-
);
148+
$clientId = $psr->getAttribute('oauth_client_id');
128149

129-
$clientId = $psr->getAttribute('oauth_client_id');
150+
// Finally, we will verify if the client that issued this token is still valid and
151+
// its tokens may still be used. If not, we will bail out since we don't want a
152+
// user to be able to send access tokens for deleted or revoked applications.
153+
if ($this->clients->revoked($clientId)) {
154+
return;
155+
}
130156

131-
// Finally, we will verify if the client that issued this token is still valid and
132-
// its tokens may still be used. If not, we will bail out since we don't want a
133-
// user to be able to send access tokens for deleted or revoked applications.
134-
if ($this->clients->revoked($clientId)) {
135-
return;
136-
}
157+
return $token ? $user->withAccessToken($token) : null;
158+
}
159+
160+
/**
161+
* Authenticate and get the incoming PSR-7 request via the Bearer token.
162+
*
163+
* @param \Illuminate\Http\Request $request
164+
* @return \Psr\Http\Message\ServerRequestInterface
165+
*/
166+
protected function getPsrRequestViaBearerToken($request)
167+
{
168+
// First, we will convert the Symfony request to a PSR-7 implementation which will
169+
// be compatible with the base OAuth2 library. The Symfony bridge can perform a
170+
// conversion for us to a Zend Diactoros implementation of the PSR-7 request.
171+
$psr = (new DiactorosFactory)->createRequest($request);
137172

138-
return $token ? $user->withAccessToken($token) : null;
173+
try {
174+
return $this->server->validateAuthenticatedRequest($psr);
139175
} catch (OAuthServerException $e) {
140-
$request->headers->set( 'Authorization', '', true );
176+
$request->headers->set('Authorization', '', true);
141177

142178
Container::getInstance()->make(
143179
ExceptionHandler::class
@@ -152,6 +188,26 @@ protected function authenticateViaBearerToken($request)
152188
* @return mixed
153189
*/
154190
protected function authenticateViaCookie($request)
191+
{
192+
if (! $token = $this->getTokenViaCookie($request)) {
193+
return;
194+
}
195+
196+
// If this user exists, we will return this user and attach a "transient" token to
197+
// the user model. The transient token assumes it has all scopes since the user
198+
// is physically logged into the application via the application's interface.
199+
if ($user = $this->provider->retrieveById($token['sub'])) {
200+
return $user->withAccessToken(new TransientToken);
201+
}
202+
}
203+
204+
/**
205+
* Get the token cookie via the incoming request.
206+
*
207+
* @param \Illuminate\Http\Request $request
208+
* @return mixed
209+
*/
210+
protected function getTokenViaCookie($request)
155211
{
156212
// If we need to retrieve the token from the cookie, it'll be encrypted so we must
157213
// first decrypt the cookie and then attempt to find the token value within the
@@ -170,12 +226,7 @@ protected function authenticateViaCookie($request)
170226
return;
171227
}
172228

173-
// If this user exists, we will return this user and attach a "transient" token to
174-
// the user model. The transient token assumes it has all scopes since the user
175-
// is physically logged into the application via the application's interface.
176-
if ($user = $this->provider->retrieveById($token['sub'])) {
177-
return $user->withAccessToken(new TransientToken);
178-
}
229+
return $token;
179230
}
180231

181232
/**

tests/TokenGuardTest.php

Lines changed: 112 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,9 @@ public function test_users_may_be_retrieved_from_cookies()
104104
$request->headers->set('X-CSRF-TOKEN', 'token');
105105
$request->cookies->set('laravel_token',
106106
$encrypter->encrypt(JWT::encode([
107-
'sub' => 1, 'csrf' => 'token',
107+
'sub' => 1,
108+
'aud' => 1,
109+
'csrf' => 'token',
108110
'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(),
109111
], str_repeat('a', 16)), false)
110112
);
@@ -130,7 +132,9 @@ public function test_cookie_xsrf_is_verified_against_header()
130132
$request->headers->set('X-CSRF-TOKEN', 'wrong_token');
131133
$request->cookies->set('laravel_token',
132134
$encrypter->encrypt(JWT::encode([
133-
'sub' => 1, 'csrf' => 'token',
135+
'sub' => 1,
136+
'aud' => 1,
137+
'csrf' => 'token',
134138
'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(),
135139
], str_repeat('a', 16)))
136140
);
@@ -154,7 +158,9 @@ public function test_expired_cookies_may_not_be_used()
154158
$request->headers->set('X-CSRF-TOKEN', 'token');
155159
$request->cookies->set('laravel_token',
156160
$encrypter->encrypt(JWT::encode([
157-
'sub' => 1, 'csrf' => 'token',
161+
'sub' => 1,
162+
'aud' => 1,
163+
'csrf' => 'token',
158164
'expiry' => Carbon::now()->subMinutes(10)->getTimestamp(),
159165
], str_repeat('a', 16)))
160166
);
@@ -180,6 +186,7 @@ public function test_csrf_check_can_be_disabled()
180186
$request->cookies->set('laravel_token',
181187
$encrypter->encrypt(JWT::encode([
182188
'sub' => 1,
189+
'aud' => 1,
183190
'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(),
184191
], str_repeat('a', 16)), false)
185192
);
@@ -190,9 +197,111 @@ public function test_csrf_check_can_be_disabled()
190197

191198
$this->assertEquals($expectedUser, $user);
192199
}
200+
201+
public function test_client_can_be_pulled_via_bearer_token()
202+
{
203+
$resourceServer = Mockery::mock('League\OAuth2\Server\ResourceServer');
204+
$userProvider = Mockery::mock('Illuminate\Contracts\Auth\UserProvider');
205+
$tokens = Mockery::mock('Laravel\Passport\TokenRepository');
206+
$clients = Mockery::mock('Laravel\Passport\ClientRepository');
207+
$encrypter = Mockery::mock('Illuminate\Contracts\Encryption\Encrypter');
208+
209+
$guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter);
210+
211+
$request = Request::create('/');
212+
$request->headers->set('Authorization', 'Bearer token');
213+
214+
$resourceServer->shouldReceive('validateAuthenticatedRequest')->andReturn($psr = Mockery::mock());
215+
$psr->shouldReceive('getAttribute')->with('oauth_client_id')->andReturn(1);
216+
$clients->shouldReceive('findActive')->with(1)->andReturn(new TokenGuardTestClient);
217+
218+
$client = $guard->client($request);
219+
220+
$this->assertInstanceOf('TokenGuardTestClient', $client);
221+
}
222+
223+
public function test_no_client_is_returned_when_oauth_throws_exception()
224+
{
225+
$container = new Container;
226+
Container::setInstance($container);
227+
$container->instance('Illuminate\Contracts\Debug\ExceptionHandler', $handler = Mockery::mock());
228+
$handler->shouldReceive('report')->once()->with(Mockery::type('League\OAuth2\Server\Exception\OAuthServerException'));
229+
230+
$resourceServer = Mockery::mock('League\OAuth2\Server\ResourceServer');
231+
$userProvider = Mockery::mock('Illuminate\Contracts\Auth\UserProvider');
232+
$tokens = Mockery::mock('Laravel\Passport\TokenRepository');
233+
$clients = Mockery::mock('Laravel\Passport\ClientRepository');
234+
$encrypter = Mockery::mock('Illuminate\Contracts\Encryption\Encrypter');
235+
236+
$guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter);
237+
238+
$request = Request::create('/');
239+
$request->headers->set('Authorization', 'Bearer token');
240+
241+
$resourceServer->shouldReceive('validateAuthenticatedRequest')->andThrow(
242+
new League\OAuth2\Server\Exception\OAuthServerException('message', 500, 'error type')
243+
);
244+
245+
$this->assertNull($guard->client($request));
246+
247+
// Assert that `validateAuthenticatedRequest` isn't called twice on failure.
248+
$this->assertNull($guard->client($request));
249+
}
250+
251+
public function test_null_is_returned_if_no_client_is_found()
252+
{
253+
$resourceServer = Mockery::mock('League\OAuth2\Server\ResourceServer');
254+
$userProvider = Mockery::mock('Illuminate\Contracts\Auth\UserProvider');
255+
$tokens = Mockery::mock('Laravel\Passport\TokenRepository');
256+
$clients = Mockery::mock('Laravel\Passport\ClientRepository');
257+
$encrypter = Mockery::mock('Illuminate\Contracts\Encryption\Encrypter');
258+
259+
$guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter);
260+
261+
$request = Request::create('/');
262+
$request->headers->set('Authorization', 'Bearer token');
263+
264+
$resourceServer->shouldReceive('validateAuthenticatedRequest')->andReturn($psr = Mockery::mock());
265+
$psr->shouldReceive('getAttribute')->with('oauth_client_id')->andReturn(1);
266+
$clients->shouldReceive('findActive')->with(1)->andReturn(null);
267+
268+
$this->assertNull($guard->client($request));
269+
}
270+
271+
public function test_clients_may_be_retrieved_from_cookies()
272+
{
273+
$resourceServer = Mockery::mock('League\OAuth2\Server\ResourceServer');
274+
$userProvider = Mockery::mock('Illuminate\Contracts\Auth\UserProvider');
275+
$tokens = Mockery::mock('Laravel\Passport\TokenRepository');
276+
$clients = Mockery::mock('Laravel\Passport\ClientRepository');
277+
$encrypter = new Illuminate\Encryption\Encrypter(str_repeat('a', 16));
278+
279+
$guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter);
280+
281+
$request = Request::create('/');
282+
$request->headers->set('X-CSRF-TOKEN', 'token');
283+
$request->cookies->set('laravel_token',
284+
$encrypter->encrypt(JWT::encode([
285+
'sub' => 1,
286+
'aud' => 1,
287+
'csrf' => 'token',
288+
'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(),
289+
], str_repeat('a', 16)), false)
290+
);
291+
292+
$clients->shouldReceive('findActive')->with(1)->andReturn($expectedClient = new TokenGuardTestClient);
293+
294+
$client = $guard->client($request);
295+
296+
$this->assertEquals($expectedClient, $client);
297+
}
193298
}
194299

195300
class TokenGuardTestUser
196301
{
197302
use Laravel\Passport\HasApiTokens;
198303
}
304+
305+
class TokenGuardTestClient
306+
{
307+
}

0 commit comments

Comments
 (0)