sqrl-2.0.0-rc1/src/Nut.php
src/Nut.php
<?php
namespace Drupal\sqrl;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\sqrl\Exception\NutException;
use Drupal\user\Entity\User;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Provides nut services.
*/
final class Nut implements ContainerInjectionInterface {
use StringManipulation;
use StringTranslationTrait;
public const MODE_BUILD = 'build';
public const MODE_FETCH = 'fetch';
public const IS_COOKIE = FALSE;
public const STATUS_INITED = 'inited';
public const STATUS_INVALID = 'invalid';
public const STATUS_BUILT = 'built';
public const STATUS_FETCHED = 'fetched';
/**
* The mode.
*
* @var string
*/
private string $mode = self::MODE_BUILD;
/**
* The status.
*
* @var string
*/
private string $status = self::STATUS_INITED;
/**
* The expiry status.
*
* @var bool
*/
private bool $expired;
/**
* The public nut.
*
* @var string
*/
protected string $nutPublic;
/**
* The client time.
*
* @var int
*/
protected int $clientTime;
/**
* The client IP address.
*
* @var string
*/
protected string $clientIP;
/**
* The client operation.
*
* @var string
*/
protected string $clientOperation;
/**
* The client operation parameters.
*
* @var array
*/
protected array $clientOperationParams = [];
/**
* The client messages.
*
* @var array
*/
protected array $clientMessages = [];
/**
* The client UID.
*
* @var int
*/
protected int $clientUid;
/**
* The client login token.
*
* @var string
*/
protected string $clientLoginToken;
/**
* The client cancel token.
*
* @var string
*/
protected string $clientCancelToken;
/**
* The time service.
*
* @var \Drupal\Component\Datetime\TimeInterface
*/
protected TimeInterface $time;
/**
* The request.
*
* @var \Symfony\Component\HttpFoundation\Request
*/
protected Request $request;
/**
* The state.
*
* @var \Drupal\sqrl\State
*/
protected State $state;
/**
* The sqrl service.
*
* @var \Drupal\sqrl\Sqrl
*/
protected Sqrl $sqrl;
/**
* The log channel.
*
* @var \Drupal\sqrl\Log
*/
protected Log $log;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected AccountInterface $currentUser;
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected MessengerInterface $messenger;
/**
* Constructs the nut service.
*
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
* @param \Symfony\Component\HttpFoundation\RequestStack $request
* The request stack.
* @param \Drupal\sqrl\State $state
* The state.
* @param \Drupal\sqrl\Sqrl $sqrl
* The sqrl service.
* @param \Drupal\sqrl\Log $log
* The log channel.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
*/
public function __construct(AccountInterface $current_user, TimeInterface $time, RequestStack $request, State $state, Sqrl $sqrl, Log $log, MessengerInterface $messenger) {
$this->currentUser = $current_user;
$this->time = $time;
$this->request = $request->getCurrentRequest();
$this->state = $state;
$this->sqrl = $sqrl;
$this->log = $log;
$this->messenger = $messenger;
$this->clientLoginToken = $this->base64Encode($this->randomBytes(8));
$this->clientCancelToken = $this->base64Encode($this->randomBytes(8));
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): Nut {
return new Nut(
$container->get('current_user'),
$container->get('datetime.time'),
$container->get('request_stack'),
$container->get('sqrl.state'),
$container->get('sqrl.handler'),
$container->get('sqrl.log'),
$container->get('messenger')
);
}
/**
* Sets the client operation.
*
* @param string $op
* The client operation.
*
* @return self
* This nut service.
*/
public function setClientOperation(string $op): Nut {
$this->clientOperation = $op;
return $this;
}
/**
* Gets the client operation.
*
* @return string
* The client operation.
*/
public function getClientOperation(): string {
return $this->clientOperation;
}
/**
* Gets the client UID.
*
* @return int
* The client UID.
*/
public function getClientUid(): int {
return $this->clientUid;
}
/**
* Gets the login token.
*
* @return string
* The login token.
*/
public function getLoginToken(): string {
return $this->clientLoginToken;
}
/**
* Gets the cancel token.
*
* @return string
* The cancel token.
*/
public function getCancelToken(): string {
return $this->clientCancelToken;
}
/**
* Gets the public nut.
*
* @return string
* The public nut.
*
* @throws \JsonException
*/
public function getPublicNut(): string {
$this->build();
return $this->nutPublic;
}
/**
* Determines if the nut is valid.
*
* @return bool
* TRUE, if the nut is valid, FALSE otherwise.
*/
public function isValid(): bool {
return ($this->status !== self::STATUS_INVALID);
}
/**
* Determines if the nut is expired.
*
* @return bool
* TRUE, if the nut is expired, FALSE otherwise.
*/
public function isExpired(): bool {
return $this->expired ?? FALSE;
}
/**
* Determines if the client IP is valid.
*
* @return bool
* TRUE, if the client IP is valid, FALSE otherwise.
*/
public function isIpValid(): bool {
return ($this->clientIP === $this->request->getClientIp());
}
/**
* Gets all the parameters.
*
* @return array
* Key values pairs of all parameters.
*/
private function getParams(): array {
return [
'time' => $this->time->getRequestTime(),
'op' => $this->getClientOperation(),
'ip' => $this->request->getClientIp(),
'params' => $this->clientOperationParams,
'messages to browser' => $this->clientMessages,
'uid' => $this->currentUser->id(),
'login token' => $this->clientLoginToken,
'cancel token' => $this->clientCancelToken,
];
}
/**
* Builds the nut.
*
* @throws \JsonException
*/
private function build(): void {
if ($this->mode !== self::MODE_BUILD || $this->status === self::STATUS_BUILT) {
return;
}
$this->status = self::STATUS_BUILT;
$this->nutPublic = $this->base64Encode($this->randomBytes(8));
$this->state->setNut($this->nutPublic, $this->getParams());
}
/**
* Gets the nut as a string.
*
* @return string
* The nut.
*
* @throws \JsonException
*/
public function __toString(): string {
return $this->getPublicNut();
}
/**
* Receives and validates the nut.
*/
public function fetch(): void {
$this->mode = self::MODE_FETCH;
if ($this->status === self::STATUS_FETCHED) {
return;
}
try {
$this->nutPublic = $this->fetchNut();
$this->load();
$this->validateExpiration();
$this->status = self::STATUS_FETCHED;
}
catch (NutException | \JsonException $e) {
// @todo Logging.
$this->status = self::STATUS_INVALID;
$this->log->debug('Fetch NUT error: ' . $e->getMessage());
}
}
/**
* Gets a list of possible accounts for a form select widget.
*
* @return array
* The list of possible accounts.
*
* @throws \JsonException
*/
public function getAccountsForSelect(): array {
$result = [];
foreach ($this->state->getAuth($this->getPublicNut(), FALSE) as $uid) {
/** @var \Drupal\user\UserInterface $user */
$user = User::load($uid);
if ($user->isActive()) {
$result[$uid] = $user->label();
}
}
return $result;
}
/**
* Callback to poll for the public nut while the browser is waiting.
*
* @param string|null $token
* The token.
*
* @return \Drupal\Core\Url|null
* The redirect url, if the nut has been received and validated, NULL
* otherwise.
*
* @throws \JsonException
*/
public function poll(?string $token = NULL): ?Url {
$uids = $this->state->getAuth($this->getPublicNut());
if (!empty($uids)) {
$op = $this->getClientOperation();
$route = 'user.page';
switch ($op) {
case 'login':
case 'register':
/** @var \Drupal\user\UserInterface[] $users */
$users = [];
foreach ($uids as $uid) {
/** @var \Drupal\user\UserInterface $user */
$user = User::load($uid);
if ($user->isActive()) {
$users[] = $user;
}
}
$count = count($users);
if ($count === 0) {
// None of the user accounts linked to the SQRL ID is active.
$this->messenger->addError($this->t('No active user account, you can not login with this SQRL identity.'));
return NULL;
}
if ($count === 1) {
// Exactly one active user account is linked to the SQRL ID, let
// them log in.
$account = reset($users);
user_login_finalize($account);
}
else {
// More than one active user accounts are linked to the SQRL ID,
// the user needs to select which one to log into.
$this->state->setAuth($this->getPublicNut(), $users);
$this->sqrl->setNut($this);
return $this->sqrl->getUrl('sqrl.ident.select', ['token' => $token]);
}
break;
case 'link':
// User linked their account to their SQRL identity.
break;
case 'unlink':
// User unlinked their account from their SQRL identity.
break;
case 'profile':
// More than one active user accounts are linked to the SQRL ID,
// the user needs to select which one to log into.
$this->state->setAuth($this->getPublicNut(), $this->getClientUid());
$this->sqrl->setNut($this);
return $this->sqrl->getUrl('sqrl.profile.edit', [
'user' => $this->getClientUid(),
'token' => $token,
]);
}
foreach ($this->state->getMessages($this->getPublicNut()) as $message) {
$this->messenger->addMessage($message['message'], $message['type']);
}
return Url::fromRoute($route);
}
return NULL;
}
/**
* Extract nut from the get request.
*
* @return string
* The nut.
*
* @throws \Drupal\sqrl\Exception\NutException
*/
private function fetchNut(): string {
$nut = $this->request->query->get('nut');
if (!$nut) {
throw new NutException('Nut missing from GET request');
}
return $nut;
}
/**
* Validates the expiration time.
*/
private function validateExpiration(): void {
$this->expired = FALSE;
if ($this->clientTime + State::EXPIRE_NUT < $this->time->getRequestTime()) {
$this->expired = TRUE;
}
}
/**
* Extract all arguments from the nut.
*
* @throws \Drupal\sqrl\Exception\NutException
* @throws \JsonException
*/
private function load(): void {
$params = $this->state->getNut($this->nutPublic);
if (empty($params)) {
throw new NutException('No params received from implementing framework');
}
if (empty($params['time'])) {
throw new NutException('Wrong params received from implementing framework');
}
if (empty($params['op'])) {
throw new NutException('Wrong params received from implementing framework');
}
if (empty($params['ip'])) {
throw new NutException('Wrong params received from implementing framework');
}
if (!isset($params['params']) || !is_array($params['params'])) {
throw new NutException('Wrong params received from implementing framework');
}
if (!isset($params['messages to browser']) || !is_array($params['messages to browser'])) {
throw new NutException('Wrong params received from implementing framework');
}
if (!isset($params['uid'])) {
throw new NutException('Wrong params received from implementing framework');
}
if (!isset($params['login token'])) {
throw new NutException('Wrong params received from implementing framework');
}
if (!isset($params['cancel token'])) {
throw new NutException('Wrong params received from implementing framework');
}
$this->clientTime = $params['time'];
$this->clientOperation = $params['op'];
$this->clientIP = $params['ip'];
$this->clientOperationParams = $params['params'];
$this->clientMessages = $params['messages to browser'];
$this->clientUid = $params['uid'];
$this->clientLoginToken = $params['login token'];
$this->clientCancelToken = $params['cancel token'];
}
}
