From 78a7136d456d2657b326093544fb3fcfb43a0526 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Thu, 30 May 2024 22:08:54 +0330 Subject: [PATCH 01/38] add device code grant --- ...000001_create_oauth_device_codes_table.php | 44 ++++++ resources/views/device.blade.php | 68 +++++++++ routes/web.php | 16 +++ src/Bridge/Client.php | 14 +- src/Bridge/DeviceCode.php | 111 +++++++++++++++ src/Bridge/DeviceCodeRepository.php | 88 ++++++++++++ src/Contracts/DeviceCodeViewResponse.php | 16 +++ src/DeviceCode.php | 76 ++++++++++ .../Controllers/AccessTokenController.php | 2 +- .../ApproveAuthorizationController.php | 2 - .../Controllers/AuthorizationController.php | 2 +- .../DenyAuthorizationController.php | 2 - .../DeviceAuthorizationController.php | 130 ++++++++++++++++++ src/Http/Controllers/DeviceCodeController.php | 48 +++++++ .../RetrievesAuthRequestFromSession.php | 22 +-- .../RetrievesDeviceCodeFromSession.php | 40 ++++++ .../Responses/AuthorizationViewResponse.php | 60 +------- src/Http/Responses/DeviceCodeViewResponse.php | 10 ++ src/Http/Responses/ViewResponsable.php | 67 +++++++++ src/Passport.php | 53 +++++++ src/PassportServiceProvider.php | 30 ++++ tests/Feature/DeviceCodeControllerTest.php | 37 +++++ .../ApproveAuthorizationControllerTest.php | 52 ++++++- .../Unit/DenyAuthorizationControllerTest.php | 31 ++++- 24 files changed, 930 insertions(+), 91 deletions(-) create mode 100644 database/migrations/2024_06_01_000001_create_oauth_device_codes_table.php create mode 100644 resources/views/device.blade.php create mode 100644 src/Bridge/DeviceCode.php create mode 100644 src/Bridge/DeviceCodeRepository.php create mode 100644 src/Contracts/DeviceCodeViewResponse.php create mode 100644 src/DeviceCode.php create mode 100644 src/Http/Controllers/DeviceAuthorizationController.php create mode 100644 src/Http/Controllers/DeviceCodeController.php create mode 100644 src/Http/Controllers/RetrievesDeviceCodeFromSession.php create mode 100644 src/Http/Responses/DeviceCodeViewResponse.php create mode 100644 src/Http/Responses/ViewResponsable.php create mode 100644 tests/Feature/DeviceCodeControllerTest.php diff --git a/database/migrations/2024_06_01_000001_create_oauth_device_codes_table.php b/database/migrations/2024_06_01_000001_create_oauth_device_codes_table.php new file mode 100644 index 000000000..0045bf711 --- /dev/null +++ b/database/migrations/2024_06_01_000001_create_oauth_device_codes_table.php @@ -0,0 +1,44 @@ +char('id', 80)->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->foreignUuid('client_id')->index(); + $table->char('user_code', 8); + $table->text('scopes'); + $table->boolean('revoked'); + $table->dateTime('user_approved_at')->nullable(); + $table->dateTime('last_polled_at')->nullable(); + $table->dateTime('expires_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('oauth_device_codes'); + } + + /** + * Get the migration connection name. + * + * @return string|null + */ + public function getConnection() + { + return $this->connection ?? config('passport.connection'); + } +}; diff --git a/resources/views/device.blade.php b/resources/views/device.blade.php new file mode 100644 index 000000000..712877c22 --- /dev/null +++ b/resources/views/device.blade.php @@ -0,0 +1,68 @@ + + + + + + + + {{ config('app.name') }} - Authorization + + + + + + + +
+
+
+
+
+ Device Authorization +
+
+ +

Enter the code displayed on your device .

+ +
+ +
+ @csrf + + + + +
+
+
+
+
+
+
+ + diff --git a/routes/web.php b/routes/web.php index fa2ecf3fe..679a92dbc 100644 --- a/routes/web.php +++ b/routes/web.php @@ -8,6 +8,12 @@ 'middleware' => 'throttle', ]); +Route::post('/device/code', [ + 'uses' => 'DeviceCodeController@issueDeviceCode', + 'as' => 'device.code', + 'middleware' => 'throttle', +]); + Route::get('/authorize', [ 'uses' => 'AuthorizationController@authorize', 'as' => 'authorizations.authorize', @@ -32,6 +38,16 @@ 'as' => 'authorizations.deny', ]); + Route::get('/device', [ + 'uses' => 'DeviceAuthorizationController@userCode', + 'as' => 'device', + ]); + + Route::get('/device', [ + 'uses' => 'DeviceAuthorizationController@authorize', + 'as' => 'device.authorize', + ]); + Route::get('/tokens', [ 'uses' => 'AuthorizedAccessTokenController@forUser', 'as' => 'tokens.index', diff --git a/src/Bridge/Client.php b/src/Bridge/Client.php index 50b129c90..3c25f5437 100644 --- a/src/Bridge/Client.php +++ b/src/Bridge/Client.php @@ -20,16 +20,22 @@ class Client implements ClientEntityInterface */ public function __construct( string $identifier, - string $name, - string $redirectUri, + ?string $name = null, + ?string $redirectUri = null, bool $isConfidential = false, ?string $provider = null ) { $this->setIdentifier($identifier); - $this->name = $name; + if (! is_null($name)) { + $this->name = $name; + } + + if (! is_null($redirectUri)) { + $this->redirectUri = explode(',', $redirectUri); + } + $this->isConfidential = $isConfidential; - $this->redirectUri = explode(',', $redirectUri); $this->provider = $provider; } } diff --git a/src/Bridge/DeviceCode.php b/src/Bridge/DeviceCode.php new file mode 100644 index 000000000..872d2ad76 --- /dev/null +++ b/src/Bridge/DeviceCode.php @@ -0,0 +1,111 @@ +setIdentifier($identifier); + } + + if (! is_null($userIdentifier)) { + $this->setUserIdentifier($userIdentifier); + } + + if (! is_null($clientIdentifier)) { + $this->setClient(new Client($clientIdentifier)); + } + + foreach ($scopes as $scope) { + $this->addScope(new Scope($scope)); + } + + if ($userApproved) { + $this->traitSetUserApproved($userApproved); + } + + if (! is_null($lastPolledAt)) { + $this->traitSetLastPolledAt($lastPolledAt); + } + + if (! is_null($expiryDateTime)) { + $this->setExpiryDateTime($expiryDateTime); + } + + $this->isUserDirty = false; + $this->isLastPolledAtDirty = false; + } + + /** + * {@inheritdoc} + */ + public function setUserApproved(bool $userApproved): void + { + $this->isUserDirty = true; + + $this->traitSetUserApproved($userApproved); + } + + /** + * {@inheritdoc} + */ + public function setLastPolledAt(DateTimeImmutable $lastPolledAt): void + { + $this->isLastPolledAtDirty = true; + + $this->traitSetLastPolledAt($lastPolledAt); + } + + /** + * Determine if the "user identifier" and "user approved" properties has changed. + */ + public function isUserDirty(): bool + { + return $this->isUserDirty; + } + + /** + * Determine if the "last polled at" property has changed. + */ + public function isLastPolledAtDirty(): bool + { + return $this->isLastPolledAtDirty; + } +} diff --git a/src/Bridge/DeviceCodeRepository.php b/src/Bridge/DeviceCodeRepository.php new file mode 100644 index 000000000..b8ea9b351 --- /dev/null +++ b/src/Bridge/DeviceCodeRepository.php @@ -0,0 +1,88 @@ +isLastPolledAtDirty()) { + Passport::deviceCode()->whereKey($deviceCodeEntity->getIdentifier())->update([ + 'last_polled_at' => $deviceCodeEntity->getLastPolledAt(), + ]); + } elseif ($deviceCodeEntity->isUserDirty()) { + Passport::deviceCode()->whereKey($deviceCodeEntity->getIdentifier())->update([ + 'user_id' => $deviceCodeEntity->getUserIdentifier(), + 'user_approved_at' => $deviceCodeEntity->getUserApproved() ? new DateTime : null, + ]); + } else { + Passport::deviceCode()->forceFill([ + 'id' => $deviceCodeEntity->getIdentifier(), + 'user_id' => null, + 'client_id' => $deviceCodeEntity->getClient()->getIdentifier(), + 'user_code' => $deviceCodeEntity->getUserCode(), + 'scopes' => $this->formatScopesForStorage($deviceCodeEntity->getScopes()), + 'revoked' => false, + 'user_approved_at' => null, + 'last_polled_at' => null, + 'expires_at' => $deviceCodeEntity->getExpiryDateTime(), + ])->save(); + } + } + + /** + * {@inheritdoc} + */ + public function getDeviceCodeEntityByDeviceCode(string $deviceCode): ?DeviceCodeEntityInterface + { + $record = Passport::deviceCode() + ->whereKey($deviceCode) + ->where(['revoked' => false]) + ->first(); + + return $record ? new DeviceCode( + $record->getKey(), + $record->user_id, + $record->client_id, + $record->scopes, + ! is_null($record->user_approved_at), + $record->last_polled_at, + $record->expires_at + ) : null; + } + + /** + * {@inheritdoc} + */ + public function revokeDeviceCode(string $codeId): void + { + Passport::deviceCode()->whereKey($codeId)->update(['revoked' => true]); + } + + /** + * {@inheritdoc} + */ + public function isDeviceCodeRevoked(string $codeId): bool + { + // Already checked on `getDeviceCodeEntityByDeviceCode` no need to query twice. + return false; + } +} diff --git a/src/Contracts/DeviceCodeViewResponse.php b/src/Contracts/DeviceCodeViewResponse.php new file mode 100644 index 000000000..e96575c7c --- /dev/null +++ b/src/Contracts/DeviceCodeViewResponse.php @@ -0,0 +1,16 @@ + 'array', + 'revoked' => 'bool', + 'user_approved_at' => 'datetime', + 'last_polled_at' => 'datetime', + 'expires_at' => 'datetime', + ]; + + /** + * Indicates if the model should be timestamped. + * + * @var bool + */ + public $timestamps = false; + + /** + * The "type" of the primary key ID. + * + * @var string + */ + protected $keyType = 'string'; + + /** + * Get the client that owns the authentication code. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function client() + { + return $this->belongsTo(Passport::clientModel()); + } + + /** + * Get the current connection name for the model. + * + * @return string|null + */ + public function getConnectionName() + { + return $this->connection ?? config('passport.connection'); + } +} diff --git a/src/Http/Controllers/AccessTokenController.php b/src/Http/Controllers/AccessTokenController.php index 7c3b395bc..3166027c2 100644 --- a/src/Http/Controllers/AccessTokenController.php +++ b/src/Http/Controllers/AccessTokenController.php @@ -9,7 +9,7 @@ class AccessTokenController { - use HandlesOAuthErrors; + use ConvertsPsrResponses, HandlesOAuthErrors; /** * The authorization server. diff --git a/src/Http/Controllers/ApproveAuthorizationController.php b/src/Http/Controllers/ApproveAuthorizationController.php index 6a2d41511..b675ea031 100644 --- a/src/Http/Controllers/ApproveAuthorizationController.php +++ b/src/Http/Controllers/ApproveAuthorizationController.php @@ -36,8 +36,6 @@ public function __construct(AuthorizationServer $server) */ public function approve(Request $request) { - $this->assertValidAuthToken($request); - $authRequest = $this->getAuthRequestFromSession($request); $authRequest->setAuthorizationApproved(true); diff --git a/src/Http/Controllers/AuthorizationController.php b/src/Http/Controllers/AuthorizationController.php index 38942232b..318cede41 100644 --- a/src/Http/Controllers/AuthorizationController.php +++ b/src/Http/Controllers/AuthorizationController.php @@ -18,7 +18,7 @@ class AuthorizationController { - use HandlesOAuthErrors; + use ConvertsPsrResponses, HandlesOAuthErrors; /** * The authorization server. diff --git a/src/Http/Controllers/DenyAuthorizationController.php b/src/Http/Controllers/DenyAuthorizationController.php index 3a46e6174..4c287667b 100644 --- a/src/Http/Controllers/DenyAuthorizationController.php +++ b/src/Http/Controllers/DenyAuthorizationController.php @@ -36,8 +36,6 @@ public function __construct(AuthorizationServer $server) */ public function deny(Request $request) { - $this->assertValidAuthToken($request); - $authRequest = $this->getAuthRequestFromSession($request); $authRequest->setAuthorizationApproved(false); diff --git a/src/Http/Controllers/DeviceAuthorizationController.php b/src/Http/Controllers/DeviceAuthorizationController.php new file mode 100644 index 000000000..f666b50b2 --- /dev/null +++ b/src/Http/Controllers/DeviceAuthorizationController.php @@ -0,0 +1,130 @@ +server = $server; + $this->deviceCodeViewResponse = $deviceCodeViewResponse; + $this->authorizationViewResponse = $authorizationViewResponse; + } + + /** + * Show the form for entering user code. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\Response|\Laravel\Passport\Contracts\DeviceCodeViewResponse + */ + public function userCode(Request $request) + { + return $this->deviceCodeViewResponse->withParameters([ + 'userCode' => $request->query('user_code'), + ]); + } + + /** + * Authorize a client to access the user's account. + * + * @param \Illuminate\Http\Request $request + * @param \Laravel\Passport\ClientRepository $clients + * @return \Illuminate\Http\Response|\Laravel\Passport\Contracts\AuthorizationViewResponse + */ + public function authorize(Request $request, ClientRepository $clients) + { + $deviceCode = Passport::deviceCode()->with('client') + ->where('user_code', $request->user_code) + ->first(); + + $request->session()->put('authToken', $authToken = Str::random()); + $request->session()->put('deviceCode', $deviceCode->getKey()); + + return $this->authorizationViewResponse->withParameters([ + 'client' => $deviceCode->client, + 'user' => $request->user(), + 'scopes' => Passport::scopesFor($deviceCode->scopes), + 'request' => $request, + 'authToken' => $authToken, + ]); + } + + /** + * Approve the authorization request. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\Response + * + * @throws \Laravel\Passport\Exceptions\OAuthServerException + */ + public function approve(Request $request) + { + $this->withErrorHandling(fn () => $this->server->completeDeviceAuthorizationRequest( + $request->session()->pull('deviceCode'), + $request->user()->getAuthIdentifier(), + true + )); + + return 'approved'; + } + + /** + * Deny the authorization request. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\Response + * + * @throws \Laravel\Passport\Exceptions\OAuthServerException + */ + public function deny(Request $request) + { + $this->withErrorHandling(fn () => $this->server->completeDeviceAuthorizationRequest( + $request->session()->pull('deviceCode'), + $request->user()->getAuthIdentifier(), + false + )); + + return 'denied'; + } +} diff --git a/src/Http/Controllers/DeviceCodeController.php b/src/Http/Controllers/DeviceCodeController.php new file mode 100644 index 000000000..689d1e478 --- /dev/null +++ b/src/Http/Controllers/DeviceCodeController.php @@ -0,0 +1,48 @@ +server = $server; + } + + /** + * Issue a device code for the client. + * + * @param \Psr\Http\Message\ServerRequestInterface $request + * @return \Illuminate\Http\Response + * + * @throws \Laravel\Passport\Exceptions\OAuthServerException + */ + public function issueDeviceCode(ServerRequestInterface $request) + { + return $this->withErrorHandling(function () use ($request) { + return $this->convertResponse( + $this->server->respondToDeviceAuthorizationRequest($request, new Psr7Response) + ); + }); + } +} diff --git a/src/Http/Controllers/RetrievesAuthRequestFromSession.php b/src/Http/Controllers/RetrievesAuthRequestFromSession.php index 0a23e1ec4..482020f7a 100644 --- a/src/Http/Controllers/RetrievesAuthRequestFromSession.php +++ b/src/Http/Controllers/RetrievesAuthRequestFromSession.php @@ -10,33 +10,23 @@ trait RetrievesAuthRequestFromSession { /** - * Make sure the auth token matches the one in the session. + * Get the authorization request from the session. * * @param \Illuminate\Http\Request $request - * @return void + * @return \League\OAuth2\Server\RequestTypes\AuthorizationRequest * * @throws \Laravel\Passport\Exceptions\InvalidAuthTokenException + * @throws \Exception */ - protected function assertValidAuthToken(Request $request) + protected function getAuthRequestFromSession(Request $request) { - if ($request->has('auth_token') && $request->session()->get('authToken') !== $request->get('auth_token')) { + if (! $request->has('auth_token') || $request->session()->pull('authToken') !== $request->get('auth_token')) { $request->session()->forget(['authToken', 'authRequest']); throw InvalidAuthTokenException::different(); } - } - /** - * Get the authorization request from the session. - * - * @param \Illuminate\Http\Request $request - * @return \League\OAuth2\Server\RequestTypes\AuthorizationRequest - * - * @throws \Exception - */ - protected function getAuthRequestFromSession(Request $request) - { - return tap($request->session()->get('authRequest'), function ($authRequest) use ($request) { + return tap($request->session()->pull('authRequest'), function ($authRequest) use ($request) { if (! $authRequest) { throw new Exception('Authorization request was not present in the session.'); } diff --git a/src/Http/Controllers/RetrievesDeviceCodeFromSession.php b/src/Http/Controllers/RetrievesDeviceCodeFromSession.php new file mode 100644 index 000000000..eaabb10b2 --- /dev/null +++ b/src/Http/Controllers/RetrievesDeviceCodeFromSession.php @@ -0,0 +1,40 @@ +has('auth_token') || $request->session()->get('authToken') !== $request->get('auth_token')) { + $request->session()->forget(['authToken', 'deviceCode']); + + throw InvalidAuthTokenException::different(); + } + } + + /** + * Get the device code from the session. + * + * @throws \Exception + */ + protected function getDeviceCodeFromSession(Request $request): string + { + return tap($request->session()->pull('deviceCode'), function ($deviceCode) { + if (! $deviceCode) { + throw new Exception('Device code was not present in the session.'); + } + + return $deviceCode; + }); + } +} diff --git a/src/Http/Responses/AuthorizationViewResponse.php b/src/Http/Responses/AuthorizationViewResponse.php index 36761d486..3ff02af81 100644 --- a/src/Http/Responses/AuthorizationViewResponse.php +++ b/src/Http/Responses/AuthorizationViewResponse.php @@ -2,67 +2,9 @@ namespace Laravel\Passport\Http\Responses; -use Illuminate\Contracts\Support\Responsable; use Laravel\Passport\Contracts\AuthorizationViewResponse as AuthorizationViewResponseContract; class AuthorizationViewResponse implements AuthorizationViewResponseContract { - /** - * The name of the view or the callable used to generate the view. - * - * @var string - */ - protected $view; - - /** - * An array of arguments that may be passed to the view response and used in the view. - * - * @var string - */ - protected $parameters; - - /** - * Create a new response instance. - * - * @param callable|string $view - * @return void - */ - public function __construct($view) - { - $this->view = $view; - } - - /** - * Add parameters to response. - * - * @param array $parameters - * @return $this - */ - public function withParameters($parameters = []) - { - $this->parameters = $parameters; - - return $this; - } - - /** - * Create an HTTP response that represents the object. - * - * @param \Illuminate\Http\Request $request - * @return \Symfony\Component\HttpFoundation\Response - */ - public function toResponse($request) - { - if (! is_callable($this->view) || is_string($this->view)) { - return response()->view($this->view, $this->parameters); - } - - $response = call_user_func($this->view, $this->parameters); - - if ($response instanceof Responsable) { - return $response->toResponse($request); - } - - return $response; - } + use ViewResponsable; } diff --git a/src/Http/Responses/DeviceCodeViewResponse.php b/src/Http/Responses/DeviceCodeViewResponse.php new file mode 100644 index 000000000..7c3a9e29d --- /dev/null +++ b/src/Http/Responses/DeviceCodeViewResponse.php @@ -0,0 +1,10 @@ +view = $view; + } + + /** + * Add parameters to response. + * + * @param array $parameters + * @return $this + */ + public function withParameters($parameters = []) + { + $this->parameters = $parameters; + + return $this; + } + + /** + * Create an HTTP response that represents the object. + * + * @param \Illuminate\Http\Request $request + * @return \Symfony\Component\HttpFoundation\Response + */ + public function toResponse($request) + { + if (! is_callable($this->view) || is_string($this->view)) { + return response()->view($this->view, $this->parameters); + } + + $response = call_user_func($this->view, $this->parameters); + + if ($response instanceof Responsable) { + return $response->toResponse($request); + } + + return $response; + } +} diff --git a/src/Passport.php b/src/Passport.php index d24f768c5..299fa1f9a 100644 --- a/src/Passport.php +++ b/src/Passport.php @@ -7,7 +7,9 @@ use DateTimeInterface; use Illuminate\Contracts\Encryption\Encrypter; use Laravel\Passport\Contracts\AuthorizationViewResponse as AuthorizationViewResponseContract; +use Laravel\Passport\Contracts\DeviceCodeViewResponse as DeviceCodeViewResponseContract; use Laravel\Passport\Http\Responses\AuthorizationViewResponse; +use Laravel\Passport\Http\Responses\DeviceCodeViewResponse; use League\OAuth2\Server\ResourceServer; use Mockery; use Psr\Http\Message\ServerRequestInterface; @@ -107,6 +109,13 @@ class Passport */ public static $authCodeModel = 'Laravel\Passport\AuthCode'; + /** + * The device code model class name. + * + * @var string + */ + public static $deviceCodeModel = 'Laravel\Passport\DeviceCode'; + /** * The client model class name. * @@ -499,6 +508,37 @@ public static function authCode() return new static::$authCodeModel; } + /** + * Set the device code model class name. + * + * @param string $deviceCodeModel + * @return void + */ + public static function useDeviceCodeModel($deviceCodeModel) + { + static::$deviceCodeModel = $deviceCodeModel; + } + + /** + * Get the device code model class name. + * + * @return string + */ + public static function deviceCodeModel() + { + return static::$deviceCodeModel; + } + + /** + * Get a new device code model instance. + * + * @return \Laravel\Passport\DeviceCode + */ + public static function deviceCode() + { + return new static::$deviceCodeModel; + } + /** * Set the client model class name. * @@ -683,6 +723,19 @@ public static function authorizationView($view) }); } + /** + * Specify which view should be used as the device code view. + * + * @param callable|string $view + * @return void + */ + public static function deviceCodeView($view) + { + app()->singleton(DeviceCodeViewResponseContract::class, function ($app) use ($view) { + return new DeviceCodeViewResponse($view); + }); + } + /** * Configure Passport to not register its routes. * diff --git a/src/PassportServiceProvider.php b/src/PassportServiceProvider.php index c3eb97a81..b576bfd72 100644 --- a/src/PassportServiceProvider.php +++ b/src/PassportServiceProvider.php @@ -12,6 +12,7 @@ use Illuminate\Support\Facades\Request; use Illuminate\Support\Facades\Route; use Illuminate\Support\ServiceProvider; +use Laravel\Passport\Bridge\DeviceCodeRepository; use Laravel\Passport\Bridge\PersonalAccessGrant; use Laravel\Passport\Bridge\RefreshTokenRepository; use Laravel\Passport\Guards\TokenGuard; @@ -23,6 +24,7 @@ use League\OAuth2\Server\CryptKey; use League\OAuth2\Server\Grant\AuthCodeGrant; use League\OAuth2\Server\Grant\ClientCredentialsGrant; +use League\OAuth2\Server\Grant\DeviceCodeGrant; use League\OAuth2\Server\Grant\ImplicitGrant; use League\OAuth2\Server\Grant\PasswordGrant; use League\OAuth2\Server\Grant\RefreshTokenGrant; @@ -139,6 +141,7 @@ public function register() $this->registerGuard(); Passport::authorizationView('passport::authorize'); + Passport::deviceCodeView('passport::device'); } /** @@ -179,6 +182,10 @@ protected function registerAuthorizationServer() $this->makeImplicitGrant(), Passport::tokensExpireIn() ); } + + $server->enableGrantType( + $this->makeDeviceCodeGrant(), Passport::tokensExpireIn() + ); }); }); } @@ -250,6 +257,29 @@ protected function makeImplicitGrant() return new ImplicitGrant(Passport::tokensExpireIn()); } + + /** + * Create and configure an instance of the Device Code grant. + * + * @return \League\OAuth2\Server\Grant\DeviceCodeGrant + */ + protected function makeDeviceCodeGrant() + { + $grant = new DeviceCodeGrant( + $this->app->make(DeviceCodeRepository::class), + $this->app->make(RefreshTokenRepository::class), + new DateInterval('PT10M'), + route('passport.device.code'), + 5 + ); + + $grant->setRefreshTokenTTL(Passport::refreshTokensExpireIn()); + $grant->setIncludeVerificationUriComplete(true); + $grant->setIntervalVisibility(true); + + return $grant; + } + /** * Make the authorization service instance. * diff --git a/tests/Feature/DeviceCodeControllerTest.php b/tests/Feature/DeviceCodeControllerTest.php new file mode 100644 index 000000000..1c2e7b3ac --- /dev/null +++ b/tests/Feature/DeviceCodeControllerTest.php @@ -0,0 +1,37 @@ +create(); + + $response = $this->post( + '/oauth/device/code', + [ + 'client_id' => $client->getKey(), + 'scope' => '', + ] + ); + + $response->assertOk(); + + $response = $response->json(); + + $this->assertArrayHasKey('device_code', $response); + $this->assertArrayHasKey('user_code', $response); + $this->assertArrayHasKey('expires_in', $response); + // $this->assertArrayHasKey('interval', $response); // TODO https://github.com/thephpleague/oauth2-server/pull/1410 + $this->assertSame('http://localhost/oauth/device/code', $response['verification_uri']); + $this->assertStringStartsWith('http://localhost/oauth/device/code?user_code=', $response['verification_uri_complete']); + } +} diff --git a/tests/Unit/ApproveAuthorizationControllerTest.php b/tests/Unit/ApproveAuthorizationControllerTest.php index 9f7e3d841..092b1be0b 100644 --- a/tests/Unit/ApproveAuthorizationControllerTest.php +++ b/tests/Unit/ApproveAuthorizationControllerTest.php @@ -29,8 +29,8 @@ public function test_complete_authorization_request() $request->shouldReceive('has')->with('auth_token')->andReturn(true); $request->shouldReceive('get')->with('auth_token')->andReturn('foo'); - $session->shouldReceive('get')->once()->with('authToken')->andReturn('foo'); - $session->shouldReceive('get') + $session->shouldReceive('pull')->once()->with('authToken')->andReturn('foo'); + $session->shouldReceive('pull') ->once() ->with('authRequest') ->andReturn($authRequest = m::mock(AuthorizationRequest::class)); @@ -51,6 +51,54 @@ public function test_complete_authorization_request() $this->assertSame('response', $controller->approve($request)->getContent()); } + + public function test_auth_request_should_exist() + { + $this->expectException('Exception'); + $this->expectExceptionMessage('Authorization request was not present in the session.'); + + $server = m::mock(AuthorizationServer::class); + + $controller = new ApproveAuthorizationController($server); + + $request = m::mock(Request::class); + + $request->shouldReceive('session')->andReturn($session = m::mock()); + $request->shouldReceive('user')->never(); + $request->shouldReceive('input')->never(); + $request->shouldReceive('has')->with('auth_token')->andReturn(true); + $request->shouldReceive('get')->with('auth_token')->andReturn('foo'); + + $session->shouldReceive('pull')->once()->with('authToken')->andReturn('foo'); + $session->shouldReceive('pull')->once()->with('authRequest')->andReturnNull(); + + $server->shouldReceive('completeAuthorizationRequest')->never(); + + $controller->approve($request); + } + + public function test_auth_token_should_exist_on_request() + { + $this->expectException('\Laravel\Passport\Exceptions\InvalidAuthTokenException'); + $this->expectExceptionMessage('The provided auth token for the request is different from the session auth token.'); + + $server = m::mock(AuthorizationServer::class); + + $controller = new ApproveAuthorizationController($server); + + $request = m::mock(Request::class); + + $request->shouldReceive('session')->andReturn($session = m::mock()); + $request->shouldReceive('user')->never(); + $request->shouldReceive('input')->never(); + $request->shouldReceive('has')->with('auth_token')->andReturn(false); + + $session->shouldReceive('forget')->once()->with(['authToken', 'authRequest']); + + $server->shouldReceive('completeAuthorizationRequest')->never(); + + $controller->approve($request); + } } class ApproveAuthorizationControllerFakeUser diff --git a/tests/Unit/DenyAuthorizationControllerTest.php b/tests/Unit/DenyAuthorizationControllerTest.php index 3497790ca..de9db065d 100644 --- a/tests/Unit/DenyAuthorizationControllerTest.php +++ b/tests/Unit/DenyAuthorizationControllerTest.php @@ -31,8 +31,8 @@ public function test_authorization_can_be_denied() $request->shouldReceive('has')->with('auth_token')->andReturn(true); $request->shouldReceive('get')->with('auth_token')->andReturn('foo'); - $session->shouldReceive('get')->once()->with('authToken')->andReturn('foo'); - $session->shouldReceive('get') + $session->shouldReceive('pull')->once()->with('authToken')->andReturn('foo'); + $session->shouldReceive('pull') ->once() ->with('authRequest') ->andReturn($authRequest = m::mock( @@ -68,8 +68,31 @@ public function test_auth_request_should_exist() $request->shouldReceive('has')->with('auth_token')->andReturn(true); $request->shouldReceive('get')->with('auth_token')->andReturn('foo'); - $session->shouldReceive('get')->once()->with('authToken')->andReturn('foo'); - $session->shouldReceive('get')->once()->with('authRequest')->andReturnNull(); + $session->shouldReceive('pull')->once()->with('authToken')->andReturn('foo'); + $session->shouldReceive('pull')->once()->with('authRequest')->andReturnNull(); + + $server->shouldReceive('completeAuthorizationRequest')->never(); + + $controller->deny($request); + } + + public function test_auth_token_should_exist_on_request() + { + $this->expectException('\Laravel\Passport\Exceptions\InvalidAuthTokenException'); + $this->expectExceptionMessage('The provided auth token for the request is different from the session auth token.'); + + $server = m::mock(AuthorizationServer::class); + + $controller = new DenyAuthorizationController($server); + + $request = m::mock(Request::class); + + $request->shouldReceive('session')->andReturn($session = m::mock()); + $request->shouldReceive('user')->never(); + $request->shouldReceive('input')->never(); + $request->shouldReceive('has')->with('auth_token')->andReturn(false); + + $session->shouldReceive('forget')->once()->with(['authToken', 'authRequest']); $server->shouldReceive('completeAuthorizationRequest')->never(); From fc9f827749fac1021c4b1b538c1c334a8979a38b Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Thu, 30 May 2024 22:58:16 +0330 Subject: [PATCH 02/38] formatting --- src/Bridge/Client.php | 2 +- src/PassportServiceProvider.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Bridge/Client.php b/src/Bridge/Client.php index 3c25f5437..5169aec21 100644 --- a/src/Bridge/Client.php +++ b/src/Bridge/Client.php @@ -30,7 +30,7 @@ public function __construct( if (! is_null($name)) { $this->name = $name; } - + if (! is_null($redirectUri)) { $this->redirectUri = explode(',', $redirectUri); } diff --git a/src/PassportServiceProvider.php b/src/PassportServiceProvider.php index b576bfd72..6786c266d 100644 --- a/src/PassportServiceProvider.php +++ b/src/PassportServiceProvider.php @@ -257,7 +257,6 @@ protected function makeImplicitGrant() return new ImplicitGrant(Passport::tokensExpireIn()); } - /** * Create and configure an instance of the Device Code grant. * From abba12d259b85f4c2a6cbea4cbd3e6d5982b6f1a Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Fri, 5 Jul 2024 21:48:07 +0330 Subject: [PATCH 03/38] wip --- resources/views/device.blade.php | 93 +++++++------------ .../DeviceAuthorizationController.php | 8 +- .../RetrievesAuthRequestFromSession.php | 1 + .../RetrievesDeviceCodeFromSession.php | 15 +-- 4 files changed, 42 insertions(+), 75 deletions(-) diff --git a/resources/views/device.blade.php b/resources/views/device.blade.php index 712877c22..f0df8b721 100644 --- a/resources/views/device.blade.php +++ b/resources/views/device.blade.php @@ -1,68 +1,43 @@ - - - - - - +@extends('layouts.app') - {{ config('app.name') }} - Authorization - - - - - - - +@section('content')
-
-
-
- Device Authorization -
-
- -

Enter the code displayed on your device .

+
+
+
{{ __('Device Authorization') }}
-
- -
- @csrf - - - - -
-
+
+ {{ __('Enter the code displayed on your device.') }} + +
+ @csrf + +
+ + +
+ + + @error('user_code') + + {{ $message }} + + @enderror +
+
+ +
+
+ +
+
+
- - +@endsection diff --git a/src/Http/Controllers/DeviceAuthorizationController.php b/src/Http/Controllers/DeviceAuthorizationController.php index f666b50b2..2433443ef 100644 --- a/src/Http/Controllers/DeviceAuthorizationController.php +++ b/src/Http/Controllers/DeviceAuthorizationController.php @@ -4,7 +4,6 @@ use Illuminate\Http\Request; use Illuminate\Support\Str; -use Laravel\Passport\ClientRepository; use Laravel\Passport\Contracts\AuthorizationViewResponse; use Laravel\Passport\Contracts\DeviceCodeViewResponse; use Laravel\Passport\Passport; @@ -69,10 +68,9 @@ public function userCode(Request $request) * Authorize a client to access the user's account. * * @param \Illuminate\Http\Request $request - * @param \Laravel\Passport\ClientRepository $clients * @return \Illuminate\Http\Response|\Laravel\Passport\Contracts\AuthorizationViewResponse */ - public function authorize(Request $request, ClientRepository $clients) + public function authorize(Request $request) { $deviceCode = Passport::deviceCode()->with('client') ->where('user_code', $request->user_code) @@ -101,7 +99,7 @@ public function authorize(Request $request, ClientRepository $clients) public function approve(Request $request) { $this->withErrorHandling(fn () => $this->server->completeDeviceAuthorizationRequest( - $request->session()->pull('deviceCode'), + $this->getDeviceCodeFromSession(), $request->user()->getAuthIdentifier(), true )); @@ -120,7 +118,7 @@ public function approve(Request $request) public function deny(Request $request) { $this->withErrorHandling(fn () => $this->server->completeDeviceAuthorizationRequest( - $request->session()->pull('deviceCode'), + $this->getDeviceCodeFromSession(), $request->user()->getAuthIdentifier(), false )); diff --git a/src/Http/Controllers/RetrievesAuthRequestFromSession.php b/src/Http/Controllers/RetrievesAuthRequestFromSession.php index c6bae0138..99b0e8501 100644 --- a/src/Http/Controllers/RetrievesAuthRequestFromSession.php +++ b/src/Http/Controllers/RetrievesAuthRequestFromSession.php @@ -6,6 +6,7 @@ use Illuminate\Http\Request; use Laravel\Passport\Bridge\User; use Laravel\Passport\Exceptions\InvalidAuthTokenException; +use League\OAuth2\Server\RequestTypes\AuthorizationRequest; trait RetrievesAuthRequestFromSession { diff --git a/src/Http/Controllers/RetrievesDeviceCodeFromSession.php b/src/Http/Controllers/RetrievesDeviceCodeFromSession.php index eaabb10b2..89d490b3c 100644 --- a/src/Http/Controllers/RetrievesDeviceCodeFromSession.php +++ b/src/Http/Controllers/RetrievesDeviceCodeFromSession.php @@ -9,26 +9,19 @@ trait RetrievesDeviceCodeFromSession { /** - * Make sure the auth token matches the one in the session. + * Get the device code from the session. * * @throws \Laravel\Passport\Exceptions\InvalidAuthTokenException + * @throws \Exception */ - protected function assertValidDeviceCode(Request $request): void + protected function getDeviceCodeFromSession(Request $request): string { - if (! $request->has('auth_token') || $request->session()->get('authToken') !== $request->get('auth_token')) { + if ($request->isNotFilled('auth_token') || $request->session()->pull('authToken') !== $request->get('auth_token')) { $request->session()->forget(['authToken', 'deviceCode']); throw InvalidAuthTokenException::different(); } - } - /** - * Get the device code from the session. - * - * @throws \Exception - */ - protected function getDeviceCodeFromSession(Request $request): string - { return tap($request->session()->pull('deviceCode'), function ($deviceCode) { if (! $deviceCode) { throw new Exception('Device code was not present in the session.'); From 6d3908f986fb61ac924c643b9a7ed18f49f4a6e7 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Fri, 5 Jul 2024 21:50:15 +0330 Subject: [PATCH 04/38] wip --- src/Http/Controllers/DeviceAuthorizationController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Http/Controllers/DeviceAuthorizationController.php b/src/Http/Controllers/DeviceAuthorizationController.php index 2433443ef..eaaf63877 100644 --- a/src/Http/Controllers/DeviceAuthorizationController.php +++ b/src/Http/Controllers/DeviceAuthorizationController.php @@ -99,7 +99,7 @@ public function authorize(Request $request) public function approve(Request $request) { $this->withErrorHandling(fn () => $this->server->completeDeviceAuthorizationRequest( - $this->getDeviceCodeFromSession(), + $this->getDeviceCodeFromSession($request), $request->user()->getAuthIdentifier(), true )); @@ -118,7 +118,7 @@ public function approve(Request $request) public function deny(Request $request) { $this->withErrorHandling(fn () => $this->server->completeDeviceAuthorizationRequest( - $this->getDeviceCodeFromSession(), + $this->getDeviceCodeFromSession($request), $request->user()->getAuthIdentifier(), false )); From 1bf6ded4a61419924bb7cc91b804439fd9f8277b Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Fri, 5 Jul 2024 21:54:21 +0330 Subject: [PATCH 05/38] wip --- .../ApproveAuthorizationControllerTest.php | 48 ------------------- 1 file changed, 48 deletions(-) diff --git a/tests/Unit/ApproveAuthorizationControllerTest.php b/tests/Unit/ApproveAuthorizationControllerTest.php index a97c57f88..b572441cb 100644 --- a/tests/Unit/ApproveAuthorizationControllerTest.php +++ b/tests/Unit/ApproveAuthorizationControllerTest.php @@ -51,54 +51,6 @@ public function test_complete_authorization_request() $this->assertSame('response', $controller->approve($request)->getContent()); } - - public function test_auth_request_should_exist() - { - $this->expectException('Exception'); - $this->expectExceptionMessage('Authorization request was not present in the session.'); - - $server = m::mock(AuthorizationServer::class); - - $controller = new ApproveAuthorizationController($server); - - $request = m::mock(Request::class); - - $request->shouldReceive('session')->andReturn($session = m::mock()); - $request->shouldReceive('user')->never(); - $request->shouldReceive('input')->never(); - $request->shouldReceive('has')->with('auth_token')->andReturn(true); - $request->shouldReceive('get')->with('auth_token')->andReturn('foo'); - - $session->shouldReceive('pull')->once()->with('authToken')->andReturn('foo'); - $session->shouldReceive('pull')->once()->with('authRequest')->andReturnNull(); - - $server->shouldReceive('completeAuthorizationRequest')->never(); - - $controller->approve($request); - } - - public function test_auth_token_should_exist_on_request() - { - $this->expectException('\Laravel\Passport\Exceptions\InvalidAuthTokenException'); - $this->expectExceptionMessage('The provided auth token for the request is different from the session auth token.'); - - $server = m::mock(AuthorizationServer::class); - - $controller = new ApproveAuthorizationController($server); - - $request = m::mock(Request::class); - - $request->shouldReceive('session')->andReturn($session = m::mock()); - $request->shouldReceive('user')->never(); - $request->shouldReceive('input')->never(); - $request->shouldReceive('has')->with('auth_token')->andReturn(false); - - $session->shouldReceive('forget')->once()->with(['authToken', 'authRequest']); - - $server->shouldReceive('completeAuthorizationRequest')->never(); - - $controller->approve($request); - } } class ApproveAuthorizationControllerFakeUser From e3f55ba5310362aa33b4657468ce9d927aaed72c Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Fri, 5 Jul 2024 23:23:37 +0330 Subject: [PATCH 06/38] wip --- resources/views/device.blade.php | 4 +-- routes/web.php | 12 ++++----- src/Bridge/DeviceCodeRepository.php | 5 ++-- .../DeviceAuthorizationController.php | 27 ++++++++++++++----- src/PassportServiceProvider.php | 2 +- tests/Feature/DeviceCodeControllerTest.php | 6 +++-- 6 files changed, 35 insertions(+), 21 deletions(-) diff --git a/resources/views/device.blade.php b/resources/views/device.blade.php index f0df8b721..de21e3c85 100644 --- a/resources/views/device.blade.php +++ b/resources/views/device.blade.php @@ -10,14 +10,14 @@
{{ __('Enter the code displayed on your device.') }} -
+ @csrf
- + @error('user_code') diff --git a/routes/web.php b/routes/web.php index 679a92dbc..06459666d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -14,6 +14,11 @@ 'middleware' => 'throttle', ]); +Route::get('/device', [ + 'uses' => 'DeviceAuthorizationController@userCode', + 'as' => 'device', +]); + Route::get('/authorize', [ 'uses' => 'AuthorizationController@authorize', 'as' => 'authorizations.authorize', @@ -38,12 +43,7 @@ 'as' => 'authorizations.deny', ]); - Route::get('/device', [ - 'uses' => 'DeviceAuthorizationController@userCode', - 'as' => 'device', - ]); - - Route::get('/device', [ + Route::get('/device/authorize', [ 'uses' => 'DeviceAuthorizationController@authorize', 'as' => 'device.authorize', ]); diff --git a/src/Bridge/DeviceCodeRepository.php b/src/Bridge/DeviceCodeRepository.php index b8ea9b351..ddd34e039 100644 --- a/src/Bridge/DeviceCodeRepository.php +++ b/src/Bridge/DeviceCodeRepository.php @@ -39,7 +39,7 @@ public function persistDeviceCode(DeviceCodeEntityInterface $deviceCodeEntity): 'user_id' => null, 'client_id' => $deviceCodeEntity->getClient()->getIdentifier(), 'user_code' => $deviceCodeEntity->getUserCode(), - 'scopes' => $this->formatScopesForStorage($deviceCodeEntity->getScopes()), + 'scopes' => $this->scopesToArray($deviceCodeEntity->getScopes()), 'revoked' => false, 'user_approved_at' => null, 'last_polled_at' => null, @@ -82,7 +82,6 @@ public function revokeDeviceCode(string $codeId): void */ public function isDeviceCodeRevoked(string $codeId): bool { - // Already checked on `getDeviceCodeEntityByDeviceCode` no need to query twice. - return false; + return Passport::deviceCode()->whereKey($codeId)->where('revoked', false)->doesntExist(); } } diff --git a/src/Http/Controllers/DeviceAuthorizationController.php b/src/Http/Controllers/DeviceAuthorizationController.php index eaaf63877..af4978576 100644 --- a/src/Http/Controllers/DeviceAuthorizationController.php +++ b/src/Http/Controllers/DeviceAuthorizationController.php @@ -55,27 +55,40 @@ public function __construct(AuthorizationServer $server, * Show the form for entering user code. * * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response|\Laravel\Passport\Contracts\DeviceCodeViewResponse + * @return \Illuminate\Http\RedirectResponse|\Laravel\Passport\Contracts\DeviceCodeViewResponse */ public function userCode(Request $request) { - return $this->deviceCodeViewResponse->withParameters([ - 'userCode' => $request->query('user_code'), - ]); + if ($userCode = $request->query('user_code')) { + return to_route('passport.device.authorize', [ + 'user_code' => $userCode, + ]); + } + + return $this->deviceCodeViewResponse; } /** * Authorize a client to access the user's account. * * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response|\Laravel\Passport\Contracts\AuthorizationViewResponse + * @return \Illuminate\Http\RedirectResponse|\Laravel\Passport\Contracts\AuthorizationViewResponse */ public function authorize(Request $request) { - $deviceCode = Passport::deviceCode()->with('client') - ->where('user_code', $request->user_code) + $deviceCode = Passport::deviceCode() + ->with('client') + ->where('user_code', $userCode = $request->query('user_code')) ->first(); + if (! $deviceCode) { + return to_route('passport.device') + ->withInput(['user_code' => $userCode]) + ->withErrors([ + 'user_code' => 'Incorrect code.', + ]); + } + $request->session()->put('authToken', $authToken = Str::random()); $request->session()->put('deviceCode', $deviceCode->getKey()); diff --git a/src/PassportServiceProvider.php b/src/PassportServiceProvider.php index 3be84193c..dd143c3ab 100644 --- a/src/PassportServiceProvider.php +++ b/src/PassportServiceProvider.php @@ -265,7 +265,7 @@ protected function makeDeviceCodeGrant() $this->app->make(DeviceCodeRepository::class), $this->app->make(RefreshTokenRepository::class), new DateInterval('PT10M'), - route('passport.device.code'), + route('passport.device'), 5 ); diff --git a/tests/Feature/DeviceCodeControllerTest.php b/tests/Feature/DeviceCodeControllerTest.php index 1c2e7b3ac..878ad61e2 100644 --- a/tests/Feature/DeviceCodeControllerTest.php +++ b/tests/Feature/DeviceCodeControllerTest.php @@ -12,6 +12,8 @@ class DeviceCodeControllerTest extends PassportTestCase public function testIssuingDeviceCode() { + $this->withoutExceptionHandling(); + /** @var Client $client */ $client = ClientFactory::new()->create(); @@ -31,7 +33,7 @@ public function testIssuingDeviceCode() $this->assertArrayHasKey('user_code', $response); $this->assertArrayHasKey('expires_in', $response); // $this->assertArrayHasKey('interval', $response); // TODO https://github.com/thephpleague/oauth2-server/pull/1410 - $this->assertSame('http://localhost/oauth/device/code', $response['verification_uri']); - $this->assertStringStartsWith('http://localhost/oauth/device/code?user_code=', $response['verification_uri_complete']); + $this->assertSame('http://localhost/oauth/device', $response['verification_uri']); + $this->assertStringStartsWith('http://localhost/oauth/device?user_code=', $response['verification_uri_complete']); } } From 6311bf84cbfb9ea02929e730cb0c07b1ffd6184d Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Fri, 5 Jul 2024 23:24:27 +0330 Subject: [PATCH 07/38] wip --- resources/views/device.blade.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/resources/views/device.blade.php b/resources/views/device.blade.php index de21e3c85..6aa248075 100644 --- a/resources/views/device.blade.php +++ b/resources/views/device.blade.php @@ -11,8 +11,6 @@ {{ __('Enter the code displayed on your device.') }} - @csrf -
From 0fb9b53d1cd35b641d8edb073071b6d9748e900b Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Sat, 6 Jul 2024 12:06:34 +0330 Subject: [PATCH 08/38] wip --- routes/web.php | 1 + src/Bridge/DeviceCodeRepository.php | 5 +--- src/Http/Responses/ViewResponsable.php | 1 + tests/Feature/DeviceCodeControllerTest.php | 32 ++++++++-------------- 4 files changed, 15 insertions(+), 24 deletions(-) diff --git a/routes/web.php b/routes/web.php index 06459666d..93b240aaf 100644 --- a/routes/web.php +++ b/routes/web.php @@ -17,6 +17,7 @@ Route::get('/device', [ 'uses' => 'DeviceAuthorizationController@userCode', 'as' => 'device', + 'middleware' => 'web', ]); Route::get('/authorize', [ diff --git a/src/Bridge/DeviceCodeRepository.php b/src/Bridge/DeviceCodeRepository.php index ddd34e039..a2b1a8634 100644 --- a/src/Bridge/DeviceCodeRepository.php +++ b/src/Bridge/DeviceCodeRepository.php @@ -53,10 +53,7 @@ public function persistDeviceCode(DeviceCodeEntityInterface $deviceCodeEntity): */ public function getDeviceCodeEntityByDeviceCode(string $deviceCode): ?DeviceCodeEntityInterface { - $record = Passport::deviceCode() - ->whereKey($deviceCode) - ->where(['revoked' => false]) - ->first(); + $record = Passport::deviceCode()->whereKey($deviceCode)->where(['revoked' => false])->first(); return $record ? new DeviceCode( $record->getKey(), diff --git a/src/Http/Responses/ViewResponsable.php b/src/Http/Responses/ViewResponsable.php index 4dca7d241..13e72f0e9 100644 --- a/src/Http/Responses/ViewResponsable.php +++ b/src/Http/Responses/ViewResponsable.php @@ -29,6 +29,7 @@ trait ViewResponsable public function __construct($view) { $this->view = $view; + $this->parameters = []; } /** diff --git a/tests/Feature/DeviceCodeControllerTest.php b/tests/Feature/DeviceCodeControllerTest.php index 878ad61e2..456199db2 100644 --- a/tests/Feature/DeviceCodeControllerTest.php +++ b/tests/Feature/DeviceCodeControllerTest.php @@ -2,7 +2,6 @@ namespace Laravel\Passport\Tests\Feature; -use Laravel\Passport\Client; use Laravel\Passport\Database\Factories\ClientFactory; use Orchestra\Testbench\Concerns\WithLaravelMigrations; @@ -12,28 +11,21 @@ class DeviceCodeControllerTest extends PassportTestCase public function testIssuingDeviceCode() { - $this->withoutExceptionHandling(); - - /** @var Client $client */ $client = ClientFactory::new()->create(); - $response = $this->post( - '/oauth/device/code', - [ - 'client_id' => $client->getKey(), - 'scope' => '', - ] - ); + $response = $this->post('/oauth/device/code', [ + 'client_id' => $client->getKey(), + 'scope' => '', + ]); $response->assertOk(); - - $response = $response->json(); - - $this->assertArrayHasKey('device_code', $response); - $this->assertArrayHasKey('user_code', $response); - $this->assertArrayHasKey('expires_in', $response); - // $this->assertArrayHasKey('interval', $response); // TODO https://github.com/thephpleague/oauth2-server/pull/1410 - $this->assertSame('http://localhost/oauth/device', $response['verification_uri']); - $this->assertStringStartsWith('http://localhost/oauth/device?user_code=', $response['verification_uri_complete']); + $json = $response->json(); + + $this->assertArrayHasKey('device_code', $json); + $this->assertArrayHasKey('user_code', $json); + $this->assertArrayHasKey('expires_in', $json); + // $this->assertArrayHasKey('interval', $json); // TODO https://github.com/thephpleague/oauth2-server/pull/1410 + $this->assertSame('http://localhost/oauth/device', $json['verification_uri']); + $this->assertStringStartsWith('http://localhost/oauth/device?user_code=', $json['verification_uri_complete']); } } From 2ce23653e11daa82aa4c85b4ba7d3c01e83a2267 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Sat, 6 Jul 2024 18:26:25 +0330 Subject: [PATCH 09/38] wip --- .../Controllers/DeviceAuthorizationController.php | 4 +++- src/Http/Responses/AuthorizationViewResponse.php | 10 ---------- src/Http/Responses/DeviceCodeViewResponse.php | 10 ---------- ...ViewResponsable.php => SimpleViewResponse.php} | 11 +++++++---- src/Passport.php | 15 +++++++-------- tests/Unit/AuthorizationControllerTest.php | 2 +- 6 files changed, 18 insertions(+), 34 deletions(-) delete mode 100644 src/Http/Responses/AuthorizationViewResponse.php delete mode 100644 src/Http/Responses/DeviceCodeViewResponse.php rename src/Http/Responses/{ViewResponsable.php => SimpleViewResponse.php} (80%) diff --git a/src/Http/Controllers/DeviceAuthorizationController.php b/src/Http/Controllers/DeviceAuthorizationController.php index af4978576..af1d3c9f0 100644 --- a/src/Http/Controllers/DeviceAuthorizationController.php +++ b/src/Http/Controllers/DeviceAuthorizationController.php @@ -65,7 +65,9 @@ public function userCode(Request $request) ]); } - return $this->deviceCodeViewResponse; + return $this->deviceCodeViewResponse->withParameters([ + 'request' => $request, + ]); } /** diff --git a/src/Http/Responses/AuthorizationViewResponse.php b/src/Http/Responses/AuthorizationViewResponse.php deleted file mode 100644 index 3ff02af81..000000000 --- a/src/Http/Responses/AuthorizationViewResponse.php +++ /dev/null @@ -1,10 +0,0 @@ -view = $view; - $this->parameters = []; } /** diff --git a/src/Passport.php b/src/Passport.php index bf453bd1a..904f01b43 100644 --- a/src/Passport.php +++ b/src/Passport.php @@ -6,10 +6,9 @@ use DateInterval; use DateTimeInterface; use Illuminate\Contracts\Encryption\Encrypter; -use Laravel\Passport\Contracts\AuthorizationViewResponse as AuthorizationViewResponseContract; -use Laravel\Passport\Contracts\DeviceCodeViewResponse as DeviceCodeViewResponseContract; -use Laravel\Passport\Http\Responses\AuthorizationViewResponse; -use Laravel\Passport\Http\Responses\DeviceCodeViewResponse; +use Laravel\Passport\Contracts\AuthorizationViewResponse; +use Laravel\Passport\Contracts\DeviceCodeViewResponse; +use Laravel\Passport\Http\Responses\SimpleViewResponse; use League\OAuth2\Server\ResourceServer; use Mockery; use Psr\Http\Message\ServerRequestInterface; @@ -647,8 +646,8 @@ public static function tokenEncryptionKey(Encrypter $encrypter) */ public static function authorizationView($view) { - app()->singleton(AuthorizationViewResponseContract::class, function ($app) use ($view) { - return new AuthorizationViewResponse($view); + app()->singleton(AuthorizationViewResponse::class, function ($app) use ($view) { + return new SimpleViewResponse($view); }); } @@ -660,8 +659,8 @@ public static function authorizationView($view) */ public static function deviceCodeView($view) { - app()->singleton(DeviceCodeViewResponseContract::class, function ($app) use ($view) { - return new DeviceCodeViewResponse($view); + app()->singleton(DeviceCodeViewResponse::class, function ($app) use ($view) { + return new SimpleViewResponse($view); }); } diff --git a/tests/Unit/AuthorizationControllerTest.php b/tests/Unit/AuthorizationControllerTest.php index 12e703353..66bdfb633 100644 --- a/tests/Unit/AuthorizationControllerTest.php +++ b/tests/Unit/AuthorizationControllerTest.php @@ -10,7 +10,7 @@ use Laravel\Passport\Exceptions\AuthenticationException; use Laravel\Passport\Exceptions\OAuthServerException; use Laravel\Passport\Http\Controllers\AuthorizationController; -use Laravel\Passport\Http\Responses\AuthorizationViewResponse; +use Laravel\Passport\Contracts\AuthorizationViewResponse; use Laravel\Passport\Passport; use Laravel\Passport\Token; use Laravel\Passport\TokenRepository; From 77ef335825248ebaf8f24ecc71a563b6f77a66cb Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Sat, 6 Jul 2024 18:34:14 +0330 Subject: [PATCH 10/38] wip --- resources/views/authorize.blade.php | 77 ++++++---------------- tests/Unit/AuthorizationControllerTest.php | 2 +- 2 files changed, 21 insertions(+), 58 deletions(-) diff --git a/resources/views/authorize.blade.php b/resources/views/authorize.blade.php index d0a4a991c..8a713c323 100644 --- a/resources/views/authorize.blade.php +++ b/resources/views/authorize.blade.php @@ -1,87 +1,51 @@ - - - - - - +@extends('layouts.app') - {{ config('app.name') }} - Authorization - - - - - - - +@section('content')
-
-
+
+
- Authorization Request + {{ __('Authorization Request') }}
+

{{ $client->name }} is requesting permission to access your account.

@if (count($scopes) > 0) -
-

This application will be able to:

- -
    - @foreach ($scopes as $scope) -
  • {{ $scope->description }}
  • - @endforeach -
+
+

This application will be able to:

+ +
    + @foreach ($scopes as $scope) +
  • {{ $scope->description }}
  • + @endforeach +
@endif -
+
- + @csrf - + -
+ @csrf @method('DELETE') - +
@@ -89,5 +53,4 @@
- - +@endsection diff --git a/tests/Unit/AuthorizationControllerTest.php b/tests/Unit/AuthorizationControllerTest.php index 66bdfb633..d02756995 100644 --- a/tests/Unit/AuthorizationControllerTest.php +++ b/tests/Unit/AuthorizationControllerTest.php @@ -7,10 +7,10 @@ use Laravel\Passport\Bridge\Scope; use Laravel\Passport\Client; use Laravel\Passport\ClientRepository; +use Laravel\Passport\Contracts\AuthorizationViewResponse; use Laravel\Passport\Exceptions\AuthenticationException; use Laravel\Passport\Exceptions\OAuthServerException; use Laravel\Passport\Http\Controllers\AuthorizationController; -use Laravel\Passport\Contracts\AuthorizationViewResponse; use Laravel\Passport\Passport; use Laravel\Passport\Token; use Laravel\Passport\TokenRepository; From 4f5c8c1c2109adcdea7e2127d0ae48510115e212 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Sat, 6 Jul 2024 19:57:16 +0330 Subject: [PATCH 11/38] wip --- .../views/device-authorize-result.blade.php | 19 +++++ resources/views/device-authorize.blade.php | 50 ++++++++++++ ...e.blade.php => device-user-code.blade.php} | 2 +- routes/web.php | 12 ++- .../DeviceAuthorizationResultViewResponse.php | 16 ++++ .../DeviceAuthorizationViewResponse.php | 16 ++++ ...nse.php => DeviceUserCodeViewResponse.php} | 2 +- .../DeviceAuthorizationController.php | 79 +++++++++++-------- src/Http/Responses/SimpleViewResponse.php | 12 ++- src/Passport.php | 38 ++++++--- src/PassportServiceProvider.php | 4 +- 11 files changed, 202 insertions(+), 48 deletions(-) create mode 100644 resources/views/device-authorize-result.blade.php create mode 100644 resources/views/device-authorize.blade.php rename resources/views/{device.blade.php => device-user-code.blade.php} (97%) create mode 100644 src/Contracts/DeviceAuthorizationResultViewResponse.php create mode 100644 src/Contracts/DeviceAuthorizationViewResponse.php rename src/Contracts/{DeviceCodeViewResponse.php => DeviceUserCodeViewResponse.php} (83%) diff --git a/resources/views/device-authorize-result.blade.php b/resources/views/device-authorize-result.blade.php new file mode 100644 index 000000000..046c657a3 --- /dev/null +++ b/resources/views/device-authorize-result.blade.php @@ -0,0 +1,19 @@ +@extends('layouts.app') + +@section('content') +
+
+
+
+
+ {{ __($approved ? 'Success!' : 'Canceled!') }} +
+ +
+ {{ __($approved ? 'Continue on your device.' : 'Device authorization canceled.') }} +
+
+
+
+
+@endsection diff --git a/resources/views/device-authorize.blade.php b/resources/views/device-authorize.blade.php new file mode 100644 index 000000000..7a5581473 --- /dev/null +++ b/resources/views/device-authorize.blade.php @@ -0,0 +1,50 @@ +@extends('layouts.app') + +@section('content') +
+
+
+
+
{{ __('Device Authorization') }}
+ +
+

{{ $client->name }} is requesting permission to access your account.

+ + @if (count($scopes) > 0) +
+

This application will be able to:

+ +
    + @foreach ($scopes as $scope) +
  • {{ $scope->description }}
  • + @endforeach +
+
+ @endif + +
+
+ @csrf + + + + + +
+ +
+ @csrf + @method('DELETE') + + + + + +
+
+
+
+
+
+
+@endsection diff --git a/resources/views/device.blade.php b/resources/views/device-user-code.blade.php similarity index 97% rename from resources/views/device.blade.php rename to resources/views/device-user-code.blade.php index 6aa248075..918da72e7 100644 --- a/resources/views/device.blade.php +++ b/resources/views/device-user-code.blade.php @@ -10,7 +10,7 @@
{{ __('Enter the code displayed on your device.') }} -
+
diff --git a/routes/web.php b/routes/web.php index 93b240aaf..6e1db7beb 100644 --- a/routes/web.php +++ b/routes/web.php @@ -46,7 +46,17 @@ Route::get('/device/authorize', [ 'uses' => 'DeviceAuthorizationController@authorize', - 'as' => 'device.authorize', + 'as' => 'device.authorizations.authorize', + ]); + + Route::post('/device/authorize', [ + 'uses' => 'DeviceAuthorizationController@approve', + 'as' => 'device.authorizations.approve', + ]); + + Route::delete('/device/authorize', [ + 'uses' => 'DeviceAuthorizationController@deny', + 'as' => 'device.authorizations.deny', ]); Route::get('/tokens', [ diff --git a/src/Contracts/DeviceAuthorizationResultViewResponse.php b/src/Contracts/DeviceAuthorizationResultViewResponse.php new file mode 100644 index 000000000..ddf217329 --- /dev/null +++ b/src/Contracts/DeviceAuthorizationResultViewResponse.php @@ -0,0 +1,16 @@ +server = $server; - $this->deviceCodeViewResponse = $deviceCodeViewResponse; - $this->authorizationViewResponse = $authorizationViewResponse; + $this->deviceUserCodeViewResponse = $deviceUserCodeViewResponse; + $this->deviceAuthorizationViewResponse = $deviceAuthorizationViewResponse; + $this->deviceAuthorizationResultViewResponse = $deviceAuthorizationResultViewResponse; } /** - * Show the form for entering user code. + * Show the form for entering the user code. * * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\RedirectResponse|\Laravel\Passport\Contracts\DeviceCodeViewResponse + * @return \Illuminate\Http\RedirectResponse|\Laravel\Passport\Contracts\DeviceUserCodeViewResponse */ public function userCode(Request $request) { if ($userCode = $request->query('user_code')) { - return to_route('passport.device.authorize', [ + return to_route('passport.device.authorizations.authorize', [ 'user_code' => $userCode, ]); } - return $this->deviceCodeViewResponse->withParameters([ + return $this->deviceUserCodeViewResponse->withParameters([ 'request' => $request, ]); } /** - * Authorize a client to access the user's account. + * Authorize a device to access the user's account. * * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\RedirectResponse|\Laravel\Passport\Contracts\AuthorizationViewResponse + * @return \Illuminate\Http\RedirectResponse|\Laravel\Passport\Contracts\DeviceAuthorizationViewResponse */ public function authorize(Request $request) { + if (! $userCode = $request->query('user_code')) { + return to_route('passport.device'); + } + $deviceCode = Passport::deviceCode() ->with('client') - ->where('user_code', $userCode = $request->query('user_code')) + ->where('user_code', $userCode) ->first(); if (! $deviceCode) { @@ -94,7 +109,7 @@ public function authorize(Request $request) $request->session()->put('authToken', $authToken = Str::random()); $request->session()->put('deviceCode', $deviceCode->getKey()); - return $this->authorizationViewResponse->withParameters([ + return $this->deviceAuthorizationViewResponse->withParameters([ 'client' => $deviceCode->client, 'user' => $request->user(), 'scopes' => Passport::scopesFor($deviceCode->scopes), @@ -104,12 +119,10 @@ public function authorize(Request $request) } /** - * Approve the authorization request. + * Approve the device authorization request. * * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response - * - * @throws \Laravel\Passport\Exceptions\OAuthServerException + * @return \Laravel\Passport\Contracts\DeviceAuthorizationResultViewResponse */ public function approve(Request $request) { @@ -119,16 +132,17 @@ public function approve(Request $request) true )); - return 'approved'; + return $this->deviceAuthorizationResultViewResponse->withParameters([ + 'request' => $request, + 'approved' => true, + ]); } /** - * Deny the authorization request. + * Deny the device authorization request. * * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response - * - * @throws \Laravel\Passport\Exceptions\OAuthServerException + * @return \Laravel\Passport\Contracts\DeviceAuthorizationResultViewResponse */ public function deny(Request $request) { @@ -138,6 +152,9 @@ public function deny(Request $request) false )); - return 'denied'; + return $this->deviceAuthorizationResultViewResponse->withParameters([ + 'request' => $request, + 'approved' => false, + ]); } } diff --git a/src/Http/Responses/SimpleViewResponse.php b/src/Http/Responses/SimpleViewResponse.php index 5c3ad9097..8af8ed17f 100644 --- a/src/Http/Responses/SimpleViewResponse.php +++ b/src/Http/Responses/SimpleViewResponse.php @@ -3,12 +3,16 @@ namespace Laravel\Passport\Http\Responses; use Illuminate\Contracts\Support\Responsable; -use Laravel\Passport\Contracts\AuthorizationViewResponse as AuthorizationViewResponseContract; -use Laravel\Passport\Contracts\DeviceCodeViewResponse as DeviceCodeViewResponseContract; +use Laravel\Passport\Contracts\AuthorizationViewResponse; +use Laravel\Passport\Contracts\DeviceAuthorizationResultViewResponse; +use Laravel\Passport\Contracts\DeviceAuthorizationViewResponse; +use Laravel\Passport\Contracts\DeviceUserCodeViewResponse; class SimpleViewResponse implements - AuthorizationViewResponseContract, - DeviceCodeViewResponseContract + AuthorizationViewResponse, + DeviceAuthorizationViewResponse, + DeviceAuthorizationResultViewResponse, + DeviceUserCodeViewResponse { /** * The name of the view or the callable used to generate the view. diff --git a/src/Passport.php b/src/Passport.php index 904f01b43..20fa051c5 100644 --- a/src/Passport.php +++ b/src/Passport.php @@ -7,7 +7,9 @@ use DateTimeInterface; use Illuminate\Contracts\Encryption\Encrypter; use Laravel\Passport\Contracts\AuthorizationViewResponse; -use Laravel\Passport\Contracts\DeviceCodeViewResponse; +use Laravel\Passport\Contracts\DeviceAuthorizationResultViewResponse; +use Laravel\Passport\Contracts\DeviceAuthorizationViewResponse; +use Laravel\Passport\Contracts\DeviceUserCodeViewResponse; use Laravel\Passport\Http\Responses\SimpleViewResponse; use League\OAuth2\Server\ResourceServer; use Mockery; @@ -646,22 +648,40 @@ public static function tokenEncryptionKey(Encrypter $encrypter) */ public static function authorizationView($view) { - app()->singleton(AuthorizationViewResponse::class, function ($app) use ($view) { - return new SimpleViewResponse($view); - }); + app()->singleton(AuthorizationViewResponse::class, fn () => new SimpleViewResponse($view)); } /** - * Specify which view should be used as the device code view. + * Specify which view should be used as the device authorization view. * * @param callable|string $view * @return void */ - public static function deviceCodeView($view) + public static function deviceAuthorizationView($view) { - app()->singleton(DeviceCodeViewResponse::class, function ($app) use ($view) { - return new SimpleViewResponse($view); - }); + app()->singleton(DeviceAuthorizationViewResponse::class, fn () => new SimpleViewResponse($view)); + } + + /** + * Specify which view should be used as the device authorization result view. + * + * @param callable|string $view + * @return void + */ + public static function deviceAuthorizationResultView($view) + { + app()->singleton(DeviceAuthorizationResultViewResponse::class, fn () => new SimpleViewResponse($view)); + } + + /** + * Specify which view should be used as the device user code view. + * + * @param callable|string $view + * @return void + */ + public static function deviceUserCodeView($view) + { + app()->singleton(DeviceUserCodeViewResponse::class, fn () => new SimpleViewResponse($view)); } /** diff --git a/src/PassportServiceProvider.php b/src/PassportServiceProvider.php index dd143c3ab..8addbdf1c 100644 --- a/src/PassportServiceProvider.php +++ b/src/PassportServiceProvider.php @@ -138,7 +138,9 @@ public function register() $this->registerGuard(); Passport::authorizationView('passport::authorize'); - Passport::deviceCodeView('passport::device'); + Passport::deviceAuthorizationView('passport::device-authorize'); + Passport::deviceAuthorizationResultView('passport::device-authorize-result'); + Passport::deviceUserCodeView('passport::device-user-code'); } /** From 63255d19f9e4906ae85758fa2219d5f0d635418e Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Sun, 7 Jul 2024 00:11:45 +0330 Subject: [PATCH 12/38] wip --- src/Bridge/DeviceCodeRepository.php | 4 ++-- src/Http/Controllers/DeviceAuthorizationController.php | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Bridge/DeviceCodeRepository.php b/src/Bridge/DeviceCodeRepository.php index a2b1a8634..5a627e2d4 100644 --- a/src/Bridge/DeviceCodeRepository.php +++ b/src/Bridge/DeviceCodeRepository.php @@ -61,8 +61,8 @@ public function getDeviceCodeEntityByDeviceCode(string $deviceCode): ?DeviceCode $record->client_id, $record->scopes, ! is_null($record->user_approved_at), - $record->last_polled_at, - $record->expires_at + $record->last_polled_at?->toDateTimeImmutable(), + $record->expires_at?->toDateTimeImmutable() ) : null; } diff --git a/src/Http/Controllers/DeviceAuthorizationController.php b/src/Http/Controllers/DeviceAuthorizationController.php index 4e8077bb3..0dc59f490 100644 --- a/src/Http/Controllers/DeviceAuthorizationController.php +++ b/src/Http/Controllers/DeviceAuthorizationController.php @@ -96,6 +96,8 @@ public function authorize(Request $request) $deviceCode = Passport::deviceCode() ->with('client') ->where('user_code', $userCode) + ->where('expires_at', '>', now()) + ->where('revoked', false) ->first(); if (! $deviceCode) { From 42013068c7444fc444cd092cbfc7729d12a64e58 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Sun, 7 Jul 2024 00:22:52 +0330 Subject: [PATCH 13/38] wip --- src/Console/PurgeCommand.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Console/PurgeCommand.php b/src/Console/PurgeCommand.php index eacb3b327..74c5e4687 100644 --- a/src/Console/PurgeCommand.php +++ b/src/Console/PurgeCommand.php @@ -41,6 +41,7 @@ public function handle() Passport::token()->where('revoked', 1)->orWhereDate('expires_at', '<', $expired)->delete(); Passport::authCode()->where('revoked', 1)->orWhereDate('expires_at', '<', $expired)->delete(); Passport::refreshToken()->where('revoked', 1)->orWhereDate('expires_at', '<', $expired)->delete(); + Passport::deviceCode()->where('revoked', 1)->orWhereDate('expires_at', '<', $expired)->delete(); $this->option('hours') ? $this->components->info('Purged revoked items and items expired for more than '.$this->option('hours').' hours.') @@ -49,12 +50,14 @@ public function handle() Passport::token()->where('revoked', 1)->delete(); Passport::authCode()->where('revoked', 1)->delete(); Passport::refreshToken()->where('revoked', 1)->delete(); + Passport::deviceCode()->where('revoked', 1)->delete(); $this->components->info('Purged revoked items.'); } elseif ($this->option('expired')) { Passport::token()->whereDate('expires_at', '<', $expired)->delete(); Passport::authCode()->whereDate('expires_at', '<', $expired)->delete(); Passport::refreshToken()->whereDate('expires_at', '<', $expired)->delete(); + Passport::deviceCode()->whereDate('expires_at', '<', $expired)->delete(); $this->option('hours') ? $this->components->info('Purged items expired for more than '.$this->option('hours').' hours.') From 4341491341eccb1582033740628351ab526c0a75 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Fri, 16 Aug 2024 10:38:15 +0330 Subject: [PATCH 14/38] fix controllers --- ...000001_create_oauth_device_codes_table.php | 4 +- routes/web.php | 22 ++-- src/Bridge/DeviceCodeRepository.php | 8 +- .../ApproveDeviceAuthorizationController.php | 37 ++++++ .../DenyDeviceAuthorizationController.php | 37 ++++++ .../DeviceAuthorizationController.php | 115 +----------------- src/Http/Controllers/DeviceCodeController.php | 30 +---- .../Controllers/DeviceUserCodeController.php | 33 +++++ .../RetrievesDeviceCodeFromSession.php | 12 +- 9 files changed, 137 insertions(+), 161 deletions(-) create mode 100644 src/Http/Controllers/ApproveDeviceAuthorizationController.php create mode 100644 src/Http/Controllers/DenyDeviceAuthorizationController.php create mode 100644 src/Http/Controllers/DeviceUserCodeController.php diff --git a/database/migrations/2024_06_01_000001_create_oauth_device_codes_table.php b/database/migrations/2024_06_01_000001_create_oauth_device_codes_table.php index 0045bf711..e1d6fa069 100644 --- a/database/migrations/2024_06_01_000001_create_oauth_device_codes_table.php +++ b/database/migrations/2024_06_01_000001_create_oauth_device_codes_table.php @@ -34,10 +34,8 @@ public function down(): void /** * Get the migration connection name. - * - * @return string|null */ - public function getConnection() + public function getConnection(): ?string { return $this->connection ?? config('passport.connection'); } diff --git a/routes/web.php b/routes/web.php index 6e1db7beb..dd38f1240 100644 --- a/routes/web.php +++ b/routes/web.php @@ -8,24 +8,24 @@ 'middleware' => 'throttle', ]); +Route::get('/authorize', [ + 'uses' => 'AuthorizationController@authorize', + 'as' => 'authorizations.authorize', + 'middleware' => 'web', +]); + Route::post('/device/code', [ - 'uses' => 'DeviceCodeController@issueDeviceCode', + 'uses' => 'DeviceCodeController', 'as' => 'device.code', 'middleware' => 'throttle', ]); Route::get('/device', [ - 'uses' => 'DeviceAuthorizationController@userCode', + 'uses' => 'DeviceUserCodeController', 'as' => 'device', 'middleware' => 'web', ]); -Route::get('/authorize', [ - 'uses' => 'AuthorizationController@authorize', - 'as' => 'authorizations.authorize', - 'middleware' => 'web', -]); - $guard = config('passport.guard', null); Route::middleware(['web', $guard ? 'auth:'.$guard : 'auth'])->group(function () { @@ -45,17 +45,17 @@ ]); Route::get('/device/authorize', [ - 'uses' => 'DeviceAuthorizationController@authorize', + 'uses' => 'DeviceAuthorizationController', 'as' => 'device.authorizations.authorize', ]); Route::post('/device/authorize', [ - 'uses' => 'DeviceAuthorizationController@approve', + 'uses' => 'ApproveDeviceAuthorizationController', 'as' => 'device.authorizations.approve', ]); Route::delete('/device/authorize', [ - 'uses' => 'DeviceAuthorizationController@deny', + 'uses' => 'DenyDeviceAuthorizationController', 'as' => 'device.authorizations.deny', ]); diff --git a/src/Bridge/DeviceCodeRepository.php b/src/Bridge/DeviceCodeRepository.php index 5a627e2d4..95ef79f2a 100644 --- a/src/Bridge/DeviceCodeRepository.php +++ b/src/Bridge/DeviceCodeRepository.php @@ -25,14 +25,14 @@ public function getNewDeviceCode(): DeviceCodeEntityInterface public function persistDeviceCode(DeviceCodeEntityInterface $deviceCodeEntity): void { if ($deviceCodeEntity->isLastPolledAtDirty()) { - Passport::deviceCode()->whereKey($deviceCodeEntity->getIdentifier())->update([ + Passport::deviceCode()->whereKey($deviceCodeEntity->getIdentifier())->forceFill([ 'last_polled_at' => $deviceCodeEntity->getLastPolledAt(), - ]); + ])->save(); } elseif ($deviceCodeEntity->isUserDirty()) { - Passport::deviceCode()->whereKey($deviceCodeEntity->getIdentifier())->update([ + Passport::deviceCode()->whereKey($deviceCodeEntity->getIdentifier())->forceFill([ 'user_id' => $deviceCodeEntity->getUserIdentifier(), 'user_approved_at' => $deviceCodeEntity->getUserApproved() ? new DateTime : null, - ]); + ])->save(); } else { Passport::deviceCode()->forceFill([ 'id' => $deviceCodeEntity->getIdentifier(), diff --git a/src/Http/Controllers/ApproveDeviceAuthorizationController.php b/src/Http/Controllers/ApproveDeviceAuthorizationController.php new file mode 100644 index 000000000..4b0a78636 --- /dev/null +++ b/src/Http/Controllers/ApproveDeviceAuthorizationController.php @@ -0,0 +1,37 @@ +withErrorHandling(fn () => $this->server->completeDeviceAuthorizationRequest( + $this->getDeviceCodeFromSession($request), + $request->user()->getAuthIdentifier(), + true + )); + + return $this->viewResponse->withParameters([ + 'request' => $request, + 'approved' => true, + ]); + } +} diff --git a/src/Http/Controllers/DenyDeviceAuthorizationController.php b/src/Http/Controllers/DenyDeviceAuthorizationController.php new file mode 100644 index 000000000..6410efedd --- /dev/null +++ b/src/Http/Controllers/DenyDeviceAuthorizationController.php @@ -0,0 +1,37 @@ +withErrorHandling(fn () => $this->server->completeDeviceAuthorizationRequest( + $this->getDeviceCodeFromSession($request), + $request->user()->getAuthIdentifier(), + false + )); + + return $this->viewResponse->withParameters([ + 'request' => $request, + 'approved' => false, + ]); + } +} diff --git a/src/Http/Controllers/DeviceAuthorizationController.php b/src/Http/Controllers/DeviceAuthorizationController.php index 0dc59f490..3780f15ce 100644 --- a/src/Http/Controllers/DeviceAuthorizationController.php +++ b/src/Http/Controllers/DeviceAuthorizationController.php @@ -2,92 +2,25 @@ namespace Laravel\Passport\Http\Controllers; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Str; -use Laravel\Passport\Contracts\DeviceAuthorizationResultViewResponse; use Laravel\Passport\Contracts\DeviceAuthorizationViewResponse; -use Laravel\Passport\Contracts\DeviceUserCodeViewResponse; use Laravel\Passport\Passport; -use League\OAuth2\Server\AuthorizationServer; class DeviceAuthorizationController { - use ConvertsPsrResponses, HandlesOAuthErrors, RetrievesDeviceCodeFromSession; - - /** - * The authorization server. - * - * @var \League\OAuth2\Server\AuthorizationServer - */ - protected $server; - - /** - * The user code view response implementation. - * - * @var \Laravel\Passport\Contracts\DeviceUserCodeViewResponse - */ - protected $deviceUserCodeViewResponse; - - /** - * The authorization view response implementation. - * - * @var \Laravel\Passport\Contracts\DeviceAuthorizationViewResponse - */ - protected $deviceAuthorizationViewResponse; - - /** - * The authorization result view response implementation. - * - * @var \Laravel\Passport\Contracts\DeviceAuthorizationResultViewResponse - */ - protected $deviceAuthorizationResultViewResponse; - /** * Create a new controller instance. - * - * @param \League\OAuth2\Server\AuthorizationServer $server - * @param \Laravel\Passport\Contracts\DeviceUserCodeViewResponse $deviceUserCodeViewResponse - * @param \Laravel\Passport\Contracts\DeviceAuthorizationViewResponse $deviceAuthorizationViewResponse - * @param \Laravel\Passport\Contracts\DeviceAuthorizationResultViewResponse $deviceAuthorizationResultViewResponse - * @return void */ - public function __construct(AuthorizationServer $server, - DeviceUserCodeViewResponse $deviceUserCodeViewResponse, - DeviceAuthorizationViewResponse $deviceAuthorizationViewResponse, - DeviceAuthorizationResultViewResponse $deviceAuthorizationResultViewResponse) + public function __construct(protected DeviceAuthorizationViewResponse $viewResponse) { - $this->server = $server; - $this->deviceUserCodeViewResponse = $deviceUserCodeViewResponse; - $this->deviceAuthorizationViewResponse = $deviceAuthorizationViewResponse; - $this->deviceAuthorizationResultViewResponse = $deviceAuthorizationResultViewResponse; - } - - /** - * Show the form for entering the user code. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\RedirectResponse|\Laravel\Passport\Contracts\DeviceUserCodeViewResponse - */ - public function userCode(Request $request) - { - if ($userCode = $request->query('user_code')) { - return to_route('passport.device.authorizations.authorize', [ - 'user_code' => $userCode, - ]); - } - - return $this->deviceUserCodeViewResponse->withParameters([ - 'request' => $request, - ]); } /** * Authorize a device to access the user's account. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\RedirectResponse|\Laravel\Passport\Contracts\DeviceAuthorizationViewResponse */ - public function authorize(Request $request) + public function __invoke(Request $request): RedirectResponse|DeviceAuthorizationViewResponse { if (! $userCode = $request->query('user_code')) { return to_route('passport.device'); @@ -111,7 +44,7 @@ public function authorize(Request $request) $request->session()->put('authToken', $authToken = Str::random()); $request->session()->put('deviceCode', $deviceCode->getKey()); - return $this->deviceAuthorizationViewResponse->withParameters([ + return $this->viewResponse->withParameters([ 'client' => $deviceCode->client, 'user' => $request->user(), 'scopes' => Passport::scopesFor($deviceCode->scopes), @@ -119,44 +52,4 @@ public function authorize(Request $request) 'authToken' => $authToken, ]); } - - /** - * Approve the device authorization request. - * - * @param \Illuminate\Http\Request $request - * @return \Laravel\Passport\Contracts\DeviceAuthorizationResultViewResponse - */ - public function approve(Request $request) - { - $this->withErrorHandling(fn () => $this->server->completeDeviceAuthorizationRequest( - $this->getDeviceCodeFromSession($request), - $request->user()->getAuthIdentifier(), - true - )); - - return $this->deviceAuthorizationResultViewResponse->withParameters([ - 'request' => $request, - 'approved' => true, - ]); - } - - /** - * Deny the device authorization request. - * - * @param \Illuminate\Http\Request $request - * @return \Laravel\Passport\Contracts\DeviceAuthorizationResultViewResponse - */ - public function deny(Request $request) - { - $this->withErrorHandling(fn () => $this->server->completeDeviceAuthorizationRequest( - $this->getDeviceCodeFromSession($request), - $request->user()->getAuthIdentifier(), - false - )); - - return $this->deviceAuthorizationResultViewResponse->withParameters([ - 'request' => $request, - 'approved' => false, - ]); - } } diff --git a/src/Http/Controllers/DeviceCodeController.php b/src/Http/Controllers/DeviceCodeController.php index 689d1e478..39bb0838d 100644 --- a/src/Http/Controllers/DeviceCodeController.php +++ b/src/Http/Controllers/DeviceCodeController.php @@ -2,6 +2,7 @@ namespace Laravel\Passport\Http\Controllers; +use Illuminate\Http\Response; use League\OAuth2\Server\AuthorizationServer; use Nyholm\Psr7\Response as Psr7Response; use Psr\Http\Message\ServerRequestInterface; @@ -10,39 +11,20 @@ class DeviceCodeController { use ConvertsPsrResponses, HandlesOAuthErrors; - /** - * The authorization server. - * - * @var \League\OAuth2\Server\AuthorizationServer - */ - protected $server; - /** * Create a new controller instance. - * - * @param \League\OAuth2\Server\AuthorizationServer $server - * @param \Laravel\Passport\TokenRepository $tokens - * @return void */ - public function __construct(AuthorizationServer $server) + public function __construct(protected AuthorizationServer $server) { - $this->server = $server; } /** * Issue a device code for the client. - * - * @param \Psr\Http\Message\ServerRequestInterface $request - * @return \Illuminate\Http\Response - * - * @throws \Laravel\Passport\Exceptions\OAuthServerException */ - public function issueDeviceCode(ServerRequestInterface $request) + public function __invoke(ServerRequestInterface $request): Response { - return $this->withErrorHandling(function () use ($request) { - return $this->convertResponse( - $this->server->respondToDeviceAuthorizationRequest($request, new Psr7Response) - ); - }); + return $this->withErrorHandling(fn () => $this->convertResponse( + $this->server->respondToDeviceAuthorizationRequest($request, new Psr7Response) + )); } } diff --git a/src/Http/Controllers/DeviceUserCodeController.php b/src/Http/Controllers/DeviceUserCodeController.php new file mode 100644 index 000000000..ae0fc291a --- /dev/null +++ b/src/Http/Controllers/DeviceUserCodeController.php @@ -0,0 +1,33 @@ +query('user_code')) { + return to_route('passport.device.authorizations.authorize', [ + 'user_code' => $userCode, + ]); + } + + return $this->viewResponse->withParameters([ + 'request' => $request, + ]); + } +} diff --git a/src/Http/Controllers/RetrievesDeviceCodeFromSession.php b/src/Http/Controllers/RetrievesDeviceCodeFromSession.php index 89d490b3c..b0b6217a7 100644 --- a/src/Http/Controllers/RetrievesDeviceCodeFromSession.php +++ b/src/Http/Controllers/RetrievesDeviceCodeFromSession.php @@ -16,18 +16,14 @@ trait RetrievesDeviceCodeFromSession */ protected function getDeviceCodeFromSession(Request $request): string { - if ($request->isNotFilled('auth_token') || $request->session()->pull('authToken') !== $request->get('auth_token')) { + if ($request->isNotFilled('auth_token') || + $request->session()->pull('authToken') !== $request->get('auth_token')) { $request->session()->forget(['authToken', 'deviceCode']); throw InvalidAuthTokenException::different(); } - return tap($request->session()->pull('deviceCode'), function ($deviceCode) { - if (! $deviceCode) { - throw new Exception('Device code was not present in the session.'); - } - - return $deviceCode; - }); + return $request->session()->pull('deviceCode') + ?? throw new Exception('Device code was not present in the session.'); } } From 43f273ccf9540ae04799c83e0bc733330d92e496 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Fri, 16 Aug 2024 10:55:58 +0330 Subject: [PATCH 15/38] formatting --- src/DeviceCode.php | 9 +++------ src/Passport.php | 38 ++++++++++++-------------------------- 2 files changed, 15 insertions(+), 32 deletions(-) diff --git a/src/DeviceCode.php b/src/DeviceCode.php index 3a26eb136..d2d70f806 100644 --- a/src/DeviceCode.php +++ b/src/DeviceCode.php @@ -3,6 +3,7 @@ namespace Laravel\Passport; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; class DeviceCode extends Model { @@ -56,20 +57,16 @@ class DeviceCode extends Model /** * Get the client that owns the authentication code. - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ - public function client() + public function client(): BelongsTo { return $this->belongsTo(Passport::clientModel()); } /** * Get the current connection name for the model. - * - * @return string|null */ - public function getConnectionName() + public function getConnectionName(): ?string { return $this->connection ?? config('passport.connection'); } diff --git a/src/Passport.php b/src/Passport.php index 20fa051c5..428731f02 100644 --- a/src/Passport.php +++ b/src/Passport.php @@ -3,6 +3,7 @@ namespace Laravel\Passport; use Carbon\Carbon; +use Closure; use DateInterval; use DateTimeInterface; use Illuminate\Contracts\Encryption\Encrypter; @@ -113,9 +114,9 @@ class Passport /** * The device code model class name. * - * @var string + * @var class-string<\Laravel\Passport\DeviceCode> */ - public static $deviceCodeModel = 'Laravel\Passport\DeviceCode'; + public static string $deviceCodeModel = DeviceCode::class; /** * The client model class name. @@ -493,10 +494,9 @@ public static function authCode() /** * Set the device code model class name. * - * @param string $deviceCodeModel - * @return void + * @param class-string<\Laravel\Passport\DeviceCode> $deviceCodeModel */ - public static function useDeviceCodeModel($deviceCodeModel) + public static function useDeviceCodeModel(string $deviceCodeModel): void { static::$deviceCodeModel = $deviceCodeModel; } @@ -504,19 +504,17 @@ public static function useDeviceCodeModel($deviceCodeModel) /** * Get the device code model class name. * - * @return string + * @return class-string<\Laravel\Passport\DeviceCode> */ - public static function deviceCodeModel() + public static function deviceCodeModel(): string { return static::$deviceCodeModel; } /** * Get a new device code model instance. - * - * @return \Laravel\Passport\DeviceCode */ - public static function deviceCode() + public static function deviceCode(): DeviceCode { return new static::$deviceCodeModel; } @@ -642,44 +640,32 @@ public static function tokenEncryptionKey(Encrypter $encrypter) /** * Specify which view should be used as the authorization view. - * - * @param callable|string $view - * @return void */ - public static function authorizationView($view) + public static function authorizationView(Closure|string $view): void { app()->singleton(AuthorizationViewResponse::class, fn () => new SimpleViewResponse($view)); } /** * Specify which view should be used as the device authorization view. - * - * @param callable|string $view - * @return void */ - public static function deviceAuthorizationView($view) + public static function deviceAuthorizationView(Closure|string $view): void { app()->singleton(DeviceAuthorizationViewResponse::class, fn () => new SimpleViewResponse($view)); } /** * Specify which view should be used as the device authorization result view. - * - * @param callable|string $view - * @return void */ - public static function deviceAuthorizationResultView($view) + public static function deviceAuthorizationResultView(Closure|string $view): void { app()->singleton(DeviceAuthorizationResultViewResponse::class, fn () => new SimpleViewResponse($view)); } /** * Specify which view should be used as the device user code view. - * - * @param callable|string $view - * @return void */ - public static function deviceUserCodeView($view) + public static function deviceUserCodeView(Closure|string $view): void { app()->singleton(DeviceUserCodeViewResponse::class, fn () => new SimpleViewResponse($view)); } From da26e7d5452b6bfcffeb62c0af8f0012654cfaa7 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Fri, 16 Aug 2024 17:03:37 +0330 Subject: [PATCH 16/38] add tests --- database/factories/ClientFactory.php | 10 + src/Bridge/DeviceCodeRepository.php | 8 +- src/PassportServiceProvider.php | 6 +- .../Feature/DeviceAuthorizationGrantTest.php | 203 ++++++++++++++++++ tests/Feature/DeviceCodeControllerTest.php | 31 --- 5 files changed, 220 insertions(+), 38 deletions(-) create mode 100644 tests/Feature/DeviceAuthorizationGrantTest.php delete mode 100644 tests/Feature/DeviceCodeControllerTest.php diff --git a/database/factories/ClientFactory.php b/database/factories/ClientFactory.php index 6791b911e..4dd54f903 100644 --- a/database/factories/ClientFactory.php +++ b/database/factories/ClientFactory.php @@ -73,4 +73,14 @@ public function asClientCredentials() 'grant_types' => ['client_credentials'], ]); } + + /** + * Use as a Device Code client. + */ + public function asDeviceCodeClient(): static + { + return $this->state([ + 'grant_types' => ['urn:ietf:params:oauth:grant-type:device_code', 'refresh_token'], + ]); + } } diff --git a/src/Bridge/DeviceCodeRepository.php b/src/Bridge/DeviceCodeRepository.php index 95ef79f2a..5a627e2d4 100644 --- a/src/Bridge/DeviceCodeRepository.php +++ b/src/Bridge/DeviceCodeRepository.php @@ -25,14 +25,14 @@ public function getNewDeviceCode(): DeviceCodeEntityInterface public function persistDeviceCode(DeviceCodeEntityInterface $deviceCodeEntity): void { if ($deviceCodeEntity->isLastPolledAtDirty()) { - Passport::deviceCode()->whereKey($deviceCodeEntity->getIdentifier())->forceFill([ + Passport::deviceCode()->whereKey($deviceCodeEntity->getIdentifier())->update([ 'last_polled_at' => $deviceCodeEntity->getLastPolledAt(), - ])->save(); + ]); } elseif ($deviceCodeEntity->isUserDirty()) { - Passport::deviceCode()->whereKey($deviceCodeEntity->getIdentifier())->forceFill([ + Passport::deviceCode()->whereKey($deviceCodeEntity->getIdentifier())->update([ 'user_id' => $deviceCodeEntity->getUserIdentifier(), 'user_approved_at' => $deviceCodeEntity->getUserApproved() ? new DateTime : null, - ])->save(); + ]); } else { Passport::deviceCode()->forceFill([ 'id' => $deviceCodeEntity->getIdentifier(), diff --git a/src/PassportServiceProvider.php b/src/PassportServiceProvider.php index 8addbdf1c..4d829a160 100644 --- a/src/PassportServiceProvider.php +++ b/src/PassportServiceProvider.php @@ -138,9 +138,9 @@ public function register() $this->registerGuard(); Passport::authorizationView('passport::authorize'); - Passport::deviceAuthorizationView('passport::device-authorize'); - Passport::deviceAuthorizationResultView('passport::device-authorize-result'); - Passport::deviceUserCodeView('passport::device-user-code'); + // Passport::deviceAuthorizationView(fn ($params) => $params); + // Passport::deviceAuthorizationResultView(fn ($params) => $params); + // Passport::deviceUserCodeView(fn ($params) => $params); } /** diff --git a/tests/Feature/DeviceAuthorizationGrantTest.php b/tests/Feature/DeviceAuthorizationGrantTest.php new file mode 100644 index 000000000..bc6215417 --- /dev/null +++ b/tests/Feature/DeviceAuthorizationGrantTest.php @@ -0,0 +1,203 @@ + $params); + Passport::deviceAuthorizationResultView(fn ($params) => $params); + Passport::deviceUserCodeView(fn ($params) => $params); + } + + public function testIssueDeviceCode() + { + $client = ClientFactory::new()->asDeviceCodeClient()->create(); + + $response = $this->post('/oauth/device/code', [ + 'client_id' => $client->getKey(), + 'scope' => '', + ]); + + $response->assertOk(); + $json = $response->json(); + + $this->assertArrayHasKey('device_code', $json); + $this->assertArrayHasKey('user_code', $json); + // $this->assertSame(5, $json['interval']); // TODO https://github.com/thephpleague/oauth2-server/pull/1410 + $this->assertSame(600, $json['expires_in']); + $this->assertSame('http://localhost/oauth/device', $json['verification_uri']); + $this->assertSame('http://localhost/oauth/device?user_code='.$json['user_code'], $json['verification_uri_complete']); + } + + public function testRequestAccessTokenAuthorizationPending() + { + $client = ClientFactory::new()->asDeviceCodeClient()->create(); + + ['device_code' => $deviceCode] = $this->post('/oauth/device/code', [ + 'client_id' => $client->getKey(), + 'scope' => '', + ])->json(); + + $response = $this->post('/oauth/token', [ + 'grant_type' => 'urn:ietf:params:oauth:grant-type:device_code', + 'client_id' => $client->getKey(), + 'client_secret' => $client->plainSecret, + 'device_code' => $deviceCode, + ]); + + $response->assertBadRequest(); + $json = $response->json(); + + $this->assertArrayHasKey('error', $json); + $this->assertArrayHasKey('error_description', $json); + $this->assertSame('authorization_pending', $json['error']); + } + + public function testAuthorizationWithoutUserCodeRedirects() + { + $user = UserFactory::new()->create(); + + $response = $this->actingAs($user)->get('/oauth/device/authorize'); + $response->assertRedirect('/oauth/device'); + $response->assertRedirectToRoute('passport.device'); + } + + public function testVerificationUrl() + { + $client = ClientFactory::new()->asDeviceCodeClient()->create(); + + [ + 'verification_uri' => $verificationUri, + 'verification_uri_complete' => $verificationUriComplete, + 'user_code' => $userCode, + ] = $this->post('/oauth/device/code', [ + 'client_id' => $client->getKey(), + 'scope' => '', + ])->json(); + + $response = $this->get($verificationUri); + $response->assertOk(); + $this->assertEqualsCanonicalizing(['request'], array_keys($response->json())); + + $user = UserFactory::new()->create(); + + $response = $this->actingAs($user, 'web')->get($verificationUriComplete); + $response->assertRedirect('/oauth/device/authorize?user_code='.$userCode); + $response->assertRedirectToRoute('passport.device.authorizations.authorize', [ + 'user_code' => $userCode + ]); + } + + public function testAuthorizationWithInvalidUserCode() + { + $user = UserFactory::new()->create(); + + $response = $this->actingAs($user, 'web')->get('/oauth/device/authorize?user_code=12345678'); + $response->assertRedirectToRoute('passport.device'); + $response->assertSessionHasInput('user_code', '12345678'); + $response->assertSessionHasErrors(['user_code' => 'Incorrect code.']); + } + + public function testRequestAccessToken() + { + Passport::tokensCan([ + 'create' => 'Create', + 'read' => 'Read', + 'update' => 'Update', + 'delete' => 'Delete', + ]); + + $client = ClientFactory::new()->asDeviceCodeClient()->create(); + + ['device_code' => $deviceCode, 'user_code' => $userCode] = $this->post('/oauth/device/code', [ + 'client_id' => $client->getKey(), + 'scope' => 'create read', + ])->json(); + + $user = UserFactory::new()->create(); + $this->actingAs($user, 'web'); + + $response = $this->get('/oauth/device/authorize?user_code='.$userCode); + $response->assertOk(); + $response->assertSessionHas('deviceCode', $deviceCode); + $response->assertSessionHas('authToken'); + $json = $response->json(); + $this->assertEqualsCanonicalizing(['client', 'user', 'scopes', 'request', 'authToken'], array_keys($json)); + $this->assertSame(collect(Passport::scopesFor(['create', 'read']))->toArray(), $json['scopes']); + + ['authToken' => $authToken] = $json; + + $response = $this->post('/oauth/device/authorize', [ + 'auth_token' => $authToken, + ]); + $response->assertOk(); + $response->assertSessionMissing(['deviceCode', 'authToken']); + $json = $response->json(); + $this->assertEqualsCanonicalizing(['approved', 'request'], array_keys($json)); + $this->assertSame(true, $json['approved']); + + $response = $this->post('/oauth/token', [ + 'grant_type' => 'urn:ietf:params:oauth:grant-type:device_code', + 'client_id' => $client->getKey(), + 'client_secret' => $client->plainSecret, + 'device_code' => $deviceCode, + ]); + + $response->assertOk(); + $json = $response->json(); + + $this->assertArrayHasKey('access_token', $json); + $this->assertArrayHasKey('refresh_token', $json); + $this->assertSame('Bearer', $json['token_type']); + $this->assertSame(31536000, $json['expires_in']); + } + + public function testDenyAuthorization() + { + $client = ClientFactory::new()->asDeviceCodeClient()->create(); + + ['device_code' => $deviceCode, 'user_code' => $userCode] = $this->post('/oauth/device/code', [ + 'client_id' => $client->getKey(), + 'scope' => '', + ])->json(); + + $user = UserFactory::new()->create(); + $this->actingAs($user, 'web'); + + ['authToken' => $authToken] = $this->get('/oauth/device/authorize?user_code='.$userCode)->json(); + + $response = $this->delete('/oauth/device/authorize', [ + 'auth_token' => $authToken, + ]); + $response->assertOk(); + $response->assertSessionMissing(['deviceCode', 'authToken']); + $json = $response->json(); + $this->assertEqualsCanonicalizing(['approved', 'request'], array_keys($json)); + $this->assertSame(false, $json['approved']); + + $response = $this->post('/oauth/token', [ + 'grant_type' => 'urn:ietf:params:oauth:grant-type:device_code', + 'client_id' => $client->getKey(), + 'client_secret' => $client->plainSecret, + 'device_code' => $deviceCode, + ]); + + $response->assertUnauthorized(); + $json = $response->json(); + + $this->assertArrayHasKey('error', $json); + $this->assertArrayHasKey('error_description', $json); + $this->assertSame('access_denied', $json['error']); + } +} diff --git a/tests/Feature/DeviceCodeControllerTest.php b/tests/Feature/DeviceCodeControllerTest.php deleted file mode 100644 index 456199db2..000000000 --- a/tests/Feature/DeviceCodeControllerTest.php +++ /dev/null @@ -1,31 +0,0 @@ -create(); - - $response = $this->post('/oauth/device/code', [ - 'client_id' => $client->getKey(), - 'scope' => '', - ]); - - $response->assertOk(); - $json = $response->json(); - - $this->assertArrayHasKey('device_code', $json); - $this->assertArrayHasKey('user_code', $json); - $this->assertArrayHasKey('expires_in', $json); - // $this->assertArrayHasKey('interval', $json); // TODO https://github.com/thephpleague/oauth2-server/pull/1410 - $this->assertSame('http://localhost/oauth/device', $json['verification_uri']); - $this->assertStringStartsWith('http://localhost/oauth/device?user_code=', $json['verification_uri_complete']); - } -} From 90726be2df1079d2d7b8fdfa45028f6c7f9cc982 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Fri, 16 Aug 2024 17:14:19 +0330 Subject: [PATCH 17/38] formatting --- .../Feature/DeviceAuthorizationGrantTest.php | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/Feature/DeviceAuthorizationGrantTest.php b/tests/Feature/DeviceAuthorizationGrantTest.php index bc6215417..0b0b6cd1f 100644 --- a/tests/Feature/DeviceAuthorizationGrantTest.php +++ b/tests/Feature/DeviceAuthorizationGrantTest.php @@ -94,9 +94,7 @@ public function testVerificationUrl() $response = $this->actingAs($user, 'web')->get($verificationUriComplete); $response->assertRedirect('/oauth/device/authorize?user_code='.$userCode); - $response->assertRedirectToRoute('passport.device.authorizations.authorize', [ - 'user_code' => $userCode - ]); + $response->assertRedirectToRoute('passport.device.authorizations.authorize', ['user_code' => $userCode]); } public function testAuthorizationWithInvalidUserCode() @@ -120,7 +118,10 @@ public function testRequestAccessToken() $client = ClientFactory::new()->asDeviceCodeClient()->create(); - ['device_code' => $deviceCode, 'user_code' => $userCode] = $this->post('/oauth/device/code', [ + [ + 'device_code' => $deviceCode, + 'user_code' => $userCode, + ] = $this->post('/oauth/device/code', [ 'client_id' => $client->getKey(), 'scope' => 'create read', ])->json(); @@ -138,9 +139,7 @@ public function testRequestAccessToken() ['authToken' => $authToken] = $json; - $response = $this->post('/oauth/device/authorize', [ - 'auth_token' => $authToken, - ]); + $response = $this->post('/oauth/device/authorize', ['auth_token' => $authToken]); $response->assertOk(); $response->assertSessionMissing(['deviceCode', 'authToken']); $json = $response->json(); @@ -167,7 +166,10 @@ public function testDenyAuthorization() { $client = ClientFactory::new()->asDeviceCodeClient()->create(); - ['device_code' => $deviceCode, 'user_code' => $userCode] = $this->post('/oauth/device/code', [ + [ + 'device_code' => $deviceCode, + 'user_code' => $userCode, + ] = $this->post('/oauth/device/code', [ 'client_id' => $client->getKey(), 'scope' => '', ])->json(); @@ -177,9 +179,7 @@ public function testDenyAuthorization() ['authToken' => $authToken] = $this->get('/oauth/device/authorize?user_code='.$userCode)->json(); - $response = $this->delete('/oauth/device/authorize', [ - 'auth_token' => $authToken, - ]); + $response = $this->delete('/oauth/device/authorize', ['auth_token' => $authToken]); $response->assertOk(); $response->assertSessionMissing(['deviceCode', 'authToken']); $json = $response->json(); From 2e516a9e0fb3ebc7dfa95e65921c17f81e982d1c Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Fri, 16 Aug 2024 17:17:30 +0330 Subject: [PATCH 18/38] revert unrelated changes --- resources/views/authorize.blade.php | 135 ++++++++++++++++++---------- 1 file changed, 86 insertions(+), 49 deletions(-) diff --git a/resources/views/authorize.blade.php b/resources/views/authorize.blade.php index 8a713c323..53f44e9d0 100644 --- a/resources/views/authorize.blade.php +++ b/resources/views/authorize.blade.php @@ -1,56 +1,93 @@ -@extends('layouts.app') - -@section('content') -
-
-
-
-
- {{ __('Authorization Request') }} -
+ + + + + + + + {{ config('app.name') }} - Authorization + + + + + + + +
+
+
+
+
+ Authorization Request +
+
+ +

{{ $client->name }} is requesting permission to access your account.

+ + + @if (count($scopes) > 0) +
+

This application will be able to:

+ +
    + @foreach ($scopes as $scope) +
  • {{ $scope->description }}
  • + @endforeach +
+ @endif + +
+ + + @csrf + + + + + + + + +
+ @csrf + @method('DELETE') + + + + + +
-@endsection +
+ + From 24a646473017e38d9554b26857ee01aa677a6919 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Fri, 16 Aug 2024 17:19:53 +0330 Subject: [PATCH 19/38] revert irrelevant changes --- resources/views/authorize.blade.php | 80 ++++++++++++++--------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/resources/views/authorize.blade.php b/resources/views/authorize.blade.php index 53f44e9d0..d0a4a991c 100644 --- a/resources/views/authorize.blade.php +++ b/resources/views/authorize.blade.php @@ -38,56 +38,56 @@ -
-
-
-
-
- Authorization Request -
-
- -

{{ $client->name }} is requesting permission to access your account.

+
+
+
+
+
+ Authorization Request +
+
+ +

{{ $client->name }} is requesting permission to access your account.

- - @if (count($scopes) > 0) -
-

This application will be able to:

+ + @if (count($scopes) > 0) +
+

This application will be able to:

-
    - @foreach ($scopes as $scope) -
  • {{ $scope->description }}
  • - @endforeach -
-
- @endif +
    + @foreach ($scopes as $scope) +
  • {{ $scope->description }}
  • + @endforeach +
+
+ @endif -
- -
- @csrf +
+ + + @csrf - - - - - + + + + + - -
- @csrf - @method('DELETE') + + + @csrf + @method('DELETE') - - - - -
+ + + + + +
-
From a36fd87f2cbd8b74545588ab13d1d9af020b9cf9 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Sun, 25 Aug 2024 03:24:27 +0330 Subject: [PATCH 20/38] add device option on client command --- ...000001_create_oauth_device_codes_table.php | 2 +- src/ClientRepository.php | 26 +++++++++-- src/Console/ClientCommand.php | 43 ++++++++++++++++++- 3 files changed, 64 insertions(+), 7 deletions(-) diff --git a/database/migrations/2024_06_01_000001_create_oauth_device_codes_table.php b/database/migrations/2024_06_01_000001_create_oauth_device_codes_table.php index e1d6fa069..ea078319c 100644 --- a/database/migrations/2024_06_01_000001_create_oauth_device_codes_table.php +++ b/database/migrations/2024_06_01_000001_create_oauth_device_codes_table.php @@ -15,7 +15,7 @@ public function up(): void $table->char('id', 80)->primary(); $table->foreignId('user_id')->nullable()->index(); $table->foreignUuid('client_id')->index(); - $table->char('user_code', 8); + $table->char('user_code', 8)->unique(); $table->text('scopes'); $table->boolean('revoked'); $table->dateTime('user_approved_at')->nullable(); diff --git a/src/ClientRepository.php b/src/ClientRepository.php index 61ecab47d..ec637d849 100644 --- a/src/ClientRepository.php +++ b/src/ClientRepository.php @@ -150,6 +150,19 @@ public function createImplicitGrantClient(string $name, array $redirectUris): Cl return $this->create($name, ['implicit'], $redirectUris); } + /** + * Store a new device authorization grant client. + */ + public function createDeviceAuthorizationGrantClient( + string $name, + bool $confidential = true, + ?Authenticatable $user = null + ): Client { + return $this->create( + $name, ['urn:ietf:params:oauth:grant-type:device_code', 'refresh_token'], [], null, $confidential, $user + ); + } + /** * Store a new authorization code grant client. * @@ -159,11 +172,16 @@ public function createAuthorizationCodeGrantClient( string $name, array $redirectUris, bool $confidential = true, - ?Authenticatable $user = null + ?Authenticatable $user = null, + bool $enableDeviceFlow = false ): Client { - return $this->create( - $name, ['authorization_code', 'refresh_token'], $redirectUris, null, $confidential, $user - ); + $grantTypes = ['authorization_code', 'refresh_token']; + + if ($enableDeviceFlow) { + $grantTypes[] = 'urn:ietf:params:oauth:grant-type:device_code'; + } + + return $this->create($name, $grantTypes, $redirectUris, null, $confidential, $user); } /** diff --git a/src/Console/ClientCommand.php b/src/Console/ClientCommand.php index a783dedb3..8d5754b5a 100644 --- a/src/Console/ClientCommand.php +++ b/src/Console/ClientCommand.php @@ -20,10 +20,11 @@ class ClientCommand extends Command {--password : Create a password grant client} {--client : Create a client credentials grant client} {--implicit : Create an implicit grant client} + {--device : Create a device authorization grant client} {--name= : The name of the client} {--provider= : The name of the user provider} {--redirect_uri= : The URI to redirect to after authorization } - {--public : Create a public client (Auth code grant type only) }'; + {--public : Create a public client (Auth code and device code grant types only) }'; /** * The console command description. @@ -48,6 +49,8 @@ public function handle(ClientRepository $clients) $this->createClientCredentialsClient($clients); } elseif ($this->option('implicit')) { $this->createImplicitClient($clients); + } elseif ($this->option('device')) { + $this->createDeviceCodeClient($clients); } else { $this->createAuthCodeClient($clients); } @@ -154,6 +157,33 @@ protected function createImplicitClient(ClientRepository $clients) $this->outputClientDetails($client); } + /** + * Create a device code client. + * + * @param \Laravel\Passport\ClientRepository $clients + * @return void + */ + protected function createDeviceCodeClient(ClientRepository $clients) + { + $name = $this->option('name') ?: $this->ask( + 'What should we name the client?', + config('app.name') + ); + + if (! $this->hasOption('public')) { + $this->input->setOption('public', $this->confirm( + 'Would you like to make this client public?', + true + )); + } + + $client = $clients->createDeviceAuthorizationGrantClient($name, ! $this->option('public')); + + $this->components->info('New client created successfully.'); + + $this->outputClientDetails($client); + } + /** * Create a authorization code client. * @@ -172,8 +202,17 @@ protected function createAuthCodeClient(ClientRepository $clients) url('/auth/callback') ); + $enableDeviceFlow = $this->confirm('Would you like to enable device authorization flow for this client?'); + + if (! $this->hasOption('public')) { + $this->input->setOption('public', $this->confirm( + 'Would you like to make this client public?', + false + )); + } + $client = $clients->createAuthorizationCodeGrantClient( - $name, explode(',', $redirect), ! $this->option('public'), + $name, explode(',', $redirect), ! $this->option('public'), null, $enableDeviceFlow ); $this->components->info('New client created successfully.'); From 90e566aaaa77e81885caa3d63e7bbf8c337089a5 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Sun, 25 Aug 2024 03:31:10 +0330 Subject: [PATCH 21/38] formatting --- src/Bridge/DeviceCodeRepository.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Bridge/DeviceCodeRepository.php b/src/Bridge/DeviceCodeRepository.php index 5a627e2d4..7f6b6b957 100644 --- a/src/Bridge/DeviceCodeRepository.php +++ b/src/Bridge/DeviceCodeRepository.php @@ -25,11 +25,11 @@ public function getNewDeviceCode(): DeviceCodeEntityInterface public function persistDeviceCode(DeviceCodeEntityInterface $deviceCodeEntity): void { if ($deviceCodeEntity->isLastPolledAtDirty()) { - Passport::deviceCode()->whereKey($deviceCodeEntity->getIdentifier())->update([ + Passport::deviceCode()->newQuery()->whereKey($deviceCodeEntity->getIdentifier())->update([ 'last_polled_at' => $deviceCodeEntity->getLastPolledAt(), ]); } elseif ($deviceCodeEntity->isUserDirty()) { - Passport::deviceCode()->whereKey($deviceCodeEntity->getIdentifier())->update([ + Passport::deviceCode()->newQuery()->whereKey($deviceCodeEntity->getIdentifier())->update([ 'user_id' => $deviceCodeEntity->getUserIdentifier(), 'user_approved_at' => $deviceCodeEntity->getUserApproved() ? new DateTime : null, ]); @@ -53,7 +53,7 @@ public function persistDeviceCode(DeviceCodeEntityInterface $deviceCodeEntity): */ public function getDeviceCodeEntityByDeviceCode(string $deviceCode): ?DeviceCodeEntityInterface { - $record = Passport::deviceCode()->whereKey($deviceCode)->where(['revoked' => false])->first(); + $record = Passport::deviceCode()->newQuery()->whereKey($deviceCode)->where(['revoked' => false])->first(); return $record ? new DeviceCode( $record->getKey(), @@ -71,7 +71,7 @@ public function getDeviceCodeEntityByDeviceCode(string $deviceCode): ?DeviceCode */ public function revokeDeviceCode(string $codeId): void { - Passport::deviceCode()->whereKey($codeId)->update(['revoked' => true]); + Passport::deviceCode()->newQuery()->whereKey($codeId)->update(['revoked' => true]); } /** @@ -79,6 +79,6 @@ public function revokeDeviceCode(string $codeId): void */ public function isDeviceCodeRevoked(string $codeId): bool { - return Passport::deviceCode()->whereKey($codeId)->where('revoked', false)->doesntExist(); + return Passport::deviceCode()->newQuery()->whereKey($codeId)->where('revoked', false)->doesntExist(); } } From ae8b47e61ab8923aae08896d0ce6e18df5be7aec Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Wed, 18 Sep 2024 22:42:45 +0330 Subject: [PATCH 22/38] formatting --- src/Contracts/DeviceAuthorizationResultViewResponse.php | 5 ++--- src/Contracts/DeviceAuthorizationViewResponse.php | 5 ++--- src/Contracts/DeviceUserCodeViewResponse.php | 5 ++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Contracts/DeviceAuthorizationResultViewResponse.php b/src/Contracts/DeviceAuthorizationResultViewResponse.php index ddf217329..4baccdfb8 100644 --- a/src/Contracts/DeviceAuthorizationResultViewResponse.php +++ b/src/Contracts/DeviceAuthorizationResultViewResponse.php @@ -9,8 +9,7 @@ interface DeviceAuthorizationResultViewResponse extends Responsable /** * Specify the parameters that should be passed to the view. * - * @param array $parameters - * @return $this + * @param array $parameters */ - public function withParameters($parameters = []); + public function withParameters(array $parameters = []): static; } diff --git a/src/Contracts/DeviceAuthorizationViewResponse.php b/src/Contracts/DeviceAuthorizationViewResponse.php index 2681fdfe0..b48517c73 100644 --- a/src/Contracts/DeviceAuthorizationViewResponse.php +++ b/src/Contracts/DeviceAuthorizationViewResponse.php @@ -9,8 +9,7 @@ interface DeviceAuthorizationViewResponse extends Responsable /** * Specify the parameters that should be passed to the view. * - * @param array $parameters - * @return $this + * @param array $parameters */ - public function withParameters($parameters = []); + public function withParameters(array $parameters = []): static; } diff --git a/src/Contracts/DeviceUserCodeViewResponse.php b/src/Contracts/DeviceUserCodeViewResponse.php index 290d3f1fd..97435d364 100644 --- a/src/Contracts/DeviceUserCodeViewResponse.php +++ b/src/Contracts/DeviceUserCodeViewResponse.php @@ -9,8 +9,7 @@ interface DeviceUserCodeViewResponse extends Responsable /** * Specify the parameters that should be passed to the view. * - * @param array $parameters - * @return $this + * @param array $parameters */ - public function withParameters($parameters = []); + public function withParameters(array $parameters = []): static; } From 3bb3572c38dbe745e6e1bce9a21eeeef7c77fe3b Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Wed, 18 Sep 2024 22:56:28 +0330 Subject: [PATCH 23/38] formatting --- .../views/device-authorize-result.blade.php | 19 ------- resources/views/device-authorize.blade.php | 50 ------------------- resources/views/device-user-code.blade.php | 41 --------------- src/Passport.php | 3 ++ src/PassportServiceProvider.php | 4 +- 5 files changed, 4 insertions(+), 113 deletions(-) delete mode 100644 resources/views/device-authorize-result.blade.php delete mode 100644 resources/views/device-authorize.blade.php delete mode 100644 resources/views/device-user-code.blade.php diff --git a/resources/views/device-authorize-result.blade.php b/resources/views/device-authorize-result.blade.php deleted file mode 100644 index 046c657a3..000000000 --- a/resources/views/device-authorize-result.blade.php +++ /dev/null @@ -1,19 +0,0 @@ -@extends('layouts.app') - -@section('content') -
-
-
-
-
- {{ __($approved ? 'Success!' : 'Canceled!') }} -
- -
- {{ __($approved ? 'Continue on your device.' : 'Device authorization canceled.') }} -
-
-
-
-
-@endsection diff --git a/resources/views/device-authorize.blade.php b/resources/views/device-authorize.blade.php deleted file mode 100644 index 7a5581473..000000000 --- a/resources/views/device-authorize.blade.php +++ /dev/null @@ -1,50 +0,0 @@ -@extends('layouts.app') - -@section('content') -
-
-
-
-
{{ __('Device Authorization') }}
- -
-

{{ $client->name }} is requesting permission to access your account.

- - @if (count($scopes) > 0) -
-

This application will be able to:

- -
    - @foreach ($scopes as $scope) -
  • {{ $scope->description }}
  • - @endforeach -
-
- @endif - -
-
- @csrf - - - - - -
- -
- @csrf - @method('DELETE') - - - - - -
-
-
-
-
-
-
-@endsection diff --git a/resources/views/device-user-code.blade.php b/resources/views/device-user-code.blade.php deleted file mode 100644 index 918da72e7..000000000 --- a/resources/views/device-user-code.blade.php +++ /dev/null @@ -1,41 +0,0 @@ -@extends('layouts.app') - -@section('content') -
-
-
-
-
{{ __('Device Authorization') }}
- -
- {{ __('Enter the code displayed on your device.') }} - -
-
- - -
- - - @error('user_code') - - {{ $message }} - - @enderror -
-
- -
-
- -
-
-
-
-
-
-
-
-@endsection diff --git a/src/Passport.php b/src/Passport.php index 90b51f017..5e77b4411 100644 --- a/src/Passport.php +++ b/src/Passport.php @@ -680,6 +680,9 @@ public static function viewNamespace(string $namespace): void public static function viewPrefix(string $prefix): void { static::authorizationView($prefix.'authorize'); + static::deviceAuthorizationView($prefix.'device.authorize'); + static::deviceAuthorizationResultView($prefix.'device.result'); + static::deviceUserCodeView($prefix.'device.user_code'); } /** diff --git a/src/PassportServiceProvider.php b/src/PassportServiceProvider.php index 21db352b1..06a3d7673 100644 --- a/src/PassportServiceProvider.php +++ b/src/PassportServiceProvider.php @@ -238,10 +238,8 @@ protected function makeImplicitGrant() /** * Create and configure an instance of the Device Code grant. - * - * @return \League\OAuth2\Server\Grant\DeviceCodeGrant */ - protected function makeDeviceCodeGrant() + protected function makeDeviceCodeGrant(): DeviceCodeGrant { $grant = new DeviceCodeGrant( $this->app->make(DeviceCodeRepository::class), From 662c50ccb4332ca3c09f4ba37a86898cb94c3056 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Thu, 19 Sep 2024 15:29:04 +0330 Subject: [PATCH 24/38] add more tests --- tests/Feature/DeviceAuthorizationGrantTest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/Feature/DeviceAuthorizationGrantTest.php b/tests/Feature/DeviceAuthorizationGrantTest.php index 0b0b6cd1f..538ce3f4c 100644 --- a/tests/Feature/DeviceAuthorizationGrantTest.php +++ b/tests/Feature/DeviceAuthorizationGrantTest.php @@ -2,6 +2,8 @@ namespace Laravel\Passport\Tests\Feature; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Route; use Laravel\Passport\Database\Factories\ClientFactory; use Laravel\Passport\Passport; use Orchestra\Testbench\Concerns\WithLaravelMigrations; @@ -160,6 +162,14 @@ public function testRequestAccessToken() $this->assertArrayHasKey('refresh_token', $json); $this->assertSame('Bearer', $json['token_type']); $this->assertSame(31536000, $json['expires_in']); + + Route::get('/foo', fn (Request $request) => $request->user()->token()->toJson())->middleware('auth:api'); + + $json = $this->withToken($json['access_token'], $json['token_type'])->get('/foo')->json(); + + $this->assertSame($client->getKey(), $json['oauth_client_id']); + $this->assertEquals($user->getAuthIdentifier(), $json['oauth_user_id']); + // $this->assertSame(['create', 'read'], $json['oauth_scopes']); TODO: https://github.com/thephpleague/oauth2-server/pull/1412 } public function testDenyAuthorization() From 0fd4368933c0df4c3ed3c1fc5632dafc1f151cb0 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Fri, 20 Sep 2024 15:09:52 +0330 Subject: [PATCH 25/38] formatting --- src/Passport.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Passport.php b/src/Passport.php index 5e77b4411..81b50da72 100644 --- a/src/Passport.php +++ b/src/Passport.php @@ -682,7 +682,7 @@ public static function viewPrefix(string $prefix): void static::authorizationView($prefix.'authorize'); static::deviceAuthorizationView($prefix.'device.authorize'); static::deviceAuthorizationResultView($prefix.'device.result'); - static::deviceUserCodeView($prefix.'device.user_code'); + static::deviceUserCodeView($prefix.'device.user-code'); } /** From 3a5ac7236c2b74e5fd2febdd57242ea41e1450d4 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Fri, 20 Sep 2024 17:46:49 +0330 Subject: [PATCH 26/38] remove result view --- .../ApprovedDeviceAuthorizationResponse.php | 10 ++++++++++ .../DeniedDeviceAuthorizationResponse.php | 10 ++++++++++ .../DeviceAuthorizationResultViewResponse.php | 15 -------------- .../ApproveDeviceAuthorizationController.php | 11 ++++------ .../DenyDeviceAuthorizationController.php | 11 ++++------ .../ApprovedDeviceAuthorizationResponse.php | 20 +++++++++++++++++++ .../DeniedDeviceAuthorizationResponse.php | 20 +++++++++++++++++++ src/Http/Responses/SimpleViewResponse.php | 2 -- src/Passport.php | 10 ---------- src/PassportServiceProvider.php | 14 +++++++++++++ .../Feature/DeviceAuthorizationGrantTest.php | 13 ++++-------- 11 files changed, 86 insertions(+), 50 deletions(-) create mode 100644 src/Contracts/ApprovedDeviceAuthorizationResponse.php create mode 100644 src/Contracts/DeniedDeviceAuthorizationResponse.php delete mode 100644 src/Contracts/DeviceAuthorizationResultViewResponse.php create mode 100644 src/Http/Responses/ApprovedDeviceAuthorizationResponse.php create mode 100644 src/Http/Responses/DeniedDeviceAuthorizationResponse.php diff --git a/src/Contracts/ApprovedDeviceAuthorizationResponse.php b/src/Contracts/ApprovedDeviceAuthorizationResponse.php new file mode 100644 index 000000000..07f5da34c --- /dev/null +++ b/src/Contracts/ApprovedDeviceAuthorizationResponse.php @@ -0,0 +1,10 @@ + $parameters - */ - public function withParameters(array $parameters = []): static; -} diff --git a/src/Http/Controllers/ApproveDeviceAuthorizationController.php b/src/Http/Controllers/ApproveDeviceAuthorizationController.php index 4b0a78636..95afd87dd 100644 --- a/src/Http/Controllers/ApproveDeviceAuthorizationController.php +++ b/src/Http/Controllers/ApproveDeviceAuthorizationController.php @@ -3,7 +3,7 @@ namespace Laravel\Passport\Http\Controllers; use Illuminate\Http\Request; -use Laravel\Passport\Contracts\DeviceAuthorizationResultViewResponse; +use Laravel\Passport\Contracts\ApprovedDeviceAuthorizationResponse; use League\OAuth2\Server\AuthorizationServer; class ApproveDeviceAuthorizationController @@ -14,14 +14,14 @@ class ApproveDeviceAuthorizationController * Create a new controller instance. */ public function __construct(protected AuthorizationServer $server, - protected DeviceAuthorizationResultViewResponse $viewResponse) + protected ApprovedDeviceAuthorizationResponse $response) { } /** * Approve the device authorization request. */ - public function __invoke(Request $request): DeviceAuthorizationResultViewResponse + public function __invoke(Request $request): ApprovedDeviceAuthorizationResponse { $this->withErrorHandling(fn () => $this->server->completeDeviceAuthorizationRequest( $this->getDeviceCodeFromSession($request), @@ -29,9 +29,6 @@ public function __invoke(Request $request): DeviceAuthorizationResultViewRespons true )); - return $this->viewResponse->withParameters([ - 'request' => $request, - 'approved' => true, - ]); + return $this->response; } } diff --git a/src/Http/Controllers/DenyDeviceAuthorizationController.php b/src/Http/Controllers/DenyDeviceAuthorizationController.php index 6410efedd..a4dad1c88 100644 --- a/src/Http/Controllers/DenyDeviceAuthorizationController.php +++ b/src/Http/Controllers/DenyDeviceAuthorizationController.php @@ -3,7 +3,7 @@ namespace Laravel\Passport\Http\Controllers; use Illuminate\Http\Request; -use Laravel\Passport\Contracts\DeviceAuthorizationResultViewResponse; +use Laravel\Passport\Contracts\DeniedDeviceAuthorizationResponse; use League\OAuth2\Server\AuthorizationServer; class DenyDeviceAuthorizationController @@ -14,14 +14,14 @@ class DenyDeviceAuthorizationController * Create a new controller instance. */ public function __construct(protected AuthorizationServer $server, - protected DeviceAuthorizationResultViewResponse $viewResponse) + protected DeniedDeviceAuthorizationResponse $response) { } /** * Deny the device authorization request. */ - public function __invoke(Request $request): DeviceAuthorizationResultViewResponse + public function __invoke(Request $request): DeniedDeviceAuthorizationResponse { $this->withErrorHandling(fn () => $this->server->completeDeviceAuthorizationRequest( $this->getDeviceCodeFromSession($request), @@ -29,9 +29,6 @@ public function __invoke(Request $request): DeviceAuthorizationResultViewRespons false )); - return $this->viewResponse->withParameters([ - 'request' => $request, - 'approved' => false, - ]); + return $this->response; } } diff --git a/src/Http/Responses/ApprovedDeviceAuthorizationResponse.php b/src/Http/Responses/ApprovedDeviceAuthorizationResponse.php new file mode 100644 index 000000000..11c78b360 --- /dev/null +++ b/src/Http/Responses/ApprovedDeviceAuthorizationResponse.php @@ -0,0 +1,20 @@ +with('status', 'authorization-approved'); + } +} diff --git a/src/Http/Responses/DeniedDeviceAuthorizationResponse.php b/src/Http/Responses/DeniedDeviceAuthorizationResponse.php new file mode 100644 index 000000000..5ab6cd6b6 --- /dev/null +++ b/src/Http/Responses/DeniedDeviceAuthorizationResponse.php @@ -0,0 +1,20 @@ +with('status', 'authorization-denied'); + } +} diff --git a/src/Http/Responses/SimpleViewResponse.php b/src/Http/Responses/SimpleViewResponse.php index 0b59eb59f..ef830ead2 100644 --- a/src/Http/Responses/SimpleViewResponse.php +++ b/src/Http/Responses/SimpleViewResponse.php @@ -5,14 +5,12 @@ use Closure; use Illuminate\Contracts\Support\Responsable; use Laravel\Passport\Contracts\AuthorizationViewResponse; -use Laravel\Passport\Contracts\DeviceAuthorizationResultViewResponse; use Laravel\Passport\Contracts\DeviceAuthorizationViewResponse; use Laravel\Passport\Contracts\DeviceUserCodeViewResponse; class SimpleViewResponse implements AuthorizationViewResponse, DeviceAuthorizationViewResponse, - DeviceAuthorizationResultViewResponse, DeviceUserCodeViewResponse { /** diff --git a/src/Passport.php b/src/Passport.php index 81b50da72..1041665db 100644 --- a/src/Passport.php +++ b/src/Passport.php @@ -8,7 +8,6 @@ use DateTimeInterface; use Illuminate\Contracts\Encryption\Encrypter; use Laravel\Passport\Contracts\AuthorizationViewResponse; -use Laravel\Passport\Contracts\DeviceAuthorizationResultViewResponse; use Laravel\Passport\Contracts\DeviceAuthorizationViewResponse; use Laravel\Passport\Contracts\DeviceUserCodeViewResponse; use Laravel\Passport\Http\Responses\SimpleViewResponse; @@ -681,7 +680,6 @@ public static function viewPrefix(string $prefix): void { static::authorizationView($prefix.'authorize'); static::deviceAuthorizationView($prefix.'device.authorize'); - static::deviceAuthorizationResultView($prefix.'device.result'); static::deviceUserCodeView($prefix.'device.user-code'); } @@ -701,14 +699,6 @@ public static function deviceAuthorizationView(Closure|string $view): void app()->singleton(DeviceAuthorizationViewResponse::class, fn () => new SimpleViewResponse($view)); } - /** - * Specify which view should be used as the device authorization result view. - */ - public static function deviceAuthorizationResultView(Closure|string $view): void - { - app()->singleton(DeviceAuthorizationResultViewResponse::class, fn () => new SimpleViewResponse($view)); - } - /** * Specify which view should be used as the device user code view. */ diff --git a/src/PassportServiceProvider.php b/src/PassportServiceProvider.php index f4e7dfa0a..e46c1faeb 100644 --- a/src/PassportServiceProvider.php +++ b/src/PassportServiceProvider.php @@ -15,8 +15,12 @@ use Laravel\Passport\Bridge\DeviceCodeRepository; use Laravel\Passport\Bridge\PersonalAccessGrant; use Laravel\Passport\Bridge\RefreshTokenRepository; +use Laravel\Passport\Contracts\ApprovedDeviceAuthorizationResponse as ApprovedDeviceAuthorizationResponseContract; +use Laravel\Passport\Contracts\DeniedDeviceAuthorizationResponse as DeniedDeviceAuthorizationResponseContract; use Laravel\Passport\Guards\TokenGuard; use Laravel\Passport\Http\Controllers\AuthorizationController; +use Laravel\Passport\Http\Responses\ApprovedDeviceAuthorizationResponse; +use Laravel\Passport\Http\Responses\DeniedDeviceAuthorizationResponse; use Lcobucci\JWT\Encoding\JoseEncoder; use Lcobucci\JWT\Parser as ParserContract; use Lcobucci\JWT\Token\Parser; @@ -119,12 +123,22 @@ public function register() $this->app->singleton(ClientRepository::class); + $this->registerResponseBindings(); $this->registerAuthorizationServer(); $this->registerJWTParser(); $this->registerResourceServer(); $this->registerGuard(); } + /** + * Register the response bindings. + */ + protected function registerResponseBindings(): void + { + $this->app->singleton(ApprovedDeviceAuthorizationResponseContract::class, ApprovedDeviceAuthorizationResponse::class); + $this->app->singleton(DeniedDeviceAuthorizationResponseContract::class, DeniedDeviceAuthorizationResponse::class); + } + /** * Register the authorization server. * diff --git a/tests/Feature/DeviceAuthorizationGrantTest.php b/tests/Feature/DeviceAuthorizationGrantTest.php index 538ce3f4c..e132df8c9 100644 --- a/tests/Feature/DeviceAuthorizationGrantTest.php +++ b/tests/Feature/DeviceAuthorizationGrantTest.php @@ -18,7 +18,6 @@ protected function setUp(): void parent::setUp(); Passport::deviceAuthorizationView(fn ($params) => $params); - Passport::deviceAuthorizationResultView(fn ($params) => $params); Passport::deviceUserCodeView(fn ($params) => $params); } @@ -142,11 +141,9 @@ public function testRequestAccessToken() ['authToken' => $authToken] = $json; $response = $this->post('/oauth/device/authorize', ['auth_token' => $authToken]); - $response->assertOk(); + $response->assertRedirectToRoute('passport.device'); + $response->assertSessionHas('status', 'authorization-approved'); $response->assertSessionMissing(['deviceCode', 'authToken']); - $json = $response->json(); - $this->assertEqualsCanonicalizing(['approved', 'request'], array_keys($json)); - $this->assertSame(true, $json['approved']); $response = $this->post('/oauth/token', [ 'grant_type' => 'urn:ietf:params:oauth:grant-type:device_code', @@ -190,11 +187,9 @@ public function testDenyAuthorization() ['authToken' => $authToken] = $this->get('/oauth/device/authorize?user_code='.$userCode)->json(); $response = $this->delete('/oauth/device/authorize', ['auth_token' => $authToken]); - $response->assertOk(); + $response->assertRedirectToRoute('passport.device'); + $response->assertSessionHas('status', 'authorization-denied'); $response->assertSessionMissing(['deviceCode', 'authToken']); - $json = $response->json(); - $this->assertEqualsCanonicalizing(['approved', 'request'], array_keys($json)); - $this->assertSame(false, $json['approved']); $response = $this->post('/oauth/token', [ 'grant_type' => 'urn:ietf:params:oauth:grant-type:device_code', From c4368a3fce3efd541eb0c09cf67ef206e8c09d30 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Mon, 30 Sep 2024 22:43:17 +0330 Subject: [PATCH 27/38] formatting --- database/factories/ClientFactory.php | 1 + src/DeviceCode.php | 8 +++++--- .../ApproveDeviceAuthorizationController.php | 7 ++++--- .../DenyDeviceAuthorizationController.php | 7 ++++--- .../Controllers/DeviceAuthorizationController.php | 5 +++-- src/Http/Controllers/DeviceCodeController.php | 5 +++-- src/Http/Controllers/DeviceUserCodeController.php | 5 +++-- src/Passport.php | 4 ++++ src/PassportServiceProvider.php | 14 ++++++-------- 9 files changed, 33 insertions(+), 23 deletions(-) diff --git a/database/factories/ClientFactory.php b/database/factories/ClientFactory.php index 77b793826..be945c6a2 100644 --- a/database/factories/ClientFactory.php +++ b/database/factories/ClientFactory.php @@ -88,6 +88,7 @@ public function asDeviceCodeClient(): static { return $this->state([ 'grant_types' => ['urn:ietf:params:oauth:grant-type:device_code', 'refresh_token'], + 'redirect_uris' => [], ]); } } diff --git a/src/DeviceCode.php b/src/DeviceCode.php index d2d70f806..a453fd7cb 100644 --- a/src/DeviceCode.php +++ b/src/DeviceCode.php @@ -24,14 +24,14 @@ class DeviceCode extends Model /** * The guarded attributes on the model. * - * @var array + * @var array|bool */ - protected $guarded = []; + protected $guarded = false; /** * The attributes that should be cast to native types. * - * @var array + * @var array */ protected $casts = [ 'scopes' => 'array', @@ -57,6 +57,8 @@ class DeviceCode extends Model /** * Get the client that owns the authentication code. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\Laravel\Passport\Client, $this> */ public function client(): BelongsTo { diff --git a/src/Http/Controllers/ApproveDeviceAuthorizationController.php b/src/Http/Controllers/ApproveDeviceAuthorizationController.php index 95afd87dd..e137f8b34 100644 --- a/src/Http/Controllers/ApproveDeviceAuthorizationController.php +++ b/src/Http/Controllers/ApproveDeviceAuthorizationController.php @@ -13,9 +13,10 @@ class ApproveDeviceAuthorizationController /** * Create a new controller instance. */ - public function __construct(protected AuthorizationServer $server, - protected ApprovedDeviceAuthorizationResponse $response) - { + public function __construct( + protected AuthorizationServer $server, + protected ApprovedDeviceAuthorizationResponse $response + ) { } /** diff --git a/src/Http/Controllers/DenyDeviceAuthorizationController.php b/src/Http/Controllers/DenyDeviceAuthorizationController.php index a4dad1c88..c7e99d8af 100644 --- a/src/Http/Controllers/DenyDeviceAuthorizationController.php +++ b/src/Http/Controllers/DenyDeviceAuthorizationController.php @@ -13,9 +13,10 @@ class DenyDeviceAuthorizationController /** * Create a new controller instance. */ - public function __construct(protected AuthorizationServer $server, - protected DeniedDeviceAuthorizationResponse $response) - { + public function __construct( + protected AuthorizationServer $server, + protected DeniedDeviceAuthorizationResponse $response + ) { } /** diff --git a/src/Http/Controllers/DeviceAuthorizationController.php b/src/Http/Controllers/DeviceAuthorizationController.php index 3780f15ce..89d161639 100644 --- a/src/Http/Controllers/DeviceAuthorizationController.php +++ b/src/Http/Controllers/DeviceAuthorizationController.php @@ -13,8 +13,9 @@ class DeviceAuthorizationController /** * Create a new controller instance. */ - public function __construct(protected DeviceAuthorizationViewResponse $viewResponse) - { + public function __construct( + protected DeviceAuthorizationViewResponse $viewResponse + ) { } /** diff --git a/src/Http/Controllers/DeviceCodeController.php b/src/Http/Controllers/DeviceCodeController.php index 39bb0838d..79172eb76 100644 --- a/src/Http/Controllers/DeviceCodeController.php +++ b/src/Http/Controllers/DeviceCodeController.php @@ -14,8 +14,9 @@ class DeviceCodeController /** * Create a new controller instance. */ - public function __construct(protected AuthorizationServer $server) - { + public function __construct( + protected AuthorizationServer $server + ) { } /** diff --git a/src/Http/Controllers/DeviceUserCodeController.php b/src/Http/Controllers/DeviceUserCodeController.php index ae0fc291a..7dd9c95c8 100644 --- a/src/Http/Controllers/DeviceUserCodeController.php +++ b/src/Http/Controllers/DeviceUserCodeController.php @@ -11,8 +11,9 @@ class DeviceUserCodeController /** * Create a new controller instance. */ - public function __construct(protected DeviceUserCodeViewResponse $viewResponse) - { + public function __construct( + protected DeviceUserCodeViewResponse $viewResponse + ) { } /** diff --git a/src/Passport.php b/src/Passport.php index 040972e0d..b1500ffea 100644 --- a/src/Passport.php +++ b/src/Passport.php @@ -598,6 +598,8 @@ public static function authorizationView(Closure|string $view): void /** * Specify which view should be used as the device authorization view. + * + * @param (\Closure(array): (\Symfony\Component\HttpFoundation\Response))|string $view */ public static function deviceAuthorizationView(Closure|string $view): void { @@ -606,6 +608,8 @@ public static function deviceAuthorizationView(Closure|string $view): void /** * Specify which view should be used as the device user code view. + * + * @param (\Closure(array): (\Symfony\Component\HttpFoundation\Response))|string $view */ public static function deviceUserCodeView(Closure|string $view): void { diff --git a/src/PassportServiceProvider.php b/src/PassportServiceProvider.php index 6c34c536a..eb5c82113 100644 --- a/src/PassportServiceProvider.php +++ b/src/PassportServiceProvider.php @@ -232,19 +232,17 @@ protected function makeImplicitGrant(): ImplicitGrant */ protected function makeDeviceCodeGrant(): DeviceCodeGrant { - $grant = new DeviceCodeGrant( + return tap(new DeviceCodeGrant( $this->app->make(DeviceCodeRepository::class), $this->app->make(RefreshTokenRepository::class), new DateInterval('PT10M'), route('passport.device'), 5 - ); - - $grant->setRefreshTokenTTL(Passport::refreshTokensExpireIn()); - $grant->setIncludeVerificationUriComplete(true); - $grant->setIntervalVisibility(true); - - return $grant; + ), function (DeviceCodeGrant $grant) { + $grant->setRefreshTokenTTL(Passport::refreshTokensExpireIn()); + $grant->setIncludeVerificationUriComplete(true); + $grant->setIntervalVisibility(true); + }); } /** From 6231c9e9b6db79f57595958fef9d5a263a3cc33e Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Mon, 30 Sep 2024 22:48:52 +0330 Subject: [PATCH 28/38] formatting --- src/Bridge/DeviceCode.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Bridge/DeviceCode.php b/src/Bridge/DeviceCode.php index 872d2ad76..b7240d5ce 100644 --- a/src/Bridge/DeviceCode.php +++ b/src/Bridge/DeviceCode.php @@ -30,6 +30,9 @@ class DeviceCode implements DeviceCodeEntityInterface /** * Create a new device code instance. * + * @param non-empty-string|null $identifier + * @param non-empty-string|null $userIdentifier + * @param non-empty-string|null $clientIdentifier * @param string[] $scopes */ public function __construct( From 332d16e1ac86ca05febb912de81bae33d2030a11 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Sun, 6 Oct 2024 15:22:46 +0330 Subject: [PATCH 29/38] formatting --- .../ApproveDeviceAuthorizationController.php | 11 ++++++----- .../DenyDeviceAuthorizationController.php | 10 ++++++---- .../DeviceAuthorizationController.php | 19 +++++++------------ src/Http/Controllers/DeviceCodeController.php | 8 ++++---- .../Controllers/DeviceUserCodeController.php | 16 +++++----------- 5 files changed, 28 insertions(+), 36 deletions(-) diff --git a/src/Http/Controllers/ApproveDeviceAuthorizationController.php b/src/Http/Controllers/ApproveDeviceAuthorizationController.php index e137f8b34..65da4a36e 100644 --- a/src/Http/Controllers/ApproveDeviceAuthorizationController.php +++ b/src/Http/Controllers/ApproveDeviceAuthorizationController.php @@ -14,22 +14,23 @@ class ApproveDeviceAuthorizationController * Create a new controller instance. */ public function __construct( - protected AuthorizationServer $server, - protected ApprovedDeviceAuthorizationResponse $response + protected AuthorizationServer $server ) { } /** * Approve the device authorization request. */ - public function __invoke(Request $request): ApprovedDeviceAuthorizationResponse - { + public function __invoke( + Request $request, + ApprovedDeviceAuthorizationResponse $response + ): ApprovedDeviceAuthorizationResponse { $this->withErrorHandling(fn () => $this->server->completeDeviceAuthorizationRequest( $this->getDeviceCodeFromSession($request), $request->user()->getAuthIdentifier(), true )); - return $this->response; + return $response; } } diff --git a/src/Http/Controllers/DenyDeviceAuthorizationController.php b/src/Http/Controllers/DenyDeviceAuthorizationController.php index c7e99d8af..dba47f5b6 100644 --- a/src/Http/Controllers/DenyDeviceAuthorizationController.php +++ b/src/Http/Controllers/DenyDeviceAuthorizationController.php @@ -14,15 +14,17 @@ class DenyDeviceAuthorizationController * Create a new controller instance. */ public function __construct( - protected AuthorizationServer $server, - protected DeniedDeviceAuthorizationResponse $response + protected AuthorizationServer $server ) { } /** * Deny the device authorization request. */ - public function __invoke(Request $request): DeniedDeviceAuthorizationResponse + public function __invoke( + Request $request, + DeniedDeviceAuthorizationResponse $response + ): DeniedDeviceAuthorizationResponse { $this->withErrorHandling(fn () => $this->server->completeDeviceAuthorizationRequest( $this->getDeviceCodeFromSession($request), @@ -30,6 +32,6 @@ public function __invoke(Request $request): DeniedDeviceAuthorizationResponse false )); - return $this->response; + return $response; } } diff --git a/src/Http/Controllers/DeviceAuthorizationController.php b/src/Http/Controllers/DeviceAuthorizationController.php index 89d161639..2b15e593f 100644 --- a/src/Http/Controllers/DeviceAuthorizationController.php +++ b/src/Http/Controllers/DeviceAuthorizationController.php @@ -4,25 +4,20 @@ use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Date; use Illuminate\Support\Str; use Laravel\Passport\Contracts\DeviceAuthorizationViewResponse; use Laravel\Passport\Passport; class DeviceAuthorizationController { - /** - * Create a new controller instance. - */ - public function __construct( - protected DeviceAuthorizationViewResponse $viewResponse - ) { - } - /** * Authorize a device to access the user's account. */ - public function __invoke(Request $request): RedirectResponse|DeviceAuthorizationViewResponse - { + public function __invoke( + Request $request, + DeviceAuthorizationViewResponse $viewResponse + ): RedirectResponse|DeviceAuthorizationViewResponse { if (! $userCode = $request->query('user_code')) { return to_route('passport.device'); } @@ -30,7 +25,7 @@ public function __invoke(Request $request): RedirectResponse|DeviceAuthorization $deviceCode = Passport::deviceCode() ->with('client') ->where('user_code', $userCode) - ->where('expires_at', '>', now()) + ->where('expires_at', '>', Date::now()) ->where('revoked', false) ->first(); @@ -45,7 +40,7 @@ public function __invoke(Request $request): RedirectResponse|DeviceAuthorization $request->session()->put('authToken', $authToken = Str::random()); $request->session()->put('deviceCode', $deviceCode->getKey()); - return $this->viewResponse->withParameters([ + return $viewResponse->withParameters([ 'client' => $deviceCode->client, 'user' => $request->user(), 'scopes' => Passport::scopesFor($deviceCode->scopes), diff --git a/src/Http/Controllers/DeviceCodeController.php b/src/Http/Controllers/DeviceCodeController.php index 79172eb76..f3e350045 100644 --- a/src/Http/Controllers/DeviceCodeController.php +++ b/src/Http/Controllers/DeviceCodeController.php @@ -2,10 +2,10 @@ namespace Laravel\Passport\Http\Controllers; -use Illuminate\Http\Response; use League\OAuth2\Server\AuthorizationServer; -use Nyholm\Psr7\Response as Psr7Response; +use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Symfony\Component\HttpFoundation\Response; class DeviceCodeController { @@ -22,10 +22,10 @@ public function __construct( /** * Issue a device code for the client. */ - public function __invoke(ServerRequestInterface $request): Response + public function __invoke(ServerRequestInterface $psrRequest, ResponseInterface $psrResponse): Response { return $this->withErrorHandling(fn () => $this->convertResponse( - $this->server->respondToDeviceAuthorizationRequest($request, new Psr7Response) + $this->server->respondToDeviceAuthorizationRequest($psrRequest, $psrResponse) )); } } diff --git a/src/Http/Controllers/DeviceUserCodeController.php b/src/Http/Controllers/DeviceUserCodeController.php index 7dd9c95c8..c88186b5a 100644 --- a/src/Http/Controllers/DeviceUserCodeController.php +++ b/src/Http/Controllers/DeviceUserCodeController.php @@ -8,26 +8,20 @@ class DeviceUserCodeController { - /** - * Create a new controller instance. - */ - public function __construct( - protected DeviceUserCodeViewResponse $viewResponse - ) { - } - /** * Show the form for entering the user code. */ - public function __invoke(Request $request): RedirectResponse|DeviceUserCodeViewResponse - { + public function __invoke( + Request $request, + DeviceUserCodeViewResponse $viewResponse + ): RedirectResponse|DeviceUserCodeViewResponse { if ($userCode = $request->query('user_code')) { return to_route('passport.device.authorizations.authorize', [ 'user_code' => $userCode, ]); } - return $this->viewResponse->withParameters([ + return $viewResponse->withParameters([ 'request' => $request, ]); } From 0a2a47f1162368a91f04e13fdb1993aba98168f9 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Mon, 7 Oct 2024 02:17:20 +0330 Subject: [PATCH 30/38] formatting --- src/Http/Controllers/DenyDeviceAuthorizationController.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Http/Controllers/DenyDeviceAuthorizationController.php b/src/Http/Controllers/DenyDeviceAuthorizationController.php index dba47f5b6..c3ccf72dc 100644 --- a/src/Http/Controllers/DenyDeviceAuthorizationController.php +++ b/src/Http/Controllers/DenyDeviceAuthorizationController.php @@ -24,8 +24,7 @@ public function __construct( public function __invoke( Request $request, DeniedDeviceAuthorizationResponse $response - ): DeniedDeviceAuthorizationResponse - { + ): DeniedDeviceAuthorizationResponse { $this->withErrorHandling(fn () => $this->server->completeDeviceAuthorizationRequest( $this->getDeviceCodeFromSession($request), $request->user()->getAuthIdentifier(), From 5b2f16e2cfab11a6724b3217f41ca7ab7e1cc1c8 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Mon, 7 Oct 2024 18:34:04 +0330 Subject: [PATCH 31/38] add more tests --- tests/Feature/Console/PurgeCommand.php | 5 +++ tests/Feature/RevokedTest.php | 46 ++++++++++++++++++++++++++ tests/Unit/PassportTest.php | 8 +++++ 3 files changed, 59 insertions(+) diff --git a/tests/Feature/Console/PurgeCommand.php b/tests/Feature/Console/PurgeCommand.php index 33adda95f..ff5cdbd04 100644 --- a/tests/Feature/Console/PurgeCommand.php +++ b/tests/Feature/Console/PurgeCommand.php @@ -24,6 +24,7 @@ public function test_it_can_purge_tokens() 'delete from "oauth_access_tokens" where ("revoked" = 1 or "expires_at" < \'2000-01-01 00:00:00\')', 'delete from "oauth_auth_codes" where ("revoked" = 1 or "expires_at" < \'2000-01-01 00:00:00\')', 'delete from "oauth_refresh_tokens" where ("revoked" = 1 or "expires_at" < \'2000-01-01 00:00:00\')', + 'delete from "oauth_device_codes" where ("revoked" = 1 or "expires_at" < \'2000-01-01 00:00:00\')', ], array_column($query, 'query')); } @@ -38,6 +39,7 @@ public function test_it_can_purge_revoked_tokens() 'delete from "oauth_access_tokens" where ("revoked" = 1)', 'delete from "oauth_auth_codes" where ("revoked" = 1)', 'delete from "oauth_refresh_tokens" where ("revoked" = 1)', + 'delete from "oauth_device_codes" where ("revoked" = 1)', ], array_column($query, 'query')); } @@ -54,6 +56,7 @@ public function test_it_can_purge_expired_tokens() 'delete from "oauth_access_tokens" where ("expires_at" < \'2000-01-01 00:00:00\')', 'delete from "oauth_auth_codes" where ("expires_at" < \'2000-01-01 00:00:00\')', 'delete from "oauth_refresh_tokens" where ("expires_at" < \'2000-01-01 00:00:00\')', + 'delete from "oauth_device_codes" where ("expires_at" < \'2000-01-01 00:00:00\')', ], array_column($query, 'query')); } @@ -70,6 +73,7 @@ public function test_it_can_purge_revoked_and_expired_tokens() 'delete from "oauth_access_tokens" where ("revoked" = 1 or "expires_at" < \'2000-01-01 00:00:00\')', 'delete from "oauth_auth_codes" where ("revoked" = 1 or "expires_at" < \'2000-01-01 00:00:00\')', 'delete from "oauth_refresh_tokens" where ("revoked" = 1 or "expires_at" < \'2000-01-01 00:00:00\')', + 'delete from "oauth_device_codes" where ("revoked" = 1 or "expires_at" < \'2000-01-01 00:00:00\')', ], array_column($query, 'query')); } @@ -86,6 +90,7 @@ public function test_it_can_purge_tokens_by_hours() 'delete from "oauth_access_tokens" where ("revoked" = 1 or "expires_at" < \'2000-01-01 00:00:00\')', 'delete from "oauth_auth_codes" where ("revoked" = 1 or "expires_at" < \'2000-01-01 00:00:00\')', 'delete from "oauth_refresh_tokens" where ("revoked" = 1 or "expires_at" < \'2000-01-01 00:00:00\')', + 'delete from "oauth_device_codes" where ("revoked" = 1 or "expires_at" < \'2000-01-01 00:00:00\')', ], array_column($query, 'query')); } } diff --git a/tests/Feature/RevokedTest.php b/tests/Feature/RevokedTest.php index 9a61f3dc4..f9a1d7887 100644 --- a/tests/Feature/RevokedTest.php +++ b/tests/Feature/RevokedTest.php @@ -5,6 +5,8 @@ use Laravel\Passport\Bridge\AccessTokenRepository as BridgeAccessTokenRepository; use Laravel\Passport\Bridge\AuthCode; use Laravel\Passport\Bridge\AuthCodeRepository as BridgeAuthCodeRepository; +use Laravel\Passport\Bridge\DeviceCode; +use Laravel\Passport\Bridge\DeviceCodeRepository as BridgeDeviceCodeRepository; use Laravel\Passport\Bridge\RefreshToken; use Laravel\Passport\Bridge\RefreshTokenRepository as BridgeRefreshTokenRepository; use Laravel\Passport\Tests\Feature\PassportTestCase; @@ -90,6 +92,31 @@ public function test_it_can_determine_if_a_refresh_token_is_not_revoked() $this->assertFalse($repository->isRefreshTokenRevoked('tokenId')); } + public function test_it_can_determine_if_a_device_code_is_revoked() + { + $repository = $this->deviceCodeRepository(); + $this->persistNewDeviceCode($repository, 'deviceCodeId'); + + $repository->revokeDeviceCode('deviceCodeId'); + + $this->assertTrue($repository->isDeviceCodeRevoked('deviceCodeId')); + } + + public function test_a_device_code_is_also_revoked_if_it_cannot_be_found() + { + $repository = $this->deviceCodeRepository(); + + $this->assertTrue($repository->isDeviceCodeRevoked('notExistingDeviceCodeId')); + } + + public function test_it_can_determine_if_a_device_code_is_not_revoked() + { + $repository = $this->deviceCodeRepository(); + $this->persistNewDeviceCode($repository, 'deviceCodeId'); + + $this->assertFalse($repository->isDeviceCodeRevoked('deviceCodeId')); + } + private function accessTokenRepository(): BridgeAccessTokenRepository { $events = m::mock('Illuminate\Contracts\Events\Dispatcher'); @@ -144,4 +171,23 @@ private function persistNewRefreshToken(BridgeRefreshTokenRepository $repository $repository->persistNewRefreshToken($refreshToken); } + + private function deviceCodeRepository(): BridgeDeviceCodeRepository + { + return new BridgeDeviceCodeRepository; + } + + private function persistNewDeviceCode(BridgeDeviceCodeRepository $repository, string $id): void + { + $deviceCode = m::mock(DeviceCode::class); + $deviceCode->shouldReceive('getIdentifier')->andReturn($id); + $deviceCode->shouldReceive('getClient->getIdentifier')->andReturn('clientId'); + $deviceCode->shouldReceive('getExpiryDateTime')->andReturn(CarbonImmutable::now()); + $deviceCode->shouldReceive('getScopes')->andReturn([]); + $deviceCode->shouldReceive('getUserCode')->andReturn('userCode'); + $deviceCode->shouldReceive('isLastPolledAtDirty')->andReturn(false); + $deviceCode->shouldReceive('isUserDirty')->andReturn(false); + + $repository->persistDeviceCode($deviceCode); + } } diff --git a/tests/Unit/PassportTest.php b/tests/Unit/PassportTest.php index 0056692ad..6ff205703 100644 --- a/tests/Unit/PassportTest.php +++ b/tests/Unit/PassportTest.php @@ -4,6 +4,7 @@ use Laravel\Passport\AuthCode; use Laravel\Passport\Client; +use Laravel\Passport\DeviceCode; use Laravel\Passport\Passport; use Laravel\Passport\RefreshToken; use Laravel\Passport\Token; @@ -65,6 +66,13 @@ public function test_refresh_token_model_can_be_changed() Passport::useRefreshTokenModel(RefreshToken::class); } + public function test_device_code_instance_can_be_created() + { + $deviceCode = Passport::deviceCode(); + + $this->assertInstanceOf(DeviceCode::class, $deviceCode); + $this->assertInstanceOf(Passport::deviceCodeModel(), $deviceCode); + } } class RefreshTokenStub extends RefreshToken From ca4848bc363033cee1829254bf111ecafb49dd41 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Mon, 7 Oct 2024 18:35:58 +0330 Subject: [PATCH 32/38] formatting --- tests/Unit/PassportTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Unit/PassportTest.php b/tests/Unit/PassportTest.php index 6ff205703..4ac964156 100644 --- a/tests/Unit/PassportTest.php +++ b/tests/Unit/PassportTest.php @@ -66,6 +66,7 @@ public function test_refresh_token_model_can_be_changed() Passport::useRefreshTokenModel(RefreshToken::class); } + public function test_device_code_instance_can_be_created() { $deviceCode = Passport::deviceCode(); From fb7f9326d1b724a34c5ea5fa9b044662eb2f13c5 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Mon, 7 Oct 2024 18:40:50 +0330 Subject: [PATCH 33/38] force re-run tests From a27abc7089e072c5505dbe1d33afca4319e9dede Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Mon, 14 Oct 2024 00:22:34 +0330 Subject: [PATCH 34/38] resolve stateful guard --- .../ApproveDeviceAuthorizationController.php | 6 ++++-- .../DenyDeviceAuthorizationController.php | 6 ++++-- .../Controllers/DeviceAuthorizationController.php | 11 ++++++++++- src/PassportServiceProvider.php | 14 +++++++++++--- 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/Http/Controllers/ApproveDeviceAuthorizationController.php b/src/Http/Controllers/ApproveDeviceAuthorizationController.php index 65da4a36e..1212913e4 100644 --- a/src/Http/Controllers/ApproveDeviceAuthorizationController.php +++ b/src/Http/Controllers/ApproveDeviceAuthorizationController.php @@ -2,6 +2,7 @@ namespace Laravel\Passport\Http\Controllers; +use Illuminate\Contracts\Auth\StatefulGuard; use Illuminate\Http\Request; use Laravel\Passport\Contracts\ApprovedDeviceAuthorizationResponse; use League\OAuth2\Server\AuthorizationServer; @@ -14,7 +15,8 @@ class ApproveDeviceAuthorizationController * Create a new controller instance. */ public function __construct( - protected AuthorizationServer $server + protected AuthorizationServer $server, + protected StatefulGuard $guard ) { } @@ -27,7 +29,7 @@ public function __invoke( ): ApprovedDeviceAuthorizationResponse { $this->withErrorHandling(fn () => $this->server->completeDeviceAuthorizationRequest( $this->getDeviceCodeFromSession($request), - $request->user()->getAuthIdentifier(), + $this->guard->user()->getAuthIdentifier(), true )); diff --git a/src/Http/Controllers/DenyDeviceAuthorizationController.php b/src/Http/Controllers/DenyDeviceAuthorizationController.php index c3ccf72dc..fc6f382ce 100644 --- a/src/Http/Controllers/DenyDeviceAuthorizationController.php +++ b/src/Http/Controllers/DenyDeviceAuthorizationController.php @@ -2,6 +2,7 @@ namespace Laravel\Passport\Http\Controllers; +use Illuminate\Contracts\Auth\StatefulGuard; use Illuminate\Http\Request; use Laravel\Passport\Contracts\DeniedDeviceAuthorizationResponse; use League\OAuth2\Server\AuthorizationServer; @@ -14,7 +15,8 @@ class DenyDeviceAuthorizationController * Create a new controller instance. */ public function __construct( - protected AuthorizationServer $server + protected AuthorizationServer $server, + protected StatefulGuard $guard ) { } @@ -27,7 +29,7 @@ public function __invoke( ): DeniedDeviceAuthorizationResponse { $this->withErrorHandling(fn () => $this->server->completeDeviceAuthorizationRequest( $this->getDeviceCodeFromSession($request), - $request->user()->getAuthIdentifier(), + $this->guard->user()->getAuthIdentifier(), false )); diff --git a/src/Http/Controllers/DeviceAuthorizationController.php b/src/Http/Controllers/DeviceAuthorizationController.php index 2b15e593f..8150ce234 100644 --- a/src/Http/Controllers/DeviceAuthorizationController.php +++ b/src/Http/Controllers/DeviceAuthorizationController.php @@ -2,6 +2,7 @@ namespace Laravel\Passport\Http\Controllers; +use Illuminate\Contracts\Auth\StatefulGuard; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Date; @@ -11,6 +12,14 @@ class DeviceAuthorizationController { + /** + * Create a new controller instance. + */ + public function __construct( + protected StatefulGuard $guard + ) { + } + /** * Authorize a device to access the user's account. */ @@ -42,7 +51,7 @@ public function __invoke( return $viewResponse->withParameters([ 'client' => $deviceCode->client, - 'user' => $request->user(), + 'user' => $this->guard->user(), 'scopes' => Passport::scopesFor($deviceCode->scopes), 'request' => $request, 'authToken' => $authToken, diff --git a/src/PassportServiceProvider.php b/src/PassportServiceProvider.php index eb5c82113..90d16db4e 100644 --- a/src/PassportServiceProvider.php +++ b/src/PassportServiceProvider.php @@ -17,7 +17,10 @@ use Laravel\Passport\Contracts\ApprovedDeviceAuthorizationResponse as ApprovedDeviceAuthorizationResponseContract; use Laravel\Passport\Contracts\DeniedDeviceAuthorizationResponse as DeniedDeviceAuthorizationResponseContract; use Laravel\Passport\Guards\TokenGuard; +use Laravel\Passport\Http\Controllers\ApproveDeviceAuthorizationController; use Laravel\Passport\Http\Controllers\AuthorizationController; +use Laravel\Passport\Http\Controllers\DenyDeviceAuthorizationController; +use Laravel\Passport\Http\Controllers\DeviceAuthorizationController; use Laravel\Passport\Http\Responses\ApprovedDeviceAuthorizationResponse; use Laravel\Passport\Http\Responses\DeniedDeviceAuthorizationResponse; use Lcobucci\JWT\Encoding\JoseEncoder; @@ -106,9 +109,14 @@ public function register(): void { $this->mergeConfigFrom(__DIR__.'/../config/passport.php', 'passport'); - $this->app->when(AuthorizationController::class) - ->needs(StatefulGuard::class) - ->give(fn () => Auth::guard(config('passport.guard', null))); + $this->app->when([ + AuthorizationController::class, + DeviceAuthorizationController::class, + ApproveDeviceAuthorizationController::class, + DenyDeviceAuthorizationController::class, + ]) + ->needs(StatefulGuard::class) + ->give(fn () => Auth::guard(config('passport.guard', null))); $this->app->singleton(ClientRepository::class); From 133b4b780033178593dd505e51ffc67b6551bc92 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Tue, 15 Oct 2024 22:07:02 +0330 Subject: [PATCH 35/38] add more tests --- .../Feature/DeviceAuthorizationGrantTest.php | 103 ++++++++++-------- 1 file changed, 60 insertions(+), 43 deletions(-) diff --git a/tests/Feature/DeviceAuthorizationGrantTest.php b/tests/Feature/DeviceAuthorizationGrantTest.php index e132df8c9..d5fed33e2 100644 --- a/tests/Feature/DeviceAuthorizationGrantTest.php +++ b/tests/Feature/DeviceAuthorizationGrantTest.php @@ -17,6 +17,13 @@ protected function setUp(): void { parent::setUp(); + Passport::tokensCan([ + 'create' => 'Create', + 'read' => 'Read', + 'update' => 'Update', + 'delete' => 'Delete', + ]); + Passport::deviceAuthorizationView(fn ($params) => $params); Passport::deviceUserCodeView(fn ($params) => $params); } @@ -25,13 +32,10 @@ public function testIssueDeviceCode() { $client = ClientFactory::new()->asDeviceCodeClient()->create(); - $response = $this->post('/oauth/device/code', [ + $json = $this->post('/oauth/device/code', [ 'client_id' => $client->getKey(), - 'scope' => '', - ]); - - $response->assertOk(); - $json = $response->json(); + 'scope' => 'create read', + ])->assertOk()->json(); $this->assertArrayHasKey('device_code', $json); $this->assertArrayHasKey('user_code', $json); @@ -47,18 +51,15 @@ public function testRequestAccessTokenAuthorizationPending() ['device_code' => $deviceCode] = $this->post('/oauth/device/code', [ 'client_id' => $client->getKey(), - 'scope' => '', - ])->json(); + 'scope' => 'create read', + ])->assertOk()->json(); - $response = $this->post('/oauth/token', [ + $json = $this->post('/oauth/token', [ 'grant_type' => 'urn:ietf:params:oauth:grant-type:device_code', 'client_id' => $client->getKey(), 'client_secret' => $client->plainSecret, 'device_code' => $deviceCode, - ]); - - $response->assertBadRequest(); - $json = $response->json(); + ])->assertBadRequest()->json(); $this->assertArrayHasKey('error', $json); $this->assertArrayHasKey('error_description', $json); @@ -84,12 +85,11 @@ public function testVerificationUrl() 'user_code' => $userCode, ] = $this->post('/oauth/device/code', [ 'client_id' => $client->getKey(), - 'scope' => '', - ])->json(); + 'scope' => 'create read', + ])->assertOk()->json(); - $response = $this->get($verificationUri); - $response->assertOk(); - $this->assertEqualsCanonicalizing(['request'], array_keys($response->json())); + $json = $this->get($verificationUri)->assertOk()->json(); + $this->assertEqualsCanonicalizing(['request'], array_keys($json)); $user = UserFactory::new()->create(); @@ -110,13 +110,6 @@ public function testAuthorizationWithInvalidUserCode() public function testRequestAccessToken() { - Passport::tokensCan([ - 'create' => 'Create', - 'read' => 'Read', - 'update' => 'Update', - 'delete' => 'Delete', - ]); - $client = ClientFactory::new()->asDeviceCodeClient()->create(); [ @@ -125,7 +118,7 @@ public function testRequestAccessToken() ] = $this->post('/oauth/device/code', [ 'client_id' => $client->getKey(), 'scope' => 'create read', - ])->json(); + ])->assertOk()->json(); $user = UserFactory::new()->create(); $this->actingAs($user, 'web'); @@ -138,29 +131,25 @@ public function testRequestAccessToken() $this->assertEqualsCanonicalizing(['client', 'user', 'scopes', 'request', 'authToken'], array_keys($json)); $this->assertSame(collect(Passport::scopesFor(['create', 'read']))->toArray(), $json['scopes']); - ['authToken' => $authToken] = $json; - - $response = $this->post('/oauth/device/authorize', ['auth_token' => $authToken]); + $response = $this->post('/oauth/device/authorize', ['auth_token' => $json['authToken']]); $response->assertRedirectToRoute('passport.device'); $response->assertSessionHas('status', 'authorization-approved'); $response->assertSessionMissing(['deviceCode', 'authToken']); - $response = $this->post('/oauth/token', [ + $json = $this->post('/oauth/token', [ 'grant_type' => 'urn:ietf:params:oauth:grant-type:device_code', 'client_id' => $client->getKey(), 'client_secret' => $client->plainSecret, 'device_code' => $deviceCode, - ]); - - $response->assertOk(); - $json = $response->json(); + ])->assertOk()->json(); $this->assertArrayHasKey('access_token', $json); $this->assertArrayHasKey('refresh_token', $json); $this->assertSame('Bearer', $json['token_type']); $this->assertSame(31536000, $json['expires_in']); - Route::get('/foo', fn (Request $request) => $request->user()->token()->toJson())->middleware('auth:api'); + Route::get('/foo', fn (Request $request) => $request->user()->token()->toJson()) + ->middleware('auth:api'); $json = $this->withToken($json['access_token'], $json['token_type'])->get('/foo')->json(); @@ -178,31 +167,59 @@ public function testDenyAuthorization() 'user_code' => $userCode, ] = $this->post('/oauth/device/code', [ 'client_id' => $client->getKey(), - 'scope' => '', - ])->json(); + 'scope' => 'create read', + ])->assertOk()->json(); $user = UserFactory::new()->create(); $this->actingAs($user, 'web'); - ['authToken' => $authToken] = $this->get('/oauth/device/authorize?user_code='.$userCode)->json(); + $authToken = $this->get('/oauth/device/authorize?user_code='.$userCode)->assertOk()->json('authToken'); $response = $this->delete('/oauth/device/authorize', ['auth_token' => $authToken]); $response->assertRedirectToRoute('passport.device'); $response->assertSessionHas('status', 'authorization-denied'); $response->assertSessionMissing(['deviceCode', 'authToken']); - $response = $this->post('/oauth/token', [ + $json = $this->post('/oauth/token', [ 'grant_type' => 'urn:ietf:params:oauth:grant-type:device_code', 'client_id' => $client->getKey(), 'client_secret' => $client->plainSecret, 'device_code' => $deviceCode, - ]); - - $response->assertUnauthorized(); - $json = $response->json(); + ])->assertUnauthorized()->json(); $this->assertArrayHasKey('error', $json); $this->assertArrayHasKey('error_description', $json); $this->assertSame('access_denied', $json['error']); } + + public function testRequestAccessTokenWithPublicClient() + { + $client = ClientFactory::new()->asDeviceCodeClient()->asPublic()->create(); + + [ + 'device_code' => $deviceCode, + 'user_code' => $userCode, + ] = $this->post('/oauth/device/code', [ + 'client_id' => $client->getKey(), + 'scope' => 'create read', + ])->assertOk()->json(); + + $user = UserFactory::new()->create(); + $this->actingAs($user, 'web'); + + $authToken = $this->get('/oauth/device/authorize?user_code='.$userCode)->assertOk()->json('authToken'); + + $this->post('/oauth/device/authorize', ['auth_token' => $authToken])->assertRedirect(); + + $json = $this->post('/oauth/token', [ + 'grant_type' => 'urn:ietf:params:oauth:grant-type:device_code', + 'client_id' => $client->getKey(), + 'device_code' => $deviceCode, + ])->assertOk()->json(); + + $this->assertArrayHasKey('access_token', $json); + $this->assertArrayHasKey('refresh_token', $json); + $this->assertSame('Bearer', $json['token_type']); + $this->assertSame(31536000, $json['expires_in']); + } } From eca145a8cf30d5db63c5c52109978c6cbfe42814 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Thu, 17 Oct 2024 19:29:23 +0330 Subject: [PATCH 36/38] simplify --- src/Bridge/DeviceCode.php | 65 ++-------------- src/Bridge/DeviceCodeRepository.php | 76 +++++++++++-------- .../ApproveDeviceAuthorizationController.php | 10 +-- .../DenyDeviceAuthorizationController.php | 10 +-- .../DeviceAuthorizationController.php | 44 ++++++++--- .../RetrievesDeviceCodeFromSession.php | 3 +- src/PassportServiceProvider.php | 4 - .../Feature/DeviceAuthorizationGrantTest.php | 10 +-- 8 files changed, 102 insertions(+), 120 deletions(-) diff --git a/src/Bridge/DeviceCode.php b/src/Bridge/DeviceCode.php index b7240d5ce..9308b3030 100644 --- a/src/Bridge/DeviceCode.php +++ b/src/Bridge/DeviceCode.php @@ -10,22 +10,7 @@ class DeviceCode implements DeviceCodeEntityInterface { - use EntityTrait; - use DeviceCodeTrait { - setLastPolledAt as traitSetLastPolledAt; - setUserApproved as traitSetUserApproved; - } - use TokenEntityTrait; - - /** - * Determine if the "user identifier" and "user approved" properties has changed. - */ - private bool $isUserDirty = false; - - /** - * Determine if the "last polled at" property has changed. - */ - private bool $isLastPolledAtDirty = false; + use EntityTrait, DeviceCodeTrait, TokenEntityTrait; /** * Create a new device code instance. @@ -39,6 +24,7 @@ public function __construct( ?string $identifier = null, ?string $userIdentifier = null, ?string $clientIdentifier = null, + ?string $userCode = null, array $scopes = [], bool $userApproved = false, ?DateTimeImmutable $lastPolledAt = null, @@ -56,59 +42,24 @@ public function __construct( $this->setClient(new Client($clientIdentifier)); } + if (! is_null($userCode)) { + $this->setUserCode($userCode); + } + foreach ($scopes as $scope) { $this->addScope(new Scope($scope)); } if ($userApproved) { - $this->traitSetUserApproved($userApproved); + $this->setUserApproved($userApproved); } if (! is_null($lastPolledAt)) { - $this->traitSetLastPolledAt($lastPolledAt); + $this->setLastPolledAt($lastPolledAt); } if (! is_null($expiryDateTime)) { $this->setExpiryDateTime($expiryDateTime); } - - $this->isUserDirty = false; - $this->isLastPolledAtDirty = false; - } - - /** - * {@inheritdoc} - */ - public function setUserApproved(bool $userApproved): void - { - $this->isUserDirty = true; - - $this->traitSetUserApproved($userApproved); - } - - /** - * {@inheritdoc} - */ - public function setLastPolledAt(DateTimeImmutable $lastPolledAt): void - { - $this->isLastPolledAtDirty = true; - - $this->traitSetLastPolledAt($lastPolledAt); - } - - /** - * Determine if the "user identifier" and "user approved" properties has changed. - */ - public function isUserDirty(): bool - { - return $this->isUserDirty; - } - - /** - * Determine if the "last polled at" property has changed. - */ - public function isLastPolledAtDirty(): bool - { - return $this->isLastPolledAtDirty; } } diff --git a/src/Bridge/DeviceCodeRepository.php b/src/Bridge/DeviceCodeRepository.php index 7f6b6b957..5a5059e81 100644 --- a/src/Bridge/DeviceCodeRepository.php +++ b/src/Bridge/DeviceCodeRepository.php @@ -3,6 +3,8 @@ namespace Laravel\Passport\Bridge; use DateTime; +use Illuminate\Support\Facades\Date; +use Laravel\Passport\DeviceCode as DeviceCodeModel; use Laravel\Passport\Passport; use League\OAuth2\Server\Entities\DeviceCodeEntityInterface; use League\OAuth2\Server\Repositories\DeviceCodeRepositoryInterface; @@ -24,28 +26,17 @@ public function getNewDeviceCode(): DeviceCodeEntityInterface */ public function persistDeviceCode(DeviceCodeEntityInterface $deviceCodeEntity): void { - if ($deviceCodeEntity->isLastPolledAtDirty()) { - Passport::deviceCode()->newQuery()->whereKey($deviceCodeEntity->getIdentifier())->update([ - 'last_polled_at' => $deviceCodeEntity->getLastPolledAt(), - ]); - } elseif ($deviceCodeEntity->isUserDirty()) { - Passport::deviceCode()->newQuery()->whereKey($deviceCodeEntity->getIdentifier())->update([ - 'user_id' => $deviceCodeEntity->getUserIdentifier(), - 'user_approved_at' => $deviceCodeEntity->getUserApproved() ? new DateTime : null, - ]); - } else { - Passport::deviceCode()->forceFill([ - 'id' => $deviceCodeEntity->getIdentifier(), - 'user_id' => null, - 'client_id' => $deviceCodeEntity->getClient()->getIdentifier(), - 'user_code' => $deviceCodeEntity->getUserCode(), - 'scopes' => $this->scopesToArray($deviceCodeEntity->getScopes()), - 'revoked' => false, - 'user_approved_at' => null, - 'last_polled_at' => null, - 'expires_at' => $deviceCodeEntity->getExpiryDateTime(), - ])->save(); - } + Passport::deviceCode()->newQuery()->upsert([ + 'id' => $deviceCodeEntity->getIdentifier(), + 'user_id' => $deviceCodeEntity->getUserIdentifier(), + 'client_id' => $deviceCodeEntity->getClient()->getIdentifier(), + 'user_code' => $deviceCodeEntity->getUserCode(), + 'scopes' => $this->formatScopesForStorage($deviceCodeEntity->getScopes()), + 'revoked' => false, + 'user_approved_at' => $deviceCodeEntity->getUserApproved() ? new DateTime : null, + 'last_polled_at' => $deviceCodeEntity->getLastPolledAt(), + 'expires_at' => $deviceCodeEntity->getExpiryDateTime(), + ], 'id', ['user_id', 'user_approved_at', 'last_polled_at']); } /** @@ -55,15 +46,21 @@ public function getDeviceCodeEntityByDeviceCode(string $deviceCode): ?DeviceCode { $record = Passport::deviceCode()->newQuery()->whereKey($deviceCode)->where(['revoked' => false])->first(); - return $record ? new DeviceCode( - $record->getKey(), - $record->user_id, - $record->client_id, - $record->scopes, - ! is_null($record->user_approved_at), - $record->last_polled_at?->toDateTimeImmutable(), - $record->expires_at?->toDateTimeImmutable() - ) : null; + return $record ? $this->fromDeviceCodeModel($record) : null; + } + + /* + * Get the device code entity by the given user code. + */ + public function getDeviceCodeEntityByUserCode(string $userCode): ?DeviceCodeEntityInterface + { + $record = Passport::deviceCode()->newQuery() + ->where('user_code', $userCode) + ->where('expires_at', '>', Date::now()) + ->where('revoked', false) + ->first(); + + return $record ? $this->fromDeviceCodeModel($record) : null; } /** @@ -81,4 +78,21 @@ public function isDeviceCodeRevoked(string $codeId): bool { return Passport::deviceCode()->newQuery()->whereKey($codeId)->where('revoked', false)->doesntExist(); } + + /** + * Create a new device code entity from the given device code model instance. + */ + protected function fromDeviceCodeModel(DeviceCodeModel $model): DeviceCodeEntityInterface + { + return new DeviceCode( + $model->getKey(), + $model->user_id, + $model->client_id, + $model->user_code, + $model->scopes, + ! is_null($model->user_approved_at), + $model->last_polled_at?->toDateTimeImmutable(), + $model->expires_at?->toDateTimeImmutable() + ); + } } diff --git a/src/Http/Controllers/ApproveDeviceAuthorizationController.php b/src/Http/Controllers/ApproveDeviceAuthorizationController.php index 1212913e4..5eeac5c73 100644 --- a/src/Http/Controllers/ApproveDeviceAuthorizationController.php +++ b/src/Http/Controllers/ApproveDeviceAuthorizationController.php @@ -2,7 +2,6 @@ namespace Laravel\Passport\Http\Controllers; -use Illuminate\Contracts\Auth\StatefulGuard; use Illuminate\Http\Request; use Laravel\Passport\Contracts\ApprovedDeviceAuthorizationResponse; use League\OAuth2\Server\AuthorizationServer; @@ -15,8 +14,7 @@ class ApproveDeviceAuthorizationController * Create a new controller instance. */ public function __construct( - protected AuthorizationServer $server, - protected StatefulGuard $guard + protected AuthorizationServer $server ) { } @@ -27,9 +25,11 @@ public function __invoke( Request $request, ApprovedDeviceAuthorizationResponse $response ): ApprovedDeviceAuthorizationResponse { + $deviceCode = $this->getDeviceCodeFromSession($request); + $this->withErrorHandling(fn () => $this->server->completeDeviceAuthorizationRequest( - $this->getDeviceCodeFromSession($request), - $this->guard->user()->getAuthIdentifier(), + $deviceCode->getIdentifier(), + $deviceCode->getUserIdentifier(), true )); diff --git a/src/Http/Controllers/DenyDeviceAuthorizationController.php b/src/Http/Controllers/DenyDeviceAuthorizationController.php index fc6f382ce..c1df2b10e 100644 --- a/src/Http/Controllers/DenyDeviceAuthorizationController.php +++ b/src/Http/Controllers/DenyDeviceAuthorizationController.php @@ -2,7 +2,6 @@ namespace Laravel\Passport\Http\Controllers; -use Illuminate\Contracts\Auth\StatefulGuard; use Illuminate\Http\Request; use Laravel\Passport\Contracts\DeniedDeviceAuthorizationResponse; use League\OAuth2\Server\AuthorizationServer; @@ -15,8 +14,7 @@ class DenyDeviceAuthorizationController * Create a new controller instance. */ public function __construct( - protected AuthorizationServer $server, - protected StatefulGuard $guard + protected AuthorizationServer $server ) { } @@ -27,9 +25,11 @@ public function __invoke( Request $request, DeniedDeviceAuthorizationResponse $response ): DeniedDeviceAuthorizationResponse { + $deviceCode = $this->getDeviceCodeFromSession($request); + $this->withErrorHandling(fn () => $this->server->completeDeviceAuthorizationRequest( - $this->getDeviceCodeFromSession($request), - $this->guard->user()->getAuthIdentifier(), + $deviceCode->getIdentifier(), + $deviceCode->getUserIdentifier(), false )); diff --git a/src/Http/Controllers/DeviceAuthorizationController.php b/src/Http/Controllers/DeviceAuthorizationController.php index 8150ce234..cdacf6ff1 100644 --- a/src/Http/Controllers/DeviceAuthorizationController.php +++ b/src/Http/Controllers/DeviceAuthorizationController.php @@ -5,10 +5,13 @@ use Illuminate\Contracts\Auth\StatefulGuard; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Date; use Illuminate\Support\Str; +use Laravel\Passport\Bridge\DeviceCodeRepository; +use Laravel\Passport\ClientRepository; use Laravel\Passport\Contracts\DeviceAuthorizationViewResponse; use Laravel\Passport\Passport; +use League\OAuth2\Server\Entities\DeviceCodeEntityInterface; +use League\OAuth2\Server\Entities\ScopeEntityInterface; class DeviceAuthorizationController { @@ -16,7 +19,9 @@ class DeviceAuthorizationController * Create a new controller instance. */ public function __construct( - protected StatefulGuard $guard + protected StatefulGuard $guard, + protected DeviceCodeRepository $deviceCodes, + protected ClientRepository $clients ) { } @@ -31,12 +36,7 @@ public function __invoke( return to_route('passport.device'); } - $deviceCode = Passport::deviceCode() - ->with('client') - ->where('user_code', $userCode) - ->where('expires_at', '>', Date::now()) - ->where('revoked', false) - ->first(); + $deviceCode = $this->deviceCodes->getDeviceCodeEntityByUserCode($userCode); if (! $deviceCode) { return to_route('passport.device') @@ -46,15 +46,35 @@ public function __invoke( ]); } + $user = $this->guard->user(); + $deviceCode->setUserIdentifier($user->getAuthIdentifier()); + + $scopes = $this->parseScopes($deviceCode); + $client = $this->clients->find($deviceCode->getClient()->getIdentifier()); + $request->session()->put('authToken', $authToken = Str::random()); - $request->session()->put('deviceCode', $deviceCode->getKey()); + $request->session()->put('deviceCode', $deviceCode); return $viewResponse->withParameters([ - 'client' => $deviceCode->client, - 'user' => $this->guard->user(), - 'scopes' => Passport::scopesFor($deviceCode->scopes), + 'client' => $client, + 'user' => $user, + 'scopes' => $scopes, 'request' => $request, 'authToken' => $authToken, ]); } + + /** + * Transform the device code entity's scopes into Scope instances. + * + * @return \Laravel\Passport\Scope[] + */ + protected function parseScopes(DeviceCodeEntityInterface $deviceCode): array + { + return Passport::scopesFor( + collect($deviceCode->getScopes())->map( + fn (ScopeEntityInterface $scope): string => $scope->getIdentifier() + )->unique()->all() + ); + } } diff --git a/src/Http/Controllers/RetrievesDeviceCodeFromSession.php b/src/Http/Controllers/RetrievesDeviceCodeFromSession.php index b0b6217a7..eb9e55b06 100644 --- a/src/Http/Controllers/RetrievesDeviceCodeFromSession.php +++ b/src/Http/Controllers/RetrievesDeviceCodeFromSession.php @@ -5,6 +5,7 @@ use Exception; use Illuminate\Http\Request; use Laravel\Passport\Exceptions\InvalidAuthTokenException; +use League\OAuth2\Server\Entities\DeviceCodeEntityInterface; trait RetrievesDeviceCodeFromSession { @@ -14,7 +15,7 @@ trait RetrievesDeviceCodeFromSession * @throws \Laravel\Passport\Exceptions\InvalidAuthTokenException * @throws \Exception */ - protected function getDeviceCodeFromSession(Request $request): string + protected function getDeviceCodeFromSession(Request $request): DeviceCodeEntityInterface { if ($request->isNotFilled('auth_token') || $request->session()->pull('authToken') !== $request->get('auth_token')) { diff --git a/src/PassportServiceProvider.php b/src/PassportServiceProvider.php index 90d16db4e..c3ef4751e 100644 --- a/src/PassportServiceProvider.php +++ b/src/PassportServiceProvider.php @@ -17,9 +17,7 @@ use Laravel\Passport\Contracts\ApprovedDeviceAuthorizationResponse as ApprovedDeviceAuthorizationResponseContract; use Laravel\Passport\Contracts\DeniedDeviceAuthorizationResponse as DeniedDeviceAuthorizationResponseContract; use Laravel\Passport\Guards\TokenGuard; -use Laravel\Passport\Http\Controllers\ApproveDeviceAuthorizationController; use Laravel\Passport\Http\Controllers\AuthorizationController; -use Laravel\Passport\Http\Controllers\DenyDeviceAuthorizationController; use Laravel\Passport\Http\Controllers\DeviceAuthorizationController; use Laravel\Passport\Http\Responses\ApprovedDeviceAuthorizationResponse; use Laravel\Passport\Http\Responses\DeniedDeviceAuthorizationResponse; @@ -112,8 +110,6 @@ public function register(): void $this->app->when([ AuthorizationController::class, DeviceAuthorizationController::class, - ApproveDeviceAuthorizationController::class, - DenyDeviceAuthorizationController::class, ]) ->needs(StatefulGuard::class) ->give(fn () => Auth::guard(config('passport.guard', null))); diff --git a/tests/Feature/DeviceAuthorizationGrantTest.php b/tests/Feature/DeviceAuthorizationGrantTest.php index d5fed33e2..15923d4d6 100644 --- a/tests/Feature/DeviceAuthorizationGrantTest.php +++ b/tests/Feature/DeviceAuthorizationGrantTest.php @@ -123,11 +123,11 @@ public function testRequestAccessToken() $user = UserFactory::new()->create(); $this->actingAs($user, 'web'); - $response = $this->get('/oauth/device/authorize?user_code='.$userCode); - $response->assertOk(); - $response->assertSessionHas('deviceCode', $deviceCode); - $response->assertSessionHas('authToken'); - $json = $response->json(); + $json = $this->get('/oauth/device/authorize?user_code='.$userCode) + ->assertOk() + ->assertSessionHas('deviceCode') + ->assertSessionHas('authToken') + ->json(); $this->assertEqualsCanonicalizing(['client', 'user', 'scopes', 'request', 'authToken'], array_keys($json)); $this->assertSame(collect(Passport::scopesFor(['create', 'read']))->toArray(), $json['scopes']); From b5c0165aef900f144b7a2a1b6d9368e397c0cf5a Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Thu, 17 Oct 2024 19:37:26 +0330 Subject: [PATCH 37/38] fix tests --- tests/Feature/RevokedTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/Feature/RevokedTest.php b/tests/Feature/RevokedTest.php index f9a1d7887..46beb41e3 100644 --- a/tests/Feature/RevokedTest.php +++ b/tests/Feature/RevokedTest.php @@ -181,12 +181,13 @@ private function persistNewDeviceCode(BridgeDeviceCodeRepository $repository, st { $deviceCode = m::mock(DeviceCode::class); $deviceCode->shouldReceive('getIdentifier')->andReturn($id); + $deviceCode->shouldReceive('getUserIdentifier')->andReturn(null); $deviceCode->shouldReceive('getClient->getIdentifier')->andReturn('clientId'); $deviceCode->shouldReceive('getExpiryDateTime')->andReturn(CarbonImmutable::now()); + $deviceCode->shouldReceive('getLastPolledAt')->andReturn(CarbonImmutable::now()); $deviceCode->shouldReceive('getScopes')->andReturn([]); $deviceCode->shouldReceive('getUserCode')->andReturn('userCode'); - $deviceCode->shouldReceive('isLastPolledAtDirty')->andReturn(false); - $deviceCode->shouldReceive('isUserDirty')->andReturn(false); + $deviceCode->shouldReceive('getUserApproved')->andReturn(false); $repository->persistDeviceCode($deviceCode); } From fc88d8cbf7ef51b7cc5d7372a8c900a9d34347a3 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Thu, 17 Oct 2024 19:45:51 +0330 Subject: [PATCH 38/38] formatting --- tests/Feature/DeviceAuthorizationGrantTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Feature/DeviceAuthorizationGrantTest.php b/tests/Feature/DeviceAuthorizationGrantTest.php index 15923d4d6..31eade9f8 100644 --- a/tests/Feature/DeviceAuthorizationGrantTest.php +++ b/tests/Feature/DeviceAuthorizationGrantTest.php @@ -39,7 +39,7 @@ public function testIssueDeviceCode() $this->assertArrayHasKey('device_code', $json); $this->assertArrayHasKey('user_code', $json); - // $this->assertSame(5, $json['interval']); // TODO https://github.com/thephpleague/oauth2-server/pull/1410 + // $this->assertSame(5, $json['interval']); $this->assertSame(600, $json['expires_in']); $this->assertSame('http://localhost/oauth/device', $json['verification_uri']); $this->assertSame('http://localhost/oauth/device?user_code='.$json['user_code'], $json['verification_uri_complete']); @@ -155,7 +155,7 @@ public function testRequestAccessToken() $this->assertSame($client->getKey(), $json['oauth_client_id']); $this->assertEquals($user->getAuthIdentifier(), $json['oauth_user_id']); - // $this->assertSame(['create', 'read'], $json['oauth_scopes']); TODO: https://github.com/thephpleague/oauth2-server/pull/1412 + // $this->assertSame(['create', 'read'], $json['oauth_scopes']); } public function testDenyAuthorization()