next-1.0.0-alpha2/modules/next_jwt/src/Plugin/Next/PreviewUrlGenerator/Jwt.php
modules/next_jwt/src/Plugin/Next/PreviewUrlGenerator/Jwt.php
<?php
namespace Drupal\next_jwt\Plugin\Next\PreviewUrlGenerator;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\Url;
use Drupal\jwt\Authentication\Provider\JwtAuth;
use Drupal\next\Entity\NextSiteInterface;
use Drupal\next\Exception\InvalidPreviewUrlRequest;
use Drupal\next\Plugin\ConfigurablePreviewUrlGeneratorBase;
use Drupal\next\PreviewSecretGeneratorInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides the preview_url_generator plugin based on JWT.
*
* @PreviewUrlGenerator(
* id = "jwt",
* label = "JSON Web Tokens",
* description = "This plugin generates preview URL using JSON Web Tokens. You
* can use this for user-based access control."
* )
*/
class Jwt extends ConfigurablePreviewUrlGeneratorBase {
/**
* The JWT Authentication provider.
*
* @var \Drupal\jwt\Authentication\Provider\JwtAuth
*/
protected JwtAuth $jwtAuth;
/**
* The key value storage.
*
* @var \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface
*/
protected KeyValueExpirableFactoryInterface $keyValue;
/**
* The user entity.
*
* @var \Drupal\Core\Entity\EntityInterface|null
*/
protected ?EntityInterface $user;
/**
* Jwt constructor.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Session\AccountProxyInterface $current_user
* The current user.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
* @param \Drupal\next\PreviewSecretGeneratorInterface $preview_secret_generator
* The preview secret generator.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\jwt\Authentication\Provider\JwtAuth $jwt_auth
* The JWT Authentication provider.
* @param \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface $key_value
* The key value storage.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, AccountProxyInterface $current_user, TimeInterface $time, PreviewSecretGeneratorInterface $preview_secret_generator, EntityTypeManagerInterface $entity_type_manager, JwtAuth $jwt_auth, KeyValueExpirableFactoryInterface $key_value) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $current_user, $time, $preview_secret_generator, $entity_type_manager);
$this->jwtAuth = $jwt_auth;
$this->keyValue = $key_value;
$this->user = $entity_type_manager->getStorage('user')
->load($current_user->id());
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($configuration, $plugin_id, $plugin_definition, $container->get('current_user'), $container->get('datetime.time'), $container->get('next.preview_secret_generator'), $container->get('entity_type.manager'), $container->get('jwt.authentication.jwt'), $container->get('keyvalue.expirable'));
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'secret_expiration' => NULL,
'access_token_expiration' => NULL,
];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['secret_expiration'] = [
'#title' => $this->t('Secret expiration time'),
'#description' => $this->t('The value, in seconds, to be used as expiration time for the validation secret. <strong>It is recommended to use short-lived secrets for increased security.</strong>'),
'#type' => 'number',
'#required' => TRUE,
'#default_value' => $this->configuration['secret_expiration'],
];
$form['access_token_expiration'] = [
'#title' => $this->t('Access token expiration time'),
'#description' => $this->t('The value, in seconds, to be used as expiration time for the access token.'),
'#type' => 'number',
'#required' => TRUE,
'#default_value' => $this->configuration['access_token_expiration'],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->configuration['secret_expiration'] = $form_state->getValue('secret_expiration');
$this->configuration['access_token_expiration'] = $form_state->getValue('access_token_expiration');
}
/**
* {@inheritdoc}
*/
public function generate(NextSiteInterface $next_site, EntityInterface $entity, ?string $resource_version = NULL): ?Url {
$query = [];
$query['path'] = $path = $entity->toUrl()->toString();
$query['uuid'] = $this->user->uuid();
// Create a secret based on the timestamp, path and the user uuid.
$query['timestamp'] = $timestamp = $this->time->getRequestTime();
$query['secret'] = $secret = $this->previewSecretGenerator->generate($timestamp . $path . $resource_version . $this->user->uuid());
// Generate a JWT and store it temporarily so that we can retrieve it on
// validate.
$jwt = $this->jwtAuth->generateToken();
$this->keyValue->get('next_jwt')
->setWithExpire($secret, $jwt, $this->configuration['access_token_expiration']);
return Url::fromUri($next_site->getPreviewUrl(), [
'query' => $query,
]);
}
/**
* {@inheritdoc}
*/
public function validate(Request $request) {
$body = Json::decode($request->getContent());
// Validate the path.
// We do not check for existing path. We let the next.js site handle this.
if (empty($body['path'])) {
throw new InvalidPreviewUrlRequest("Field 'path' is missing");
}
// Validate the uuid.
if (empty($body['uuid'])) {
throw new InvalidPreviewUrlRequest("Field 'uuid' is missing");
}
// Validate the timestamp.
if (empty($body['timestamp'])) {
throw new InvalidPreviewUrlRequest("Field 'timestamp' is missing");
}
$timestamp = (int) $body['timestamp'];
if ($this->time->getRequestTime() > $timestamp + (int) $this->configuration['secret_expiration']) {
throw new InvalidPreviewUrlRequest("The provided secret has expired.");
}
// Validate the secret.
if (empty($body['secret'])) {
throw new InvalidPreviewUrlRequest("Field 'secret' is missing");
}
if ($body['secret'] !== $this->previewSecretGenerator->generate($body['timestamp'] . $body['path'] . $body['resourceVersion'] . $body['uuid'])) {
throw new InvalidPreviewUrlRequest("The provided secret is invalid.");
}
// Retrieve the JWT from storage and send it.
// Since key value expire automatically handles expiration, we don't need
// to check for jwt expiration here.
$jwt = $this->keyValue->get('next_jwt')->get($body['secret']);
if (!$jwt) {
throw new InvalidPreviewUrlRequest("The provided secret is invalid.");
}
return [
'access_token' => $jwt,
];
}
}
