client(clientId: '', clientSecret: '')->isConfigured()); self::assertFalse($this->client(clientId: 'ephub', clientSecret: '')->isConfigured()); self::assertTrue($this->client()->isConfigured()); } public function testTheAuthorizationUrlCarriesTheClientAndScopes(): void { $request = $this->request(); $url = $this->client()->authorizationUrl($request); self::assertStringStartsWith('https://myep.test/authorize?', $url); parse_str((string) parse_url($url, \PHP_URL_QUERY), $query); self::assertSame('ephub', $query['client_id']); self::assertSame('code', $query['response_type']); // myep's server wants space-separated scopes; the library's default is a comma. self::assertSame('email profile roles', $query['scope']); self::assertStringEndsWith('/auth/check', $query['redirect_uri']); } public function testTheStateIsRememberedOnTheSession(): void { $request = $this->request(); $url = $this->client()->authorizationUrl($request); parse_str((string) parse_url($url, \PHP_URL_QUERY), $query); self::assertNotEmpty($query['state']); self::assertSame($query['state'], $request->getSession()->get('oauth2state')); } public function testACallbackWithoutACodeIsRefused(): void { $request = $this->request(['state' => 'abc']); $request->getSession()->set('oauth2state', 'abc'); $this->expectException(AuthorizationRequestException::class); $this->expectExceptionMessage('keinen Code'); $this->client()->fetchAccessToken($request); } /** * Without this check an attacker can hand a victim a callback URL carrying the attacker's * code and bind the victim's session to the attacker's identity. */ public function testACallbackWhoseStateDoesNotMatchIsRefused(): void { $request = $this->request(['code' => 'xyz', 'state' => 'forged']); $request->getSession()->set('oauth2state', 'expected'); $this->expectException(AuthorizationRequestException::class); $this->expectExceptionMessage('state-Parameter'); $this->client()->fetchAccessToken($request); } public function testACallbackWithNoStateOnTheSessionIsRefused(): void { $request = $this->request(['code' => 'xyz', 'state' => 'anything']); $this->expectException(AuthorizationRequestException::class); $this->client()->fetchAccessToken($request); } /** * The stored state is consumed whether or not the check passed, so the same callback URL * cannot be replayed. */ public function testTheStoredStateIsConsumedEvenOnFailure(): void { $request = $this->request(['code' => 'xyz', 'state' => 'forged']); $session = $request->getSession(); $session->set('oauth2state', 'expected'); try { $this->client()->fetchAccessToken($request); } catch (AuthorizationRequestException) { } self::assertNull($session->get('oauth2state')); } /** * @param array $query */ private function request(array $query = []): Request { $request = new Request($query); $request->setSession(new Session(new MockArraySessionStorage())); return $request; } private function client(string $clientId = 'ephub', string $clientSecret = 'secret'): EpClient { self::bootKernel(); return new EpClient(self::getContainer()->get(UrlGeneratorInterface::class), [ 'client_id' => $clientId, 'client_secret' => $clientSecret, 'url_authorize' => 'https://myep.test/authorize', 'url_access_token' => 'https://myep.test/token', 'url_userinfo' => 'https://myep.test/api/userinfo', 'scopes' => ['email', 'profile', 'roles'], ]); } }