├── .github
├── .kodiak.toml
└── workflows
│ └── main.yaml
├── LICENSE
├── composer.json
└── src
├── DI
├── FacebookAuthExtension.php
├── GitlabAuthExtension.php
└── GoogleAuthExtension.php
├── Exception
├── Logical
│ └── InvalidArgumentException.php
├── LogicalException.php
├── Runtime
│ ├── CannotAuthenticateUserException.php
│ ├── PossibleCsrfAttackException.php
│ └── UserProbablyDeniedAccessException.php
└── RuntimeException.php
├── Flow
├── AuthCodeFlow.php
├── Facebook
│ ├── FacebookAuthCodeFlow.php
│ └── FacebookProvider.php
├── Gitlab
│ ├── GitlabAuthCodeFlow.php
│ └── GitlabProvider.php
└── Google
│ ├── GoogleAuthCodeFlow.php
│ └── GoogleProvider.php
└── UI
└── Components
├── GenericAuthControl.latte
└── GenericAuthControl.php
/.github/.kodiak.toml:
--------------------------------------------------------------------------------
1 | version = 1
2 |
3 | [merge]
4 | automerge_label = "automerge"
5 | blacklist_title_regex = "^WIP.*"
6 | blacklist_labels = ["WIP"]
7 | method = "rebase"
8 | delete_branch_on_merge = true
9 | notify_on_conflict = true
10 | optimistic_updates = false
11 |
--------------------------------------------------------------------------------
/.github/workflows/main.yaml:
--------------------------------------------------------------------------------
1 | name: "build"
2 |
3 | on:
4 | pull_request:
5 | paths-ignore:
6 | - ".docs/**"
7 | push:
8 | branches:
9 | - "*"
10 | schedule:
11 | - cron: "0 8 * * 1" # At 08:00 on Monday
12 |
13 | env:
14 | extensions: "json"
15 | cache-version: "1"
16 | composer-version: "v2"
17 | composer-install: "composer update --no-interaction --no-progress --prefer-dist --prefer-stable"
18 | coverage: "none"
19 |
20 | jobs:
21 | qa:
22 | name: "Quality assurance"
23 | runs-on: "${{ matrix.operating-system }}"
24 |
25 | strategy:
26 | matrix:
27 | php-version: [ "8.1" ]
28 | operating-system: [ "ubuntu-latest" ]
29 | fail-fast: false
30 |
31 | steps:
32 | - name: "Checkout"
33 | uses: "actions/checkout@v2"
34 |
35 | - name: "Setup PHP cache environment"
36 | id: "extcache"
37 | uses: "shivammathur/cache-extensions@v1"
38 | with:
39 | php-version: "${{ matrix.php-version }}"
40 | extensions: "${{ env.extensions }}"
41 | key: "${{ env.cache-version }}"
42 |
43 | - name: "Cache PHP extensions"
44 | uses: "actions/cache@v2"
45 | with:
46 | path: "${{ steps.extcache.outputs.dir }}"
47 | key: "${{ steps.extcache.outputs.key }}"
48 | restore-keys: "${{ steps.extcache.outputs.key }}"
49 |
50 | - name: "Install PHP"
51 | uses: "shivammathur/setup-php@v2"
52 | with:
53 | php-version: "${{ matrix.php-version }}"
54 | extensions: "${{ env.extensions }}"
55 | tools: "composer:${{ env.composer-version }}, cs2pr"
56 | coverage: "${{ env.coverage }}"
57 | env:
58 | COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
59 |
60 | - name: "Setup problem matchers for PHP"
61 | run: 'echo "::add-matcher::${{ runner.tool_cache }}/php.json"'
62 |
63 | - name: "Get Composer cache directory"
64 | id: "composercache"
65 | run: 'echo "::set-output name=dir::$(composer config cache-files-dir)"'
66 |
67 | - name: "Cache PHP dependencies"
68 | uses: "actions/cache@v2"
69 | with:
70 | path: "${{ steps.composercache.outputs.dir }}"
71 | key: "${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}"
72 | restore-keys: "${{ runner.os }}-composer-"
73 |
74 | - name: "Validate Composer"
75 | run: "composer validate"
76 |
77 | - name: "Install dependencies"
78 | run: "${{ env.composer-install }}"
79 |
80 | - name: "Coding Standard"
81 | run: "make cs"
82 |
83 | static-analysis:
84 | name: "Static analysis"
85 | runs-on: "${{ matrix.operating-system }}"
86 |
87 | strategy:
88 | matrix:
89 | php-version: [ "7.4", "8.1" ]
90 | operating-system: [ "ubuntu-latest" ]
91 | fail-fast: false
92 |
93 | steps:
94 | - name: "Checkout"
95 | uses: "actions/checkout@v2"
96 |
97 | - name: "Setup PHP cache environment"
98 | id: "extcache"
99 | uses: "shivammathur/cache-extensions@v1"
100 | with:
101 | php-version: "${{ matrix.php-version }}"
102 | extensions: "${{ env.extensions }}"
103 | key: "${{ env.cache-version }}"
104 |
105 | - name: "Cache PHP extensions"
106 | uses: "actions/cache@v2"
107 | with:
108 | path: "${{ steps.extcache.outputs.dir }}"
109 | key: "${{ steps.extcache.outputs.key }}"
110 | restore-keys: "${{ steps.extcache.outputs.key }}"
111 |
112 | - name: "Install PHP"
113 | uses: "shivammathur/setup-php@v2"
114 | with:
115 | php-version: "${{ matrix.php-version }}"
116 | extensions: "${{ env.extensions }}"
117 | tools: "composer:${{ env.composer-version }}"
118 | coverage: "${{ env.coverage }}"
119 | env:
120 | COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
121 |
122 | - name: "Setup problem matchers for PHP"
123 | run: 'echo "::add-matcher::${{ runner.tool_cache }}/php.json"'
124 |
125 | - name: "Get Composer cache directory"
126 | id: "composercache"
127 | run: 'echo "::set-output name=dir::$(composer config cache-files-dir)"'
128 |
129 | - name: "Cache PHP dependencies"
130 | uses: "actions/cache@v2"
131 | with:
132 | path: "${{ steps.composercache.outputs.dir }}"
133 | key: "${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}"
134 | restore-keys: "${{ runner.os }}-composer-"
135 |
136 | - name: "Install dependencies"
137 | run: "${{ env.composer-install }}"
138 |
139 | - name: "PHPStan"
140 | run: "make phpstan"
141 |
142 | tests:
143 | name: "Tests"
144 | runs-on: "${{ matrix.operating-system }}"
145 |
146 | strategy:
147 | matrix:
148 | php-version: [ "7.2", "7.3", "7.4", "8.0", "8.1" ]
149 | operating-system: [ "ubuntu-latest" ]
150 | composer-args: [ "" ]
151 | include:
152 | - php-version: "7.2"
153 | operating-system: "ubuntu-latest"
154 | composer-args: "--prefer-lowest"
155 | fail-fast: false
156 |
157 | steps:
158 | - name: "Checkout"
159 | uses: "actions/checkout@v2"
160 |
161 | - name: "Setup PHP cache environment"
162 | id: "extcache"
163 | uses: "shivammathur/cache-extensions@v1"
164 | with:
165 | php-version: "${{ matrix.php-version }}"
166 | extensions: "${{ env.extensions }}"
167 | key: "${{ env.cache-version }}"
168 |
169 | - name: "Cache PHP extensions"
170 | uses: "actions/cache@v2"
171 | with:
172 | path: "${{ steps.extcache.outputs.dir }}"
173 | key: "${{ steps.extcache.outputs.key }}"
174 | restore-keys: "${{ steps.extcache.outputs.key }}"
175 |
176 | - name: "Install PHP"
177 | uses: "shivammathur/setup-php@v2"
178 | with:
179 | php-version: "${{ matrix.php-version }}"
180 | extensions: "${{ env.extensions }}"
181 | tools: "composer:${{ env.composer-version }}"
182 | coverage: "${{ env.coverage }}"
183 | env:
184 | COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
185 |
186 | - name: "Setup problem matchers for PHP"
187 | run: 'echo "::add-matcher::${{ runner.tool_cache }}/php.json"'
188 |
189 | - name: "Get Composer cache directory"
190 | id: "composercache"
191 | run: 'echo "::set-output name=dir::$(composer config cache-files-dir)"'
192 |
193 | - name: "Cache PHP dependencies"
194 | uses: "actions/cache@v2"
195 | with:
196 | path: "${{ steps.composercache.outputs.dir }}"
197 | key: "${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}"
198 | restore-keys: "${{ runner.os }}-composer-"
199 |
200 | - name: "Install dependencies"
201 | run: "${{ env.composer-install }} ${{ matrix.composer-args }}"
202 |
203 | - name: "Tests"
204 | run: "make tests"
205 |
206 | - name: "Upload test output"
207 | if: ${{ failure() }}
208 | uses: actions/upload-artifact@v2
209 | with:
210 | name: output
211 | path: tests/**/output
212 |
213 | tests-code-coverage:
214 | name: "Tests with code coverage"
215 | runs-on: "${{ matrix.operating-system }}"
216 |
217 | strategy:
218 | matrix:
219 | php-version: [ "7.4" ]
220 | operating-system: [ "ubuntu-latest" ]
221 | fail-fast: false
222 |
223 | if: "github.event_name == 'push'"
224 |
225 | steps:
226 | - name: "Checkout"
227 | uses: "actions/checkout@v2"
228 |
229 | - name: "Setup PHP cache environment"
230 | id: "extcache"
231 | uses: "shivammathur/cache-extensions@v1"
232 | with:
233 | php-version: "${{ matrix.php-version }}"
234 | extensions: "${{ env.extensions }}"
235 | key: "${{ env.cache-version }}"
236 |
237 | - name: "Cache PHP extensions"
238 | uses: "actions/cache@v2"
239 | with:
240 | path: "${{ steps.extcache.outputs.dir }}"
241 | key: "${{ steps.extcache.outputs.key }}"
242 | restore-keys: "${{ steps.extcache.outputs.key }}"
243 |
244 | - name: "Install PHP"
245 | uses: "shivammathur/setup-php@v2"
246 | with:
247 | php-version: "${{ matrix.php-version }}"
248 | extensions: "${{ env.extensions }}"
249 | tools: "composer:${{ env.composer-version }}"
250 | coverage: "${{ env.coverage }}"
251 | env:
252 | COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
253 |
254 | - name: "Setup problem matchers for PHP"
255 | run: 'echo "::add-matcher::${{ runner.tool_cache }}/php.json"'
256 |
257 | - name: "Get Composer cache directory"
258 | id: "composercache"
259 | run: 'echo "::set-output name=dir::$(composer config cache-files-dir)"'
260 |
261 | - name: "Cache PHP dependencies"
262 | uses: "actions/cache@v2"
263 | with:
264 | path: "${{ steps.composercache.outputs.dir }}"
265 | key: "${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}"
266 | restore-keys: "${{ runner.os }}-composer-"
267 |
268 | - name: "Install dependencies"
269 | run: "${{ env.composer-install }}"
270 |
271 | - name: "Tests"
272 | run: "make coverage-clover"
273 |
274 | - name: "Upload test output"
275 | if: ${{ failure() }}
276 | uses: actions/upload-artifact@v2
277 | with:
278 | name: output
279 | path: tests/**/output
280 |
281 | - name: "Coveralls.io"
282 | env:
283 | CI_NAME: github
284 | CI: true
285 | COVERALLS_REPO_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
286 | run: |
287 | wget https://github.com/php-coveralls/php-coveralls/releases/download/v2.1.0/php-coveralls.phar
288 | php php-coveralls.phar --verbose --config tests/.coveralls.yml
289 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Contributte
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "contributte/oauth2-client",
3 | "description": "Integration of league/oauth2-client into Nette framework",
4 | "keywords": [
5 | "nette",
6 | "oauth2",
7 | "client"
8 | ],
9 | "type": "library",
10 | "license": "MIT",
11 | "homepage": "https://github.com/contributte/oauth2-client",
12 | "authors": [
13 | {
14 | "name": "Milan Felix Šulc",
15 | "homepage": "https://f3l1x.io"
16 | }
17 | ],
18 | "require": {
19 | "php": ">=7.2",
20 | "league/oauth2-client": "^2.6.0",
21 | "nette/http": "^3.0.5"
22 | },
23 | "require-dev": {
24 | "nette/di": "^3.0.0",
25 | "nette/application": "^3.1.0",
26 | "league/oauth2-facebook": "^2.0.5",
27 | "league/oauth2-google": "^3.0.3",
28 | "mockery/mockery": "^1.3.3",
29 | "ninjify/qa": "^0.12",
30 | "ninjify/nunjuck": "^0.4",
31 | "phpstan/phpstan": "^1.0",
32 | "phpstan/phpstan-deprecation-rules": "^1.0",
33 | "phpstan/phpstan-nette": "^1.0",
34 | "phpstan/phpstan-strict-rules": "^1.0"
35 | },
36 | "autoload": {
37 | "psr-4": {
38 | "Contributte\\OAuth2Client\\": "src"
39 | }
40 | },
41 | "autoload-dev": {
42 | "psr-4": {
43 | "Tests\\Cases\\": "tests/cases",
44 | "Tests\\Fixtures\\": "tests/fixtures"
45 | }
46 | },
47 | "minimum-stability": "dev",
48 | "prefer-stable": true,
49 | "config": {
50 | "sort-packages": true,
51 | "allow-plugins": {
52 | "dealerdirect/phpcodesniffer-composer-installer": true
53 | }
54 | },
55 | "extra": {
56 | "branch-alias": {
57 | "dev-develop": "0.3.x-dev"
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/DI/FacebookAuthExtension.php:
--------------------------------------------------------------------------------
1 | Expect::string()->required(),
22 | 'clientSecret' => Expect::string()->required(),
23 | 'graphApiVersion' => Expect::string()->required(),
24 | 'options' => Expect::array(),
25 | ]);
26 | }
27 |
28 | public function loadConfiguration(): void
29 | {
30 | $builder = $this->getContainerBuilder();
31 | $config = $this->config;
32 |
33 | $providerOptions = [
34 | 'clientId' => $config->clientId,
35 | 'clientSecret' => $config->clientSecret,
36 | 'graphApiVersion' => $config->graphApiVersion,
37 | ];
38 |
39 | if (isset($config->options)) {
40 | $providerOptions = array_merge($config->options, $providerOptions);
41 | }
42 |
43 | $builder->addDefinition($this->prefix('provider'))
44 | ->setFactory(FacebookProvider::class, [$providerOptions]);
45 |
46 | $builder->addDefinition($this->prefix('authCodeFlow'))
47 | ->setFactory(FacebookAuthCodeFlow::class, ['@' . $this->prefix('provider')]);
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/src/DI/GitlabAuthExtension.php:
--------------------------------------------------------------------------------
1 | Expect::string()->required(),
22 | 'clientSecret' => Expect::string()->required(),
23 | 'domain' => Expect::string()->required(),
24 | 'options' => Expect::array(),
25 | ]);
26 | }
27 |
28 | public function loadConfiguration(): void
29 | {
30 | $builder = $this->getContainerBuilder();
31 | $config = $this->config;
32 |
33 | $providerOptions = [
34 | 'clientId' => $config->clientId,
35 | 'clientSecret' => $config->clientSecret,
36 | 'domain' => $config->domain,
37 | ];
38 |
39 | if (isset($config->options)) {
40 | $providerOptions = array_merge($config->options, $providerOptions);
41 | }
42 |
43 | $builder->addDefinition($this->prefix('provider'))
44 | ->setFactory(GitlabProvider::class, [$providerOptions]);
45 |
46 | $builder->addDefinition($this->prefix('authCodeFlow'))
47 | ->setFactory(GitlabAuthCodeFlow::class, ['@' . $this->prefix('provider')]);
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/src/DI/GoogleAuthExtension.php:
--------------------------------------------------------------------------------
1 | Expect::string()->required(),
22 | 'clientSecret' => Expect::string()->required(),
23 | 'options' => Expect::array(),
24 | ]);
25 | }
26 |
27 | public function loadConfiguration(): void
28 | {
29 | $builder = $this->getContainerBuilder();
30 | $config = $this->config;
31 |
32 | $providerOptions = [
33 | 'clientId' => $config->clientId,
34 | 'clientSecret' => $config->clientSecret,
35 | ];
36 |
37 | if (isset($config->options)) {
38 | $providerOptions = array_merge($config->options, $providerOptions);
39 | }
40 |
41 | $builder->addDefinition($this->prefix('provider'))
42 | ->setFactory(GoogleProvider::class, [$providerOptions]);
43 |
44 | $builder->addDefinition($this->prefix('authCodeFlow'))
45 | ->setFactory(GoogleAuthCodeFlow::class, ['@' . $this->prefix('provider')]);
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/src/Exception/Logical/InvalidArgumentException.php:
--------------------------------------------------------------------------------
1 | provider = $provider;
28 | $this->session = $session;
29 | }
30 |
31 | /**
32 | * @param string|mixed[]|null $redirectUriOrOptions
33 | * @param mixed[] $options
34 | */
35 | public function getAuthorizationUrl($redirectUriOrOptions = null, array $options = []): string
36 | {
37 | if (is_array($redirectUriOrOptions)) {
38 | $options = array_merge($options, $redirectUriOrOptions);
39 | } elseif (is_string($redirectUriOrOptions)) {
40 | $options['redirect_uri'] = $redirectUriOrOptions;
41 | } elseif ($redirectUriOrOptions !== null) { /** @phpstan-ignore-line */
42 | throw new RuntimeException('Parameter #1 redirectUriOrOptions of getAuthorizationUrl accepts only string or array.');
43 | }
44 |
45 | $session = $this->session->getSection(self::SESSION_NAMESPACE);
46 |
47 | $url = $this->provider->getAuthorizationUrl($options);
48 |
49 | $session['state'] = $this->provider->getState();
50 | $session['redirect_uri'] = $options['redirect_uri'] ?? null;
51 |
52 | return $url;
53 | }
54 |
55 | /**
56 | * @param mixed[] $parameters
57 | * @return AccessToken|AccessTokenInterface
58 | * @throws IdentityProviderException
59 | */
60 | public function getAccessToken(array $parameters, ?string $redirectUri = null): AccessTokenInterface
61 | {
62 | if (!isset($parameters['code'])) {
63 | throw new InvalidArgumentException('Missing "code" parameter');
64 | }
65 |
66 | if (!isset($parameters['state'])) {
67 | throw new InvalidArgumentException('Missing "state" parameter');
68 | }
69 |
70 | $session = $this->session->getSection(self::SESSION_NAMESPACE);
71 |
72 | // Possible CSRF attack
73 | if (isset($session['state']) && $parameters['state'] !== $session['state']) {
74 | unset($session['state']);
75 | unset($session['redirect_uri']);
76 | throw new PossibleCsrfAttackException();
77 | }
78 |
79 | $options = array_filter([
80 | 'code' => $parameters['code'],
81 | 'redirect_uri' => $redirectUri ?? $session['redirect_uri'] ?? null,
82 | ]);
83 |
84 | // Try to get an access token (using the authorization code grant)
85 | return $this->provider->getAccessToken('authorization_code', $options);
86 | }
87 |
88 | public function getProvider(): AbstractProvider
89 | {
90 | return $this->provider;
91 | }
92 |
93 | }
94 |
--------------------------------------------------------------------------------
/src/Flow/Facebook/FacebookAuthCodeFlow.php:
--------------------------------------------------------------------------------
1 | redirectUri = $uri;
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/Flow/Gitlab/GitlabAuthCodeFlow.php:
--------------------------------------------------------------------------------
1 | redirectUri = $uri;
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/Flow/Google/GoogleAuthCodeFlow.php:
--------------------------------------------------------------------------------
1 | redirectUri = $uri;
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/UI/Components/GenericAuthControl.latte:
--------------------------------------------------------------------------------
1 | Login
2 |
--------------------------------------------------------------------------------
/src/UI/Components/GenericAuthControl.php:
--------------------------------------------------------------------------------
1 | */
26 | public $onAuthenticated = [];
27 |
28 | /** @var array */
29 | public $onFailed = [];
30 |
31 | public function __construct(AuthCodeFlow $authCodeFlow, ?string $redirectUri = null)
32 | {
33 | $this->authCodeFlow = $authCodeFlow;
34 | $this->redirectUri = $redirectUri;
35 | }
36 |
37 | public function setTemplate(string $templatePath): void
38 | {
39 | $this->templatePath = $templatePath;
40 | }
41 |
42 | public function handleAuthenticate(): void
43 | {
44 | $this->authenticate();
45 | }
46 |
47 | public function authenticate(): void
48 | {
49 | $this->getPresenter()->redirectUrl(
50 | $this->authCodeFlow->getAuthorizationUrl(['redirect_uri' => $this->redirectUri])
51 | );
52 | }
53 |
54 | public function authorize(): ?ResourceOwnerInterface
55 | {
56 | try {
57 | $accessToken = $this->authCodeFlow->getAccessToken($this->getPresenter()->getHttpRequest()->getQuery());
58 | if (!$accessToken instanceof AccessToken) {
59 | throw new UnexpectedValueException();
60 | }
61 |
62 | $user = $this->authCodeFlow->getProvider()->getResourceOwner($accessToken);
63 | $this->authenticationSucceed($accessToken, $user);
64 | return $user;
65 | } catch (IdentityProviderException $e) {
66 | $this->authenticationFailed();
67 | }
68 |
69 | return null;
70 | }
71 |
72 | protected function authenticationFailed(): void
73 | {
74 | $this->onFailed();
75 | }
76 |
77 | protected function authenticationSucceed(AccessToken $accessToken, ResourceOwnerInterface $user): void
78 | {
79 | $this->onAuthenticated($accessToken, $user);
80 | }
81 |
82 | public function render(): void
83 | {
84 | $template = $this->getTemplate();
85 | if (!$template instanceof Template) {
86 | throw new UnexpectedValueException();
87 | }
88 |
89 | $template->render($this->templatePath ?? __DIR__ . '/GenericAuthControl.latte');
90 | }
91 |
92 | }
93 |
--------------------------------------------------------------------------------