diff --git a/src/Http/Controllers/AuthorizationController.php b/src/Http/Controllers/AuthorizationController.php index 1c13a94e..f9f7de64 100644 --- a/src/Http/Controllers/AuthorizationController.php +++ b/src/Http/Controllers/AuthorizationController.php @@ -49,13 +49,22 @@ public function authorize( ($psrRequest->getQueryParams()['response_type'] ?? null) === 'token' ); + $prompt = $request->string('prompt')->explode(' ')->map(trim(...))->filter()->values(); + + // If the prompt parameter includes "none", all other prompt values will be ignored + // An error will be returned if the end-user is not already authenticated or the + // OAuth client does not have pre-configured consent for the requested scopes. + if ($prompt->contains('none')) { + $prompt = collect(['none']); + } + if ($this->guard->guest()) { - $request->input('prompt') === 'none' + $prompt->contains('none') ? throw OAuthServerException::loginRequired($authRequest) : $this->promptForLogin($request); } - if ($request->input('prompt') === 'login' && + if ($prompt->contains('login') && ! $request->session()->get('promptedForLogin', false)) { $this->guard->logout(); $request->session()->invalidate(); @@ -72,12 +81,12 @@ public function authorize( $scopes = $this->parseScopes($authRequest); $client = $this->clients->find($authRequest->getClient()->getIdentifier()); - if ($request->input('prompt') !== 'consent' && + if ($prompt->doesntContain('consent') && ($client->skipsAuthorization($user, $scopes) || $this->hasGrantedScopes($user, $client, $scopes))) { return $this->approveRequest($authRequest, $psrResponse); } - if ($request->input('prompt') === 'none') { + if ($prompt->contains('none')) { throw OAuthServerException::consentRequired($authRequest); } diff --git a/tests/Feature/AuthorizationCodeGrantTest.php b/tests/Feature/AuthorizationCodeGrantTest.php index fe611933..ea5378ad 100644 --- a/tests/Feature/AuthorizationCodeGrantTest.php +++ b/tests/Feature/AuthorizationCodeGrantTest.php @@ -430,6 +430,89 @@ public function testPromptLogin() $response->assertRedirectToRoute('login'); } + public function testPromptLoginConsent() + { + Route::get('/foo', fn () => '')->name('login'); + + $client = ClientFactory::new()->create(); + + $query = http_build_query([ + 'client_id' => $client->getKey(), + 'redirect_uri' => $redirect = $client->redirect_uris[0], + 'response_type' => 'code', + 'scope' => 'create read update', + 'state' => Str::random(40), + ]); + + $user = UserFactory::new()->create(); + $this->actingAs($user, 'web'); + $json = $this->get('/oauth/authorize?'.$query)->json(); + + $response = $this->post('/oauth/authorize', ['auth_token' => $json['authToken']]); + parse_str(parse_url($response->headers->get('Location'), PHP_URL_QUERY), $params); + + $this->post('/oauth/token', [ + 'grant_type' => 'authorization_code', + 'client_id' => $client->getKey(), + 'client_secret' => $client->plainSecret, + 'redirect_uri' => $redirect, + 'code' => $params['code'], + ]); + + $query = http_build_query([ + 'client_id' => $client->getKey(), + 'redirect_uri' => $client->redirect_uris[0], + 'response_type' => 'code', + 'scope' => 'create read', + 'state' => Str::random(40), + 'prompt' => 'login consent', + ]); + + $this->get('/oauth/authorize?'.$query) + ->assertSessionHas('promptedForLogin', true) + ->assertRedirectToRoute('login'); + + $intendedUrl = session()->get('url.intended'); + parse_str(parse_url($intendedUrl, PHP_URL_QUERY), $params); + + dump($params); + + $this->actingAs($user, 'web'); + $json = $this->get($intendedUrl) + ->assertOk() + ->assertSessionHas('authRequest') + ->assertSessionHas('authToken') + ->json(); + + $this->assertEqualsCanonicalizing(['client', 'user', 'scopes', 'request', 'authToken'], array_keys($json)); + $this->assertSame(collect(Passport::scopesFor(['create', 'read']))->toArray(), $json['scopes']); + } + + public function testPromptContainsNone() + { + $client = ClientFactory::new()->create(); + + $query = http_build_query([ + 'client_id' => $client->getKey(), + 'redirect_uri' => $redirect = $client->redirect_uris[0], + 'response_type' => 'code', + 'state' => $state = Str::random(40), + 'prompt' => 'none login consent select_account', + ]); + + $this->actingAs(UserFactory::new()->create(), 'web'); + $response = $this->get('/oauth/authorize?'.$query); + $response->assertRedirect(); + + $location = $response->headers->get('Location'); + parse_str(parse_url($location, PHP_URL_QUERY), $params); + + $this->assertStringStartsWith($redirect.'?', $location); + $this->assertSame($state, $params['state']); + $this->assertSame('consent_required', $params['error']); + $this->assertArrayHasKey('error_description', $params); + } + public function testUnauthorizedClient() { $client = ClientFactory::new()->create([ diff --git a/tests/Unit/AuthorizationControllerTest.php b/tests/Unit/AuthorizationControllerTest.php index e2a31727..df1c7259 100644 --- a/tests/Unit/AuthorizationControllerTest.php +++ b/tests/Unit/AuthorizationControllerTest.php @@ -5,6 +5,7 @@ use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Contracts\Auth\StatefulGuard; use Illuminate\Http\Request; +use Illuminate\Support\Str; use Laravel\Passport\Bridge\Scope; use Laravel\Passport\Client; use Laravel\Passport\ClientRepository; @@ -51,7 +52,7 @@ public function test_authorization_view_is_presented() $session->shouldReceive('put')->withSomeOfArgs('authToken'); $session->shouldReceive('put')->with('authRequest', $authRequest); $session->shouldReceive('forget')->with('promptedForLogin')->once(); - $request->shouldReceive('input')->with('prompt')->andReturn(null); + $request->shouldReceive('string')->with('prompt')->andReturn(Str::of(null)); $authRequest->shouldReceive('getClient->getIdentifier')->andReturn(1); $authRequest->shouldReceive('getScopes')->andReturn([new Scope('scope-1')]); @@ -134,7 +135,7 @@ public function test_request_is_approved_if_valid_token_exists() $session->shouldReceive('forget')->with('promptedForLogin')->once(); $user->shouldReceive('getAuthIdentifier')->andReturn(1); $request->shouldNotReceive('session'); - $request->shouldReceive('input')->with('prompt')->andReturn(null); + $request->shouldReceive('string')->with('prompt')->andReturn(Str::of(null)); $authRequest->shouldReceive('getClient->getIdentifier')->once()->andReturn(1); $authRequest->shouldReceive('getScopes')->once()->andReturn([new Scope('scope-1')]); @@ -182,7 +183,7 @@ public function test_request_is_approved_if_client_can_skip_authorization() $session->shouldReceive('forget')->with('promptedForLogin')->once(); $user->shouldReceive('getAuthIdentifier')->andReturn(1); $request->shouldNotReceive('session'); - $request->shouldReceive('input')->with('prompt')->andReturn(null); + $request->shouldReceive('string')->with('prompt')->andReturn(Str::of(null)); $authRequest->shouldReceive('getClient->getIdentifier')->once()->andReturn(1); $authRequest->shouldReceive('getScopes')->once()->andReturn([new Scope('scope-1')]); @@ -226,7 +227,7 @@ public function test_authorization_view_is_presented_if_request_has_prompt_equal $session->shouldReceive('put')->withSomeOfArgs('authToken'); $session->shouldReceive('put')->with('authRequest', $authRequest); $session->shouldReceive('forget')->with('promptedForLogin')->once(); - $request->shouldReceive('input')->with('prompt')->andReturn('consent'); + $request->shouldReceive('string')->with('prompt')->andReturn(Str::of('consent')); $authRequest->shouldReceive('getClient->getIdentifier')->once()->andReturn(1); $authRequest->shouldReceive('getScopes')->once()->andReturn([new Scope('scope-1')]); @@ -275,7 +276,7 @@ public function test_authorization_denied_if_request_has_prompt_equals_to_none() $request->shouldReceive('session')->andReturn($session = m::mock()); $session->shouldReceive('forget')->with('promptedForLogin')->once(); $user->shouldReceive('getAuthIdentifier')->andReturn(1); - $request->shouldReceive('input')->with('prompt')->andReturn('none'); + $request->shouldReceive('string')->with('prompt')->andReturn(Str::of('none')); $authRequest->shouldReceive('getClient->getIdentifier')->once()->andReturn(1); $authRequest->shouldReceive('getScopes')->once()->andReturn([new Scope('scope-1')]); @@ -326,7 +327,7 @@ public function test_authorization_denied_if_unauthenticated_and_request_has_pro $request = m::mock(Request::class); $request->shouldNotReceive('user'); - $request->shouldReceive('input')->with('prompt')->andReturn('none'); + $request->shouldReceive('string')->with('prompt')->andReturn(Str::of('none')); $authRequest->shouldNotReceive('setUser'); $authRequest->shouldReceive('setAuthorizationApproved')->with(false); @@ -378,7 +379,7 @@ public function test_logout_and_prompt_login_if_request_has_prompt_equals_to_log $session->shouldReceive('get')->with('promptedForLogin', false)->once()->andReturn(false); $session->shouldReceive('put')->with('promptedForLogin', true)->once(); $session->shouldNotReceive('forget')->with('promptedForLogin'); - $request->shouldReceive('input')->with('prompt')->andReturn('login'); + $request->shouldReceive('string')->with('prompt')->andReturn(Str::of('login')); $clients = m::mock(ClientRepository::class); @@ -408,7 +409,7 @@ public function test_user_should_be_authenticated() $request->shouldReceive('session')->andReturn($session = m::mock()); $session->shouldReceive('put')->with('promptedForLogin', true)->once(); $session->shouldNotReceive('forget')->with('promptedForLogin'); - $request->shouldReceive('input')->with('prompt')->andReturn(null); + $request->shouldReceive('string')->with('prompt')->andReturn(Str::of(null)); $clients = m::mock(ClientRepository::class);