Passed
Pull Request — master (#66)
by Stefano
08:01 queued 05:51
created

OAuth2Authenticator::initProvider()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 13
c 1
b 0
f 0
nc 3
nop 2
dl 0
loc 19
rs 9.8333
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 Cake\Http\Exception\BadRequestException;
21
use Cake\Log\LogTrait;
22
use Cake\Routing\Router;
23
use Cake\Utility\Hash;
24
use Psr\Http\Message\ServerRequestInterface;
25
26
class OAuth2Authenticator extends AbstractAuthenticator
27
{
28
    use LogTrait;
29
30
    /**
31
     * External Auth provider
32
     *
33
     * @var \League\OAuth2\Client\Provider\AbstractProvider
34
     */
35
    protected $provider = null;
36
37
    /**
38
     * @inheritDoc
39
     */
40
    protected $_defaultConfig = [
41
        'sessionKey' => 'oauth2state',
42
        'redirect' => ['_name' => 'login'],
43
        'providers' => [], // configured OAuth2 providers
44
    ];
45
46
    /**
47
     * @inheritDoc
48
     */
49
    public function authenticate(ServerRequestInterface $request): ResultInterface
50
    {
51
        // extract provider from request
52
        $provider = basename($request->getUri()->getPath());
53
54
        $connect = $this->providerConnect($provider, $request);
55
        if (!empty($connect['authUrl'])) {
56
            return new Result($connect, Result::SUCCESS);
57
        }
58
59
        $usernameField = (string)$this->getConfig(sprintf('providers.%s.map.provider_username', $provider));
60
        $data = [
61
            'auth_provider' => $provider,
62
            'provider_username' => Hash::get($connect, sprintf('user.%s', $usernameField)),
63
            'access_token' => Hash::get($connect, 'token.access_token'),
64
            'provider_userdata' => (array)Hash::get($connect, 'user'),
65
        ];
66
        $user = $this->_identifier->identify($data);
67
68
        if (empty($user)) {
69
            return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND, $this->_identifier->getErrors());
70
        }
71
72
        return new Result($user, Result::SUCCESS);
73
    }
74
75
    /**
76
     * Perform Oauth2 connect action on Auth Provider.
77
     *
78
     * @param string $provider Provider name.
79
     * @param \Psr\Http\Message\ServerRequestInterface $request Request to get authentication information from.
80
     * @return array;
81
     * @throws \Cake\Http\Exception\BadRequestException
82
     */
83
    protected function providerConnect(string $provider, ServerRequestInterface $request): array
84
    {
85
        $this->initProvider($provider, $request);
86
87
        $query = $request->getQueryParams();
88
        $sessionKey = $this->getConfig('sessionKey');
89
        /** @var \Cake\Http\Session $session */
90
        $session = $request->getAttribute('session');
91
92
        if (!isset($query['code'])) {
93
            // If we don't have an authorization code then get one
94
            $options = (array)$this->getConfig(sprintf('providers.%s.options', $provider));
95
            $authUrl = $this->provider->getAuthorizationUrl($options);
96
            $session->write($sessionKey, $this->provider->getState());
97
98
            return compact('authUrl');
99
        }
100
101
        // Check given state against previously stored one to mitigate CSRF attack
102
        if (empty($query['state']) || ($query['state'] !== $session->read($sessionKey))) {
103
            $session->delete($sessionKey);
104
            throw new BadRequestException('Invalid state');
105
        }
106
107
        // Try to get an access token (using the authorization code grant)
108
        /** @var \League\OAuth2\Client\Token\AccessToken $token */
109
        $token = $this->provider->getAccessToken('authorization_code', ['code' => $query['code']]);
110
        // We got an access token, let's now get the user's details
111
        $user = $this->provider->getResourceOwner($token)->toArray();
112
        $token = $token->jsonSerialize();
113
114
        return compact('token', 'user');
115
    }
116
117
    /**
118
     * Init external auth provider via configuration
119
     *
120
     * @param string $provider Provider name.
121
     * @param \Psr\Http\Message\ServerRequestInterface $request Request to get authentication information from.
122
     * @return void
123
     */
124
    protected function initProvider(string $provider, ServerRequestInterface $request): void
125
    {
126
        $providerConf = (array)$this->getConfig(sprintf('providers.%s', $provider));
127
        if (empty($providerConf['class']) || empty($providerConf['setup'])) {
128
            throw new BadRequestException('Invalid auth provider ' . $provider);
129
        }
130
131
        $redirectUri = (array)$this->getConfig('redirect') + compact('provider');
132
        $query = $request->getQueryParams();
133
        $queryRedirectUrl = Hash::get($query, 'redirect');
134
        if (!empty($queryRedirectUrl)) {
135
            $redirectUri['?'] = ['redirect' => $queryRedirectUrl];
136
        }
137
        $redirectUri = Router::url($redirectUri, true);
138
        $this->log(sprintf('Creating %s provider with redirect url %s', $provider, $redirectUri), 'info');
139
        $setup = (array)Hash::get($providerConf, 'setup') + compact('redirectUri');
140
141
        $class = Hash::get($providerConf, 'class');
142
        $this->provider = new $class($setup);
143
    }
144
}
145