diff --git a/.docs/README.md b/.docs/README.md index b5e8d2c..3bbb6ac 100644 --- a/.docs/README.md +++ b/.docs/README.md @@ -56,7 +56,7 @@ List of all providers is [here](https://github.com/thephpleague/oauth2-client/bl This example uses Google as provider with integration through [league/oauth2-google](https://github.com/thephpleague/oauth2-google) -Install package +### Install package ```bash composer require league/oauth2-google @@ -64,7 +64,7 @@ composer require league/oauth2-google Get your oauth2 credentials (`clientId` and `clientSecret`) from [Google website](https://developers.google.com/identity/protocols/OpenIDConnect#registeringyourapp) -Register flow +### Register flow ```neon google: @@ -77,7 +77,9 @@ extensions: google: Contributte\OAuth2Client\DI\GoogleAuthExtension ``` -Create a control which can handle authentication and authorization +### A) Create custom control + +Create custom control which can handle authentication and authorization. ```php use Contributte\OAuth2Client\Flow\Google\GoogleAuthCodeFlow; @@ -158,4 +160,64 @@ Create link to authentication action Sign in with Google ``` +### B) Use `GenericAuthControl` + +Add `GenericAuthControl` control to sign presenter + +```php +use Nette\Application\UI\Presenter; +use Contributte\OAuth2Client\Flow\Google\GoogleAuthCodeFlow; +use League\OAuth2\Client\Provider\GoogleUser; +use League\OAuth2\Client\Token\AccessToken; + +class SignPresenter extends Presenter +{ + + public function actionGoogleAuthenticate(): void + { + $this['googleButton']->authenticate(); + } + + public function actionGoogleAuthorize(): void + { + $this['googleButton']->authorize(); + } + + protected function createComponentGoogleButton(): GoogleButton + { + $authControl = new GenericAuthControl( + $this->googleAuthFlow, + $this->presenter->link('//:Sign:googleAuthorize') + ); + $authControl->setTemplate(__DIR__ . "/googleAuthLatte.latte"); + $authControl->onAuthenticate[] = function(AccessToken $accessToken, GoogleUser $user) { + // TODO - try sign in user with it's email ($owner->getEmail()) + } + $authControl->onFail[] = function() { + // TODO - Identity provider failure, cannot get information about user + } + return $authControl; + } + +} +``` + +Create custom template for authentication control. + +```latte +Sign in with Google +``` + +Use control in presenter template. + +```latte +{control googleButton} +``` + +Or create link to authentication action in presenter template + +```latte +Sign in with Google +``` + That's all! diff --git a/composer.json b/composer.json index 5e9a62a..7264077 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ }, "require-dev": { "nette/di": "^3.0.0", + "nette/application": "^3.1.0", "league/oauth2-facebook": "^2.0.5", "league/oauth2-google": "^3.0.3", "mockery/mockery": "^1.3.3", diff --git a/src/UI/Components/GenericAuthControl.latte b/src/UI/Components/GenericAuthControl.latte new file mode 100644 index 0000000..a78713b --- /dev/null +++ b/src/UI/Components/GenericAuthControl.latte @@ -0,0 +1 @@ +Login diff --git a/src/UI/Components/GenericAuthControl.php b/src/UI/Components/GenericAuthControl.php new file mode 100644 index 0000000..00417d1 --- /dev/null +++ b/src/UI/Components/GenericAuthControl.php @@ -0,0 +1,92 @@ + */ + public $onAuthenticated = []; + + /** @var array */ + public $onFailed = []; + + public function __construct(AuthCodeFlow $authCodeFlow, ?string $redirectUri = null) + { + $this->authCodeFlow = $authCodeFlow; + $this->redirectUri = $redirectUri; + } + + public function setTemplate(string $templatePath): void + { + $this->templatePath = $templatePath; + } + + public function handleAuthenticate(): void + { + $this->authenticate(); + } + + public function authenticate(): void + { + $this->getPresenter()->redirectUrl( + $this->authCodeFlow->getAuthorizationUrl(['redirect_uri' => $this->redirectUri]) + ); + } + + public function authorize(): ?ResourceOwnerInterface + { + try { + $accessToken = $this->authCodeFlow->getAccessToken($this->getPresenter()->getHttpRequest()->getQuery()); + if (!$accessToken instanceof AccessToken) { + throw new UnexpectedValueException(); + } + + $user = $this->authCodeFlow->getProvider()->getResourceOwner($accessToken); + $this->authenticationSucceed($accessToken, $user); + return $user; + } catch (IdentityProviderException $e) { + $this->authenticationFailed(); + } + + return null; + } + + protected function authenticationFailed(): void + { + $this->onFailed(); + } + + protected function authenticationSucceed(AccessToken $accessToken, ResourceOwnerInterface $user): void + { + $this->onAuthenticated($accessToken, $user); + } + + public function render(): void + { + $template = $this->getTemplate(); + if (!$template instanceof Template) { + throw new UnexpectedValueException(); + } + + $template->render($this->templatePath ?? __DIR__ . '/GenericAuthControl.latte'); + } + +} diff --git a/tests/cases/UI/Components/GenericAuthControl.phpt b/tests/cases/UI/Components/GenericAuthControl.phpt new file mode 100644 index 0000000..98bb5c8 --- /dev/null +++ b/tests/cases/UI/Components/GenericAuthControl.phpt @@ -0,0 +1,105 @@ +shouldReceive('getAUthorizationUrl') + ->with(['redirect_uri' => 'https://localhost/redirect']) + ->andReturn('https://localhost/auth'); + + $authControl = new GenericAuthControl($flow, 'https://localhost/redirect'); + $presenter = new TestPresenter($authControl); + + Assert::exception(function () use ($authControl) { + $authControl->authenticate(); + }, AbortException::class); + + /** @var RedirectResponse $response */ + $response = $presenter->response; + Assert::type(RedirectResponse::class, $response); + Assert::equal('https://localhost/auth', $response->getUrl()); +}); + +Toolkit::test(function (): void { + $token = Mockery::mock(AccessToken::class); + + $provider = Mockery::mock(AbstractProvider::class); + $provider->shouldReceive('getResourceOwner') + ->andReturn(new GenericResourceOwner([], 1)); + + $flow = Mockery::mock(AuthCodeFlow::class); + $flow->shouldReceive('getAccessToken') + ->with(['code' => '123']) + ->andReturn($token); + $flow->shouldReceive('getProvider') + ->andReturn($provider); + + $request = new Request(new UrlScript('https://localhost/redirect?code=123')); + + $events = []; + + $authControl = new GenericAuthControl($flow, 'https://localhost/redirect'); + $authControl->onAuthenticated[] = function ($accessToken, $user) use (&$events) { + $events[] = ['onAuthenticated', $accessToken, $user]; + }; + $authControl->onFailed[] = function () use (&$events) { + $events[] = ['onFailed']; + }; + new TestPresenter($authControl, $request); + + $user = $authControl->authorize(); + + Assert::type(GenericResourceOwner::class, $user); + + Assert::count(1, $events); + Assert::equal('onAuthenticated', $events[0][0]); + Assert::type(AccessToken::class, $events[0][1]); + Assert::type(GenericResourceOwner::class, $events[0][2]); +}); + +Toolkit::test(function (): void { + $flow = Mockery::mock(AuthCodeFlow::class); + $flow->shouldReceive('getAccessToken') + ->andThrow(new IdentityProviderException('error', 1, null)); + + $request = new Request(new UrlScript('https://localhost/redirect?code=123')); + + $events = []; + + $authControl = new GenericAuthControl($flow, 'https://localhost/redirect'); + $authControl->onAuthenticated[] = function ($accessToken, $user) use (&$events) { + $events[] = ['onAuthenticated', $accessToken, $user]; + }; + $authControl->onFailed[] = function () use (&$events) { + $events[] = ['onFailed']; + }; + new TestPresenter($authControl, $request); + + $user = $authControl->authorize(); + + Assert::null($user); + + Assert::count(1, $events); + Assert::equal('onFailed', $events[0][0]); +}); diff --git a/tests/fixtures/Presenter/TestPresenter.php b/tests/fixtures/Presenter/TestPresenter.php new file mode 100644 index 0000000..638798d --- /dev/null +++ b/tests/fixtures/Presenter/TestPresenter.php @@ -0,0 +1,52 @@ +injectPrimary( + null, + null, + null, + $request ?? new HttpRequest(new UrlScript('http://localhost/page')), + $response ?? new HttpResponse() + ); + $this->component = $component; + $this->getComponent('subject'); + } + + protected function createComponentSubject(): Component + { + return $this->component; + } + + public function sendResponse(Response $response): void + { + $this->response = $response; + parent::sendResponse($response); + } + +}