Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 400 and 401] [Versions 400 and 402] [Versions 400 and 403]

   1  <?php
   2  
   3  namespace Packback\Lti1p3;
   4  
   5  use Packback\Lti1p3\Interfaces\ICache;
   6  use Packback\Lti1p3\Interfaces\ICookie;
   7  use Packback\Lti1p3\Interfaces\IDatabase;
   8  
   9  class LtiOidcLogin
  10  {
  11      public const COOKIE_PREFIX = 'lti1p3_';
  12  
  13      public const ERROR_MSG_LAUNCH_URL = 'No launch URL configured';
  14      public const ERROR_MSG_ISSUER = 'Could not find issuer';
  15      public const ERROR_MSG_LOGIN_HINT = 'Could not find login hint';
  16      public const ERROR_MSG_REGISTRATION = 'Could not find registration details';
  17  
  18      private $db;
  19      private $cache;
  20      private $cookie;
  21  
  22      /**
  23       * Constructor.
  24       *
  25       * @param IDatabase $database instance of the database interface used for looking up registrations and deployments
  26       * @param ICache    $cache    Instance of the Cache interface used to loading and storing launches. If non is provided launch data will be store in $_SESSION.
  27       * @param ICookie   $cookie   Instance of the Cookie interface used to set and read cookies. Will default to using $_COOKIE and setcookie.
  28       */
  29      public function __construct(IDatabase $database, ICache $cache = null, ICookie $cookie = null)
  30      {
  31          $this->db = $database;
  32          $this->cache = $cache;
  33          $this->cookie = $cookie;
  34      }
  35  
  36      /**
  37       * Static function to allow for method chaining without having to assign to a variable first.
  38       */
  39      public static function new(IDatabase $database, ICache $cache = null, ICookie $cookie = null)
  40      {
  41          return new LtiOidcLogin($database, $cache, $cookie);
  42      }
  43  
  44      /**
  45       * Calculate the redirect location to return to based on an OIDC third party initiated login request.
  46       *
  47       * @param string       $launch_url URL to redirect back to after the OIDC login. This URL must match exactly a URL white listed in the platform.
  48       * @param array|string $request    An array of request parameters. If not set will default to $_REQUEST.
  49       *
  50       * @return Redirect returns a redirect object containing the fully formed OIDC login URL
  51       */
  52      public function doOidcLoginRedirect($launch_url, array $request = null)
  53      {
  54          if ($request === null) {
  55              $request = $_REQUEST;
  56          }
  57  
  58          if (empty($launch_url)) {
  59              throw new OidcException(static::ERROR_MSG_LAUNCH_URL, 1);
  60          }
  61  
  62          // Validate Request Data.
  63          $registration = $this->validateOidcLogin($request);
  64  
  65          /*
  66           * Build OIDC Auth Response.
  67           */
  68  
  69          // Generate State.
  70          // Set cookie (short lived)
  71          $state = static::secureRandomString('state-');
  72          $this->cookie->setCookie(static::COOKIE_PREFIX.$state, $state, 60);
  73  
  74          // Generate Nonce.
  75          $nonce = static::secureRandomString('nonce-');
  76          $this->cache->cacheNonce($nonce, $state);
  77  
  78          // Build Response.
  79          $auth_params = [
  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' => $registration->getClientId(), // Registered client id.
  85              'redirect_uri' => $launch_url, // 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          // Pass back LTI message hint if we have it.
  92          if (isset($request['lti_message_hint'])) {
  93              // LTI message hint to identify LTI context within the platform.
  94              $auth_params['lti_message_hint'] = $request['lti_message_hint'];
  95          }
  96  
  97          $auth_login_return_url = $registration->getAuthLoginUrl().'?'.http_build_query($auth_params, '', '&');
  98  
  99          // Return auth redirect.
 100          return new Redirect($auth_login_return_url, http_build_query($request, '', '&'));
 101      }
 102  
 103      public function validateOidcLogin($request)
 104      {
 105          // Validate Issuer.
 106          if (empty($request['iss'])) {
 107              throw new OidcException(static::ERROR_MSG_ISSUER, 1);
 108          }
 109  
 110          // Validate Login Hint.
 111          if (empty($request['login_hint'])) {
 112              throw new OidcException(static::ERROR_MSG_LOGIN_HINT, 1);
 113          }
 114  
 115          // Fetch Registration Details.
 116          $registration = $this->db->findRegistrationByIssuer($request['iss'], $request['client_id'] ?? null);
 117  
 118          // Check we got something.
 119          if (empty($registration)) {
 120              throw new OidcException(static::ERROR_MSG_REGISTRATION, 1);
 121          }
 122  
 123          // Return Registration.
 124          return $registration;
 125      }
 126  
 127      public static function secureRandomString(string $prefix = ''): string
 128      {
 129          return $prefix.hash('sha256', random_bytes(64));
 130      }
 131  }