Passed
Pull Request — master (#822)
by Stefano
02:47
created

OAuth2Authenticator::providerConnect()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 31
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 16
c 1
b 0
f 0
dl 0
loc 31
rs 9.7333
cc 4
nc 3
nop 2
1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * BEdita, API-first content management framework
6
 * Copyright 2022 ChannelWeb Srl, Chialab Srl
7
 *
8
 * This file is part of BEdita: you can redistribute it and/or modify
9
 * it under the terms of the GNU Lesser General Public License as published
10
 * by the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
14
 */
15
namespace App\Authenticator;
16
17
use Authentication\Authenticator\AbstractAuthenticator;
18
use Authentication\Authenticator\Result;
19
use Authentication\Authenticator\ResultInterface;
20
use Cake\Core\Configure;
21
use Cake\Http\Exception\BadRequestException;
22
use Cake\Log\LogTrait;
23
use Cake\Routing\Router;
24
use Cake\Utility\Hash;
25
use Psr\Http\Message\ServerRequestInterface;
26
27
class OAuth2Authenticator extends AbstractAuthenticator
28
{
29
    use LogTrait;
30
31
    /**
32
     * External Auth provider
33
     *
34
     * @var \League\OAuth2\Client\Provider\AbstractProvider
35
     */
36
    protected $provider = null;
37
38
    /**
39
     * @inheritDoc
40
     */
41
    protected $_defaultConfig = [
42
        'fields' => [
43
            'auth_provider' => 'auth_provider',
44
            'provider_username' => 'provider_username',
45
            'access_token' => 'access_token',
46
        ],
47
        'sessionKey' => 'oauth2state',
48
        'routeUrl' => ['_name' => 'login:oauth2'],
49
    ];
50
51
    /**
52
     * @inheritDoc
53
     */
54
    public function authenticate(ServerRequestInterface $request): ResultInterface
55
    {
56
        // extract provider from request
57
        $provider = basename($request->getUri()->getPath());
58
59
        $connect = $this->providerConnect($provider, $request);
60
        if (!empty($connect['authUrl'])) {
61
            return new Result($connect, Result::SUCCESS);
62
        }
63
64
        $usernameField = (string)Configure::read(sprintf('AuthProviders.%s.map.provider_username', $provider));
65
        $data = [
66
            'auth_provider' => $provider,
67
            'provider_username' => Hash::get($connect, sprintf('user.%s', $usernameField)),
68
            'access_token' => Hash::get($connect, 'token.access_token'),
69
        ];
70
        $user = $this->_identifier->identify($data);
71
72
        if (empty($user)) {
73
            return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND, $this->_identifier->getErrors());
74
        }
75
76
        return new Result($user, Result::SUCCESS);
77
    }
78
79
    /**
80
     * Perform Oauth2 connect action on Auth Provider.
81
     *
82
     * @param string $provider Provider name.
83
     * @param \Psr\Http\Message\ServerRequestInterface $request Request to get authentication information from.
84
     * @return array;
85
     * @throws \Cake\Http\Exception\BadRequestException
86
     */
87
    protected function providerConnect(string $provider, ServerRequestInterface $request): array
88
    {
89
        $this->initProvider($provider, $request);
90
91
        $query = $request->getQueryParams();
92
        $sessionKey = $this->getConfig('sessionKey');
93
        /** @var \Cake\Http\Session $session */
94
        $session = $request->getAttribute('session');
95
96
        if (!isset($query['code'])) {
97
            // If we don't have an authorization code then get one
98
            $options = (array)Configure::read(sprintf('AuthProviders.%s.options', $provider));
99
            $authUrl = $this->provider->getAuthorizationUrl($options);
100
            $session->write($sessionKey, $this->provider->getState());
101
102
            return compact('authUrl');
103
        }
104
105
        // Check given state against previously stored one to mitigate CSRF attack
106
        if (empty($query['state']) || ($query['state'] !== $session->read($sessionKey))) {
107
            $session->delete($sessionKey);
108
            throw new BadRequestException('Invalid state');
109
        }
110
111
        // Try to get an access token (using the authorization code grant)
112
        $token = $this->provider->getAccessToken('authorization_code', ['code' => $query['code']]);
113
        // We got an access token, let's now get the user's details
114
        $user = $this->provider->getResourceOwner($token)->toArray();
115
        $token = $token->jsonSerialize();
116
117
        return compact('token', 'user');
118
    }
119
120
    /**
121
     * Init external auth provider via configuration
122
     *
123
     * @param string $provider Provider name.
124
     * @param \Psr\Http\Message\ServerRequestInterface $request Request to get authentication information from.
125
     * @return void
126
     */
127
    protected function initProvider(string $provider, ServerRequestInterface $request): void
128
    {
129
        $providerConf = (array)Configure::read(sprintf('AuthProviders.%s', $provider));
130
        if (empty($providerConf['class']) || empty($providerConf['setup'])) {
131
            throw new BadRequestException('Invalid auth provider ' . $provider);
132
        }
133
134
        $routeUrl = (array)$this->getConfig('routeUrl') + compact('provider');
135
        $query = $request->getQueryParams();
136
        $queryRedirectUrl = Hash::get($query, 'redirect');
137
        if (!empty($queryRedirectUrl)) {
138
            $routeUrl['?'] = ['redirect' => $queryRedirectUrl];
139
        }
140
        $redirectUri = Router::url($routeUrl, true);
141
        $this->log(sprintf('Creating %s provider with redirect url %s', $provider, $redirectUri), 'info');
142
        $setup = (array)Hash::get($providerConf, 'setup') + compact('redirectUri');
143
144
        $class = Hash::get($providerConf, 'class');
145
        $this->provider = new $class($setup);
146
    }
147
}
148