Skip to content

Commit

Permalink
MGMT-132: Add methods for validating and computing oauth_consumer_key…
Browse files Browse the repository at this point in the history
…_sign
  • Loading branch information
erikdonohoo committed Sep 11, 2023
1 parent 99c825e commit ed910da
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 11 deletions.
31 changes: 27 additions & 4 deletions src/Interfaces/IDatabase.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,39 @@

namespace Packback\Lti1p3\Interfaces;

use Packback\Lti1p3\LtiDepoyment;
use Packback\Lti1p3\Lti1p1Installation;
use Packback\Lti1p3\LtiDeployment;
use Packback\Lti1p3\LtiRegistration;

interface IDatabase
{
public function findRegistrationByIssuer($iss, $clientId = null): ?LtiRegistration;

public function findDeployment($iss, $deploymentId, $clientId = null): ?LtiDepoyment;
public function findDeployment($iss, $deploymentId, $clientId = null): ?LtiDeployment;

public function hasMatchingLti11Key(string $oauthConsumerKeySign): bool;
/**
* A method to assist with 1.1 -> 1.3 migrations. If you don't support migrations
* simply have this method return false.
*
* Otherwise, using the $launchData from
* a 1.3 launch attempt, determine if you have a matching 1.1 install and return
* it from this method.
*
* @param array $launchData
* @return Lti1p1Installation|null
*/
public function getMatchingLti1p1Install(array $launchData): ?Lti1p1Installation;

public function migrateFromLti11(array $launchData);
/**
* Another method to assist with 1.1 -> 1.3 migrations. Simply have this method do nothing
* if you don't support migrations.
*
* Otherwise, this method create a 1.3 deployment in your DB based on the $launchData.
* Previous to this, we validated the oauth_consumer_key_sign to ensure this migration
* can safely occur.
*
* @param array $launchData
* @return void
*/
public function migrateFromLti1p1(array $launchData);
}
41 changes: 41 additions & 0 deletions src/Lti1p1Install.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace Packback\Lti1p3;

class Lti1p1Installation
{
private string $oauth_consumer_key;
private string $oauth_consumer_secret;

public static function new()
{
return new Lti1p1Installation();
}

public function computeOauthConsumerKeySign (array $lti1p3LaunchData): string
{

}

public function getOauthConsumerKey()
{
return $this->oauth_consumer_key;
}

public function setOauthConsumerKey($oauth_consumer_key)
{
$this->oauth_consumer_key = $oauth_consumer_key;
return $this;
}

public function getOauthConsumerSecret()
{
return $this->oauth_consumer_secret;
}

public function setOauthConsumerSecret($oauth_consumer_secret)
{
$this->oauth_consumer_secret = $oauth_consumer_secret;
return $this;
}
}
65 changes: 58 additions & 7 deletions src/LtiMessageLaunch.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Packback\Lti1p3\MessageValidators\DeepLinkMessageValidator;
use Packback\Lti1p3\MessageValidators\ResourceMessageValidator;
use Packback\Lti1p3\MessageValidators\SubmissionReviewMessageValidator;
use Throwable;

class LtiMessageLaunch
{
Expand All @@ -28,6 +29,8 @@ class LtiMessageLaunch
public const ERR_INVALID_ID_TOKEN = 'Invalid id_token, JWT must contain 3 parts';
public const ERR_MISSING_NONCE = 'Missing Nonce.';
public const ERR_INVALID_NONCE = 'Invalid Nonce.';
public const ERR_OAUTH_CONSUMER_KEY_SIGN_MISMATCH = 'Failed to migrate from 1.1 -> 1.3, oauth_consumer_key_sign did not match';
public const ERR_MISSING_OAUTH_CONSUMER_KEY_SIGN = 'Failed to migrate from 1.1 -> 1.3, oauth_consumer_key_sign was not provided';

/**
* :issuerUrl and :clientId are used to substitute the queried issuerUrl
Expand Down Expand Up @@ -135,7 +138,7 @@ public function initialize(array $request = null): static

// This feels kinda icky in the middle of all the validation. How can I move it?
if ($this->shouldMigrate()) {
$this->deployment = $this->db->migrateFromLti11($this->getLaunchData());
$this->db->migrateFromLti1p1($this->getLaunchData());
}

$this->validateDeployment()
Expand All @@ -159,10 +162,56 @@ public function setRequest(array $request = null)

private function shouldMigrate(): bool
{
return $this->hasMigrationStrategy()
// I don't like this here. Move it later
&& isset($this->jwt['body'][LtiConstants::DEPLOYMENT_ID])
&& $this->db->hasMatchingLti11Key($this->getLaunchData()['oauth_consumer_key_sign']);
$missingDeployment = false;

try {
$this->validateDeployment();
} catch (LtiException $e) {
if ($e->getMessage() === static::ERR_NO_DEPLOYMENT) {
$missingDeployment = true;
}
} catch (Throwable $e) {
return false;
}

$launchData = $this->getLaunchData();
$lti1p1Install = $this->db->getMatchingLti1p1Install($launchData);
if ($missingDeployment && $lti1p1Install !== null) {
return $this->lti1p1InstallationSignatureMatches($lti1p1Install, $launchData);
}

return false;
}

private function lti1p1InstallationSignatureMatches (Lti1p1Installation $ltiInstall, array $launchData): bool
{
// Check to see if we have params to calculate oauth_consumer_key_sign
if (!isset($launchData[LtiConstants::LTI1P1]) || !isset($launchData[LtiConstants::LTI1P1]['oauth_consumer_key_sign'])) {
throw new LtiException(static::ERR_MISSING_OAUTH_CONSUMER_KEY_SIGN);
}

$lti1p1Claim = $launchData[LtiConstants::LTI1P1];
$signature = $lti1p1Claim['oauth_consumer_key_sign'];

$clientId = is_array($launchData['aud']) ? $launchData['aud'][0] : $launchData['aud'];
$issuerUrl = $launchData['iss'];
$exp = $launchData['exp'];
$nonce = $launchData['nonce'];

// Create signature
$baseString = "{$ltiInstall->getOauthConsumerKey()}&\
{$launchData[LtiConstants::DEPLOYMENT_ID]}&\
{$issuerUrl}&\
{$clientId}&\
{$exp}&\
{$nonce}";

$computedSignature = base64_encode(hash_hmac('sha256', $baseString, $ltiInstall->getOauthConsumerSecret(), true));
if ($computedSignature !== $signature) {
throw new LtiException(static::ERR_OAUTH_CONSUMER_KEY_SIGN_MISMATCH);
}

return true;
}

/**
Expand All @@ -172,10 +221,12 @@ private function shouldMigrate(): bool
* @return LtiMessageLaunch Will return $this if validation is successful
*
* @throws LtiException Will throw an LtiException if validation fails
* @deprecated Does not support LTI 1.1 migration. Use `initialize` instead.
*/
public function validate()
public function validate(array $request = null)
{
// Deprecate? Move to it's own validator?
$this->setRequest($request);

return $this->validateState()
->validateJwtFormat()
->validateNonce()
Expand Down

0 comments on commit ed910da

Please sign in to comment.