Passed
Pull Request — master (#66)
by Stefano
02:15
created

OAuth2Authenticator::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 5
rs 10
c 0
b 0
f 0
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 BEdita\WebTools\Authenticator;
16
17
use Authentication\Authenticator\AbstractAuthenticator;
18
use Authentication\Authenticator\Result;
19
use Authentication\Authenticator\ResultInterface;
20
use Authentication\Identifier\IdentifierInterface;
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
/**
28
 * Authenticator class for the OAuth2 flow.
29
 * Provides a connection to the external OAuth2 provider and use
30
 * the identifier class to verify the credentials.
31
 */
32
class OAuth2Authenticator extends AbstractAuthenticator
33
{
34
    use LogTrait;
35
36
    /**
37
     * External Auth provider
38
     *
39
     * @var \League\OAuth2\Client\Provider\AbstractProvider
40
     */
41
    protected $provider = null;
42
43
    /**
44
     * Authentication URL key
45
     *
46
     * @var string
47
     */
48
    public const AUTH_URL_KEY = 'authUrl';
49
50
    /**
51
     * @inheritDoc
52
     */
53
    protected $_defaultConfig = [
54
        'sessionKey' => 'oauth2state',
55
        'redirect' => ['_name' => 'login'], // named route used to redirect
56
        'providers' => [], // configured OAuth2 providers
57
        'urlResolver' => null,
58
    ];
59
60
    /**
61
     * Constructor
62
     *
63
     * @param \Authentication\Identifier\IdentifierInterface $identifier Identifier or identifiers collection.
64
     * @param array $config Configuration settings.
65
     */
66
    public function __construct(IdentifierInterface $identifier, array $config = [])
67
    {
68
        // Setup default URL resolver
69
        $this->setConfig('urlResolver', fn ($route) => Router::url($route, true));
70
        parent::__construct($identifier, $config);
71
    }
72
73
    /**
74
     * @inheritDoc
75
     */
76
    public function authenticate(ServerRequestInterface $request): ResultInterface
77
    {
78
        // extract provider from request
79
        $provider = basename($request->getUri()->getPath());
80
81
        $connect = $this->providerConnect($provider, $request);
82
        if (!empty($connect[static::AUTH_URL_KEY])) {
83
            return new Result($connect, Result::SUCCESS);
84
        }
85
86
        $usernameField = (string)$this->getConfig(sprintf('providers.%s.map.provider_username', $provider));
87
        $data = [
88
            'auth_provider' => $provider,
89
            'provider_username' => Hash::get($connect, sprintf('user.%s', $usernameField)),
90
            'access_token' => Hash::get($connect, 'token.access_token'),
91
            'provider_userdata' => (array)Hash::get($connect, 'user'),
92
        ];
93
        $user = $this->_identifier->identify($data);
94
95
        if (empty($user)) {
96
            return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND, $this->_identifier->getErrors());
97
        }
98
99
        return new Result($user, Result::SUCCESS);
100
    }
101
102
    /**
103
     * Perform Oauth2 connect action on Auth Provider.
104
     *
105
     * @param string $provider Provider name.
106
     * @param \Psr\Http\Message\ServerRequestInterface $request Request to get authentication information from.
107
     * @return array;
108
     * @throws \Cake\Http\Exception\BadRequestException
109
     */
110
    protected function providerConnect(string $provider, ServerRequestInterface $request): array
111
    {
112
        $this->initProvider($provider, $request);
113
114
        $query = $request->getQueryParams();
115
        $sessionKey = $this->getConfig('sessionKey');
116
        /** @var \Cake\Http\Session $session */
117
        $session = $request->getAttribute('session');
118
119
        if (!isset($query['code'])) {
120
            // If we don't have an authorization code then get one
121
            $options = (array)$this->getConfig(sprintf('providers.%s.options', $provider));
122
            $authUrl = $this->provider->getAuthorizationUrl($options);
123
            $session->write($sessionKey, $this->provider->getState());
124
125
            return [static::AUTH_URL_KEY => $authUrl];
126
        }
127
128
        // Check given state against previously stored one to mitigate CSRF attack
129
        if (empty($query['state']) || ($query['state'] !== $session->read($sessionKey))) {
130
            $session->delete($sessionKey);
131
            throw new BadRequestException('Invalid state');
132
        }
133
134
        // Try to get an access token (using the authorization code grant)
135
        /** @var \League\OAuth2\Client\Token\AccessToken $token */
136
        $token = $this->provider->getAccessToken('authorization_code', ['code' => $query['code']]);
137
        // We got an access token, let's now get the user's details
138
        $user = $this->provider->getResourceOwner($token)->toArray();
139
        $token = $token->jsonSerialize();
140
141
        return compact('token', 'user');
142
    }
143
144
    /**
145
     * Init external auth provider via configuration
146
     *
147
     * @param string $provider Provider name.
148
     * @param \Psr\Http\Message\ServerRequestInterface $request Request to get authentication information from.
149
     * @return void
150
     */
151
    protected function initProvider(string $provider, ServerRequestInterface $request): void
152
    {
153
        $providerConf = (array)$this->getConfig(sprintf('providers.%s', $provider));
154
        if (empty($providerConf['class']) || empty($providerConf['setup'])) {
155
            throw new BadRequestException('Invalid auth provider ' . $provider);
156
        }
157
158
        $redirectUri = $this->redirectUri($provider, $request);
159
        $this->log(sprintf('Creating %s provider with redirect url %s', $provider, $redirectUri), 'info');
160
        $setup = (array)Hash::get($providerConf, 'setup') + compact('redirectUri');
161
162
        $class = Hash::get($providerConf, 'class');
163
        $this->provider = new $class($setup);
164
    }
165
166
    /**
167
     * Build redirect URL from request and provider information.
168
     *
169
     * @param string $provider Provider name.
170
     * @param \Psr\Http\Message\ServerRequestInterface $request Request to get authentication information from.
171
     * @return string
172
     */
173
    protected function redirectUri(string $provider, ServerRequestInterface $request): string
174
    {
175
        $redirectUri = (array)$this->getConfig('redirect') + compact('provider');
176
        $query = $request->getQueryParams();
177
        $queryRedirectUrl = Hash::get($query, 'redirect');
178
        if (!empty($queryRedirectUrl)) {
179
            $redirectUri['?'] = ['redirect' => $queryRedirectUrl];
180
        }
181
182
        return call_user_func($this->getConfig('urlResolver'), [$redirectUri]);
183
    }
184
}
185