Skip to content

Commit

Permalink
Add effective role field into JWT access token
Browse files Browse the repository at this point in the history
  • Loading branch information
Neloop authored and Martin Kruliš committed Sep 7, 2019
1 parent 3f01330 commit 8fc544f
Show file tree
Hide file tree
Showing 10 changed files with 64 additions and 25 deletions.
6 changes: 3 additions & 3 deletions app/V1Module/presenters/LoginPresenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class LoginPresenter extends BasePresenter {
* @throws InvalidAccessTokenException
*/
private function sendAccessTokenResponse(User $user) {
$token = $this->accessManager->issueToken($user, [TokenScope::MASTER, TokenScope::REFRESH]);
$token = $this->accessManager->issueToken($user, null, [TokenScope::MASTER, TokenScope::REFRESH]);
$this->getUser()->login(new Identity($user, $this->accessManager->decodeToken($token)));

$this->sendSuccessResponse([
Expand All @@ -101,7 +101,7 @@ public function actionDefault() {

$user = $this->credentialsAuthenticator->authenticate($username, $password);
$user->updateLastAuthenticationAt();
$this->users->flush();
$this->users->flush();
$this->sendAccessTokenResponse($user);
}

Expand Down Expand Up @@ -225,7 +225,7 @@ public function actionIssueRestrictedToken() {
$this->users->flush();

$this->sendSuccessResponse([
"accessToken" => $this->accessManager->issueToken($user, $scopes, $expiration),
"accessToken" => $this->accessManager->issueToken($user, null, $scopes, $expiration),
"user" => $this->userViewFactory->getFullUser($user)
]);
}
Expand Down
14 changes: 8 additions & 6 deletions app/V1Module/security/AccessManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,14 @@ public function getUser(AccessToken $token): User {

/**
* Issue a new JWT for the user with optional scopes and optional explicit expiration time.
* @param User $user
* @param string[] $scopes Array of scopes
* @param int $exp Expiration of the token in seconds
* @param array $payload
* @param User $user
* @param string|null $effectiveRole Effective user role for issued token
* @param string[] $scopes Array of scopes
* @param int $exp Expiration of the token in seconds
* @param array $payload
* @return string
*/
public function issueToken(User $user, array $scopes = [], int $exp = null, array $payload = []) {
public function issueToken(User $user, string $effectiveRole = null, array $scopes = [], int $exp = null, array $payload = []) {
if ($exp === null) {
$exp = $this->expiration;
}
Expand All @@ -123,6 +124,7 @@ public function issueToken(User $user, array $scopes = [], int $exp = null, arra
"nbf" => time(),
"exp" => time() + $exp,
"sub" => $user->getId(),
"effrole" => $effectiveRole,
"scopes" => $scopes
]
));
Expand All @@ -131,7 +133,7 @@ public function issueToken(User $user, array $scopes = [], int $exp = null, arra
}

public function issueRefreshedToken(AccessToken $token): string {
return $this->issueToken($this->getUser($token), $token->getScopes(), $token->getExpirationTime(), $token->getPayloadData());
return $this->issueToken($this->getUser($token), null, $token->getScopes(), $token->getExpirationTime(), $token->getPayloadData());
}

/**
Expand Down
11 changes: 11 additions & 0 deletions app/V1Module/security/AccessToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ class AccessToken {
/** @var string|null The subject */
private $sub = null;

/** @var string|null Effective user role of this token */
private $effrole = null;

/** @var string[] Array of scopes this access can access */
private $scopes = [];

Expand All @@ -30,6 +33,10 @@ public function __construct($payload) {
$this->scopes = $payload->scopes;
}

if (isset($payload->effrole)) {
$this->effrole = $payload->effrole;
}

$this->payload = $payload;
}

Expand Down Expand Up @@ -91,6 +98,10 @@ public function getScopes(): array {
return $this->scopes;
}

public function getEffectiveRole(): ?string {
return $this->effrole;
}

/**
* @throws InvalidArgumentException
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public function __construct(EmailHelper $emailHelper, AccessManager $accessManag
public function process(User $user, bool $firstTime = false) {
// prepare all necessary things
$token = $this->accessManager->issueToken(
$user, [TokenScope::EMAIL_VERIFICATION], $this->tokenExpiration, ["email" => $user->getEmail()]
$user, null, [TokenScope::EMAIL_VERIFICATION], $this->tokenExpiration, ["email" => $user->getEmail()]
);

return $this->sendEmail($user, $token, $firstTime);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ public function process(Login $login, string $IPaddress) {
$this->em->flush();

// prepare all necessary things
$token = $this->accessManager->issueToken($login->getUser(), [TokenScope::CHANGE_PASSWORD],
$this->tokenExpiration);
$token = $this->accessManager->issueToken($login->getUser(), null,
[TokenScope::CHANGE_PASSWORD], $this->tokenExpiration);

$locale = $login->getUser()->getSettings()->getDefaultLanguage();
$subject = $this->createSubject($login);
Expand Down
19 changes: 16 additions & 3 deletions tests/AccessToken/AccessManager.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -113,20 +113,33 @@ class TestAccessManager extends Tester\TestCase

$user = Mockery::mock(App\Model\Entity\User::CLASS);
$user->shouldReceive("getId")->andReturn("123456");
$token = $manager->issueToken($user, ["x", "y"]);
$token = $manager->issueToken($user, null, ["x", "y"]);

$payload = JWT::decode($token, $verificationKey, ["HS256"]);
Assert::equal(["x", "y"], $payload->scopes);
}

public function testIssueTokenWithEffectiveRole() {
$users = Mockery::mock(App\Model\Repository\Users::class);
$verificationKey = "abc";
$manager = new AccessManager([ "verificationKey" => $verificationKey ], $users);

$user = Mockery::mock(App\Model\Entity\User::CLASS);
$user->shouldReceive("getId")->andReturn("123456");
$token = $manager->issueToken($user, "role-eff");

$payload = JWT::decode($token, $verificationKey, ["HS256"]);
Assert::equal("role-eff", $payload->effrole);
}

public function testIssueTokenWithExplicitExpiration() {
$users = Mockery::mock(App\Model\Repository\Users::class);
$verificationKey = "abc";
$manager = new AccessManager([ "verificationKey" => $verificationKey ], $users);

$user = Mockery::mock(App\Model\Entity\User::CLASS);
$user->shouldReceive("getId")->andReturn("123456");
$token = $manager->issueToken($user, [], 30);
$token = $manager->issueToken($user, null, [], 30);

$payload = JWT::decode($token, $verificationKey, ["HS256"]);
Assert::true((time() + 30) >= $payload->exp);
Expand All @@ -139,7 +152,7 @@ class TestAccessManager extends Tester\TestCase

$user = Mockery::mock(App\Model\Entity\User::CLASS);
$user->shouldReceive("getId")->andReturn("123456");
$token = $manager->issueToken($user, [], 30, ["sub" => "abcde", "xyz" => "uvw"]);
$token = $manager->issueToken($user, null, [], 30, ["sub" => "abcde", "xyz" => "uvw"]);

$payload = JWT::decode($token, $verificationKey, ["HS256"]);
Assert::true((time() + 30) >= $payload->exp);
Expand Down
27 changes: 20 additions & 7 deletions tests/AccessToken/AccessToken.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -13,49 +13,62 @@ class TestAccessToken extends Tester\TestCase
{

public function testNoUserId() {
$payload = new \stdClass();
$payload = new stdClass();
$token = new AccessToken($payload);
Assert::exception(function () use ($token) {
$token->getUserId();
}, InvalidAccessTokenException::CLASS);
}

public function testGetUserId() {
$payload = new \stdClass();
$payload = new stdClass();
$payload->sub = 123;
$token = new AccessToken($payload);
Assert::same("123", $token->getUserId());
}

public function testEmptyScope() {
$payload = new \stdClass();
$payload = new stdClass();
$token = new AccessToken($payload);
Assert::false($token->isInScope("bla bla"));
}

public function testWrongScope() {
$payload = new \stdClass();
$payload = new stdClass();
$payload->scopes = [ "alb alb" ];
$token = new AccessToken($payload);
Assert::false($token->isInScope("bla bla"));
}

public function testCorrectScope() {
$payload = new \stdClass();
$payload = new stdClass();
$payload->scopes = [ "bla bla" ];
$token = new AccessToken($payload);
Assert::true($token->isInScope("bla bla"));
}

public function testEmptyEffectiveRole() {
$payload = new stdClass();
$token = new AccessToken($payload);
Assert::null($token->getEffectiveRole());
}

public function testEffectiveRole() {
$payload = new stdClass();
$payload->effrole = "role-eff";
$token = new AccessToken($payload);
Assert::equal("role-eff", $token->getEffectiveRole());
}

public function testCustomClaim() {
$payload = new \stdClass();
$payload = new stdClass();
$payload->xyz = 123;
$token = new AccessToken($payload);
Assert::same(123, $token->getPayload("xyz"));
}

public function testMissingCustomClaim() {
$payload = new \stdClass();
$payload = new stdClass();
$payload->abc = 123;
$token = new AccessToken($payload);
Assert::throws(function () use ($token) { $token->getPayload("xyz"); }, InvalidArgumentException::class);
Expand Down
2 changes: 1 addition & 1 deletion tests/Presenters/EmailVerificationPresenter.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class TestEmailVerificationPresenter extends Tester\TestCase

// prepare token for email verification
$token = $this->accessManager->issueToken(
$user, [TokenScope::EMAIL_VERIFICATION], 600, ["email" => $user->getEmail()]
$user, null, [TokenScope::EMAIL_VERIFICATION], 600, ["email" => $user->getEmail()]
);
// login with obtained token
$this->presenter->user->login(new Identity($user, $this->accessManager->decodeToken($token)));
Expand Down
2 changes: 1 addition & 1 deletion tests/Presenters/ForgottenPasswordPresenter.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ class TestForgottenPasswordPresenter extends Tester\TestCase
$user = $this->presenter->users->getByEmail($this->userLogin);

// issue token for changing password
$token = $this->accessManager->issueToken($user, [TokenScope::CHANGE_PASSWORD], 600);
$token = $this->accessManager->issueToken($user, null, [TokenScope::CHANGE_PASSWORD], 600);
// login with obtained token
$this->presenter->user->login(new Identity($user, $this->accessManager->decodeToken($token)));

Expand Down
2 changes: 1 addition & 1 deletion tests/base/PresenterTestHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ public static function login(Container $container, string $login): string

/** @var \App\Security\AccessManager $accessManager */
$accessManager = $container->getByType(\App\Security\AccessManager::class);
$tokenText = $accessManager->issueToken($user, [TokenScope::MASTER, TokenScope::REFRESH]);
$tokenText = $accessManager->issueToken($user, null, [TokenScope::MASTER, TokenScope::REFRESH]);
$token = $accessManager->decodeToken($tokenText);

$userSession->login(new \App\Security\Identity($user, $token));
Expand Down

0 comments on commit 8fc544f

Please sign in to comment.