Skip to content

Commit

Permalink
GenericAuthControl
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinMystikJonas authored and f3l1x committed Jul 26, 2022
1 parent ec5d456 commit 93a92d3
Show file tree
Hide file tree
Showing 6 changed files with 316 additions and 3 deletions.
68 changes: 65 additions & 3 deletions .docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,15 @@ 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
```

Get your oauth2 credentials (`clientId` and `clientSecret`) from [Google website](https://developers.google.com/identity/protocols/OpenIDConnect#registeringyourapp)

Register flow
### Register flow

```neon
google:
Expand All @@ -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;
Expand Down Expand Up @@ -158,4 +160,64 @@ Create link to authentication action
<a href="{plink :Front:Sign:googleAuthenticate}">Sign in with Google</a>
```

### 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
<a href="{link authenticate!}">Sign in with Google</a>
```

Use control in presenter template.

```latte
{control googleButton}
```

Or create link to authentication action in presenter template

```latte
<a href="{plink :Front:Sign:googleAuthenticate}">Sign in with Google</a>
```

That's all!
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/UI/Components/GenericAuthControl.latte
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<a href="{link authenticate!}" class=="oauth2-bumake csftton">Login</a>
92 changes: 92 additions & 0 deletions src/UI/Components/GenericAuthControl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php declare(strict_types = 1);

namespace Contributte\OAuth2Client\UI\Components;

use Contributte\OAuth2Client\Flow\AuthCodeFlow;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Provider\ResourceOwnerInterface;
use League\OAuth2\Client\Token\AccessToken;
use Nette\Application\UI\Control;
use Nette\Bridges\ApplicationLatte\Template;
use UnexpectedValueException;

class GenericAuthControl extends Control
{

/** @var AuthCodeFlow */
private $authCodeFlow;

/** @var string|null */
private $redirectUri = null;

/** @var string|null */
private $templatePath = null;

/** @var array<callable> */
public $onAuthenticated = [];

/** @var array<callable> */
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');
}

}
105 changes: 105 additions & 0 deletions tests/cases/UI/Components/GenericAuthControl.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php declare(strict_types = 1);

namespace Tests\Cases\Flow;

use Contributte\OAuth2Client\Flow\AuthCodeFlow;
use Contributte\OAuth2Client\UI\Components\GenericAuthControl;
use League\OAuth2\Client\Provider\AbstractProvider;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Provider\GenericResourceOwner;
use League\OAuth2\Client\Token\AccessToken;
use Mockery;
use Nette\Application\AbortException;
use Nette\Application\Responses\RedirectResponse;
use Nette\Http\Request;
use Nette\Http\UrlScript;
use Ninjify\Nunjuck\Toolkit;
use Tester\Assert;
use Tester\Environment;
use Tests\Fixtures\Presenter\TestPresenter;

require_once __DIR__ . '/../../../bootstrap.php';

Environment::bypassFinals();

Toolkit::test(function (): void {
$flow = Mockery::mock(AuthCodeFlow::class);
$flow->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]);
});
52 changes: 52 additions & 0 deletions tests/fixtures/Presenter/TestPresenter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php declare(strict_types = 1);

namespace Tests\Fixtures\Presenter;

use Nette\Application\Response;
use Nette\Application\UI\Component;
use Nette\Application\UI\Presenter;
use Nette\Http\Request as HttpRequest;
use Nette\Http\Response as HttpResponse;
use Nette\Http\UrlScript;

final class TestPresenter extends Presenter
{

/** @var Component */
private $component;

/** @var HttpRequest */
public $httpRequest;

/** @var HttpResponse */
public $httpResponse;

/** @var Response */
public $response;

public function __construct(Component $component, ?HttpRequest $request = null, ?HttpResponse $response = null)
{
parent::__construct();
$this->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);
}

}

0 comments on commit 93a92d3

Please sign in to comment.