| 1 | 
           efrain | 
           1 | 
           <?php
  | 
        
        
            | 
            | 
           2 | 
              | 
        
        
            | 
            | 
           3 | 
           namespace Packback\Lti1p3;
  | 
        
        
            | 
            | 
           4 | 
              | 
        
        
            | 
            | 
           5 | 
           use Packback\Lti1p3\Helpers\Helpers;
  | 
        
        
            | 
            | 
           6 | 
           use Packback\Lti1p3\Interfaces\ICache;
  | 
        
        
            | 
            | 
           7 | 
           use Packback\Lti1p3\Interfaces\ICookie;
  | 
        
        
            | 
            | 
           8 | 
           use Packback\Lti1p3\Interfaces\IDatabase;
  | 
        
        
            | 
            | 
           9 | 
           use Packback\Lti1p3\Interfaces\ILtiRegistration;
  | 
        
        
            | 
            | 
           10 | 
              | 
        
        
            | 
            | 
           11 | 
           class LtiOidcLogin
  | 
        
        
            | 
            | 
           12 | 
           {
  | 
        
        
            | 
            | 
           13 | 
               public const COOKIE_PREFIX = 'lti1p3_';
  | 
        
        
            | 
            | 
           14 | 
               public const ERROR_MSG_LAUNCH_URL = 'No launch URL configured';
  | 
        
        
            | 
            | 
           15 | 
               public const ERROR_MSG_ISSUER = 'Could not find issuer';
  | 
        
        
            | 
            | 
           16 | 
               public const ERROR_MSG_LOGIN_HINT = 'Could not find login hint';
  | 
        
        
            | 
            | 
           17 | 
              | 
        
        
            | 
            | 
           18 | 
               public function __construct(
  | 
        
        
            | 
            | 
           19 | 
                   public IDatabase $db,
  | 
        
        
            | 
            | 
           20 | 
                   public ICache $cache,
  | 
        
        
            | 
            | 
           21 | 
                   public ICookie $cookie
  | 
        
        
            | 
            | 
           22 | 
               ) {
  | 
        
        
            | 
            | 
           23 | 
               }
  | 
        
        
            | 
            | 
           24 | 
              | 
        
        
            | 
            | 
           25 | 
               /**
  | 
        
        
            | 
            | 
           26 | 
                * Static function to allow for method chaining without having to assign to a variable first.
  | 
        
        
            | 
            | 
           27 | 
                */
  | 
        
        
            | 
            | 
           28 | 
               public static function new(IDatabase $db, ICache $cache, ICookie $cookie): self
  | 
        
        
            | 
            | 
           29 | 
               {
  | 
        
        
            | 
            | 
           30 | 
                   return new LtiOidcLogin($db, $cache, $cookie);
  | 
        
        
            | 
            | 
           31 | 
               }
  | 
        
        
            | 
            | 
           32 | 
              | 
        
        
            | 
            | 
           33 | 
               /**
  | 
        
        
            | 
            | 
           34 | 
                * Calculate the redirect location to return to based on an OIDC third party initiated login request.
  | 
        
        
            | 
            | 
           35 | 
                */
  | 
        
        
            | 
            | 
           36 | 
               public function getRedirectUrl(string $launchUrl, array $request): string
  | 
        
        
            | 
            | 
           37 | 
               {
  | 
        
        
            | 
            | 
           38 | 
                   // Validate request data.
  | 
        
        
            | 
            | 
           39 | 
                   $registration = $this->validateOidcLogin($request);
  | 
        
        
            | 
            | 
           40 | 
              | 
        
        
            | 
            | 
           41 | 
                   // Build OIDC Auth response.
  | 
        
        
            | 
            | 
           42 | 
                   $authParams = $this->getAuthParams($launchUrl, $registration->getClientId(), $request);
  | 
        
        
            | 
            | 
           43 | 
              | 
        
        
            | 
            | 
           44 | 
                   return Helpers::buildUrlWithQueryParams($registration->getAuthLoginUrl(), $authParams);
  | 
        
        
            | 
            | 
           45 | 
               }
  | 
        
        
            | 
            | 
           46 | 
              | 
        
        
            | 
            | 
           47 | 
               public function validateOidcLogin(array $request): ILtiRegistration
  | 
        
        
            | 
            | 
           48 | 
               {
  | 
        
        
            | 
            | 
           49 | 
                   if (!isset($request['iss'])) {
  | 
        
        
            | 
            | 
           50 | 
                       throw new OidcException(static::ERROR_MSG_ISSUER);
  | 
        
        
            | 
            | 
           51 | 
                   }
  | 
        
        
            | 
            | 
           52 | 
              | 
        
        
            | 
            | 
           53 | 
                   if (!isset($request['login_hint'])) {
  | 
        
        
            | 
            | 
           54 | 
                       throw new OidcException(static::ERROR_MSG_LOGIN_HINT);
  | 
        
        
            | 
            | 
           55 | 
                   }
  | 
        
        
            | 
            | 
           56 | 
              | 
        
        
            | 
            | 
           57 | 
                   // Fetch registration
  | 
        
        
            | 
            | 
           58 | 
                   $clientId = $request['client_id'] ?? null;
  | 
        
        
            | 
            | 
           59 | 
                   $registration = $this->db->findRegistrationByIssuer($request['iss'], $clientId);
  | 
        
        
            | 
            | 
           60 | 
              | 
        
        
            | 
            | 
           61 | 
                   if (!isset($registration)) {
  | 
        
        
            | 
            | 
           62 | 
                       $errorMsg = LtiMessageLaunch::getMissingRegistrationErrorMsg($request['iss'], $clientId);
  | 
        
        
            | 
            | 
           63 | 
              | 
        
        
            | 
            | 
           64 | 
                       throw new OidcException($errorMsg);
  | 
        
        
            | 
            | 
           65 | 
                   }
  | 
        
        
            | 
            | 
           66 | 
              | 
        
        
            | 
            | 
           67 | 
                   return $registration;
  | 
        
        
            | 
            | 
           68 | 
               }
  | 
        
        
            | 
            | 
           69 | 
              | 
        
        
            | 
            | 
           70 | 
               public function getAuthParams(string $launchUrl, string $clientId, array $request): array
  | 
        
        
            | 
            | 
           71 | 
               {
  | 
        
        
            | 
            | 
           72 | 
                   // Set cookie (short lived)
  | 
        
        
            | 
            | 
           73 | 
                   $state = static::secureRandomString('state-');
  | 
        
        
            | 
            | 
           74 | 
                   $this->cookie->setCookie(static::COOKIE_PREFIX.$state, $state, 60);
  | 
        
        
            | 
            | 
           75 | 
              | 
        
        
            | 
            | 
           76 | 
                   $nonce = static::secureRandomString('nonce-');
  | 
        
        
            | 
            | 
           77 | 
                   $this->cache->cacheNonce($nonce, $state);
  | 
        
        
            | 
            | 
           78 | 
              | 
        
        
            | 
            | 
           79 | 
                   $authParams = [
  | 
        
        
            | 
            | 
           80 | 
                       'scope' => 'openid', // OIDC Scope.
  | 
        
        
            | 
            | 
           81 | 
                       'response_type' => 'id_token', // OIDC response is always an id token.
  | 
        
        
            | 
            | 
           82 | 
                       'response_mode' => 'form_post', // OIDC response is always a form post.
  | 
        
        
            | 
            | 
           83 | 
                       'prompt' => 'none', // Don't prompt user on redirect.
  | 
        
        
            | 
            | 
           84 | 
                       'client_id' => $clientId, // Registered client id.
  | 
        
        
            | 
            | 
           85 | 
                       'redirect_uri' => $launchUrl, // URL to return to after login.
  | 
        
        
            | 
            | 
           86 | 
                       'state' => $state, // State to identify browser session.
  | 
        
        
            | 
            | 
           87 | 
                       'nonce' => $nonce, // Prevent replay attacks.
  | 
        
        
            | 
            | 
           88 | 
                       'login_hint' => $request['login_hint'], // Login hint to identify platform session.
  | 
        
        
            | 
            | 
           89 | 
                   ];
  | 
        
        
            | 
            | 
           90 | 
              | 
        
        
            | 
            | 
           91 | 
                   if (isset($request['lti_message_hint'])) {
  | 
        
        
            | 
            | 
           92 | 
                       // LTI message hint to identify LTI context within the platform.
  | 
        
        
            | 
            | 
           93 | 
                       $authParams['lti_message_hint'] = $request['lti_message_hint'];
  | 
        
        
            | 
            | 
           94 | 
                   }
  | 
        
        
            | 
            | 
           95 | 
              | 
        
        
            | 
            | 
           96 | 
                   return $authParams;
  | 
        
        
            | 
            | 
           97 | 
               }
  | 
        
        
            | 
            | 
           98 | 
              | 
        
        
            | 
            | 
           99 | 
               public static function secureRandomString(string $prefix = ''): string
  | 
        
        
            | 
            | 
           100 | 
               {
  | 
        
        
            | 
            | 
           101 | 
                   return $prefix.hash('sha256', random_bytes(64));
  | 
        
        
            | 
            | 
           102 | 
               }
  | 
        
        
            | 
            | 
           103 | 
           }
  |