Completed
Push — master ( c1a2da...091d0a )
by Raffael
02:04
created

Oidc::getAttributes()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 40
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 40
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 25
nc 4
nop 0
1
<?php
2
declare(strict_types = 1);
3
4
/**
5
 * Micro
6
 *
7
 * @author    Raffael Sahli <[email protected]>
8
 * @copyright Copyright (c) 2017 gyselroth GmbH (https://gyselroth.com)
9
 * @license   MIT https://opensource.org/licenses/MIT
10
 */
11
12
namespace Micro\Auth\Adapter;
13
14
use \Psr\Log\LoggerInterface as Logger;
15
use \Micro\Auth\Exception;
16
17
class Oidc extends AbstractAdapter
18
{
19
    /**
20
     * OpenID-connect discovery path
21
     */
22
    CONST DISCOVERY_PATH = '/.well-known/openid-configuration';
23
24
25
    /**
26
     * OpenID-connect provider url
27
     *
28
     * @var string
29
     */
30
    protected $provider_url = 'https://oidc.example.org';
31
32
33
    /**
34
     * Token validation endpoint (rfc7662)
35
     * 
36
     * @var string
37
     */
38
    protected $token_validation_url;
39
40
41
    /**
42
     * Identity attribute
43
     *
44
     * @var string
45
     */
46
    protected $identity_attribute = 'preferred_username';
47
     
48
49
    /**
50
     * Attributes
51
     * 
52
     * @var array
53
     */
54
    protected $attributes = [];
55
56
57
    /**
58
     * Access token
59
     *
60
     * @var string
61
     */
62
    private $access_token;
63
64
65
    /**
66
     * Set options
67
     *
68
     * @param   Iterable $config
69
     * @return  AdapterInterface
70
     */
71
    public function setOptions(? Iterable $config = null) : AdapterInterface
72
    {
73
        if ($config === null) {
74
            return $this;
75
        }
76
77
        foreach($config as $option => $value) {
78
            switch($option) {
79
                case 'provider_url':
80
                case 'token_validation_url':
81
                case 'identity_attribute':
82
                    $this->{$option} = (string)$value;
83
                break;
84
            }
85
        }
86
87
        return  parent::setOptions($config);        
88
    }
89
90
91
    /**
92
     * Authenticate
93
     *
94
     * @return bool
95
     */
96
    public function authenticate(): bool
97
    {
98
        if (!isset($_SERVER['HTTP_AUTHORIZATION'])) {
99
            $this->logger->debug('skip auth adapter ['.get_class($this).'], no http authorization header or access_token param found', [
100
                'category' => get_class($this)
101
            ]);
102
        
103
            return false;
104
        } else {
105
            $header = $_SERVER['HTTP_AUTHORIZATION'];
106
            $parts  = explode(' ', $header);
107
            
108
            if ($parts[0] == 'Bearer') {
109
                $this->logger->debug('found http bearer authorization header', [
110
                    'category' => get_class($this)
111
                ]);
112
                
113
                return $this->verifyToken($parts[1]);
114
            } else {
115
                $this->logger->debug('http authorization header contains no bearer string or invalid authentication string', [
116
                    'category' => get_class($this)
117
                ]);
118
            
119
                return false;
120
            }
121
        }
122
    }
123
124
    
125
    /**
126
     * Get discovery url
127
     *
128
     * @return string 
129
     */
130
    public function getDiscoveryUrl(): string
131
    {
132
        return $this->provider_url.self::DISCOVERY_PATH;    
133
    }
134
135
136
    /**
137
     * Get discovery document
138
     *
139
     * @return array
140
     */    
141
    public function getDiscoveryDocument(): array
142
    {
143
        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...
144
            return apc_get($this->provider_url);
145
        } else {
146
            $ch = curl_init();
147
            $url = $this->getDiscoveryUrl();
148
            curl_setopt($ch, CURLOPT_URL, $url);
149
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
150
151
            $this->logger->debug('fetch openid-connect discovery document from ['.$url.']', [
152
                'category' => get_class($this)
153
            ]);
154
155
            $result = curl_exec($ch);
156
            $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
157
            curl_close($ch);
158
159
            if($code === 200) {
160
                $discovery = json_decode($result, true);
161
                $this->logger->debug('received openid-connect discovery document from ['.$url.']', [
162
                    'category' => get_class($this),
163
                    'discovery'=> $discovery
164
                ]);
165
            
166
                if ($apc === true) {
167
                    apc_store($this->provider_url, $discovery);
168
                }
169
170
                return $discovery;
171
            } else {
172
                $this->logger->error('failed to receive openid-connect discovery document from ['.$url.'], request ended with status ['.$code.']', [
173
                    'category' => get_class($this),
174
                ]);
175
176
                throw new Exception('failed to get openid-connect discovery document');
177
            }
178
        }
179
    }
180
181
182
    /**
183
     * Token verification
184
     *
185
     * @param   string $token
186
     * @return  bool
187
     */
188
    protected function verifyToken(string $token): bool
189
    {
190
        if($this->token_validation_url) {
191
            $this->logger->debug('validate oauth2 token via rfc7662 token validation endpoint ['.$this->token_validation_url.']', [
192
               'category' => get_class($this),
193
            ]);
194
195
            $url = str_replace('{token}', $token, $this->token_validation_url);
196
        } else {
197
            $discovery = $this->getDiscoveryDocument();
198
            if (!(isset($discovery['userinfo_endpoint']))) {
199
                throw new Exception('userinfo_endpoint could not be determained');
200
            }
201
202
            $this->logger->debug('validate token via openid-connect userinfo_endpoint ['.$discovery['userinfo_endpoint'].']', [
203
               'category' => get_class($this),
204
            ]);
205
        
206
            $url = $discovery['userinfo_endpoint'].'?access_token='.$token;
207
        }
208
        
209
        $ch = curl_init();
210
        curl_setopt($ch, CURLOPT_URL, $url);
211
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
212
        $result = curl_exec($ch);
213
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
214
        curl_close($ch);
215
        $response = json_decode($result, true);
0 ignored issues
show
Unused Code introduced by
$response is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
216
        
217
        if($code === 200) {
218
            $attributes = json_decode($result, true);
219
            $this->logger->debug('successfully verified oauth2 access token via authorization server', [
220
               'category' => get_class($this),
221
            ]);
222
        
223
            if(!isset($attributes[$this->identity_attribute])) {
224
                throw new Exception('identity attribute '.$this->identity_attribute.' not found in oauth2 response');
225
            }
226
227
            $this->identifier = $attributes['preferred_username'];
228
        
229
            if($this->token_validation_url) {
230
                $this->attributes = $attributes;    
231
            } else {
232
                $this->access_token = $token;
233
            }
234
235
            return true;
236
        } else {
237
            $this->logger->error('failed verify oauth2 access token via authorization server, received status ['.$code.']', [
238
               'category' => get_class($this),
239
            ]);
240
            
241
            throw new Exception('failed verify oauth2 access token via authorization server');
242
        }
243
    }
244
245
246
    /**
247
     * Get attributes
248
     * 
249
     * @return array
250
     */
251
    public function getAttributes(): array
252
    {
253
        if(count($this->attributes) !== 0) {
254
            return $this->attributes;
255
        }
256
257
        $discovery = $this->getDiscoveryDocument();
258
        if (!(isset($discovery['authorization_endpoint']))) {
259
            throw new Exception('authorization_endpoint could not be determained');
260
        }
261
262
        $this->logger->debug('fetch user attributes from userinfo_endpoint ['.$discovery['userinfo_endpoint'].']', [
263
           'category' => get_class($this),
264
        ]);
265
        
266
        $url = $discovery['userinfo_endpoint'].'?access_token='.$this->access_token;
267
        $ch = curl_init();
268
        curl_setopt($ch, CURLOPT_URL, $url);
269
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
270
        $result = curl_exec($ch);
271
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
272
        curl_close($ch);
273
        $response = json_decode($result, true);
0 ignored issues
show
Unused Code introduced by
$response is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
274
        
275
        if($code === 200) {
276
            $attributes = json_decode($result, true);
277
            $this->logger->debug('successfully requested user attributes from userinfo_endpoint', [
278
               'category' => get_class($this),
279
            ]);
280
        
281
            return $this->attributes = $attributes;    
282
        } else {
283
            $this->logger->error('failed requesting user attributes from userinfo_endpoint, status code ['.$code.']', [
284
               'category' => get_class($this),
285
            ]);
286
            
287
            throw new Exception('failed requesting user attribute from userinfo_endpoint');
288
        }
289
290
    }
291
}
292