diff --git a/src/main/php/web/auth/Flow.class.php b/src/main/php/web/auth/Flow.class.php index 8f2f13b..2f45812 100755 --- a/src/main/php/web/auth/Flow.class.php +++ b/src/main/php/web/auth/Flow.class.php @@ -4,6 +4,7 @@ /** @test web.auth.unittest.FlowClassTest */ abstract class Flow { + const STATE= '%[^_]_%s'; const FRAGMENT= '_'; private $url= null; diff --git a/src/main/php/web/auth/oauth/ByAccessToken.class.php b/src/main/php/web/auth/oauth/ByAccessToken.class.php index f38e300..71d8d91 100755 --- a/src/main/php/web/auth/oauth/ByAccessToken.class.php +++ b/src/main/php/web/auth/oauth/ByAccessToken.class.php @@ -24,6 +24,22 @@ public function __construct($token, $type= 'Bearer', $scope= null, $expires= nul $this->id= null === $id ? null : ($id instanceof Secret ? $id : new Secret($id)); } + /** + * Creates an instance resulting from access token response + * + * @see https://www.oauth.com/oauth2-servers/access-tokens/access-token-response/ + */ + public static function from(array $result): self { + return new self( + $result['access_token'], + $result['token_type'] ?? 'Bearer', + $result['scope'] ?? null, + $result['expires_in'] ?? null, + $result['refresh_token'] ?? null, + $result['id_token'] ?? null + ); + } + /** @return util.Secret */ public function token() { return $this->token; } diff --git a/src/main/php/web/auth/oauth/OAuth1Flow.class.php b/src/main/php/web/auth/oauth/OAuth1Flow.class.php index c75b8e6..ebc4fe8 100755 --- a/src/main/php/web/auth/oauth/OAuth1Flow.class.php +++ b/src/main/php/web/auth/oauth/OAuth1Flow.class.php @@ -76,9 +76,10 @@ protected function request($path, $token= null, $params= []) { * @throws lang.IllegalStateException */ public function authenticate($request, $response, $session) { - $stored= $session->value($this->namespace); + $stored= $session->value($this->namespace) ?? ['flows' => []]; - // We have an access token, reset state and return an authenticated session + // We have an access token, remove and return an authenticated session. The + // authentication implementation registers the user and transmits the session. if ($token= $stored['token'] ?? null) { unset($stored['token']); $session->register($this->namespace, $stored); @@ -89,85 +90,69 @@ public function authenticate($request, $response, $session) { ))); } - $server= $request->param('oauth_token'); + // Enter authentication flow, resolving callback URI against the curren request. $uri= $this->url(true)->resolve($request); $callback= $this->callback ? $uri->resolve($this->callback) : $this->service($uri); - // Start authenticaton flow by obtaining request token and store for later use - if (null === $server || null === $stored) { - $token= $this->request('/request_token', null, ['oauth_callback' => $callback])['oauth_token']; - $stored??= ['flow' => []]; - $stored['flow'][$token]= (string)$uri; + // Check whether we are continuing an existing authentication flow based on the + // state given by the server and our session; or if we need to start a new one. + if (null === ($state= $request->param('oauth_token'))) { + $flow= null; + } else { + $flow= $this->flow($state, $stored); + } + + if (null === $flow) { + $state= $this->request('/request_token', null, ['oauth_callback' => $callback])['oauth_token']; + + $stored['flows'][$state]= ['uri' => (string)$uri, 'seed' => []]; $session->register($this->namespace, $stored); $session->transmit($response); // Redirect the user to the authorization page - $target= sprintf( - '%s/authenticate?oauth_token=%s&oauth_callback=%s', - $this->service, - urlencode($token), - urlencode($callback) - ); + $token= urlencode($state); + $target= sprintf('%s/authenticate?oauth_token=%s&oauth_callback=%s', $this->service, $token, urlencode($callback)); // If a URL fragment is present, call ourselves to capture it inside the // session; otherwise redirect the OAuth authentication service directly. - $this->redirect($response, $target, sprintf(' - var target = "%1$s"; - var hash = document.location.hash.substring(1); - + $separator= self::FRAGMENT; + return $this->redirect($response, $target, <<param(self::FRAGMENT)) { - // Store fragment, then make redirection continue (see redirect() above) - $target= $stored['flow'][$server] ?? null; - if ($target && ($fragment= $request->param(self::FRAGMENT))) { - if ($t= strstr($stored['flow'][$server], '#', true)) { - $stored['flow'][$server]= $t.'#'.$fragment; - } else { - $stored['flow'][$server].= '#'.$fragment; - } + // Caputre fragment, then continue redirection, see the script above + $flow['uri']= substr($flow['uri'], 0, strcspn($flow['uri'], '#')).'#'.$fragment; + $stored['flows'][$state]= $flow; $session->register($this->namespace, $stored); $session->transmit($response); - $response->send('document.location.replace(target)', 'text/javascript'); + $response->send('document.location.replace(target);', 'text/javascript'); return null; - } - - // Back from authentication redirect, upgrade request token to access token - // Handle previous session layout - if ($target || (($target= $stored['target'] ?? null) && ($server === $stored['oauth_token']))) { - unset($stored['flow'][$server]); + } else { // Back from authentication redirect, upgrade request token to access token $stored['token']= $this->request( '/access_token', - $server, + $state, ['oauth_verifier' => $request->param('oauth_verifier')] ); + + unset($stored['flows'][$state], $stored['flow'][$state]); $session->register($this->namespace, $stored); $session->transmit($response); - // Redirect to self - $this->finalize($response, $target); - return null; + // Redirect to self, using captured fragment if present + return $this->finalize($response, $flow['uri']); } - - throw new IllegalStateException(sprintf( - 'Flow error, unknown server state %s expecting one of %s', - $server, - implode(', ', array_keys($stored['flow'] ?? [$stored['oauth_token'] => true])) - )); } } \ No newline at end of file diff --git a/src/main/php/web/auth/oauth/OAuth2Endpoint.class.php b/src/main/php/web/auth/oauth/OAuth2Endpoint.class.php index bfc73ac..6c68be9 100644 --- a/src/main/php/web/auth/oauth/OAuth2Endpoint.class.php +++ b/src/main/php/web/auth/oauth/OAuth2Endpoint.class.php @@ -83,7 +83,7 @@ public function seed() { return $this->credentials->seed(); } /** * Returns authorization parameters * - * @param [:string] $grant + * @param [:string] $auth * @param [:string] $seed * @return [:string] */ diff --git a/src/main/php/web/auth/oauth/OAuth2Flow.class.php b/src/main/php/web/auth/oauth/OAuth2Flow.class.php index 4867e50..493d3fb 100755 --- a/src/main/php/web/auth/oauth/OAuth2Flow.class.php +++ b/src/main/php/web/auth/oauth/OAuth2Flow.class.php @@ -60,18 +60,10 @@ public function refresh(array $claims) { if (time() < $claims['expires']) return null; // Refresh token - $result= $this->backend->acquire([ + return ByAccessToken::from($this->backend->acquire([ 'grant_type' => 'refresh_token', 'refresh_token' => $claims['refresh'], - ]); - return new ByAccessToken( - $result['access_token'], - $result['token_type'] ?? 'Bearer', - $result['scope'] ?? null, - $result['expires_in'] ?? null, - $result['refresh_token'] ?? null, - $result['id_token'] ?? null - ); + ])); } /** @@ -84,36 +76,35 @@ public function refresh(array $claims) { * @throws lang.IllegalStateException */ public function authenticate($request, $response, $session) { - $stored= $session->value($this->namespace); + $stored= $session->value($this->namespace) ?? ['state' => []]; - // We have an access token, reset state and return an authenticated session - // See https://www.oauth.com/oauth2-servers/access-tokens/access-token-response/ - // and https://tools.ietf.org/html/rfc6749#section-5.1 + // We have an access token, remove and return an authenticated session. The + // authentication implementation registers the user and transmits the session. if ($token= $stored['token'] ?? null) { unset($stored['token']); $session->register($this->namespace, $stored); - return new ByAccessToken( - $token['access_token'], - $token['token_type'] ?? 'Bearer', - $token['scope'] ?? null, - $token['expires_in'] ?? null, - $token['refresh_token'] ?? null, - $token['id_token'] ?? null - ); + return ByAccessToken::from($token); } + // Enter authentication flow, resolving callback URI against the curren request. $uri= $this->url(true)->resolve($request); $callback= $this->callback ? $uri->resolve($this->callback) : $this->service($uri); - // Start authorization flow to acquire an access token - $server= $request->param('state'); - if (null === $server || null === $stored) { + // Check whether we are continuing an existing authentication flow based on the + // state given by the server and our session; or if we need to start a new one. + if (null === ($server= $request->param('state'))) { + $flow= null; + } else { + sscanf($server, self::STATE, $state, $fragment); + $flow= $this->flow($state, $stored); + } + + if (null === $flow) { $state= bin2hex($this->rand->bytes(16)); $seed= $this->backend->seed(); - $stored??= ['flow' => []]; - $stored['flow'][$state]= ['uri' => (string)$uri, 'seed' => $seed]; + $stored['flows'][$state]= ['uri' => (string)$uri, 'seed' => $seed]; $session->register($this->namespace, $stored); $session->transmit($response); @@ -128,59 +119,34 @@ public function authenticate($request, $response, $session) { $target= $this->auth->using()->params($this->backend->pass($params, $seed))->create(); // If a URL fragment is present, append it to the state parameter, which - // is passed as the last parameter to the authentication service. - $this->redirect($response, $target, sprintf(' - var target = "%1$s"; - var hash = document.location.hash.substring(1); - + // is always passed as the last parameter to the authentication service. + $separator= self::FRAGMENT; + return $this->redirect($response, $target, << 'authorization_code', 'code' => $request->param('code'), 'redirect_uri' => $callback, - 'state' => $server + 'state' => $state ]; - $stored['token']= $this->backend->acquire($params, $seed); + $stored['token']= $this->backend->acquire($params, $flow['seed']); + + unset($stored['flows'][$state], $stored['flow'][$state]); $session->register($this->namespace, $stored); $session->transmit($response); // Redirect to self, using encoded fragment if present - $this->finalize($response, $uri.(isset($state[1]) ? '#'.urldecode($state[1]) : '')); - return null; + return $this->finalize($response, $flow['uri'].(isset($fragment) ? '#'.urldecode($fragment) : '')); } - - throw new IllegalStateException(sprintf( - 'Flow error, unknown server state %s expecting one of %s', - $state[0], - implode(', ', array_keys($stored['flow'] ?? [$stored['state'] => true])) - )); } } \ No newline at end of file diff --git a/src/main/php/web/auth/oauth/OAuthFlow.class.php b/src/main/php/web/auth/oauth/OAuthFlow.class.php index 83db688..6845981 100755 --- a/src/main/php/web/auth/oauth/OAuthFlow.class.php +++ b/src/main/php/web/auth/oauth/OAuthFlow.class.php @@ -6,6 +6,15 @@ abstract class OAuthFlow extends Flow { protected $callback; + /** Locate flow stored in session based on a given state, handling deprecated session layouts */ + protected function flow($state, $stored) { + return ( + $stored['flows'][$state] ?? + (isset($stored['flow'][$state]) ? ['uri' => $stored['flow'][$state], 'seed' => []] : null) ?? + (isset($stored['target']) ? ['uri' => $stored['target'], 'seed' => []] : null) + ); + } + /** @return ?util.URI */ public function callback() { return $this->callback; } diff --git a/src/test/php/web/auth/unittest/OAuth1FlowTest.class.php b/src/test/php/web/auth/unittest/OAuth1FlowTest.class.php index 475d9b6..f2e777d 100755 --- a/src/test/php/web/auth/unittest/OAuth1FlowTest.class.php +++ b/src/test/php/web/auth/unittest/OAuth1FlowTest.class.php @@ -48,7 +48,7 @@ public function fetches_request_token_then_redirects_to_auth($path) { sprintf('%s/authenticate?oauth_token=T&oauth_callback=%s', self::AUTH, urlencode(self::CALLBACK)), $this->redirectTo($this->authenticate($fixture, $path, $session)) ); - Assert::equals('http://localhost'.$path, current($session->value(self::SNS)['flow'])); + Assert::equals(['uri' => 'http://localhost'.$path, 'seed' => []], current($session->value(self::SNS)['flows'])); } #[Test, Values(from: 'fragments')] @@ -63,7 +63,7 @@ public function fetches_request_token_then_redirects_to_auth_with_fragment_in_sp sprintf('%s/authenticate?oauth_token=T&oauth_callback=%s', self::AUTH, urlencode(self::CALLBACK)), $this->redirectTo($this->authenticate($fixture, '/#'.$fragment, $session)) ); - Assert::equals('http://localhost/#'.$fragment, current($session->value(self::SNS)['flow'])); + Assert::equals(['uri' => 'http://localhost/#'.$fragment, 'seed' => []], current($session->value(self::SNS)['flows'])); } #[Test] @@ -73,20 +73,26 @@ public function exchanges_request_token_for_access_token() { 'request' => function($path, $token= null, $params= []) use($access) { return $access; } ]); $session= (new ForTesting())->create(); - $session->register(self::SNS, ['oauth_token' => 'REQUEST-TOKEN', 'target' => self::SERVICE]); + $session->register(self::SNS, ['flows' => ['REQUEST-TOKEN' => ['uri' => self::SERVICE, 'seed' => []]]]); $res= $this->authenticate($fixture, '/?oauth_token=REQUEST-TOKEN&oauth_verifier=ABC', $session); Assert::equals(self::SERVICE, $res->headers()['Location']); Assert::equals($access, $session->value(self::SNS)['token']); } - #[Test, Expect(IllegalStateException::class)] - public function raises_exception_on_state_mismatch() { - $fixture= new OAuth1Flow(self::AUTH, [self::ID, self::SECRET], self::CALLBACK); + #[Test] + public function redirects_when_opened_with_server_state_and_previous_flow() { + $request= ['oauth_token' => 'T']; + $fixture= newinstance(OAuth1Flow::class, [self::AUTH, [self::ID, self::SECRET], self::CALLBACK], [ + 'request' => function($path, $token= null, $params= []) use($request) { return $request; } + ]); $session= (new ForTesting())->create(); - $session->register(self::SNS, ['oauth_token' => 'REQUEST-TOKEN', 'target' => self::SERVICE]); + $session->register(self::SNS, ['flows' => ['PREVIOUS-TOKEN' => ['uri' => self::SERVICE, 'seed' => []]]]); - $this->authenticate($fixture, '/?oauth_token=MISMATCHED-TOKEN&oauth_verifier=ABC', $session); + Assert::equals( + sprintf('%s/authenticate?oauth_token=T&oauth_callback=%s', self::AUTH, urlencode(self::CALLBACK)), + $this->redirectTo($this->authenticate($fixture, '/?oauth_token=REQUEST-TOKEN&oauth_verifier=ABC', $session)) + ); } #[Test] @@ -137,11 +143,11 @@ public function appends_fragment($fragment) { $req= new Request(new TestInput('GET', '/?oauth_token=SHARED_STATE&'.OAuth1Flow::FRAGMENT.'='.urlencode($fragment))); $res= new Response(new TestOutput()); $session= (new ForTesting())->create(); - $session->register(self::SNS, ['flow' => ['SHARED_STATE' => 'http://localhost/']]); + $session->register(self::SNS, ['flows' => ['SHARED_STATE' => ['uri' => 'http://localhost/', 'seed' => []]]]); $fixture->authenticate($req, $res, $session); - Assert::equals('http://localhost/#'.$fragment, current($session->value(self::SNS)['flow'])); + Assert::equals(['uri' => 'http://localhost/#'.$fragment, 'seed' => []], current($session->value(self::SNS)['flows'])); } #[Test, Values(from: 'fragments')] @@ -151,11 +157,11 @@ public function replaces_fragment($fragment) { $req= new Request(new TestInput('GET', '/?oauth_token=SHARED_STATE&'.OAuth1Flow::FRAGMENT.'='.urlencode($fragment))); $res= new Response(new TestOutput()); $session= (new ForTesting())->create(); - $session->register(self::SNS, ['flow' => ['SHARED_STATE' => 'http://localhost/#original']]); + $session->register(self::SNS, ['flows' => ['SHARED_STATE' => ['uri' => 'http://localhost/#original', 'seed' => []]]]); $fixture->authenticate($req, $res, $session); - Assert::equals('http://localhost/#'.$fragment, current($session->value(self::SNS)['flow'])); + Assert::equals(['uri' => 'http://localhost/#'.$fragment, 'seed' => []], current($session->value(self::SNS)['flows'])); } /** @deprecated */ @@ -205,7 +211,7 @@ public function session_namespace($namespace) { $session= (new ForTesting())->create(); $this->authenticate($fixture->namespaced($namespace), '/target', $session); - Assert::equals('http://localhost/target', current($session->value($namespace)['flow'])); + Assert::equals(['uri' => 'http://localhost/target', 'seed' => []], current($session->value($namespace)['flows'])); } #[Test] @@ -223,8 +229,11 @@ public function parallel_requests_stored() { $this->authenticate($fixture, '/favicon.ico', $session); Assert::equals( - ['http://localhost/new', 'http://localhost/favicon.ico'], - array_values($session->value(self::SNS)['flow']) + [ + ['uri' => 'http://localhost/new', 'seed' => []], + ['uri' => 'http://localhost/favicon.ico', 'seed' => []], + ], + array_values($session->value(self::SNS)['flows']) ); } } \ No newline at end of file diff --git a/src/test/php/web/auth/unittest/OAuth2FlowTest.class.php b/src/test/php/web/auth/unittest/OAuth2FlowTest.class.php index 6701f79..8c263fd 100755 --- a/src/test/php/web/auth/unittest/OAuth2FlowTest.class.php +++ b/src/test/php/web/auth/unittest/OAuth2FlowTest.class.php @@ -37,7 +37,7 @@ private function assertLoginWith($service, $scope, $res, $session) { self::CONSUMER[0], implode('+', $scope), urlencode($service), - array_key_last($session->value(self::SNS)['flow']) + array_key_last($session->value(self::SNS)['flows']) ); Assert::equals($url, $this->redirectTo($res)); } @@ -91,7 +91,7 @@ public function redirects_to_auth($path) { $this->authenticate($fixture, $path, $session), $session ); - Assert::equals(['uri' => 'http://localhost'.$path, 'seed' => []], current($session->value(self::SNS)['flow'])); + Assert::equals(['uri' => 'http://localhost'.$path, 'seed' => []], current($session->value(self::SNS)['flows'])); } #[Test, Values(from: 'paths')] @@ -105,7 +105,7 @@ public function redirects_to_auth_with_relative_callback($path) { $this->authenticate($fixture, $path, $session), $session ); - Assert::equals(['uri' => 'http://localhost'.$path, 'seed' => []], current($session->value(self::SNS)['flow'])); + Assert::equals(['uri' => 'http://localhost'.$path, 'seed' => []], current($session->value(self::SNS)['flows'])); } #[Test, Values(from: 'paths')] @@ -119,7 +119,7 @@ public function redirects_to_auth_using_request($path) { $this->authenticate($fixture->target(new UseRequest()), $path, $session), $session ); - Assert::equals(['uri' => 'http://localhost'.$path, 'seed' => []], current($session->value(self::SNS)['flow'])); + Assert::equals(['uri' => 'http://localhost'.$path, 'seed' => []], current($session->value(self::SNS)['flows'])); } #[Test, Values(from: 'paths')] @@ -133,7 +133,7 @@ public function redirects_to_auth_using_url($path) { $this->authenticate($fixture->target(new UseURL(self::SERVICE)), $path, $session), $session ); - Assert::equals(['uri' => self::SERVICE.$path, 'seed' => []], current($session->value(self::SNS)['flow'])); + Assert::equals(['uri' => self::SERVICE.$path, 'seed' => []], current($session->value(self::SNS)['flows'])); } #[Test, Values(from: 'fragments')] @@ -147,7 +147,7 @@ public function redirects_to_sso_with_fragment($fragment) { $this->authenticate($fixture, '/#'.$fragment, $session), $session ); - Assert::equals(['uri' => 'http://localhost/#'.$fragment, 'seed' => []], current($session->value(self::SNS)['flow'])); + Assert::equals(['uri' => 'http://localhost/#'.$fragment, 'seed' => []], current($session->value(self::SNS)['flows'])); } #[Test, Values([[['user']], [['user', 'openid']]])] @@ -167,7 +167,7 @@ public function redirects_to_auth_and_passes_scope($scopes) { public function redirects_to_auth_when_previous_redirect_incomplete() { $fixture= new OAuth2Flow(self::AUTH, self::TOKENS, self::CONSUMER, self::CALLBACK); $session= (new ForTesting())->create(); - $session->register(self::SNS, ['flow' => ['PREVIOUS_STATE' => ['uri' => self::SERVICE, 'seed' => []]]]); + $session->register(self::SNS, ['flows' => ['PREVIOUS_STATE' => ['uri' => self::SERVICE, 'seed' => []]]]); $this->assertLoginWith( self::CALLBACK, @@ -181,10 +181,10 @@ public function redirects_to_auth_when_previous_redirect_incomplete() { public function does_not_reuse_state_when_previous_redirect_incomplete() { $fixture= new OAuth2Flow(self::AUTH, self::TOKENS, self::CONSUMER, self::CALLBACK); $session= (new ForTesting())->create(); - $session->register(self::SNS, ['flow' => ['PREVIOUS_STATE' => ['uri' => self::SERVICE, 'seed' => []]]]); + $session->register(self::SNS, ['flows' => ['PREVIOUS_STATE' => ['uri' => self::SERVICE, 'seed' => []]]]); $this->authenticate($fixture, '/new', $session); - Assert::notEquals('PREVIOUS_STATE', array_key_last($session->value(self::SNS)['flow'])); + Assert::notEquals('PREVIOUS_STATE', array_key_last($session->value(self::SNS)['flows'])); } #[Test] @@ -196,7 +196,7 @@ public function passes_client_id_and_secret() { ]); $fixture= new OAuth2Flow(self::AUTH, $tokens, $credentials, self::CALLBACK); $session= (new ForTesting())->create(); - $session->register(self::SNS, ['flow' => [$state => ['uri' => self::SERVICE, 'seed' => []]]]); + $session->register(self::SNS, ['flows' => [$state => ['uri' => self::SERVICE, 'seed' => []]]]); $this->authenticate($fixture, '/?code=SERVER_CODE&state='.$state, $session); Assert::equals('authorization_code', $passed['grant_type']); @@ -214,7 +214,7 @@ public function passes_client_id_assertion_and_rs256_jwt() { ]); $fixture= new OAuth2Flow(self::AUTH, $tokens, $credentials, self::CALLBACK); $session= (new ForTesting())->create(); - $session->register(self::SNS, ['flow' => [$state => ['uri' => self::SERVICE, 'seed' => []]]]); + $session->register(self::SNS, ['flows' => [$state => ['uri' => self::SERVICE, 'seed' => []]]]); $this->authenticate($fixture, '/?code=SERVER_CODE&state='.$state, $session); Assert::equals('authorization_code', $passed['grant_type']); @@ -233,7 +233,7 @@ public function gets_access_token_and_redirects_to_self() { ]); $fixture= new OAuth2Flow(self::AUTH, $tokens, self::CONSUMER, self::CALLBACK); $session= (new ForTesting())->create(); - $session->register(self::SNS, ['flow' => [$state => ['uri' => self::SERVICE, 'seed' => []]]]); + $session->register(self::SNS, ['flows' => [$state => ['uri' => self::SERVICE, 'seed' => []]]]); $res= $this->authenticate($fixture, '/?code=SERVER_CODE&state='.$state, $session); Assert::equals(self::SERVICE, $res->headers()['Location']); @@ -242,7 +242,7 @@ public function gets_access_token_and_redirects_to_self() { /** @deprecated */ #[Test] - public function gets_access_token_using_previous_session_layout() { + public function gets_access_token_using_target_session_layout() { $token= ['access_token' => '', 'token_type' => 'Bearer']; $state= 'SHAREDSTATE'; $tokens= newinstance(OAuth2Endpoint::class, [self::TOKENS], [ @@ -284,20 +284,25 @@ public function gets_access_token_and_redirects_to_self_with_fragment($fragment) ]); $fixture= new OAuth2Flow(self::AUTH, $tokens, self::CONSUMER, self::CALLBACK); $session= (new ForTesting())->create(); - $session->register(self::SNS, ['flow' => [$state => ['uri' => self::SERVICE, 'seed' => []]]]); + $session->register(self::SNS, ['flows' => [$state => ['uri' => self::SERVICE, 'seed' => []]]]); $res= $this->authenticate($fixture, '/?code=SERVER_CODE&state='.$state.OAuth2Flow::FRAGMENT.urlencode($fragment), $session); Assert::equals(self::SERVICE.'#'.$fragment, $res->headers()['Location']); Assert::equals($token, $session->value(self::SNS)['token']); } - #[Test, Expect(IllegalStateException::class)] - public function raises_exception_on_state_mismatch() { + #[Test] + public function redirects_when_opened_with_server_state_and_previous_flow() { $fixture= new OAuth2Flow(self::AUTH, self::TOKENS, self::CONSUMER, self::CALLBACK); $session= (new ForTesting())->create(); - $session->register(self::SNS, ['flow' => ['CLIENTSTATE' => ['uri' => self::SERVICE, 'seed' => []]]]); + $session->register(self::SNS, ['flows' => ['PREVIOUS_STATE' => ['uri' => self::SERVICE, 'seed' => []]]]); - $this->authenticate($fixture, '/?state=SERVERSTATE&code=SERVER_CODE', $session); + $this->assertLoginWith( + self::CALLBACK, + $fixture->scopes(), + $this->authenticate($fixture, '/?state=SERVERSTATE&code=SERVER_CODE', $session), + $session + ); } #[Test] @@ -326,7 +331,7 @@ public function returns_client_in_final_step($token) { } #[Test] - public function resets_state_after_returning_client() { + public function removes_token_after_returning_it() { $token= ['access_token' => '', 'token_type' => 'Bearer']; $fixture= new OAuth2Flow(self::AUTH, self::TOKENS, self::CONSUMER, self::CALLBACK); @@ -436,7 +441,7 @@ public function parallel_requests_stored() { ['uri' => 'http://localhost/new', 'seed' => []], ['uri' => 'http://localhost/favicon.ico', 'seed' => []], ], - array_values($session->value(self::SNS)['flow']) + array_values($session->value(self::SNS)['flows']) ); } } \ No newline at end of file