diff --git a/database/migrations/2016_06_01_000004_create_oauth_clients_table.php b/database/migrations/2016_06_01_000004_create_oauth_clients_table.php index 1dc541a31..f0884ee7b 100644 --- a/database/migrations/2016_06_01_000004_create_oauth_clients_table.php +++ b/database/migrations/2016_06_01_000004_create_oauth_clients_table.php @@ -18,6 +18,7 @@ public function up() $table->unsignedBigInteger('user_id')->nullable()->index(); $table->string('name'); $table->string('secret', 100)->nullable(); + $table->string('provider')->nullable(); $table->text('redirect'); $table->boolean('personal_access_client'); $table->boolean('password_client'); diff --git a/src/Bridge/Client.php b/src/Bridge/Client.php index 43eec2029..70539ac80 100644 --- a/src/Bridge/Client.php +++ b/src/Bridge/Client.php @@ -16,6 +16,13 @@ class Client implements ClientEntityInterface */ protected $identifier; + /** + * The client's provider. + * + * @var string + */ + public $provider; + /** * Create a new client instance. * @@ -23,15 +30,17 @@ class Client implements ClientEntityInterface * @param string $name * @param string $redirectUri * @param bool $isConfidential + * @param string|null $provider * @return void */ - public function __construct($identifier, $name, $redirectUri, $isConfidential = false) + public function __construct($identifier, $name, $redirectUri, $isConfidential = false, $provider = null) { $this->setIdentifier((string) $identifier); $this->name = $name; $this->isConfidential = $isConfidential; $this->redirectUri = explode(',', $redirectUri); + $this->provider = $provider; } /** diff --git a/src/Bridge/ClientRepository.php b/src/Bridge/ClientRepository.php index 68e891363..325fdf55f 100644 --- a/src/Bridge/ClientRepository.php +++ b/src/Bridge/ClientRepository.php @@ -38,7 +38,11 @@ public function getClientEntity($clientIdentifier) } return new Client( - $clientIdentifier, $record->name, $record->redirect, $record->confidential() + $clientIdentifier, + $record->name, + $record->redirect, + $record->confidential(), + $record->provider ); } diff --git a/src/Bridge/UserRepository.php b/src/Bridge/UserRepository.php index 332afbbf6..b73404ad2 100644 --- a/src/Bridge/UserRepository.php +++ b/src/Bridge/UserRepository.php @@ -32,7 +32,7 @@ public function __construct(Hasher $hasher) */ public function getUserEntityByUserCredentials($username, $password, $grantType, ClientEntityInterface $clientEntity) { - $provider = config('auth.guards.api.provider'); + $provider = $clientEntity->provider ?: config('auth.guards.api.provider'); if (is_null($model = config('auth.providers.'.$provider.'.model'))) { throw new RuntimeException('Unable to determine authentication model from configuration.'); diff --git a/src/Client.php b/src/Client.php index d860d7a32..f4424c0cc 100644 --- a/src/Client.php +++ b/src/Client.php @@ -55,8 +55,10 @@ class Client extends Model */ public function user() { + $provider = $this->provider ?: config('auth.guards.api.provider'); + return $this->belongsTo( - config('auth.providers.'.config('auth.guards.api.provider').'.model') + config("auth.providers.{$provider}.model") ); } diff --git a/src/ClientRepository.php b/src/ClientRepository.php index a8b9431ef..e04dfc0d8 100644 --- a/src/ClientRepository.php +++ b/src/ClientRepository.php @@ -104,17 +104,19 @@ public function personalAccessClient() * @param int $userId * @param string $name * @param string $redirect + * @param string|null $provider * @param bool $personalAccess * @param bool $password * @param bool $confidential * @return \Laravel\Passport\Client */ - public function create($userId, $name, $redirect, $personalAccess = false, $password = false, $confidential = true) + public function create($userId, $name, $redirect, $provider = null, $personalAccess = false, $password = false, $confidential = true) { $client = Passport::client()->forceFill([ 'user_id' => $userId, 'name' => $name, 'secret' => ($confidential || $personalAccess) ? Str::random(40) : null, + 'provider' => $provider, 'redirect' => $redirect, 'personal_access_client' => $personalAccess, 'password_client' => $password, @@ -136,7 +138,7 @@ public function create($userId, $name, $redirect, $personalAccess = false, $pass */ public function createPersonalAccessClient($userId, $name, $redirect) { - return tap($this->create($userId, $name, $redirect, true), function ($client) { + return tap($this->create($userId, $name, $redirect, null, true), function ($client) { $accessClient = Passport::personalAccessClient(); $accessClient->client_id = $client->id; $accessClient->save(); @@ -149,11 +151,12 @@ public function createPersonalAccessClient($userId, $name, $redirect) * @param int $userId * @param string $name * @param string $redirect + * @param string|null $provider * @return \Laravel\Passport\Client */ - public function createPasswordGrantClient($userId, $name, $redirect) + public function createPasswordGrantClient($userId, $name, $redirect, $provider = null) { - return $this->create($userId, $name, $redirect, false, true); + return $this->create($userId, $name, $redirect, $provider, false, true); } /** diff --git a/src/Console/ClientCommand.php b/src/Console/ClientCommand.php index 5ae9bd035..a312a401e 100644 --- a/src/Console/ClientCommand.php +++ b/src/Console/ClientCommand.php @@ -18,6 +18,7 @@ class ClientCommand extends Command {--password : Create a password grant client} {--client : Create a client credentials grant client} {--name= : The name of the client} + {--provider= : The name of the user provider} {--redirect_uri= : The URI to redirect to after authorization } {--user_id= : The user ID the client should be assigned to } {--public : Create a public client (Auth code grant type only) }'; @@ -83,8 +84,16 @@ protected function createPasswordClient(ClientRepository $clients) config('app.name').' Password Grant Client' ); + $providers = array_keys(config('auth.providers')); + + $provider = $this->option('provider') ?: $this->choice( + 'Which user provider should this client use to retrieve users?', + $providers, + in_array('users', $providers) ? 'users' : null + ); + $client = $clients->createPasswordGrantClient( - null, $name, 'http://localhost' + null, $name, 'http://localhost', $provider ); $this->info('Password grant client created successfully.'); @@ -136,7 +145,7 @@ protected function createAuthCodeClient(ClientRepository $clients) ); $client = $clients->create( - $userId, $name, $redirect, false, false, ! $this->option('public') + $userId, $name, $redirect, null, false, false, ! $this->option('public') ); $this->info('New client created successfully.'); diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index 89b80440d..3498cb986 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -29,8 +29,10 @@ class InstallCommand extends Command */ public function handle() { + $provider = in_array('users', array_keys(config('auth.providers'))) ? 'users' : null; + $this->call('passport:keys', ['--force' => $this->option('force'), '--length' => $this->option('length')]); $this->call('passport:client', ['--personal' => true, '--name' => config('app.name').' Personal Access Client']); - $this->call('passport:client', ['--password' => true, '--name' => config('app.name').' Password Grant Client']); + $this->call('passport:client', ['--password' => true, '--name' => config('app.name').' Password Grant Client', '--provider' => $provider]); } } diff --git a/src/Guards/TokenGuard.php b/src/Guards/TokenGuard.php index 23244366a..f30468310 100644 --- a/src/Guards/TokenGuard.php +++ b/src/Guards/TokenGuard.php @@ -5,7 +5,6 @@ use Exception; use Firebase\JWT\JWT; use Illuminate\Container\Container; -use Illuminate\Contracts\Auth\UserProvider; use Illuminate\Contracts\Debug\ExceptionHandler; use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Cookie\Middleware\EncryptCookies; @@ -16,6 +15,7 @@ use Laminas\Diactoros\UploadedFileFactory; use Laravel\Passport\ClientRepository; use Laravel\Passport\Passport; +use Laravel\Passport\PassportUserProvider; use Laravel\Passport\TokenRepository; use Laravel\Passport\TransientToken; use League\OAuth2\Server\Exception\OAuthServerException; @@ -34,7 +34,7 @@ class TokenGuard /** * The user provider implementation. * - * @var \Illuminate\Contracts\Auth\UserProvider + * @var \Laravel\Passport\PassportUserProvider */ protected $provider; @@ -63,18 +63,19 @@ class TokenGuard * Create a new token guard instance. * * @param \League\OAuth2\Server\ResourceServer $server - * @param \Illuminate\Contracts\Auth\UserProvider $provider + * @param \Laravel\Passport\PassportUserProvider $provider * @param \Laravel\Passport\TokenRepository $tokens * @param \Laravel\Passport\ClientRepository $clients * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter * @return void */ - public function __construct(ResourceServer $server, - UserProvider $provider, - TokenRepository $tokens, - ClientRepository $clients, - Encrypter $encrypter) - { + public function __construct( + ResourceServer $server, + PassportUserProvider $provider, + TokenRepository $tokens, + ClientRepository $clients, + Encrypter $encrypter + ) { $this->server = $server; $this->tokens = $tokens; $this->clients = $clients; @@ -82,6 +83,23 @@ public function __construct(ResourceServer $server, $this->encrypter = $encrypter; } + /** + * Determine if the requested provider matches the client's provider. + * + * @param \Illuminate\Http\Request $request + * @return bool + */ + protected function hasValidProvider(Request $request) + { + $client = $this->client($request); + + if ($client && ! $client->provider) { + return true; + } + + return $client && $client->provider === $this->provider->getProviderName(); + } + /** * Get the user for the incoming request. * @@ -90,6 +108,10 @@ public function __construct(ResourceServer $server, */ public function user(Request $request) { + if (! $this->hasValidProvider($request)) { + return; + } + if ($request->bearerToken()) { return $this->authenticateViaBearerToken($request); } elseif ($request->cookie(Passport::cookie())) { diff --git a/src/Http/Middleware/CheckCredentials.php b/src/Http/Middleware/CheckCredentials.php index 8c2323c0d..c2efdb8e5 100644 --- a/src/Http/Middleware/CheckCredentials.php +++ b/src/Http/Middleware/CheckCredentials.php @@ -101,7 +101,7 @@ protected function validate($psr, $scopes) abstract protected function validateCredentials($token); /** - * Validate token credentials. + * Validate token scopes. * * @param \Laravel\Passport\Token $token * @param array $scopes diff --git a/src/PassportServiceProvider.php b/src/PassportServiceProvider.php index 1f3bd4040..4f1b34dc5 100644 --- a/src/PassportServiceProvider.php +++ b/src/PassportServiceProvider.php @@ -276,7 +276,7 @@ protected function makeGuard(array $config) return new RequestGuard(function ($request) use ($config) { return (new TokenGuard( $this->app->make(ResourceServer::class), - Auth::createUserProvider($config['provider']), + new PassportUserProvider(Auth::createUserProvider($config['provider']), $config['provider']), $this->app->make(TokenRepository::class), $this->app->make(ClientRepository::class), $this->app->make('encrypter') diff --git a/src/PassportUserProvider.php b/src/PassportUserProvider.php new file mode 100644 index 000000000..b94d82fa0 --- /dev/null +++ b/src/PassportUserProvider.php @@ -0,0 +1,86 @@ +provider = $provider; + $this->providerName = $providerName; + } + + /** + * {@inheritdoc} + */ + public function retrieveById($identifier) + { + return $this->provider->retrieveById($identifier); + } + + /** + * {@inheritdoc} + */ + public function retrieveByToken($identifier, $token) + { + return $this->provider->retrieveByToken($identifier, $token); + } + + /** + * {@inheritdoc} + */ + public function updateRememberToken(Authenticatable $user, $token) + { + $this->provider->updateRememberToken($user, $token); + } + + /** + * {@inheritdoc} + */ + public function retrieveByCredentials(array $credentials) + { + return $this->provider->retrieveByCredentials($credentials); + } + + /** + * {@inheritdoc} + */ + public function validateCredentials(Authenticatable $user, array $credentials) + { + return $this->provider->validateCredentials($user, $credentials); + } + + /** + * Get the name of the user provider. + * + * @return string + */ + public function getProviderName() + { + return $this->providerName; + } +} diff --git a/tests/BridgeClientRepositoryTest.php b/tests/BridgeClientRepositoryTest.php index 3c9ba6391..d648ed975 100644 --- a/tests/BridgeClientRepositoryTest.php +++ b/tests/BridgeClientRepositoryTest.php @@ -194,6 +194,8 @@ class BridgeClientRepositoryTestClientStub public $password_client = false; + public $provider = null; + public $grant_types; public function firstParty() diff --git a/tests/TokenGuardTest.php b/tests/TokenGuardTest.php index 3940c9a84..2b679a558 100644 --- a/tests/TokenGuardTest.php +++ b/tests/TokenGuardTest.php @@ -5,7 +5,6 @@ use Carbon\Carbon; use Firebase\JWT\JWT; use Illuminate\Container\Container; -use Illuminate\Contracts\Auth\UserProvider; use Illuminate\Contracts\Debug\ExceptionHandler; use Illuminate\Encryption\Encrypter; use Illuminate\Http\Request; @@ -13,6 +12,7 @@ use Laravel\Passport\Guards\TokenGuard; use Laravel\Passport\HasApiTokens; use Laravel\Passport\Passport; +use Laravel\Passport\PassportUserProvider; use Laravel\Passport\TokenRepository; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\ResourceServer; @@ -30,7 +30,7 @@ protected function tearDown(): void public function test_user_can_be_pulled_via_bearer_token() { $resourceServer = m::mock(ResourceServer::class); - $userProvider = m::mock(UserProvider::class); + $userProvider = m::mock(PassportUserProvider::class); $tokens = m::mock(TokenRepository::class); $clients = m::mock(ClientRepository::class); $encrypter = m::mock(Encrypter::class); @@ -45,8 +45,10 @@ public function test_user_can_be_pulled_via_bearer_token() $psr->shouldReceive('getAttribute')->with('oauth_client_id')->andReturn(1); $psr->shouldReceive('getAttribute')->with('oauth_access_token_id')->andReturn('token'); $userProvider->shouldReceive('retrieveById')->with(1)->andReturn(new TokenGuardTestUser); + $userProvider->shouldReceive('getProviderName')->andReturn(null); $tokens->shouldReceive('find')->once()->with('token')->andReturn($token = m::mock()); $clients->shouldReceive('revoked')->with(1)->andReturn(false); + $clients->shouldReceive('findActive')->with(1)->andReturn(new TokenGuardTestClient); $user = $guard->user($request); @@ -62,7 +64,7 @@ public function test_no_user_is_returned_when_oauth_throws_exception() $handler->shouldReceive('report')->once()->with(m::type(OAuthServerException::class)); $resourceServer = m::mock(ResourceServer::class); - $userProvider = m::mock(UserProvider::class); + $userProvider = m::mock(PassportUserProvider::class); $tokens = m::mock(TokenRepository::class); $clients = m::mock(ClientRepository::class); $encrypter = m::mock(Encrypter::class); @@ -85,11 +87,15 @@ public function test_no_user_is_returned_when_oauth_throws_exception() public function test_null_is_returned_if_no_user_is_found() { $resourceServer = m::mock(ResourceServer::class); - $userProvider = m::mock(UserProvider::class); + $userProvider = m::mock(PassportUserProvider::class); $tokens = m::mock(TokenRepository::class); $clients = m::mock(ClientRepository::class); $encrypter = m::mock(Encrypter::class); + $clients->shouldReceive('findActive') + ->with(1) + ->andReturn(new TokenGuardTestClient); + $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter); $request = Request::create('/'); @@ -97,7 +103,9 @@ public function test_null_is_returned_if_no_user_is_found() $resourceServer->shouldReceive('validateAuthenticatedRequest')->andReturn($psr = m::mock()); $psr->shouldReceive('getAttribute')->with('oauth_user_id')->andReturn(1); + $psr->shouldReceive('getAttribute')->with('oauth_client_id')->andReturn(1); $userProvider->shouldReceive('retrieveById')->with(1)->andReturn(null); + $userProvider->shouldReceive('getProviderName')->andReturn(null); $this->assertNull($guard->user($request)); } @@ -105,11 +113,15 @@ public function test_null_is_returned_if_no_user_is_found() public function test_users_may_be_retrieved_from_cookies_with_csrf_token_header() { $resourceServer = m::mock(ResourceServer::class); - $userProvider = m::mock(UserProvider::class); + $userProvider = m::mock(PassportUserProvider::class); $tokens = m::mock(TokenRepository::class); $clients = m::mock(ClientRepository::class); $encrypter = new Encrypter(str_repeat('a', 16)); + $clients->shouldReceive('findActive') + ->with(1) + ->andReturn(new TokenGuardTestClient); + $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter); $request = Request::create('/'); @@ -124,6 +136,7 @@ public function test_users_may_be_retrieved_from_cookies_with_csrf_token_header( ); $userProvider->shouldReceive('retrieveById')->with(1)->andReturn($expectedUser = new TokenGuardTestUser); + $userProvider->shouldReceive('getProviderName')->andReturn(null); $user = $guard->user($request); @@ -133,11 +146,15 @@ public function test_users_may_be_retrieved_from_cookies_with_csrf_token_header( public function test_users_may_be_retrieved_from_cookies_with_xsrf_token_header() { $resourceServer = m::mock(ResourceServer::class); - $userProvider = m::mock(UserProvider::class); + $userProvider = m::mock(PassportUserProvider::class); $tokens = m::mock(TokenRepository::class); $clients = m::mock(ClientRepository::class); $encrypter = new Encrypter(str_repeat('a', 16)); + $clients->shouldReceive('findActive') + ->with(1) + ->andReturn(new TokenGuardTestClient); + $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter); $request = Request::create('/'); @@ -152,6 +169,7 @@ public function test_users_may_be_retrieved_from_cookies_with_xsrf_token_header( ); $userProvider->shouldReceive('retrieveById')->with(1)->andReturn($expectedUser = new TokenGuardTestUser); + $userProvider->shouldReceive('getProviderName')->andReturn(null); $user = $guard->user($request); @@ -161,7 +179,7 @@ public function test_users_may_be_retrieved_from_cookies_with_xsrf_token_header( public function test_cookie_xsrf_is_verified_against_csrf_token_header() { $resourceServer = m::mock(ResourceServer::class); - $userProvider = m::mock(UserProvider::class); + $userProvider = m::mock(PassportUserProvider::class); $tokens = m::mock(TokenRepository::class); $clients = m::mock(ClientRepository::class); $encrypter = new Encrypter(str_repeat('a', 16)); @@ -187,7 +205,7 @@ public function test_cookie_xsrf_is_verified_against_csrf_token_header() public function test_cookie_xsrf_is_verified_against_xsrf_token_header() { $resourceServer = m::mock(ResourceServer::class); - $userProvider = m::mock(UserProvider::class); + $userProvider = m::mock(PassportUserProvider::class); $tokens = m::mock(TokenRepository::class); $clients = m::mock(ClientRepository::class); $encrypter = new Encrypter(str_repeat('a', 16)); @@ -213,7 +231,7 @@ public function test_cookie_xsrf_is_verified_against_xsrf_token_header() public function test_xsrf_token_cookie_without_a_token_header_is_not_accepted() { $resourceServer = m::mock(ResourceServer::class); - $userProvider = m::mock(UserProvider::class); + $userProvider = m::mock(PassportUserProvider::class); $tokens = m::mock(TokenRepository::class); $clients = m::mock(ClientRepository::class); $encrypter = new Encrypter(str_repeat('a', 16)); @@ -239,7 +257,7 @@ public function test_xsrf_token_cookie_without_a_token_header_is_not_accepted() public function test_expired_cookies_may_not_be_used() { $resourceServer = m::mock(ResourceServer::class); - $userProvider = m::mock(UserProvider::class); + $userProvider = m::mock(PassportUserProvider::class); $tokens = m::mock(TokenRepository::class); $clients = m::mock(ClientRepository::class); $encrypter = new Encrypter(str_repeat('a', 16)); @@ -265,11 +283,15 @@ public function test_expired_cookies_may_not_be_used() public function test_csrf_check_can_be_disabled() { $resourceServer = m::mock(ResourceServer::class); - $userProvider = m::mock(UserProvider::class); + $userProvider = m::mock(PassportUserProvider::class); $tokens = m::mock(TokenRepository::class); $clients = m::mock(ClientRepository::class); $encrypter = new Encrypter(str_repeat('a', 16)); + $clients->shouldReceive('findActive') + ->with(1) + ->andReturn(new TokenGuardTestClient); + $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter); Passport::ignoreCsrfToken(); @@ -284,6 +306,7 @@ public function test_csrf_check_can_be_disabled() ); $userProvider->shouldReceive('retrieveById')->with(1)->andReturn($expectedUser = new TokenGuardTestUser); + $userProvider->shouldReceive('getProviderName')->andReturn(null); $user = $guard->user($request); @@ -293,7 +316,7 @@ public function test_csrf_check_can_be_disabled() public function test_client_can_be_pulled_via_bearer_token() { $resourceServer = m::mock(ResourceServer::class); - $userProvider = m::mock(UserProvider::class); + $userProvider = m::mock(PassportUserProvider::class); $tokens = m::mock(TokenRepository::class); $clients = m::mock(ClientRepository::class); $encrypter = m::mock(Encrypter::class); @@ -320,7 +343,7 @@ public function test_no_client_is_returned_when_oauth_throws_exception() $handler->shouldReceive('report')->once()->with(m::type(OAuthServerException::class)); $resourceServer = m::mock(ResourceServer::class); - $userProvider = m::mock(UserProvider::class); + $userProvider = m::mock(PassportUserProvider::class); $tokens = m::mock(TokenRepository::class); $clients = m::mock(ClientRepository::class); $encrypter = m::mock(Encrypter::class); @@ -343,7 +366,7 @@ public function test_no_client_is_returned_when_oauth_throws_exception() public function test_null_is_returned_if_no_client_is_found() { $resourceServer = m::mock(ResourceServer::class); - $userProvider = m::mock(UserProvider::class); + $userProvider = m::mock(PassportUserProvider::class); $tokens = m::mock(TokenRepository::class); $clients = m::mock(ClientRepository::class); $encrypter = m::mock(Encrypter::class); @@ -363,7 +386,7 @@ public function test_null_is_returned_if_no_client_is_found() public function test_clients_may_be_retrieved_from_cookies() { $resourceServer = m::mock(ResourceServer::class); - $userProvider = m::mock(UserProvider::class); + $userProvider = m::mock(PassportUserProvider::class); $tokens = m::mock(TokenRepository::class); $clients = m::mock(ClientRepository::class); $encrypter = new Encrypter(str_repeat('a', 16)); @@ -396,4 +419,5 @@ class TokenGuardTestUser class TokenGuardTestClient { + public $provider; }