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 |
}
|