Skip to content

Commit

Permalink
JWT tokens.
Browse files Browse the repository at this point in the history
  • Loading branch information
steveworley committed Aug 8, 2021
1 parent 8a8007b commit d574e53
Show file tree
Hide file tree
Showing 10 changed files with 197 additions and 130 deletions.
2 changes: 2 additions & 0 deletions config/install/quant.token_settings.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
timeout: '+1 minute'
disable: false
secret: null
strict: false
62 changes: 19 additions & 43 deletions quant.install
Original file line number Diff line number Diff line change
Expand Up @@ -8,50 +8,12 @@
use Drupal\Core\Database\Database;

/**
* Implements hook_schema().
*
* Defines a simple table to store salted tokens.
*
* @see hook_schema()
*
* @ingroup quant
* Perform setup tasks for Quant.
*/
function quant_schema() {
$schema['quant_token'] = [
'description' => 'Stores short-lived access tokens.',
'fields' => [
'pid' => [
'type' => 'serial',
'not null' => TRUE,
'description' => 'Primary key: Unique token ID.',
],
'token' => [
'type' => 'varchar',
'not null' => TRUE,
'length' => 255,
'default' => '',
'description' => 'The token value.',
],
'route' => [
'type' => 'text',
'size' => 'normal',
'not null' => FALSE,
'description' => 'A path to register for the token',
],
'created' => [
'type' => 'int',
'size' => 'normal',
'not null' => TRUE,
'description' => 'When the token was created.',
],
],
'primary key' => ['pid'],
'indexes' => [
'token' => ['token'],
],
];

return $schema;
function quant_install() {
$config = \Drupal::configFactory()->getEditable('quant.token_settings');
$config->set('secret', bin2hex(random_bytes(32)));
$config->save();
}

/**
Expand Down Expand Up @@ -94,3 +56,17 @@ function quant_update_9002(&$sandbox) {
$config->set('disable', FALSE);
$config->save();
}

/**
* Support JWT for internal request tokens.
*/
function quant_update_9003(&$sandbox) {
$config = \Drupal::configFactory()->getEditable('quant.token_settings');
$config->set('secret', bin2hex(random_bytes(32)));
$config->set('strict', FALSE);
$config->save();

// Remove the token table.
$schema = \Database::getConnection()->schema();
$schema->dropTable('quant_token');
}
1 change: 0 additions & 1 deletion quant.links.menu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ quant.token:
parent: quant
weight: 2


quant.metadata:
title: 'Metadata'
route_name: quant.metadata
Expand Down
5 changes: 5 additions & 0 deletions quant.links.task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ quant.seed:
title: 'Seed'
base_route: quant.config

quant.token:
route_name: quant.token
title: 'Token'
base_route: quant.config

quant.metadata:
route_name: quant.metadata
title: Metadata
Expand Down
26 changes: 15 additions & 11 deletions quant.module
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\quant\Exception\ExpiredTokenException;
use Drupal\quant\Exception\InvalidTokenException;
use Drupal\quant\Exception\StrictTokenException;
use Drupal\quant\Exception\TokenValidationDisabledException;
use Drupal\quant\Plugin\QueueItem\RouteItem;
use Drupal\quant\Seed;
Expand Down Expand Up @@ -162,17 +163,11 @@ function quant_shutdown(array $context = []) {
}
}

/**
* Implements hook_cron().
*/
function quant_cron() {
\Drupal::service('quant.token_manager')->release();
}

/**
* Implements hook_node_access().
*/
function quant_node_access(NodeInterface $node, $op, AccountInterface $account) {
$strict = \Drupal::config('quant.token_settings')->get('strict');
$request = \Drupal::request();

if (!$request->headers->has('quant-revision') && !$request->headers->has('quant-token')) {
Expand All @@ -183,16 +178,25 @@ function quant_node_access(NodeInterface $node, $op, AccountInterface $account)
$url = Url::fromRoute('entity.node.canonical', ['node' => $node->id()], $options)->toString();

try {
\Drupal::service('quant.token_manager')->validate($url, TRUE);
\Drupal::service('quant.token_manager')->validate($url, $strict);
}
catch (TokenValidationDisabledException $e) {

// Allow access when token validation is disabled.
}
catch (ExpiredTokenException $e) {
throw new ServiceUnavailableHttpException(NULL, t('Service route is not available.'));
throw new ServiceUnavailableHttpException(NULL, t('Token request: time mismatch. Received [:token_time] expected [:server_time]', [
':token_time' => $e->getTime(),
':server_time' => $e->getServerTime(),
]));
}
catch (StrictTokenException $e) {
throw new ServiceUnavailableHttpException(NULL, t('Token request: route mismatch. Received [:route] expected [:expected]', [
':route' => $e->getTokenRoute(),
':expected' => $e->getExpectedRoute(),
]));
}
catch (InvalidTokenException $e) {
throw new ServiceUnavailableHttpException(NULL, t('Service route is not available.'));
throw new ServiceUnavailableHttpException(NULL, t('Token request: Invalid token'));
}

// If the token validation didn't trigger an exception - then the
Expand Down
1 change: 1 addition & 0 deletions quant.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ services:
- '@database'
- '@request_stack'
- '@config.factory'
- '@datetime.time'
12 changes: 6 additions & 6 deletions src/Exception/ExpiredTokenException.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,17 @@ class ExpiredTokenException extends \Exception {
/**
* The matched record.
*
* @var object
* @var int
*/
protected $record;
protected $sTime;

/**
* {@inheritdoc}
*/
public function __construct(string $token, int $time = 0, $record = [], string $message = "The token has expired", int $code = 0, \Throwable $previous = NULL) {
public function __construct(string $token, int $time = 0, $sTime = 0, string $message = "The token has expired", int $code = 0, \Throwable $previous = NULL) {
$this->token = $token;
$this->time = $time;
$this->record = $record;
$this->sTime = $sTime;

parent::__construct($message, $code, $previous);
}
Expand All @@ -56,8 +56,8 @@ public function getTime() {
/**
* Getter for the database record.
*/
public function getRecord() {
return $this->record;
public function getServerTime() {
return $this->sTime;
}

}
63 changes: 63 additions & 0 deletions src/Exception/StrictTokenException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

namespace Drupal\quant\Exception;

/**
* The token has expired.
*/
class StrictTokenException extends \Exception {

/**
* Request token.
*
* @var string
*/
protected $token;

/**
* Request time.
*
* @var string
*/
protected $tokenRoute;

/**
* The matched record.
*
* @var string
*/
protected $expectedRoute;

/**
* {@inheritdoc}
*/
public function __construct(string $token, $token_route = NULL, $expected_route = NULL, string $message = "The token routes do not match", int $code = 0, \Throwable $previous = NULL) {
$this->token = $token;
$this->tokenRoute = $token_route;
$this->expectedRoute = $expected_route;

parent::__construct($message, $code, $previous);
}

/**
* Getter for the token.
*/
public function getToken() {
return $this->token;
}

/**
* Getter for the time.
*/
public function getTokenRoute() {
return $this->tokenRoute;
}

/**
* Getter for the database record.
*/
public function getExpectedRoute() {
return $this->expectedRoute;
}

}
29 changes: 29 additions & 0 deletions src/Form/TokenForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@ protected function getEditableConfigNames() {
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config(static::SETTINGS);

$form['disable'] = [
'#type' => 'checkbox',
'#title' => $this->t('Disable token verification'),
'#description' => $this->t('Not recommended for production environments, this disables token verification'),
'#default_value' => $config->get('disable'),
];

$form['strict'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enable strict tokens'),
'#description' => $this->t('Allow token verificaiton process to perform route validations, this may not work for all Drupal configurations.'),
'#default_value' => $config->get('strict'),
];

$form['timeout'] = [
'#type' => 'textfield',
'#title' => $this->t('Token timeout'),
Expand All @@ -42,6 +56,12 @@ public function buildForm(array $form, FormStateInterface $form_state) {
'#required' => TRUE,
];

$form['generate_secret'] = [
'#type' => 'checkbox',
'#title' => $this->t('Generate a new secret token'),
'#description' => $this->t('Regenerate the secret token used to sign internal requests'),
];

return parent::buildForm($form, $form_state);
}

Expand All @@ -64,8 +84,17 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$config = $this->config(static::SETTINGS);
$editable = $this->configFactory->getEditable(static::SETTINGS);

if ($form_state->getValue('generate_secret') || empty($config->get('secret'))) {
$editable->set('secret', bin2hex(random_bytes(32)));
}

$this->configFactory->getEditable(static::SETTINGS)
->set('timeout', $form_state->getValue('timeout'))
->set('disable', $form_state->getValue('disable'))
->set('strict', $form_state->get('strict'))
->save();

parent::submitForm($form, $form_state);
Expand Down
Loading

0 comments on commit d574e53

Please sign in to comment.