Failed Conditions
Push — master ( 02e26d...9d1659 )
by Florent
04:05
created

UserInfoEndpoint   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 214
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
wmc 27
lcom 1
cbo 6
dl 0
loc 214
rs 10
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A enableSignature() 0 5 1
A enableEncryption() 0 4 1
B process() 0 27 4
C buildUserinfoContent() 0 26 7
A getEndpointClaims() 0 13 3
A getClient() 0 9 2
A getUserAccount() 0 9 3
A checkRedirectUri() 0 6 2
A checkScope() 0 6 3
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2014-2018 Spomky-Labs
9
 *
10
 * This software may be modified and distributed under the terms
11
 * of the MIT license.  See the LICENSE file for details.
12
 */
13
14
namespace OAuth2Framework\Component\OpenIdConnect\UserInfoEndpoint;
15
16
use Http\Message\ResponseFactory;
17
use Psr\Http\Server\RequestHandlerInterface;
18
use Psr\Http\Server\MiddlewareInterface;
19
use Jose\Component\Core\JWKSet;
20
use Jose\Component\Encryption\JWEBuilder;
21
use Jose\Component\Signature\JWSBuilder;
22
use OAuth2Framework\Component\Core\AccessToken\AccessToken;
23
use OAuth2Framework\Component\Core\Client\Client;
24
use OAuth2Framework\Component\Core\Client\ClientRepository;
25
use OAuth2Framework\Component\Core\UserAccount\UserAccountId;
26
use OAuth2Framework\Component\Core\UserAccount\UserAccount;
27
use OAuth2Framework\Component\Core\UserAccount\UserAccountRepository;
28
use OAuth2Framework\Component\Core\Message\OAuth2Message;
29
use OAuth2Framework\Component\OpenIdConnect\IdTokenBuilderFactory;
30
use Psr\Http\Message\ResponseInterface;
31
use Psr\Http\Message\ServerRequestInterface;
32
33
class UserInfoEndpoint implements MiddlewareInterface
34
{
35
    /**
36
     * @var JWKSet|null
37
     */
38
    private $signatureKeys = null;
39
40
    /**
41
     * @var JWSBuilder|null
42
     */
43
    private $jwsBuilder = null;
44
45
    /**
46
     * @var JWEBuilder|null
47
     */
48
    private $jweBuilder = null;
49
50
    /**
51
     * @var ClientRepository
52
     */
53
    private $clientRepository;
54
55
    /**
56
     * @var UserAccountRepository
57
     */
58
    private $userAccountRepository;
59
60
    /**
61
     * @var ResponseFactory
62
     */
63
    private $responseFactory;
64
65
    /**
66
     * @var IdTokenBuilderFactory
67
     */
68
    private $idTokenBuilderFactory;
69
70
    /**
71
     * UserInfoEndpoint constructor.
72
     *
73
     * @param IdTokenBuilderFactory $idTokenBuilderFactory
74
     * @param ClientRepository      $clientRepository
75
     * @param UserAccountRepository $userAccountRepository
76
     * @param ResponseFactory       $responseFactory
77
     */
78
    public function __construct(IdTokenBuilderFactory $idTokenBuilderFactory, ClientRepository $clientRepository, UserAccountRepository $userAccountRepository, ResponseFactory $responseFactory)
79
    {
80
        $this->idTokenBuilderFactory = $idTokenBuilderFactory;
81
        $this->clientRepository = $clientRepository;
82
        $this->userAccountRepository = $userAccountRepository;
83
        $this->responseFactory = $responseFactory;
84
    }
85
86
    /**
87
     * @param JWSBuilder $jwsBuilder
88
     * @param JWKSet     $signatureKeys
89
     */
90
    public function enableSignature(JWSBuilder $jwsBuilder, JWKSet $signatureKeys)
91
    {
92
        $this->jwsBuilder = $jwsBuilder;
93
        $this->signatureKeys = $signatureKeys;
94
    }
95
96
    /**
97
     * @param JWEBuilder $jweBuilder
98
     */
99
    public function enableEncryption(JWEBuilder $jweBuilder)
100
    {
101
        $this->jweBuilder = $jweBuilder;
102
    }
103
104
    /**
105
     * {@inheritdoc}
106
     */
107
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
108
    {
109
        /**
110
         * @var AccessToken
111
         */
112
        $accessToken = $request->getAttribute('access_token');
113
        if (!$accessToken instanceof AccessToken) {
114
            throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_TOKEN, 'The access token is missing or invalid.');
115
        }
116
117
        $this->checkScope($accessToken);
118
        $this->checkRedirectUri($accessToken);
119
120
        $client = $this->getClient($accessToken);
121
        $user = $this->getUserAccount($accessToken);
122
123
        $idToken = $this->buildUserinfoContent($client, $user, $accessToken, $isJwt);
124
125
        $response = $this->responseFactory->createResponse();
126
        $response->getBody()->write($idToken);
127
        $headers = ['Content-Type' => sprintf('application/%s; charset=UTF-8', $isJwt ? 'jwt' : 'json'), 'Cache-Control' => 'no-cache, no-store, max-age=0, must-revalidate, private', 'Pragma' => 'no-cache'];
128
        foreach ($headers as $k => $v) {
129
            $response = $response->withHeader($k, $v);
130
        }
131
132
        return $response;
133
    }
134
135
    /**
136
     * @param Client      $client
137
     * @param UserAccount $userAccount
138
     * @param AccessToken $accessToken
139
     * @param bool|null   $isJwt
140
     *
141
     * @return string
142
     */
143
    private function buildUserinfoContent(Client $client, UserAccount $userAccount, AccessToken $accessToken, ? bool &$isJwt): string
144
    {
145
        $isJwt = false;
146
        $requestedClaims = $this->getEndpointClaims($accessToken);
147
        $idTokenBuilder = $this->idTokenBuilderFactory->createBuilder($client, $userAccount, $accessToken->getMetadata('redirect_uri'));
148
149
        if ($client->has('userinfo_signed_response_alg') && null !== $this->jwsBuilder) {
150
            $isJwt = true;
151
            $signatureAlgorithm = $client->get('userinfo_signed_response_alg');
152
            $idTokenBuilder = $idTokenBuilder->withSignature($this->jwsBuilder, $this->signatureKeys, $signatureAlgorithm);
153
        }
154
        if ($client->has('userinfo_encrypted_response_alg') && $client->has('userinfo_encrypted_response_enc') && null !== $this->jweBuilder) {
155
            $isJwt = true;
156
            $keyEncryptionAlgorithm = $client->get('userinfo_encrypted_response_alg');
157
            $contentEncryptionAlgorithm = $client->get('userinfo_encrypted_response_enc');
158
            $idTokenBuilder = $idTokenBuilder->withEncryption($this->jweBuilder, $keyEncryptionAlgorithm, $contentEncryptionAlgorithm);
159
        }
160
        $idTokenBuilder = $idTokenBuilder->withAccessToken($accessToken);
161
        $idTokenBuilder = $idTokenBuilder->withRequestedClaims($requestedClaims);
162
        if ($client->has('require_auth_time')) {
163
            $idTokenBuilder->withAuthenticationTime();
164
        }
165
        $idToken = $idTokenBuilder->build();
166
167
        return $idToken;
168
    }
169
170
    /**
171
     * @param AccessToken $accessToken
172
     *
173
     * @return array
174
     */
175
    private function getEndpointClaims(AccessToken $accessToken): array
176
    {
177
        if (!$accessToken->hasMetadata('requested_claims')) {
178
            return [];
179
        }
180
181
        $requested_claims = $accessToken->getMetadata('requested_claims');
182
        if (true === array_key_exists('userinfo', $requested_claims)) {
183
            return $requested_claims['userinfo'];
184
        }
185
186
        return [];
187
    }
188
189
    /**
190
     * @param AccessToken $accessToken
191
     *
192
     * @throws OAuth2Message
193
     *
194
     * @return Client
195
     */
196
    private function getClient(AccessToken $accessToken): Client
197
    {
198
        $clientId = $accessToken->getClientId();
199
        if (null === $client = $this->clientRepository->find($clientId)) {
200
            throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_REQUEST, 'Unable to find the client.');
201
        }
202
203
        return $client;
204
    }
205
206
    /**
207
     * @param AccessToken $accessToken
208
     *
209
     * @throws OAuth2Message
210
     *
211
     * @return UserAccount
212
     */
213
    private function getUserAccount(AccessToken $accessToken): UserAccount
214
    {
215
        $userAccountId = $accessToken->getResourceOwnerId();
216
        if (!$userAccountId instanceof UserAccountId || null === $userAccount = $this->userAccountRepository->find($userAccountId)) {
217
            throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_REQUEST, 'Unable to find the resource owner.');
218
        }
219
220
        return $userAccount;
221
    }
222
223
    /**
224
     * @param AccessToken $accessToken
225
     *
226
     * @throws OAuth2Message
227
     */
228
    private function checkRedirectUri(AccessToken $accessToken)
229
    {
230
        if (!$accessToken->hasMetadata('redirect_uri')) {
231
            throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_TOKEN, 'The access token has not been issued through the authorization endpoint and cannot be used.');
232
        }
233
    }
234
235
    /**
236
     * @param AccessToken $accessToken
237
     *
238
     * @throws OAuth2Message
239
     */
240
    private function checkScope(AccessToken $accessToken)
241
    {
242
        if (!$accessToken->hasParameter('scope') || !in_array('openid', explode(' ', $accessToken->getParameter('scope')))) {
243
            throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_TOKEN, 'The access token does not contain the "openid" scope.');
244
        }
245
    }
246
}
247