Passed
Push — master ( 80df18...80df18 )
by Dāvis
04:16
created

OpenIDConnectProvider::buildParams()   C

Complexity

Conditions 7
Paths 13

Size

Total Lines 33
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 27
nc 13
nop 1
dl 0
loc 33
rs 6.7272
c 0
b 0
f 0
1
<?php
2
3
namespace Sludio\HelperBundle\Openidconnect\Provider;
4
5
use Lcobucci\JWT\Signer;
6
use Lcobucci\JWT\Signer\Key;
7
use Lcobucci\JWT\Signer\Rsa\Sha256;
8
use Lcobucci\JWT\Token;
9
use League\OAuth2\Client\Grant\AbstractGrant;
10
use League\OAuth2\Client\Token\AccessToken as BaseAccessToken;
11
use Psr\Http\Message\ResponseInterface;
12
use Sludio\HelperBundle\Openidconnect\Component\Providerable;
13
use Sludio\HelperBundle\Openidconnect\Security\Exception\InvalidTokenException;
14
use Sludio\HelperBundle\Openidconnect\Specification;
15
use Symfony\Bundle\FrameworkBundle\Routing\Router;
16
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
17
18
class OpenIDConnectProvider extends BaseProvider implements Providerable
19
{
20
    const METHOD_POST = 'POST';
21
    const METHOD_GET = 'GET';
22
23
    /**
24
     * @var string
25
     */
26
    protected $publicKey;
27
28
    /**
29
     * @var Signer
30
     */
31
    protected $signer;
32
33
    /**
34
     * @var Specification\ValidatorChain
35
     */
36
    protected $validatorChain;
37
38
    /**
39
     * @var string
40
     */
41
    protected $idTokenIssuer;
42
    /**
43
     * @var Uri[]
44
     */
45
    protected $uris = [];
46
47
    /**
48
     * @var bool
49
     */
50
    protected $useSession;
51
    /**
52
     * @var Router
53
     */
54
    private $router;
55
    /**
56
     * @var string
57
     */
58
    private $baseUri;
59
60
    /**
61
     * @param array  $options
62
     * @param array  $collaborators
63
     * @param Router $router
64
     */
65
    public function __construct(array $options = [], array $collaborators = [], Router $router)
66
    {
67
        $this->signer = new Sha256();
68
69
        $this->validatorChain = new Specification\ValidatorChain();
70
        $this->validatorChain->setValidators([
71
            new Specification\NotEmpty('iat', true),
72
            new Specification\GreaterOrEqualsTo('exp', true),
73
            new Specification\EqualsTo('iss', true),
74
            new Specification\EqualsTo('aud', true),
75
            new Specification\NotEmpty('sub', true),
76
            new Specification\LesserOrEqualsTo('nbf'),
77
            new Specification\EqualsTo('jti'),
78
            new Specification\EqualsTo('azp'),
79
            new Specification\EqualsTo('nonce'),
80
        ]);
81
82
        $this->router = $router;
83
84
        parent::__construct($options, $collaborators);
85
        $this->buildParams($options);
86
    }
87
88
    private function buildParams(array $options = [])
89
    {
90
        if (!empty($options)) {
91
            $this->clientId = $options['client_key'];
92
            $this->clientSecret = $options['client_secret'];
93
            unset($options['client_secret'], $options['client_key']);
94
            $this->idTokenIssuer = $options['id_token_issuer'];
95
            $this->publicKey = 'file://'.$options['public_key'];
96
            $this->state = $this->getRandomState();
97
            $this->baseUri = $options['base_uri'];
98
            $this->useSession = $options['use_session'];
99
            $url = null;
100
            switch ($options['redirect']['type']) {
101
                case 'uri':
102
                    $url = $options['redirect']['uri'];
103
                    break;
104
                case 'route':
105
                    $params = !empty($options['redirect']['params']) ? $options['redirect']['params'] : [];
106
                    $url = $this->router->generate($options['redirect']['route'], $params, UrlGeneratorInterface::ABSOLUTE_URL);
107
                    break;
108
            }
109
            $this->redirectUri = $url;
110
111
            /** @var $options array[] */
112
            foreach ($options['uris'] as $name => $uri) {
113
                $opt = [
114
                    'client_id' => $this->clientId,
115
                    'redirect_uri' => $this->redirectUri,
116
                    'state' => $this->state,
117
                    'base_uri' => $this->baseUri,
118
                ];
119
                $method = isset($uri['method']) ? $uri['method'] : self::METHOD_POST;
120
                $this->uris[$name] = new Uri($uri, $opt, $this->useSession, $method);
121
            }
122
        }
123
    }
124
125
    /**
126
     * Requests an access token using a specified grant and option set.
127
     *
128
     * @param  mixed $grant
129
     * @param  array $options
130
     *
131
     * @return BaseAccessToken
132
     * @throws InvalidTokenException
133
     * @throws \BadMethodCallException
134
     */
135
    public function getAccessToken($grant, array $options = [])
136
    {
137
        /** @var BaseAccessToken $token */
138
        $accessToken = parent::getAccessToken($grant, $options);
139
140
        if (null === $accessToken) {
141
            throw new InvalidTokenException('Invalid access token.');
142
        }
143
144
        $token = $accessToken->getIdToken();
145
146
        // id_token is empty.
147
        if (null === $token) {
148
            throw new InvalidTokenException('Expected an id_token but did not receive one from the authorization server.');
149
        }
150
151
        // If the ID Token is received via direct communication between the Client and the Token Endpoint
152
        // (which it is in this flow), the TLS server validation MAY be used to validate the issuer in place of checking
153
        // the token signature. The Client MUST validate the signature of all other ID Tokens according to JWS [JWS]
154
        // using the algorithm specified in the JWT alg Header Parameter. The Client MUST use the keys provided by
155
        // the Issuer.
156
        //
157
        // The alg value SHOULD be the default of RS256 or the algorithm sent by the Client in the
158
        // id_token_signed_response_alg parameter during Registration.
159
        if (false === $token->verify($this->signer, $this->getPublicKey())) {
0 ignored issues
show
Bug introduced by
$this->getPublicKey() of type Lcobucci\JWT\Signer\Key is incompatible with the type string expected by parameter $key of Lcobucci\JWT\Token::verify(). ( Ignorable by Annotation )

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

159
        if (false === $token->verify($this->signer, /** @scrutinizer ignore-type */ $this->getPublicKey())) {
Loading history...
160
            throw new InvalidTokenException('Received an invalid id_token from authorization server.');
161
        }
162
163
        // validations
164
        // @see http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
165
        // validate the iss (issuer)
166
        // - The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery)
167
        // MUST exactly match the value of the iss (issuer) Claim.
168
        // validate the aud
169
        // - The Client MUST validate that the aud (audience) Claim contains its client_id value registered at the Issuer
170
        // identified by the iss (issuer) Claim as an audience. The aud (audience) Claim MAY contain an array with more
171
        // than one element. The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience,
172
        // or if it contains additional audiences not trusted by the Client.
173
        // - If a nonce value was sent in the Authentication Request, a nonce Claim MUST be present and its value checked
174
        // to verify that it is the same value as the one that was sent in the Authentication Request. The Client SHOULD
175
        // check the nonce value for replay attacks. The precise method for detecting replay attacks is Client specific.
176
        // - If the auth_time Claim was requested, either through a specific request for this Claim or by using
177
        // the max_age parameter, the Client SHOULD check the auth_time Claim value and request re-authentication if it
178
        // determines too much time has elapsed since the last End-User authentication.
179
        // TODO
180
        // If the acr Claim was requested, the Client SHOULD check that the asserted Claim Value is appropriate.
181
        // The meaning and processing of acr Claim Values is out of scope for this specification.
182
        $currentTime = time();
183
        $data = [
184
            'iss' => $this->getIdTokenIssuer(),
185
            'exp' => $currentTime,
186
            'auth_time' => $currentTime,
187
            'iat' => $currentTime,
188
            'nbf' => $currentTime,
189
            'aud' => $this->clientId,
190
        ];
191
192
        // If the ID Token contains multiple audiences, the Client SHOULD verify that an azp Claim is present.
193
        // If an azp (authorized party) Claim is present, the Client SHOULD verify that its client_id is the Claim Value.
194
        if ($token->hasClaim('azp')) {
195
            $data['azp'] = $this->clientId;
196
        }
197
198
        if (false === $this->validatorChain->validate($data, $token)) {
199
            throw new InvalidTokenException('The id_token did not pass validation.');
200
        }
201
202
        if ($this->useSession) {
203
            $_SESSION['access_token'] = $accessToken->getToken();
204
            $_SESSION['refresh_token'] = $accessToken->getRefreshToken();
205
            $_SESSION['id_token'] = $accessToken->getIdTokenHint();
206
        }
207
208
        return $accessToken;
209
    }
210
211
    public function getPublicKey()
212
    {
213
        return new Key($this->publicKey);
214
    }
215
216
    /**
217
     * Get the issuer of the OpenID Connect id_token
218
     *
219
     * @return string
220
     */
221
    protected function getIdTokenIssuer()
222
    {
223
        return $this->idTokenIssuer;
224
    }
225
226
    /**
227
     * @return Specification\ValidatorChain
228
     */
229
    public function getValidatorChain()
230
    {
231
        return $this->validatorChain;
232
    }
233
234
    public function getBaseAuthorizationUrl()
235
    {
236
        return '';
237
    }
238
239
    public function getBaseAccessTokenUrl(array $params)
240
    {
241
        return '';
242
    }
243
244
    public function getDefaultScopes()
245
    {
246
        return [];
247
    }
248
249
    public function getResourceOwnerDetailsUrl(BaseAccessToken $token)
250
    {
251
    }
252
253
    public function getUri($name)
254
    {
255
        return $this->uris[$name];
256
    }
257
258
    /**
259
     * Returns all options that are required.
260
     *
261
     * @return array
262
     */
263
    protected function getRequiredOptions()
264
    {
265
        return [];
266
    }
267
268
    /**
269
     * Overload parent as OpenID Connect specification states scopes shall be separated by spaces
270
     *
271
     * @return string
272
     */
273
    protected function getScopeSeparator()
274
    {
275
        return ' ';
276
    }
277
278
    /**
279
     * Creates an access token from a response.
280
     *
281
     * The grant that was used to fetch the response can be used to provide
282
     * additional context.
283
     *
284
     * @param  array             $response
285
     *
286
     * @param AbstractGrant|null $grant
287
     *
288
     * @return AccessToken
289
     */
290
    protected function createAccessToken(array $response, AbstractGrant $grant = null)
291
    {
292
        if ($this->check()) {
293
            return new AccessToken($response);
294
        }
295
296
        return null;
297
    }
298
299
    public function check()
300
    {
301
        return true;
302
    }
303
304
    protected function createResourceOwner(array $response, BaseAccessToken $token)
305
    {
306
        return [];
307
    }
308
309
    protected function checkResponse(ResponseInterface $response, $data)
310
    {
311
    }
312
}
313