Issues (23)

src/Adapter/Oidc.php (18 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Micro
7
 *
8
 * @copyright   Copryright (c) 2015-2018 gyselroth GmbH (https://gyselroth.com)
9
 * @license     MIT https://opensource.org/licenses/MIT
10
 */
11
12
namespace Micro\Auth\Adapter;
13
14
use Micro\Auth\Adapter\Oidc\Exception as OidcException;
15
use Micro\Auth\Exception;
16
use Psr\Log\LoggerInterface;
17
18
class Oidc extends AbstractAdapter
19
{
20
    /**
21
     * OpenID-connect discovery path.
22
     */
23
    const DISCOVERY_PATH = '/.well-known/openid-configuration';
24
25
    /**
26
     * OpenID-connect provider url.
27
     *
28
     * @var string
29
     */
30
    protected $provider_url = 'https://oidc.example.org';
31
32
    /**
33
     * Token validation endpoint (rfc7662).
34
     *
35
     * @var string
36
     */
37
    protected $token_validation_url;
38
39
    /**
40
     * Attributes.
41
     *
42
     * @var array
43
     */
44
    protected $attributes = [];
45
46
    /**
47
     * LoggerInterface.
48
     *
49
     * @var LoggerInterface
50
     */
51
    protected $logger;
52
53
    /**
54
     * Access token.
55
     *
56
     * @var string
57
     */
58
    private $access_token;
59
60
    /**
61
     * Init adapter.
62
     *
63
     * @param LoggerInterface $logger
64
     * @param iterable        $config
65
     */
66
    public function __construct(LoggerInterface $logger, ?Iterable $config = null)
67
    {
68
        $this->logger = $logger;
69
        $this->identity_attribute = 'preferred_username';
70
        $this->setOptions($config);
71
    }
72
73
    /**
74
     * Set options.
75
     *
76
     * @param iterable $config
77
     *
78
     * @return AdapterInterface
79
     */
80
    public function setOptions(? Iterable $config = null): AdapterInterface
81
    {
82
        if (null === $config) {
83
            return $this;
84
        }
85
86
        foreach ($config as $option => $value) {
87
            switch ($option) {
88
                case 'provider_url':
89
                case 'token_validation_url':
90
                    $this->{$option} = (string) $value;
91
                    unset($config[$option]);
92
93
                break;
94
            }
95
        }
96
97
        return  parent::setOptions($config);
98
    }
99
100
    /**
101
     * Authenticate.
102
     *
103
     * @return bool
104
     */
105
    public function authenticate(): bool
106
    {
107
        if (isset($_GET['access_token'])) {
108
            $this->logger->warning('found access_token in query string, you should use a bearer token instead due security reasons https://tools.ietf.org/html/rfc6750#section-2.3', [
109
                'category' => get_class($this),
110
            ]);
111
112
            return $this->verifyToken($_GET['access_token']);
113
        }
114
        if (isset($_SERVER['HTTP_AUTHORIZATION'])) {
115
            $header = $_SERVER['HTTP_AUTHORIZATION'];
116
            $parts = explode(' ', $header);
117
118
            if ('Bearer' === $parts[0]) {
119
                $this->logger->debug('found http bearer authorization header', [
120
                    'category' => get_class($this),
121
                ]);
122
123
                return $this->verifyToken($parts[1]);
124
            }
125
            $this->logger->debug('no bearer token provided', [
126
                    'category' => get_class($this),
127
                ]);
128
129
            return false;
130
        }
131
132
        $this->logger->debug('http authorization header contains no bearer string or invalid authentication string', [
133
                    'category' => get_class($this),
134
                ]);
135
136
        return false;
137
    }
138
139
    /**
140
     * Get discovery url.
141
     *
142
     * @return string
143
     */
144
    public function getDiscoveryUrl(): string
145
    {
146
        return $this->provider_url.self::DISCOVERY_PATH;
147
    }
148
149
    /**
150
     * Get discovery document.
151
     *
152
     * @return array
153
     */
154
    public function getDiscoveryDocument(): array
155
    {
156
        if ($apc = extension_loaded('apc') && apc_exists($this->provider_url)) {
0 ignored issues
show
Comprehensibility introduced by
Consider adding parentheses for clarity. Current Interpretation: $apc = (extension_loaded...s($this->provider_url)), Probably Intended Meaning: ($apc = extension_loaded...ts($this->provider_url)
Loading history...
$this->provider_url of type string is incompatible with the type boolean|string[] expected by parameter $keys of apc_exists(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

156
        if ($apc = extension_loaded('apc') && apc_exists(/** @scrutinizer ignore-type */ $this->provider_url)) {
Loading history...
157
            return apc_get($this->provider_url);
0 ignored issues
show
The function apc_get was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

157
            return /** @scrutinizer ignore-call */ apc_get($this->provider_url);
Loading history...
158
        }
159
        $ch = curl_init();
160
        $url = $this->getDiscoveryUrl();
161
        curl_setopt($ch, CURLOPT_URL, $url);
0 ignored issues
show
It seems like $ch can also be of type false; however, parameter $ch of curl_setopt() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

161
        curl_setopt(/** @scrutinizer ignore-type */ $ch, CURLOPT_URL, $url);
Loading history...
162
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
163
164
        $this->logger->debug('fetch openid-connect discovery document from ['.$url.']', [
165
                'category' => get_class($this),
166
            ]);
167
168
        $result = curl_exec($ch);
0 ignored issues
show
It seems like $ch can also be of type false; however, parameter $ch of curl_exec() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

168
        $result = curl_exec(/** @scrutinizer ignore-type */ $ch);
Loading history...
169
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
0 ignored issues
show
It seems like $ch can also be of type false; however, parameter $ch of curl_getinfo() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

169
        $code = curl_getinfo(/** @scrutinizer ignore-type */ $ch, CURLINFO_HTTP_CODE);
Loading history...
170
        curl_close($ch);
0 ignored issues
show
It seems like $ch can also be of type false; however, parameter $ch of curl_close() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

170
        curl_close(/** @scrutinizer ignore-type */ $ch);
Loading history...
171
172
        if (200 === $code) {
173
            $discovery = json_decode($result, true);
174
            $this->logger->debug('received openid-connect discovery document from ['.$url.']', [
175
                    'category' => get_class($this),
176
                    'discovery' => $discovery,
177
                ]);
178
179
            if (true === $apc) {
0 ignored issues
show
The condition true === $apc is always false.
Loading history...
180
                apc_store($this->provider_url, $discovery);
181
            }
182
183
            return $discovery;
184
        }
185
        $this->logger->error('failed to receive openid-connect discovery document from ['.$url.'], request ended with status ['.$code.']', [
186
                    'category' => get_class($this),
187
                ]);
188
189
        throw new OidcException\DiscoveryNotFound('failed to get openid-connect discovery document');
190
    }
191
192
    /**
193
     * Get attributes.
194
     *
195
     * @return array
196
     */
197
    public function getAttributes(): array
198
    {
199
        if (0 !== count($this->attributes)) {
200
            return $this->attributes;
201
        }
202
203
        $discovery = $this->getDiscoveryDocument();
204
        if (!(isset($discovery['authorization_endpoint']))) {
205
            throw new OidcException\AuthorizationEndpointNotSet('authorization_endpoint could not be determained');
206
        }
207
208
        $this->logger->debug('fetch user attributes from userinfo_endpoint ['.$discovery['userinfo_endpoint'].']', [
209
           'category' => get_class($this),
210
        ]);
211
212
        $url = $discovery['userinfo_endpoint'].'?access_token='.$this->access_token;
213
        $ch = curl_init();
214
        curl_setopt($ch, CURLOPT_URL, $url);
0 ignored issues
show
It seems like $ch can also be of type false; however, parameter $ch of curl_setopt() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

214
        curl_setopt(/** @scrutinizer ignore-type */ $ch, CURLOPT_URL, $url);
Loading history...
215
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
216
        $result = curl_exec($ch);
0 ignored issues
show
It seems like $ch can also be of type false; however, parameter $ch of curl_exec() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

216
        $result = curl_exec(/** @scrutinizer ignore-type */ $ch);
Loading history...
217
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
0 ignored issues
show
It seems like $ch can also be of type false; however, parameter $ch of curl_getinfo() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

217
        $code = curl_getinfo(/** @scrutinizer ignore-type */ $ch, CURLINFO_HTTP_CODE);
Loading history...
218
        curl_close($ch);
0 ignored issues
show
It seems like $ch can also be of type false; however, parameter $ch of curl_close() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

218
        curl_close(/** @scrutinizer ignore-type */ $ch);
Loading history...
219
        $response = json_decode($result, true);
0 ignored issues
show
The assignment to $response is dead and can be removed.
Loading history...
220
221
        if (200 === $code) {
222
            $attributes = json_decode($result, true);
223
            $this->logger->debug('successfully requested user attributes from userinfo_endpoint', [
224
               'category' => get_class($this),
225
            ]);
226
227
            return $this->attributes = $attributes;
228
        }
229
        $this->logger->error('failed requesting user attributes from userinfo_endpoint, status code ['.$code.']', [
230
               'category' => get_class($this),
231
            ]);
232
233
        throw new OidcException\UserInfoRequestFailed('failed requesting user attribute from userinfo_endpoint');
234
    }
235
236
    /**
237
     * Token verification.
238
     *
239
     * @param string $token
240
     *
241
     * @return bool
242
     */
243
    protected function verifyToken(string $token): bool
244
    {
245
        if ($this->token_validation_url) {
246
            $this->logger->debug('validate oauth2 token via rfc7662 token validation endpoint ['.$this->token_validation_url.']', [
247
               'category' => get_class($this),
248
            ]);
249
250
            $url = str_replace('{token}', $token, $this->token_validation_url);
251
        } else {
252
            $discovery = $this->getDiscoveryDocument();
253
            if (!(isset($discovery['userinfo_endpoint']))) {
254
                throw new OidcException\UserEndpointNotSet('userinfo_endpoint could not be determained');
255
            }
256
257
            $this->logger->debug('validate token via openid-connect userinfo_endpoint ['.$discovery['userinfo_endpoint'].']', [
258
               'category' => get_class($this),
259
            ]);
260
261
            $url = $discovery['userinfo_endpoint'].'?access_token='.$token;
262
        }
263
264
        $ch = curl_init();
265
        curl_setopt($ch, CURLOPT_URL, $url);
0 ignored issues
show
It seems like $ch can also be of type false; however, parameter $ch of curl_setopt() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

265
        curl_setopt(/** @scrutinizer ignore-type */ $ch, CURLOPT_URL, $url);
Loading history...
266
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
267
        $result = curl_exec($ch);
0 ignored issues
show
It seems like $ch can also be of type false; however, parameter $ch of curl_exec() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

267
        $result = curl_exec(/** @scrutinizer ignore-type */ $ch);
Loading history...
268
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
0 ignored issues
show
It seems like $ch can also be of type false; however, parameter $ch of curl_getinfo() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

268
        $code = curl_getinfo(/** @scrutinizer ignore-type */ $ch, CURLINFO_HTTP_CODE);
Loading history...
269
        curl_close($ch);
0 ignored issues
show
It seems like $ch can also be of type false; however, parameter $ch of curl_close() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

269
        curl_close(/** @scrutinizer ignore-type */ $ch);
Loading history...
270
        $response = json_decode($result, true);
0 ignored issues
show
The assignment to $response is dead and can be removed.
Loading history...
271
272
        if (200 === $code) {
273
            $attributes = json_decode($result, true);
274
            $this->logger->debug('successfully verified oauth2 access token via authorization server', [
275
               'category' => get_class($this),
276
            ]);
277
278
            if (!isset($attributes[$this->identity_attribute])) {
279
                throw new Exception\IdentityAttributeNotFound('identity attribute '.$this->identity_attribute.' not found in oauth2 response');
280
            }
281
282
            $this->identifier = $attributes[$this->identity_attribute];
283
284
            if ($this->token_validation_url) {
285
                $this->attributes = $attributes;
286
            } else {
287
                $this->access_token = $token;
288
            }
289
290
            return true;
291
        }
292
        $this->logger->error('failed verify oauth2 access token via authorization server, received status ['.$code.']', [
293
               'category' => get_class($this),
294
            ]);
295
296
        throw new OidcException\InvalidAccessToken('failed verify oauth2 access token via authorization server');
297
    }
298
}
299