Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
 
3
namespace Packback\Lti1p3;
4
 
5
use Exception;
6
use Firebase\JWT\JWT;
7
use GuzzleHttp\Client;
8
use GuzzleHttp\Exception\ClientException;
9
use Packback\Lti1p3\Interfaces\ICache;
10
use Packback\Lti1p3\Interfaces\ILtiRegistration;
11
use Packback\Lti1p3\Interfaces\ILtiServiceConnector;
12
use Packback\Lti1p3\Interfaces\IServiceRequest;
13
use Psr\Http\Message\ResponseInterface;
14
 
15
class LtiServiceConnector implements ILtiServiceConnector
16
{
17
    public const NEXT_PAGE_REGEX = '/<([^>]*)>; ?rel="next"/i';
18
    private bool $debuggingMode = false;
19
 
20
    public function __construct(
21
        private ICache $cache,
22
        private Client $client
23
    ) {
24
    }
25
 
26
    public function setDebuggingMode(bool $enable): self
27
    {
28
        $this->debuggingMode = $enable;
29
 
30
        return $this;
31
    }
32
 
33
    public function getAccessToken(ILtiRegistration $registration, array $scopes): string
34
    {
35
        // Get a unique cache key for the access token
36
        $accessTokenKey = $this->getAccessTokenCacheKey($registration, $scopes);
37
        // Get access token from cache if it exists
38
        $accessToken = $this->cache->getAccessToken($accessTokenKey);
39
 
40
        if (isset($accessToken)) {
41
            return $accessToken;
42
        }
43
 
44
        // Build up JWT to exchange for an auth token
45
        $clientId = $registration->getClientId();
46
        $jwtClaim = [
47
            'iss' => $clientId,
48
            'sub' => $clientId,
49
            'aud' => $registration->getAuthServer(),
50
            'iat' => time() - 5,
51
            'exp' => time() + 60,
52
            'jti' => 'lti-service-token'.hash('sha256', random_bytes(64)),
53
        ];
54
 
55
        // Sign the JWT with our private key (given by the platform on registration)
56
        $jwt = JWT::encode($jwtClaim, $registration->getToolPrivateKey(), 'RS256', $registration->getKid());
57
 
58
        // Build auth token request headers
59
        $authRequest = [
60
            'grant_type' => 'client_credentials',
61
            'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
62
            'client_assertion' => $jwt,
63
            'scope' => implode(' ', $scopes),
64
        ];
65
 
66
        // Get Access
67
        $request = new ServiceRequest(
68
            ServiceRequest::METHOD_POST,
69
            $registration->getAuthTokenUrl(),
70
            ServiceRequest::TYPE_AUTH
71
        );
72
        $request->setPayload(['form_params' => $authRequest])
73
            ->setMaskResponseLogs(true);
74
        $response = $this->makeRequest($request);
75
 
76
        $tokenData = $this->getResponseBody($response);
77
 
78
        // Cache access token
79
        $this->cache->cacheAccessToken($accessTokenKey, $tokenData['access_token']);
80
 
81
        return $tokenData['access_token'];
82
    }
83
 
84
    public function makeRequest(IServiceRequest $request): ResponseInterface
85
    {
86
        $response = $this->client->request(
87
            $request->getMethod(),
88
            $request->getUrl(),
89
            $request->getPayload()
90
        );
91
 
92
        if ($this->debuggingMode) {
93
            $this->logRequest(
94
                $request,
95
                $this->getResponseHeaders($response),
96
                $this->getResponseBody($response)
97
            );
98
        }
99
 
100
        return $response;
101
    }
102
 
103
    public function getResponseHeaders(ResponseInterface $response): ?array
104
    {
105
        $responseHeaders = $response->getHeaders();
106
        array_walk($responseHeaders, function (&$value) {
107
            $value = $value[0];
108
        });
109
 
110
        return $responseHeaders;
111
    }
112
 
113
    public function getResponseBody(ResponseInterface $response): ?array
114
    {
115
        $responseBody = (string) $response->getBody();
116
 
117
        return json_decode($responseBody, true);
118
    }
119
 
120
    public function makeServiceRequest(
121
        ILtiRegistration $registration,
122
        array $scopes,
123
        IServiceRequest $request,
124
        bool $shouldRetry = true
125
    ): array {
126
        $request->setAccessToken($this->getAccessToken($registration, $scopes));
127
 
128
        try {
129
            $response = $this->makeRequest($request);
130
        } catch (ClientException $e) {
131
            $status = $e->getResponse()->getStatusCode();
132
 
133
            // If the error was due to invalid authentication and the request
134
            // should be retried, clear the access token and retry it.
135
            if ($status === 401 && $shouldRetry) {
136
                $key = $this->getAccessTokenCacheKey($registration, $scopes);
137
                $this->cache->clearAccessToken($key);
138
 
139
                return $this->makeServiceRequest($registration, $scopes, $request, false);
140
            }
141
 
142
            throw $e;
143
        }
144
 
145
        return [
146
            'headers' => $this->getResponseHeaders($response),
147
            'body' => $this->getResponseBody($response),
148
            'status' => $response->getStatusCode(),
149
        ];
150
    }
151
 
152
    public function getAll(
153
        ILtiRegistration $registration,
154
        array $scopes,
155
        IServiceRequest $request,
156
        ?string $key = null
157
    ): array {
158
        if ($request->getMethod() !== ServiceRequest::METHOD_GET) {
159
            throw new Exception('An invalid method was specified by an LTI service requesting all items.');
160
        }
161
 
162
        $results = [];
163
        $nextUrl = $request->getUrl();
164
 
165
        while ($nextUrl) {
166
            $request->setUrl($nextUrl);
167
            $response = $this->makeServiceRequest($registration, $scopes, $request);
168
            $pageResults = $this->getResultsFromResponse($response, $key);
169
            $results = array_merge($results, $pageResults);
170
            $nextUrl = $this->getNextUrl($response['headers']);
171
        }
172
 
173
        return $results;
174
    }
175
 
176
    public static function getLogMessage(
177
        IServiceRequest $request,
178
        array $responseHeaders,
179
        ?array $responseBody
180
    ): string {
181
        if ($request->getMaskResponseLogs()) {
182
            $responseHeaders = self::maskValues($responseHeaders);
183
            $responseBody = self::maskValues($responseBody);
184
        }
185
 
186
        $contextArray = [
187
            'request_method' => $request->getMethod(),
188
            'request_url' => $request->getUrl(),
189
            'response_headers' => $responseHeaders,
190
            'response_body' => $responseBody,
191
        ];
192
 
193
        $requestBody = $request->getPayload()['body'] ?? null;
194
 
195
        if (isset($requestBody)) {
196
            $contextArray['request_body'] = $requestBody;
197
        }
198
 
199
        return implode(' ', array_filter([
200
            $request->getErrorPrefix(),
201
            json_decode($requestBody)->userId ?? null,
202
            json_encode($contextArray),
203
        ]));
204
    }
205
 
206
    private function logRequest(
207
        IServiceRequest $request,
208
        array $responseHeaders,
209
        ?array $responseBody
210
    ): void {
211
        error_log(self::getLogMessage($request, $responseHeaders, $responseBody));
212
    }
213
 
214
    private static function maskValues(?array $payload): ?array
215
    {
216
        if (!isset($payload) || empty($payload)) {
217
            return $payload;
218
        }
219
 
220
        foreach ($payload as $key => $value) {
221
            $payload[$key] = '***';
222
        }
223
 
224
        return $payload;
225
    }
226
 
227
    private function getAccessTokenCacheKey(ILtiRegistration $registration, array $scopes): string
228
    {
229
        sort($scopes);
230
        $scopeKey = md5(implode('|', $scopes));
231
 
232
        return $registration->getIssuer().$registration->getClientId().$scopeKey;
233
    }
234
 
235
    private function getResultsFromResponse(array $response, ?string $key = null): array
236
    {
237
        if (isset($key)) {
238
            return $response['body'][$key] ?? [];
239
        }
240
 
241
        return $response['body'] ?? [];
242
    }
243
 
244
    private function getNextUrl(array $headers): ?string
245
    {
246
        $subject = $headers['Link'] ?? $headers['link'] ?? '';
247
        preg_match(static::NEXT_PAGE_REGEX, $subject, $matches);
248
 
249
        return $matches[1] ?? null;
250
    }
251
}