From 404b345775c91cd04443e77e8d6164b9727a3448 Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Mon, 22 Oct 2018 16:47:22 +0200 Subject: [PATCH] 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: https://github.com/laravel/passport/issues/143 --- src/Guards/TokenGuard.php | 123 +++++++++++++++++++++++++++----------- tests/TokenGuardTest.php | 115 ++++++++++++++++++++++++++++++++++- 2 files changed, 199 insertions(+), 39 deletions(-) diff --git a/src/Guards/TokenGuard.php b/src/Guards/TokenGuard.php index 5bc0491f1..af94f679b 100644 --- a/src/Guards/TokenGuard.php +++ b/src/Guards/TokenGuard.php @@ -92,6 +92,29 @@ public function user(Request $request) } } + /** + * Get the client for the incoming request. + * + * @param \Illuminate\Http\Request $request + * @return mixed + */ + public function client(Request $request) + { + if ($request->bearerToken()) { + if (! $psr = $this->getPsrRequestViaBearerToken($request)) { + return; + } + + return $this->clients->findActive( + $psr->getAttribute('oauth_client_id') + ); + } elseif ($request->cookie(Passport::cookie())) { + if ($token = $this->getTokenViaCookie($request)) { + return $this->clients->findActive($token['aud']); + } + } + } + /** * Authenticate the incoming request via the Bearer token. * @@ -100,44 +123,57 @@ public function user(Request $request) */ protected function authenticateViaBearerToken($request) { - // First, we will convert the Symfony request to a PSR-7 implementation which will - // be compatible with the base OAuth2 library. The Symfony bridge can perform a - // conversion for us to a Zend Diactoros implementation of the PSR-7 request. - $psr = (new DiactorosFactory)->createRequest($request); + if (! $psr = $this->getPsrRequestViaBearerToken($request)) { + return; + } - try { - $psr = $this->server->validateAuthenticatedRequest($psr); + // If the access token is valid we will retrieve the user according to the user ID + // associated with the token. We will use the provider implementation which may + // be used to retrieve users from Eloquent. Next, we'll be ready to continue. + $user = $this->provider->retrieveById( + $psr->getAttribute('oauth_user_id') + ); - // If the access token is valid we will retrieve the user according to the user ID - // associated with the token. We will use the provider implementation which may - // be used to retrieve users from Eloquent. Next, we'll be ready to continue. - $user = $this->provider->retrieveById( - $psr->getAttribute('oauth_user_id') - ); + if (! $user) { + return; + } - if (! $user) { - return; - } + // Next, we will assign a token instance to this user which the developers may use + // to determine if the token has a given scope, etc. This will be useful during + // authorization such as within the developer's Laravel model policy classes. + $token = $this->tokens->find( + $psr->getAttribute('oauth_access_token_id') + ); - // Next, we will assign a token instance to this user which the developers may use - // to determine if the token has a given scope, etc. This will be useful during - // authorization such as within the developer's Laravel model policy classes. - $token = $this->tokens->find( - $psr->getAttribute('oauth_access_token_id') - ); + $clientId = $psr->getAttribute('oauth_client_id'); - $clientId = $psr->getAttribute('oauth_client_id'); + // Finally, we will verify if the client that issued this token is still valid and + // its tokens may still be used. If not, we will bail out since we don't want a + // user to be able to send access tokens for deleted or revoked applications. + if ($this->clients->revoked($clientId)) { + return; + } - // Finally, we will verify if the client that issued this token is still valid and - // its tokens may still be used. If not, we will bail out since we don't want a - // user to be able to send access tokens for deleted or revoked applications. - if ($this->clients->revoked($clientId)) { - return; - } + return $token ? $user->withAccessToken($token) : null; + } + + /** + * Authenticate and get the incoming PSR-7 request via the Bearer token. + * + * @param \Illuminate\Http\Request $request + * @return \Psr\Http\Message\ServerRequestInterface + */ + protected function getPsrRequestViaBearerToken($request) + { + // First, we will convert the Symfony request to a PSR-7 implementation which will + // be compatible with the base OAuth2 library. The Symfony bridge can perform a + // conversion for us to a Zend Diactoros implementation of the PSR-7 request. + $psr = (new DiactorosFactory)->createRequest($request); - return $token ? $user->withAccessToken($token) : null; + try { + return $this->server->validateAuthenticatedRequest($psr); } catch (OAuthServerException $e) { - $request->headers->set( 'Authorization', '', true ); + $request->headers->set('Authorization', '', true); Container::getInstance()->make( ExceptionHandler::class @@ -152,6 +188,26 @@ protected function authenticateViaBearerToken($request) * @return mixed */ protected function authenticateViaCookie($request) + { + if (! $token = $this->getTokenViaCookie($request)) { + return; + } + + // If this user exists, we will return this user and attach a "transient" token to + // the user model. The transient token assumes it has all scopes since the user + // is physically logged into the application via the application's interface. + if ($user = $this->provider->retrieveById($token['sub'])) { + return $user->withAccessToken(new TransientToken); + } + } + + /** + * Get the token cookie via the incoming request. + * + * @param \Illuminate\Http\Request $request + * @return mixed + */ + protected function getTokenViaCookie($request) { // If we need to retrieve the token from the cookie, it'll be encrypted so we must // first decrypt the cookie and then attempt to find the token value within the @@ -170,12 +226,7 @@ protected function authenticateViaCookie($request) return; } - // If this user exists, we will return this user and attach a "transient" token to - // the user model. The transient token assumes it has all scopes since the user - // is physically logged into the application via the application's interface. - if ($user = $this->provider->retrieveById($token['sub'])) { - return $user->withAccessToken(new TransientToken); - } + return $token; } /** diff --git a/tests/TokenGuardTest.php b/tests/TokenGuardTest.php index c075bdd15..279b63266 100644 --- a/tests/TokenGuardTest.php +++ b/tests/TokenGuardTest.php @@ -104,7 +104,9 @@ public function test_users_may_be_retrieved_from_cookies() $request->headers->set('X-CSRF-TOKEN', 'token'); $request->cookies->set('laravel_token', $encrypter->encrypt(JWT::encode([ - 'sub' => 1, 'csrf' => 'token', + 'sub' => 1, + 'aud' => 1, + 'csrf' => 'token', 'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(), ], str_repeat('a', 16)), false) ); @@ -130,7 +132,9 @@ public function test_cookie_xsrf_is_verified_against_header() $request->headers->set('X-CSRF-TOKEN', 'wrong_token'); $request->cookies->set('laravel_token', $encrypter->encrypt(JWT::encode([ - 'sub' => 1, 'csrf' => 'token', + 'sub' => 1, + 'aud' => 1, + 'csrf' => 'token', 'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(), ], str_repeat('a', 16))) ); @@ -154,7 +158,9 @@ public function test_expired_cookies_may_not_be_used() $request->headers->set('X-CSRF-TOKEN', 'token'); $request->cookies->set('laravel_token', $encrypter->encrypt(JWT::encode([ - 'sub' => 1, 'csrf' => 'token', + 'sub' => 1, + 'aud' => 1, + 'csrf' => 'token', 'expiry' => Carbon::now()->subMinutes(10)->getTimestamp(), ], str_repeat('a', 16))) ); @@ -180,6 +186,7 @@ public function test_csrf_check_can_be_disabled() $request->cookies->set('laravel_token', $encrypter->encrypt(JWT::encode([ 'sub' => 1, + 'aud' => 1, 'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(), ], str_repeat('a', 16)), false) ); @@ -190,9 +197,111 @@ public function test_csrf_check_can_be_disabled() $this->assertEquals($expectedUser, $user); } + + public function test_client_can_be_pulled_via_bearer_token() + { + $resourceServer = Mockery::mock('League\OAuth2\Server\ResourceServer'); + $userProvider = Mockery::mock('Illuminate\Contracts\Auth\UserProvider'); + $tokens = Mockery::mock('Laravel\Passport\TokenRepository'); + $clients = Mockery::mock('Laravel\Passport\ClientRepository'); + $encrypter = Mockery::mock('Illuminate\Contracts\Encryption\Encrypter'); + + $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter); + + $request = Request::create('/'); + $request->headers->set('Authorization', 'Bearer token'); + + $resourceServer->shouldReceive('validateAuthenticatedRequest')->andReturn($psr = Mockery::mock()); + $psr->shouldReceive('getAttribute')->with('oauth_client_id')->andReturn(1); + $clients->shouldReceive('findActive')->with(1)->andReturn(new TokenGuardTestClient); + + $client = $guard->client($request); + + $this->assertInstanceOf('TokenGuardTestClient', $client); + } + + public function test_no_client_is_returned_when_oauth_throws_exception() + { + $container = new Container; + Container::setInstance($container); + $container->instance('Illuminate\Contracts\Debug\ExceptionHandler', $handler = Mockery::mock()); + $handler->shouldReceive('report')->once()->with(Mockery::type('League\OAuth2\Server\Exception\OAuthServerException')); + + $resourceServer = Mockery::mock('League\OAuth2\Server\ResourceServer'); + $userProvider = Mockery::mock('Illuminate\Contracts\Auth\UserProvider'); + $tokens = Mockery::mock('Laravel\Passport\TokenRepository'); + $clients = Mockery::mock('Laravel\Passport\ClientRepository'); + $encrypter = Mockery::mock('Illuminate\Contracts\Encryption\Encrypter'); + + $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter); + + $request = Request::create('/'); + $request->headers->set('Authorization', 'Bearer token'); + + $resourceServer->shouldReceive('validateAuthenticatedRequest')->andThrow( + new League\OAuth2\Server\Exception\OAuthServerException('message', 500, 'error type') + ); + + $this->assertNull($guard->client($request)); + + // Assert that `validateAuthenticatedRequest` isn't called twice on failure. + $this->assertNull($guard->client($request)); + } + + public function test_null_is_returned_if_no_client_is_found() + { + $resourceServer = Mockery::mock('League\OAuth2\Server\ResourceServer'); + $userProvider = Mockery::mock('Illuminate\Contracts\Auth\UserProvider'); + $tokens = Mockery::mock('Laravel\Passport\TokenRepository'); + $clients = Mockery::mock('Laravel\Passport\ClientRepository'); + $encrypter = Mockery::mock('Illuminate\Contracts\Encryption\Encrypter'); + + $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter); + + $request = Request::create('/'); + $request->headers->set('Authorization', 'Bearer token'); + + $resourceServer->shouldReceive('validateAuthenticatedRequest')->andReturn($psr = Mockery::mock()); + $psr->shouldReceive('getAttribute')->with('oauth_client_id')->andReturn(1); + $clients->shouldReceive('findActive')->with(1)->andReturn(null); + + $this->assertNull($guard->client($request)); + } + + public function test_clients_may_be_retrieved_from_cookies() + { + $resourceServer = Mockery::mock('League\OAuth2\Server\ResourceServer'); + $userProvider = Mockery::mock('Illuminate\Contracts\Auth\UserProvider'); + $tokens = Mockery::mock('Laravel\Passport\TokenRepository'); + $clients = Mockery::mock('Laravel\Passport\ClientRepository'); + $encrypter = new Illuminate\Encryption\Encrypter(str_repeat('a', 16)); + + $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter); + + $request = Request::create('/'); + $request->headers->set('X-CSRF-TOKEN', 'token'); + $request->cookies->set('laravel_token', + $encrypter->encrypt(JWT::encode([ + 'sub' => 1, + 'aud' => 1, + 'csrf' => 'token', + 'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(), + ], str_repeat('a', 16)), false) + ); + + $clients->shouldReceive('findActive')->with(1)->andReturn($expectedClient = new TokenGuardTestClient); + + $client = $guard->client($request); + + $this->assertEquals($expectedClient, $client); + } } class TokenGuardTestUser { use Laravel\Passport\HasApiTokens; } + +class TokenGuardTestClient +{ +}