Browse Source

Add JWT Token Authentication (#38)

* Add JWT Token Authentication

This adds JWT token authentication to Islandora. We send these tokens
along with broadcast messages, and can use them to authenticate with
Fedora as well as call back into Islandora.

* Updated the version for JWT in the composer file.

* JwtEventSubscriber tests.

* Fixing copy/pasta mistake in tests (#2)

* Update JWT Structure to Eliminate Nesting

No java JWT parsing libraries like when a JWT contains nested
structures, even when that is allowed in the standard. This
commit updates our code so we put the drupal data at the
root level of the JWT claims, instead of in a subclaim.

* Coding standards for new sniffs
pull/756/head
Jonathan Green 8 years ago committed by Nick Ruest
parent
commit
afcd377f9e
  1. 15
      .github/PULL_REQUEST_TEMPLATE.md
  2. 1
      .gitignore
  3. 3
      composer.json
  4. 1
      config/install/core.entity_form_display.fedora_resource.rdf_source.default.yml
  5. 1
      config/install/core.entity_view_mode.fedora_resource.teaser.yml
  6. 1
      config/install/field.field.fedora_resource.rdf_source.field_ldp_contains.yml
  7. 1
      config/install/islandora.settings.yml
  8. 1
      config/install/rdf.mapping.fedora_resource_type.non_rdf_source.yml
  9. 1
      config/install/rdf.mapping.fedora_resource_type.rdf_source.yml
  10. 4
      config/install/rest.resource.entity.fedora_resource.yml
  11. 1
      config/install/rules.reaction.broadcast_create_event.yml
  12. 1
      config/install/rules.reaction.broadcast_delete_event.yml
  13. 1
      config/install/rules.reaction.broadcast_update_event.yml
  14. 1
      config/install/views.view.fedora_entities_reference.yml
  15. 1
      config/schema/islandora.schema.yml
  16. 2
      islandora.info.yml
  17. 1
      islandora.links.action.yml
  18. 6
      islandora.services.yml
  19. 4
      src/Controller/FedoraResourceAddController.php
  20. 20
      src/Entity/FedoraResource.php
  21. 140
      src/EventSubscriber/JwtEventSubscriber.php
  22. 41
      src/Plugin/RulesAction/Broadcaster.php
  23. 6
      src/Plugin/Search/FedoraEntitySearch.php
  24. 2
      src/StompFactory.php
  25. 6
      tests/src/Kernel/BroadcasterTest.php
  26. 2
      tests/src/Kernel/IslandoraKernelTestBase.php
  27. 141
      tests/src/Kernel/JwtEventSubscriberTest.php

15
.github/PULL_REQUEST_TEMPLATE.md

@ -1,20 +1,24 @@
**GitHub Issue**: (link)
* Other Relevant Links (Google Groups discussion, related pull requests, Release pull requests, etc.)
* Other Relevant Links (Google Groups discussion, related pull requests,
Release pull requests, etc.)
# What does this Pull Request do?
A brief description of what the intended result of the PR will be and/or what problem it solves.
A brief description of what the intended result of the PR will be and/or what
problem it solves.
# What's new?
A in-depth description of the changes made by this PR. Technical details and possible side effects.
A in-depth description of the changes made by this PR. Technical details and
possible side effects.
* Changes x feature to such that y
* Added x
* Removed y
* Does this change require documentation to be updated?
* Does this change add any new dependencies?
* Does this change require any other modifications to be made to the repository (ie. Regeneration activity, etc.)?
* Does this change require any other modifications to be made to the repository
(ie. Regeneration activity, etc.)?
* Could this change impact execution of existing code?
# How should this be tested?
@ -26,7 +30,8 @@ A description of what steps someone could take to:
* Good testing instructions help get your PR completed faster.
# Additional Notes:
Any additional information that you think would be helpful when reviewing this PR.
Any additional information that you think would be helpful when reviewing this
PR.
# Interested parties
Tag (@ mention) interested parties or, if unsure, @Islandora-CLAW/committers

1
.gitignore vendored

@ -1 +1,2 @@
vendor
/.idea/

3
composer.json

@ -19,7 +19,8 @@
"drupal/rules": "^3.0@alpha",
"drupal/search_api": "^1.0@beta",
"islandora/claw-jsonld": "dev-8.x-1.x",
"stomp-php/stomp-php": "4.*"
"stomp-php/stomp-php": "4.*",
"drupal/jwt": "1.0.0-alpha6"
},
"require-dev": {
"phpunit/phpunit": "^4.8",

1
config/install/core.entity_form_display.fedora_resource.rdf_source.default.yml

@ -64,4 +64,3 @@ content:
placeholder: ''
third_party_settings: { }
hidden: { }

1
config/install/core.entity_view_mode.fedora_resource.teaser.yml

@ -11,4 +11,3 @@ id: fedora_resource.teaser
label: Teaser
targetEntityType: fedora_resource
cache: true

1
config/install/field.field.fedora_resource.rdf_source.field_ldp_contains.yml

@ -28,4 +28,3 @@ settings:
display_name: entity_reference_1
arguments: { }
field_type: entity_reference

1
config/install/islandora.settings.yml

@ -3,4 +3,3 @@ fedora_rest_endpoint: 'http://localhost:8080/fcrepo/rest'
_core:
default_config_hash: nDZDR2rrpXXQ-D_7BYrdDFAXYOsB5hgH6vCAMV5I3w8
broadcast_queue: islandora-connector-broadcast

1
config/install/rdf.mapping.fedora_resource_type.non_rdf_source.yml

@ -45,4 +45,3 @@ fieldMappings:
properties:
- 'ldp:contains'
mapping_type: rel

1
config/install/rdf.mapping.fedora_resource_type.rdf_source.yml

@ -49,4 +49,3 @@ fieldMappings:
properties:
- 'ldp:contains'
mapping_type: rel

4
config/install/rest.resource.entity.fedora_resource.yml

@ -21,6 +21,4 @@ configuration:
- json
- xml
supported_auth:
- basic_auth
- cookie
- jwt_auth

1
config/install/rules.reaction.broadcast_create_event.yml

@ -54,4 +54,3 @@ expression:
rules_tokens: { }
provides_mapping: { }
action_id: islandora_broadcast

1
config/install/rules.reaction.broadcast_delete_event.yml

@ -54,4 +54,3 @@ expression:
rules_tokens: { }
provides_mapping: { }
action_id: islandora_broadcast

1
config/install/rules.reaction.broadcast_update_event.yml

@ -54,4 +54,3 @@ expression:
rules_tokens: { }
provides_mapping: { }
action_id: islandora_broadcast

1
config/install/views.view.fedora_entities_reference.yml

@ -167,4 +167,3 @@ display:
- 'languages:language_interface'
- user.permissions
tags: { }

1
config/schema/islandora.schema.yml

@ -11,4 +11,3 @@ islandora.settings:
broadcast_queue:
type: string
label: 'Queue that handles distributing messages amongst multiple recipients'

2
islandora.info.yml

@ -15,4 +15,4 @@ dependencies:
- jsonld
- rules
- search_api
version: '8.x-1.x-dev'
- jwt

1
islandora.links.action.yml

@ -8,4 +8,3 @@ entity.fedora_resource_type.add_form:
title: 'Add Fedora resource type'
appears_on:
- entity.fedora_resource_type.collection

6
islandora.services.yml

@ -12,3 +12,9 @@ services:
class: Stomp\StatefulStomp
factory: ['Drupal\islandora\StompFactory', create]
arguments: ['@config.factory']
islandora.jwt-subscriber:
class: Drupal\islandora\EventSubscriber\JwtEventSubscriber
factory: ['Drupal\islandora\EventSubscriber\JwtEventSubscriber', create]
arguments: ['@entity_type.manager', '@current_user']
tags:
- { name: event_subscriber }

4
src/Controller/FedoraResourceAddController.php

@ -86,7 +86,7 @@ class FedoraResourceAddController extends ControllerBase {
/**
* Presents the creation form for fedora_resource entities of given type.
*
* @param EntityInterface $fedora_resource_type
* @param \Drupal\Core\Entity\EntityInterface $fedora_resource_type
* The custom bundle to add.
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request object.
@ -104,7 +104,7 @@ class FedoraResourceAddController extends ControllerBase {
/**
* Provides the page title for this controller.
*
* @param EntityInterface $fedora_resource_type
* @param \Drupal\Core\Entity\EntityInterface $fedora_resource_type
* The custom bundle/type being added.
*
* @return string

20
src/Entity/FedoraResource.php

@ -69,26 +69,6 @@ class FedoraResource extends ContentEntityBase implements FedoraResourceInterfac
use EntityChangedTrait;
/**
* Override this to have a rel ='uuid' for islandora entities.
*
* Stolen (but not inherited from EntityInterface)
*
* @param string $rel
* The link relationship type, for example: canonical or edit-form.
* @param array $options
* See \Drupal\Core\Routing\UrlGeneratorInterface::generateFromRoute() for
* the available options.
*
* @return \Drupal\Core\Url
* The URL object.
*/
public function toUrl($rel = 'canonical', array $options = []) {
// TODO: I Will override this to have a rel ='uuid' for islandora entities
// TODO: Change the autogenerated stub.
return parent::toUrl($rel, $options);
}
/**
* Gets an array of placeholders for Fedora Resource Entity.
*

140
src/EventSubscriber/JwtEventSubscriber.php

@ -0,0 +1,140 @@
<?php
namespace Drupal\islandora\EventSubscriber;
use Drupal\jwt\Authentication\Event\JwtAuthValidateEvent;
use Drupal\jwt\Authentication\Event\JwtAuthValidEvent;
use Drupal\jwt\Authentication\Event\JwtAuthGenerateEvent;
use Drupal\jwt\Authentication\Event\JwtAuthEvents;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Class JwtEventSubscriber.
*
* @package Drupal\islandora\EventSubscriber
*/
class JwtEventSubscriber implements EventSubscriberInterface {
/**
* User storage to load users.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $userStorage;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructor.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $userStorage
* User storage to load users.
* @param \Drupal\Core\Session\AccountInterface $user
* The current user.
*/
public function __construct(
EntityStorageInterface $userStorage,
AccountInterface $user
) {
$this->userStorage = $userStorage;
$this->currentUser = $user;
}
/**
* Factory.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityManager
* Entity manager to get user storage.
* @param \Drupal\Core\Session\AccountInterface $user
* The current user.
*/
public static function create(
EntityTypeManagerInterface $entityManager,
AccountInterface $user
) {
return new static(
$entityManager->getStorage('user'),
$user
);
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[JwtAuthEvents::VALIDATE][] = ['validate'];
$events[JwtAuthEvents::VALID][] = ['loadUser'];
$events[JwtAuthEvents::GENERATE][] = ['setIslandoraClaims'];
return $events;
}
/**
* Sets claims for a Islandora consumer on the JWT.
*
* @param \Drupal\jwt\Authentication\Event\JwtAuthGenerateEvent $event
* The event.
*/
public function setIslandoraClaims(JwtAuthGenerateEvent $event) {
global $base_secure_url;
// Standard claims, validated at JWT validation time.
$event->addClaim('iat', time());
$event->addClaim('exp', strtotime('+2 hour'));
// Islandora claims we need to validate.
$event->addClaim('uid', $this->currentUser->id());
$event->addClaim('name', $this->currentUser->getAccountName());
$event->addClaim('roles', $this->currentUser->getRoles(FALSE));
$event->addClaim('url', $base_secure_url);
}
/**
* Validates that the Islandora data is present in the JWT.
*
* @param \Drupal\jwt\Authentication\Event\JwtAuthValidateEvent $event
* A JwtAuth event.
*/
public function validate(JwtAuthValidateEvent $event) {
$token = $event->getToken();
$uid = $token->getClaim('uid');
$name = $token->getClaim('name');
$roles = $token->getClaim('roles');
$url = $token->getClaim('url');
if ($uid === NULL || $name === NULL || $roles === NULL || $url === NULL) {
$event->invalidate("Expected data missing from payload.");
return;
}
$user = $this->userStorage->load($uid);
if ($user === NULL) {
$event->invalidate("Specified UID does not exist.");
}
elseif ($user->getAccountName() !== $name) {
$event->invalidate("Account name does not match.");
}
}
/**
* Load and set a Drupal user to be authentication based on the JWT's uid.
*
* @param \Drupal\jwt\Authentication\Event\JwtAuthValidEvent $event
* A JwtAuth event.
*/
public function loadUser(JwtAuthValidEvent $event) {
$token = $event->getToken();
$uid = $token->getClaim('uid');
$user = $this->userStorage->load($uid);
$event->setUser($user);
}
}

41
src/Plugin/RulesAction/Broadcaster.php

@ -5,6 +5,7 @@ namespace Drupal\islandora\Plugin\RulesAction;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\islandora\Form\IslandoraSettingsForm;
use Drupal\rules\Core\RulesActionBase;
use Drupal\jwt\Authentication\Provider\JwtAuth;
use Stomp\Exception\StompException;
use Stomp\StatefulStomp;
use Stomp\Transport\Message;
@ -45,6 +46,13 @@ class Broadcaster extends RulesActionBase implements ContainerFactoryPluginInter
*/
protected $broadcastQueue;
/**
* The JWT Auth Service.
*
* @var \Drupal\jwt\Authentication\Provider\JwtAuth
*/
protected $auth;
/**
* Constructs a BroadcastAction.
*
@ -58,12 +66,22 @@ class Broadcaster extends RulesActionBase implements ContainerFactoryPluginInter
* Name of queue that will handle distributing the broadcast.
* @param \Stomp\StatefulStomp $stomp
* Stomp client.
* @param \Drupal\jwt\Authentication\Provider\JwtAuth $auth
* JWT Auth client.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, $broadcast_queue, StatefulStomp $stomp) {
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
$broadcast_queue,
StatefulStomp $stomp,
JwtAuth $auth
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->broadcastQueue = $broadcast_queue;
$this->stomp = $stomp;
$this->auth = $auth;
}
/**
@ -79,7 +97,8 @@ class Broadcaster extends RulesActionBase implements ContainerFactoryPluginInter
$plugin_id,
$plugin_definition,
$broadcastQueue,
$container->get('islandora.stomp')
$container->get('islandora.stomp'),
$container->get('jwt.authentication.jwt')
);
}
@ -95,9 +114,25 @@ class Broadcaster extends RulesActionBase implements ContainerFactoryPluginInter
// Transform recipients array into comma searated list.
$recipients = array_map('trim', $recipients);
$recipients = implode(',', $recipients);
$headers = ['IslandoraBroadcastRecipients' => $recipients];
// Include a token for later authentication in the message.
$token = $this->auth->generateToken();
if ($token !== NULL) {
$headers['Authorization'] = "Bearer $token";
}
else {
// JWT isn't properly configured. Log and notify user.
\Drupal::logger('islandora')->error(
'Error getting JWT token for message: @msg', ['@msg' => $e->getMessage()]
);
drupal_set_message(
t('Error getting JWT token for message. Check JWT Configuration.'), 'error'
);
}
// Transform message from string into a proper message object.
$message = new Message($message, ['IslandoraBroadcastRecipients' => $recipients]);
$message = new Message($message, $headers);
// Send the message.
try {

6
src/Plugin/Search/FedoraEntitySearch.php

@ -155,7 +155,7 @@ class FedoraResourceSearch extends ConfigurableSearchPluginBase implements Acces
/**
* {@inheritdoc}
*/
static public function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
@ -189,6 +189,8 @@ class FedoraResourceSearch extends ConfigurableSearchPluginBase implements Acces
* A config object for 'search.settings'.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Render\RendererInterface $renderer
* Renderer.
* @param \Drupal\Core\Session\AccountInterface $account
* The $account object to use for checking for access to advanced search.
*/
@ -458,7 +460,7 @@ class FedoraResourceSearch extends ConfigurableSearchPluginBase implements Acces
/**
* Adds the configured rankings to the search query.
*
* @param SelectExtender $query
* @param \Drupal\Core\Database\Query\SelectExtender $query
* A query object that has been extended with the Search DB Extender.
*/
protected function addFedoraResourceRankings(SelectExtender $query) {

2
src/StompFactory.php

@ -21,7 +21,7 @@ class StompFactory {
* @return \Stomp\StatefulStomp
* Stomp client.
*/
static public function create(ConfigFactoryInterface $config) {
public static function create(ConfigFactoryInterface $config) {
// Get broker url from config.
$settings = $config->get(IslandoraSettingsForm::CONFIG_NAME);
$brokerUrl = $settings->get(IslandoraSettingsForm::BROKER_URL);

6
tests/src/Kernel/BroadcasterTest.php

@ -97,7 +97,7 @@ class BroadcasterTest extends IslandoraKernelTestBase {
/**
* Utility function to create a broadcaster action from a Stomp prophecy.
*
* @param StatefulStomp $stomp
* @param \Stomp\StatefulStomp $stomp
* Stomp instance or prophecy.
*
* @return \Drupal\islandora\Plugin\RulesAction\Broadcaster
@ -106,6 +106,7 @@ class BroadcasterTest extends IslandoraKernelTestBase {
protected function createBroadcaster(StatefulStomp $stomp) {
// Pull the plugin definition out of the plugin system.
$actionManager = $this->container->get('plugin.manager.rules_action');
$jwt = $this->container->get('jwt.authentication.jwt');
$definitions = $actionManager->getDefinitions();
$pluginDefinition = $definitions['islandora_broadcast'];
@ -114,7 +115,8 @@ class BroadcasterTest extends IslandoraKernelTestBase {
'islandora_broadcast',
$pluginDefinition,
$this->testQueue,
$stomp
$stomp,
$jwt
);
// Set the required contexts for the action to run.

2
tests/src/Kernel/IslandoraKernelTestBase.php

@ -30,6 +30,8 @@ abstract class IslandoraKernelTestBase extends KernelTestBase {
'rules',
'jsonld',
'views',
'key',
'jwt',
'islandora',
];

141
tests/src/Kernel/JwtEventSubscriberTest.php

@ -0,0 +1,141 @@
<?php
namespace Drupal\Tests\islandora\Kernel;
use Drupal\jwt\Authentication\Event\JwtAuthGenerateEvent;
use Drupal\jwt\Authentication\Event\JwtAuthValidEvent;
use Drupal\jwt\Authentication\Event\JwtAuthValidateEvent;
use Drupal\jwt\JsonWebToken\JsonWebToken;
use Drupal\jwt\JsonWebToken\JsonWebTokenInterface;
use Drupal\simpletest\UserCreationTrait;
use Drupal\core\Entity\EntityStorageInterface;
use Drupal\islandora\EventSubscriber\JwtEventSubscriber;
/**
* JwtEventSubscriber tests.
*
* @group islandora
* @coversDefaultClass \Drupal\islandora\EventSubscriber\JwtEventSubscriber
*/
class JwtEventSubscriberTest extends IslandoraKernelTestBase {
use UserCreationTrait;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $user;
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->user = $this->createUser();
}
/**
* @covers \Drupal\islandora\EventSubscriber\JwtEventSubscriber::setIslandoraClaims
*/
public function testGeneratesValidToken() {
$entity_storage = $this->container->get('entity_type.manager')->getStorage('user');
$subscriber = new JwtEventSubscriber($entity_storage, $this->user);
// Generate a new token.
$jwt = new JsonWebToken();
$event = new JwtAuthGenerateEvent($jwt);
$subscriber->setIslandoraClaims($event);
// Validate it.
$validateEvent = new JwtAuthValidateEvent($jwt);
$subscriber->validate($validateEvent);
$this->assert($validateEvent->isValid(), "Generated tokens must be valid.");
}
/**
* @covers \Drupal\islandora\EventSubscriber\JwtEventSubscriber::validate
*/
public function testInvalidatesMalformedToken() {
$entity_storage = $this->container->get('entity_type.manager')->getStorage('user');
$subscriber = new JwtEventSubscriber($entity_storage, $this->user);
// Create a new event with mock jwt that returns null for all functions.
$prophecy = $this->prophesize(JsonWebTokenInterface::class);
$jwt = $prophecy->reveal();
$event = new JwtAuthValidateEvent($jwt);
$subscriber->validate($event);
assert(!$event->isValid(), "Malformed event must be invalidated");
}
/**
* @covers \Drupal\islandora\EventSubscriber\JwtEventSubscriber::validate
*/
public function testInvalidatesBadUid() {
// Mock user entity storage, returns null when loading user.
$prophecy = $this->prophesize(EntityStorageInterface::class);
$entity_storage = $prophecy->reveal();
$subscriber = new JwtEventSubscriber($entity_storage, $this->user);
// Generate a new token.
$jwt = new JsonWebToken();
$event = new JwtAuthGenerateEvent($jwt);
$subscriber->setIslandoraClaims($event);
// Validate it.
$validateEvent = new JwtAuthValidateEvent($jwt);
$subscriber->validate($validateEvent);
assert(!$validateEvent->isValid(), "Event must be invalidated when user cannot be loaded.");
}
/**
* @covers \Drupal\islandora\EventSubscriber\JwtEventSubscriber::validate
*/
public function testInvliadatesBadAccount() {
$anotherUser = $this->createUser();
// Mock user entity storage, loads the wrong user.
$prophecy = $this->prophesize(EntityStorageInterface::class);
$prophecy->load($this->user->id())->willReturn($anotherUser);
$entity_storage = $prophecy->reveal();
$subscriber = new JwtEventSubscriber($entity_storage, $this->user);
// Generate a new token.
$jwt = new JsonWebToken();
$event = new JwtAuthGenerateEvent($jwt);
$subscriber->setIslandoraClaims($event);
// Validate it.
$validateEvent = new JwtAuthValidateEvent($jwt);
$subscriber->validate($validateEvent);
assert(!$validateEvent->isValid(), "Event must be invalidated when users don't align.");
}
/**
* @covers \Drupal\islandora\EventSubscriber\JwtEventSubscriber::loadUser
*/
public function testLoadsUser() {
$entity_storage = $this->container->get('entity_type.manager')->getStorage('user');
$subscriber = new JwtEventSubscriber($entity_storage, $this->user);
// Generate a new token.
$jwt = new JsonWebToken();
$event = new JwtAuthGenerateEvent($jwt);
$subscriber->setIslandoraClaims($event);
$validEvent = new JwtAuthValidEvent($jwt);
$subscriber->loadUser($validEvent);
$this->assert($validEvent->getUser()->id() == $this->user->id(), "Correct user must be loaded to valid event.");
}
}
Loading…
Cancel
Save