diff --git a/config/install/jwt.config.yml b/config/install/jwt.config.yml new file mode 100644 index 0000000..927fe34 --- /dev/null +++ b/config/install/jwt.config.yml @@ -0,0 +1 @@ +auth_header: Authorization diff --git a/config/schema/jwt.schema.yml b/config/schema/jwt.schema.yml index 7daaca2..488492e 100644 --- a/config/schema/jwt.schema.yml +++ b/config/schema/jwt.schema.yml @@ -10,6 +10,9 @@ jwt.config: key_id: type: string label: 'The key ID to use' + auth_header: + type: string + label: 'The authorization header, default to "Authorization".' key.type.jwt_hs: type: mapping diff --git a/jwt.install b/jwt.install index 8da1f67..2825598 100644 --- a/jwt.install +++ b/jwt.install @@ -21,3 +21,13 @@ function jwt_requirements($phase) { return $requirements; } + +/** + * Preset the default authorization header to "Authorization". + */ +function jwt_update_8001() { + \Drupal::configFactory() + ->getEditable('jwt.config') + ->set('auth_header', 'Authorization') + ->save(); +} diff --git a/jwt.services.yml b/jwt.services.yml index 55f082b..c4765f6 100644 --- a/jwt.services.yml +++ b/jwt.services.yml @@ -1,7 +1,12 @@ services: + jwt.config: + class: Drupal\Core\Config\ImmutableConfig + factory: config.factory:get + arguments: + - jwt.config jwt.authentication.jwt: class: Drupal\jwt\Authentication\Provider\JwtAuth - arguments: [ '@jwt.transcoder', '@event_dispatcher' ] + arguments: [ '@jwt.transcoder', '@event_dispatcher', '@jwt.config' ] tags: - { name: authentication_provider, provider_id: 'jwt_auth', global: TRUE, priority: 100 } jwt.page_cache_request_policy.disallow_jwt_auth_requests: diff --git a/src/Authentication/Provider/JwtAuth.php b/src/Authentication/Provider/JwtAuth.php index 5cb5bc4..6195ad0 100644 --- a/src/Authentication/Provider/JwtAuth.php +++ b/src/Authentication/Provider/JwtAuth.php @@ -2,6 +2,7 @@ namespace Drupal\jwt\Authentication\Provider; +use Drupal\Core\Config\ImmutableConfig; use Drupal\jwt\Transcoder\JwtTranscoderInterface; use Drupal\jwt\Transcoder\JwtDecodeException; use Drupal\jwt\Authentication\Event\JwtAuthGenerateEvent; @@ -19,6 +20,13 @@ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; */ class JwtAuth implements AuthenticationProviderInterface { + /** + * The authorization header to be used. + * + * @var string + */ + protected $authHeader; + /** * The JWT Transcoder service. * @@ -40,20 +48,24 @@ class JwtAuth implements AuthenticationProviderInterface { * The jwt transcoder service. * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher * The event dispatcher service. + * @param \Drupal\Core\Config\ImmutableConfig $config + * The JWT config. */ public function __construct( JwtTranscoderInterface $transcoder, - EventDispatcherInterface $event_dispatcher + EventDispatcherInterface $event_dispatcher, + ImmutableConfig $config ) { $this->transcoder = $transcoder; $this->eventDispatcher = $event_dispatcher; + $this->authHeader = $config->get('auth_header'); } /** * {@inheritdoc} */ public function applies(Request $request) { - $auth = $request->headers->get('Authorization'); + $auth = $request->headers->get($this->authHeader); return preg_match('/^Bearer .+/', $auth); } @@ -112,7 +124,7 @@ class JwtAuth implements AuthenticationProviderInterface { * Raw JWT String if on request, false if not. */ protected function getJwtFromRequest(Request $request) { - $auth_header = $request->headers->get('Authorization'); + $auth_header = $request->headers->get($this->authHeader); $matches = []; if (!$hasJWT = preg_match('/^Bearer (.*)/', $auth_header, $matches)) { return FALSE; diff --git a/src/Form/ConfigForm.php b/src/Form/ConfigForm.php index 261e6d2..67e8c45 100644 --- a/src/Form/ConfigForm.php +++ b/src/Form/ConfigForm.php @@ -142,6 +142,16 @@ class ConfigForm extends ConfigFormBase { '#required' => TRUE, ]; + $form['jwt_auth_header'] = [ + '#type' => 'textfield', + '#title' => $this->t('Authorization header'), + '#description' => $this->t('Allows to set up an authorization header. The default is "Authorization".'), + '#default_value' => $this->config('jwt.config')->get('auth_header'), + '#weight' => 11, + '#required' => TRUE, + ]; + + return parent::buildForm($form, $form_state); } @@ -179,6 +189,10 @@ class ConfigForm extends ConfigFormBase { if (isset($values['jwt_key'])) { $this->config('jwt.config')->set('key_id', $values['jwt_key'])->save(); } + + if (isset($values['jwt_auth_header'])) { + $this->config('jwt.config')->set('auth_header', $values['jwt_auth_header'])->save(); + } } } diff --git a/tests/src/Functional/AuthHeaderTest.php b/tests/src/Functional/AuthHeaderTest.php new file mode 100644 index 0000000..4b3e17f --- /dev/null +++ b/tests/src/Functional/AuthHeaderTest.php @@ -0,0 +1,138 @@ +getEditable('jwt.config') + ->set('algorithm', 'HS256') + ->set('key_id', 'jwt_test_hmac') + ->save(); + } + + /** + * Tests the JWT authorization. + * + * @param $uid + * User ID to which should be authorized. + * @param $auth_header + * The authorization header. + * @param bool $negate + * Optional. Negate authorization by checking user is not authorized. + */ + protected function assertJwtAuthorization($uid, $auth_header, $negate = FALSE) { + // Get the http client to have a clean request with only a single header. + $client = $this->getHttpClient(); + /** @var \GuzzleHttp\Client $client*/ + $response = $client->get($this->buildUrl(''), [ + 'headers' => [ + $auth_header => 'Bearer ' . $this->token, + ], + ]); + // Status code must be 200, the user just should not be logged in. + $this->assertEquals(200, $response->getStatusCode()); + // Cache tag header should contain user related records. + $cache_tag_header = $response->getHeader('X-Drupal-Cache-Tags')[0]; + $response_body = (string) $response->getBody(); + // Test some headers and response body to make sure user is (not) + // authenticated. + if (!$negate) { + $this->assertContains('user:1', $cache_tag_header); + $this->assertContains('user_view', $cache_tag_header); + $this->assertContains('Member for', $response_body); + } + else { + $this->assertNotContains('user:1', $cache_tag_header); + $this->assertNotContains('user_view', $cache_tag_header); + $this->assertNotContains('Member for', $response_body); + } + } + + /** + * Tests the user is JWT authorized. + * + * @param $uid + * User ID to which should be authorized. + * @param $auth_header + * The authorization header. Default to authorization. + */ + protected function assertIsUserJwtAuthorized($uid, $auth_header = 'Authorization') { + $this->assertJwtAuthorization($uid, $auth_header); + } + + /** + * Tests the user is not JWT authorized. + * + * @param $uid + * User ID to which should be authorized. + * @param $auth_header + * The authorization header. Default to authorization. + */ + protected function assertIsNotUserJwtAuthorized($uid, $auth_header = 'Authorization') { + $this->assertJwtAuthorization($uid, $auth_header, TRUE); + } + + /** + * Tests authorization headers. + */ + public function testAuthHeaders() { + // Log as admin. + $this->drupalLogin($this->rootUser); + // Get the JWT token. + $this->drupalGet('jwt/token'); + // Check the response and grab the JWT token. + $this->assertSession()->statusCodeEquals(200); + $response = json_decode($this->getSession()->getPage()->getContent(), TRUE); + $this->assertNotEmpty($response['token']); + $this->token = $response['token']; + // Test the authentication works. + $this->assertIsUserJwtAuthorized($this->loggedInUser->id()); + + // Change the auth header and check it works. + $values = [ + 'jwt_algorithm' => 'HS256', + 'jwt_key' => 'jwt_test_hmac', + 'jwt_auth_header' => $this->customAuthHeader, + ]; + $this->drupalPostForm('admin/config/system/jwt', $values, 'Save configuration'); + // The default header should not work anymore. + $this->assertIsNotUserJwtAuthorized($this->loggedInUser->id()); + // The new header should work. + $this->assertIsUserJwtAuthorized($this->loggedInUser->id(), $this->customAuthHeader); + } + +}