Completed
Push — master ( 9cd38e...eb909a )
by Alberto
15s queued 12s
created

OAuth2Authenticator::authenticate()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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