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