See Release Notes
Long Term Support Release
Differences Between: [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]
1 <?php 2 3 namespace Packback\Lti1p3; 4 5 use Firebase\JWT\JWT; 6 use Packback\Lti1p3\Interfaces\ICache; 7 use Packback\Lti1p3\Interfaces\IHttpClient; 8 use Packback\Lti1p3\Interfaces\IHttpException; 9 use Packback\Lti1p3\Interfaces\IHttpResponse; 10 use Packback\Lti1p3\Interfaces\ILtiRegistration; 11 use Packback\Lti1p3\Interfaces\ILtiServiceConnector; 12 use Packback\Lti1p3\Interfaces\IServiceRequest; 13 14 class LtiServiceConnector implements ILtiServiceConnector 15 { 16 public const NEXT_PAGE_REGEX = '/<([^>]*)>; ?rel="next"/i'; 17 18 private $cache; 19 private $client; 20 private $debuggingMode = false; 21 22 public function __construct( 23 ICache $cache, 24 IHttpClient $client 25 ) { 26 $this->cache = $cache; 27 $this->client = $client; 28 } 29 30 public function setDebuggingMode(bool $enable): void 31 { 32 $this->debuggingMode = $enable; 33 } 34 35 public function getAccessToken(ILtiRegistration $registration, array $scopes) 36 { 37 // Get a unique cache key for the access token 38 $accessTokenKey = $this->getAccessTokenCacheKey($registration, $scopes); 39 // Get access token from cache if it exists 40 $accessToken = $this->cache->getAccessToken($accessTokenKey); 41 if ($accessToken) { 42 return $accessToken; 43 } 44 45 // Build up JWT to exchange for an auth token 46 $clientId = $registration->getClientId(); 47 $jwtClaim = [ 48 'iss' => $clientId, 49 'sub' => $clientId, 50 'aud' => $registration->getAuthServer(), 51 'iat' => time() - 5, 52 'exp' => time() + 60, 53 'jti' => 'lti-service-token'.hash('sha256', random_bytes(64)), 54 ]; 55 56 // Sign the JWT with our private key (given by the platform on registration) 57 $jwt = JWT::encode($jwtClaim, $registration->getToolPrivateKey(), 'RS256', $registration->getKid()); 58 59 // Build auth token request headers 60 $authRequest = [ 61 'grant_type' => 'client_credentials', 62 'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', 63 'client_assertion' => $jwt, 64 'scope' => implode(' ', $scopes), 65 ]; 66 67 // Get Access 68 $request = new ServiceRequest( 69 ServiceRequest::METHOD_POST, 70 $registration->getAuthTokenUrl(), 71 ServiceRequest::TYPE_AUTH 72 ); 73 $request->setPayload(['form_params' => $authRequest]); 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) 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(IHttpResponse $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(IHttpResponse $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 (IHttpException $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 $response = $this->makeServiceRequest($registration, $scopes, $request); 167 168 $page_results = $key === null ? ($response['body'] ?? []) : ($response['body'][$key] ?? []); 169 $results = array_merge($results, $page_results); 170 171 $nextUrl = $this->getNextUrl($response['headers']); 172 if ($nextUrl) { 173 $request->setUrl($nextUrl); 174 } 175 } 176 177 return $results; 178 } 179 180 private function logRequest( 181 IServiceRequest $request, 182 array $responseHeaders, 183 ?array $responseBody 184 ): void { 185 $contextArray = [ 186 'request_method' => $request->getMethod(), 187 'request_url' => $request->getUrl(), 188 'response_headers' => $responseHeaders, 189 'response_body' => json_encode($responseBody), 190 ]; 191 192 $requestBody = $request->getPayload()['body'] ?? null; 193 194 if (!empty($requestBody)) { 195 $contextArray['request_body'] = $requestBody; 196 } 197 198 error_log(implode(' ', array_filter([ 199 $request->getErrorPrefix(), 200 json_decode($requestBody)->userId ?? null, 201 print_r($contextArray, true), 202 ]))); 203 } 204 205 private function getAccessTokenCacheKey(ILtiRegistration $registration, array $scopes) 206 { 207 sort($scopes); 208 $scopeKey = md5(implode('|', $scopes)); 209 210 return $registration->getIssuer().$registration->getClientId().$scopeKey; 211 } 212 213 private function getNextUrl(array $headers) 214 { 215 $subject = $headers['Link'] ?? ''; 216 preg_match(static::NEXT_PAGE_REGEX, $subject, $matches); 217 218 return $matches[1] ?? null; 219 } 220 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body