-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit f615492
Showing
10 changed files
with
321 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
vendor |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
# magento-jwt-refresh-service | ||
|
||
Adds an API endpoint `POST /V1/integration/admin/token/refresh` to refresh the | ||
JWT from the Authorization header. Does not currently do customer tokens but PRs | ||
are welcome. | ||
|
||
e.g.: | ||
|
||
``` | ||
$ curl --json '{"username": "username", "password": "password"}' http://mysite.docker/rest/V1/integration/admin/token/ | ||
"eyJraWQiOiIxIiwiYWxnIjoiSFMyNTYifQ.eyJ1aWQiOjY4LCJ1dHlwaWQiOjIsImlhdCI6MTY4OTM1MDYxOCwiZXhwIjoxNjg5MzU0MjE4fQ.Y2J6BVDFzBjSWP8MtfEievFplU21YyG40h56CLDIo9c" | ||
$ curl -XPOST --data "" -H "Authorization: Bearer eyJraWQiO<etc>" http://mysite.docker/rest/V1/integration/admin/token/refresh/ | ||
"eyJraWQiOiIxIiwiYWxnIjoiSFMyNTYifQ.eyJ1aWQiOjY4LCJ1dHlwaWQiOjIsImlhdCI6MTY4OTM1MDYxOCwiZXhwIjoxNjg5MzU0MjQ3fQ.7TM1LlZ-1ONAQroFO_HVJWCqdl-ig8CCV1Sl-D3eCoA" | ||
``` | ||
|
||
## Shouldn't JWT be short lived and only extended using refresh tokens? | ||
|
||
I guess. A typical recommendation is that refresh tokens should be valid for | ||
seven days, so I will accept a PR which validates that the provided JWT was | ||
not issued over 7 days ago. This module goes to the effort of ensuring that | ||
refreshing the token does not update the issue date, so it should not be too | ||
difficult to enforce a maximum age. | ||
|
||
(You can use base64 -d on the above curl example to verify that the iat claims | ||
of the two tokens are the same, but the expt claim is extended.) | ||
|
||
## BYOT | ||
|
||
If you aren't using bearer tokens but you are using JWT, you can provide a token to be refreshed like this: | ||
|
||
```php | ||
class Whatever | ||
{ | ||
public function __construct( | ||
private \FTS\JwtRefreshService\Api\JwtRefreshServiceInterfaceFactory $jwtRefreshServiceFactory | ||
) {} | ||
|
||
public function refreshToken(string $token) | ||
{ | ||
$jwtRefreshService = $this->jwtRefreshServiceFactory->create([ | ||
'token' => $token | ||
]); | ||
return $jwtRefreshService->refreshAdminToken(); | ||
} | ||
} | ||
``` | ||
|
||
## Why so complicated | ||
|
||
https://github.com/maaarghk/magento-jwt-refresh-service/blob/7827d6b2c5d227bc8f5cedd45615ef0e1b451ec2/src/Model/Api/JwtRefreshService.php#L48-L58 | ||
|
||
|
||
## Help | ||
|
||
I am unlikely to fulfil any feature requests, so please provide a merge request | ||
alongside any that you have. | ||
|
||
MRs with tests also welcome - it should be straightforward enough to verify the | ||
token returned has the same claims and user context, and that the refreshed | ||
token still validates. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"name": "maaarghk/magento-jwt-refresh-service", | ||
"description": "Add an API service for refreshing Magento JWTs", | ||
"version": "1.0.0", | ||
"type": "magento2-module", | ||
"license": "MIT", | ||
"autoload": { | ||
"files": ["src/registration.php"], | ||
"psr-4": { | ||
"FTS\\JwtRefreshService\\": "src/" | ||
} | ||
}, | ||
"require": { | ||
"magento/module-jwt-framework-adapter": "^100.4.2", | ||
"magento/module-jwt-user-token": "^100.4.1" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
<?php | ||
namespace FTS\JwtRefreshService\Api; | ||
|
||
use Magento\Framework\Exception\AuthorizationException; | ||
use Magento\Framework\Exception\InvalidArgumentException; | ||
|
||
interface JwtRefreshServiceInterface | ||
{ | ||
/** | ||
* @return string JWT with updated expiry time | ||
* @throws AuthorizationException when current request has no or invalid bearer token | ||
* @throws InvalidArgumentException when current request bearer token is not a JWT | ||
*/ | ||
public function refreshAdminToken() : string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
<?php | ||
namespace FTS\JwtRefreshService\Model\Api; | ||
|
||
use FTS\JwtRefreshService\Api\JwtRefreshServiceInterface; | ||
use FTS\JwtRefreshService\Plugin\IssuedAtDateOverride; | ||
use Magento\Authorization\Model\UserContextInterface; | ||
use Magento\Framework\Exception\AuthorizationException; | ||
use Magento\Framework\Exception\InvalidArgumentException; | ||
use Magento\Framework\Jwt\Claim\IssuedAtFactory; | ||
use Magento\Framework\Webapi\Request; | ||
use Magento\Integration\Api\Data\UserToken; | ||
use Magento\Integration\Api\Exception\UserTokenException; | ||
use Magento\Integration\Api\UserTokenReaderInterface; | ||
use Magento\Integration\Api\UserTokenValidatorInterface; | ||
use Magento\Integration\Model\UserToken\UserTokenParametersFactory; | ||
use Magento\JwtUserToken\Model\Data\JwtUserContext; | ||
use Magento\JwtUserToken\Model\Issuer; | ||
|
||
class JwtRefreshService implements JwtRefreshServiceInterface | ||
{ | ||
/** | ||
* @param ?string $token If provided, this token will be refreshed. Otherwise, the current request's bearer token will be used | ||
*/ | ||
public function __construct( | ||
private readonly UserTokenReaderInterface $userTokenReader, | ||
private readonly UserTokenValidatorInterface $userTokenValidator, | ||
private readonly Request $request, | ||
private readonly UserTokenParametersFactory $userTokenParametersFactory, | ||
private readonly IssuedAtFactory $issuedAtClaimFactory, | ||
private readonly Issuer $tokenIssuer, | ||
private readonly IssuedAtDateOverride $issuedAtDateOverride, | ||
private readonly ?string $token = null | ||
) {} | ||
|
||
public function refreshAdminToken() : string | ||
{ | ||
$token = $this->getUserToken(); | ||
if (!$token->getUserContext() instanceof JwtUserContext) { | ||
throw new InvalidArgumentException(__("This endpoint can only be used to refresh JSON Web Tokens")); | ||
} | ||
|
||
// This may seem redundant since webapi.xml specifies that access to the 'Magento_Backend::admin' resource is | ||
// required - but it allows other modules to use the service class directly. | ||
if ($token->getUserContext()->getUserType() !== UserContextInterface::USER_TYPE_ADMIN) { | ||
throw new AuthorizationException(__("This endpoint can only be used to refresh admin user tokens")); | ||
} | ||
|
||
// We don't want the "issued at" time of the new token to change | ||
// compared to the original token, because the new token should be | ||
// affected by revocations based on the initial login time. | ||
// | ||
// Although the UserTokenParameters class takes an "issued" parameter, | ||
// we can't simply use that as it is always used to calculate the | ||
// expiry date (i.e. if we set it to the original issue time then the | ||
// token does not have its expiry time refreshed.) | ||
// | ||
// So, we use a plugin on the JWT manager class to rewrite the JWT | ||
// payload that it receives. | ||
$iat = $this->issuedAtClaimFactory->create([ | ||
'value' => $token->getData()->getIssued(), | ||
'duplicate' => true | ||
]); | ||
$this->issuedAtDateOverride->setNewIssuedAt($iat); | ||
|
||
// Issue a new token based on the existing context | ||
$userTokenParams = $this->userTokenParametersFactory->create(); | ||
return $this->tokenIssuer->create($token->getUserContext(), $userTokenParams); | ||
} | ||
|
||
/** | ||
* @throws AuthorizationException | ||
*/ | ||
private function getUserToken() : UserToken | ||
{ | ||
if ($this->token) { | ||
$token = $this->token; | ||
} else { | ||
$token = $this->getBearerTokenFromRequest(); | ||
} | ||
try { | ||
$userToken = $this->userTokenReader->read($token); | ||
$this->userTokenValidator->validate($userToken); | ||
return $userToken; | ||
} catch (UserTokenException $e) { | ||
$this->bail(); | ||
} | ||
} | ||
|
||
/** | ||
* @throws AuthorizationException | ||
*/ | ||
private function getBearerTokenFromRequest() : string | ||
{ | ||
// This code is from Magento\Webapi\Model\Authorization\UserTokenContext | ||
// which we would ideally just use directly, but it does not expose the | ||
// parsed token to us, only the user ID and type. | ||
$authorizationHeaderValue = $this->request->getHeader('Authorization'); | ||
if (!$authorizationHeaderValue) { | ||
$this->bail(); | ||
} | ||
$headerPieces = explode(' ', $authorizationHeaderValue); | ||
if (count($headerPieces) !== 2 || strtolower($headerPieces[0]) !== 'bearer') { | ||
$this->bail(); | ||
} | ||
return $headerPieces[1]; | ||
} | ||
|
||
/** | ||
* @throws AuthorizationException | ||
*/ | ||
private function bail() | ||
{ | ||
throw new AuthorizationException(__('Invalid bearer token')); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
<?php | ||
namespace FTS\JwtRefreshService\Plugin; | ||
|
||
use Magento\Framework\Exception\RuntimeException; | ||
use Magento\Framework\Jwt\Claim\IssuedAt; | ||
use Magento\Framework\Jwt\EncryptionSettingsInterface; | ||
use Magento\Framework\Jwt\Jwe\Jwe; | ||
use Magento\Framework\Jwt\Jwe\JweInterface; | ||
use Magento\Framework\Jwt\Jws\Jws; | ||
use Magento\Framework\Jwt\Jws\JwsInterface; | ||
use Magento\Framework\Jwt\JwtInterface; | ||
use Magento\Framework\Jwt\Payload\ClaimsPayload; | ||
use Magento\Framework\Jwt\Payload\ClaimsPayloadInterface; | ||
use Magento\JwtFrameworkAdapter\Model\JwtManager; | ||
|
||
class IssuedAtDateOverride | ||
{ | ||
private IssuedAt $newIssuedAt; | ||
|
||
public function setNewIssuedAt(IssuedAt $issuedAt) : self | ||
{ | ||
$this->newIssuedAt = $issuedAt; | ||
return $this; | ||
} | ||
|
||
/** | ||
* If we're in the middle of refreshing a JWT, we want to override the | ||
* incoming JWT parameters to have a different issued at date. | ||
* | ||
* @param JwtManager $subject | ||
* @param JwtInterface $jwt | ||
* @param EncryptionSettingsInterface $encryptionSettings | ||
* @return array | ||
* @throws RuntimeException Unknown type of JWT | ||
*/ | ||
public function beforeCreate( | ||
JwtManager $subject, | ||
JwtInterface $jwt, | ||
EncryptionSettingsInterface $encryptionSettings | ||
): array { | ||
// Proceed as normal if no override has been set | ||
if (!isset($this->newIssuedAt)) { | ||
return [$jwt, $encryptionSettings]; | ||
} | ||
if (!$jwt instanceof JwsInterface && !$jwt instanceof JweInterface) { | ||
throw new RuntimeException(__("Unable to override issue date claim of JWT - unhandled token type (JWS and JWE are supported)")); | ||
} | ||
// We need to rebuild $jwt but with updated claims. This code for making | ||
// a JwtInterface is taken from Magento\JwtUserToken\Model\Issuer. | ||
$claimsPayload = $jwt->getPayload(); | ||
if (!$claimsPayload instanceof ClaimsPayloadInterface) { | ||
throw new RuntimeException(__("Unable to override issue date claim of JWT - JWT payload does not contain claims")); | ||
} | ||
// Override the auto-generated iat claim with the one provided | ||
$claims = $claimsPayload->getClaims(); | ||
$claims['iat'] = $this->newIssuedAt; | ||
$newClaimsPayload = new ClaimsPayload($claims); | ||
// create a new JWT with the updated claims and continue with the build | ||
if ($jwt instanceof JwsInterface) { | ||
$newJwt = new Jws( | ||
$jwt->getProtectedHeaders(), | ||
$newClaimsPayload, | ||
$jwt->getUnprotectedHeaders() | ||
); | ||
} else { | ||
$newJwt = new Jwe( | ||
$jwt->getProtectedHeader(), | ||
$jwt->getSharedUnprotectedHeader(), | ||
$jwt->getPerRecipientUnprotectedHeaders(), | ||
$newClaimsPayload | ||
); | ||
} | ||
return [$newJwt, $encryptionSettings]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<?xml version="1.0"?> | ||
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> | ||
<preference for="FTS\JwtRefreshService\Api\JwtRefreshServiceInterface" type="FTS\JwtRefreshService\Model\Api\JwtRefreshService" /> | ||
<type name="Magento\JwtFrameworkAdapter\Model\JwtManager"> | ||
<plugin | ||
name="fts_jwt_refresh_service_issued_at_date_override" | ||
type="FTS\JwtRefreshService\Plugin\IssuedAtDateOverride" /> | ||
</type> | ||
</config> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<?xml version="1.0"?> | ||
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> | ||
<module name="FTS_JwtRefreshService"> | ||
<sequence> | ||
<module name="Magento_Integration" /> | ||
<module name="Magento_JwtFrameworkAdapter" /> | ||
<module name="Magento_JwtUserToken" /> | ||
<module name="Magento_Webapi" /> | ||
</sequence> | ||
</module> | ||
</config> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<?xml version="1.0"?> | ||
<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd"> | ||
<route url="/V1/integration/admin/token/refresh" method="POST"> | ||
<service class="FTS\JwtRefreshService\Api\JwtRefreshServiceInterface" method="refreshAdminToken" /> | ||
<resources> | ||
<resource ref="Magento_Backend::admin" /> | ||
</resources> | ||
</route> | ||
</routes> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
<?php | ||
\Magento\Framework\Component\ComponentRegistrar::register( | ||
\Magento\Framework\Component\ComponentRegistrar::MODULE, | ||
'FTS_JwtRefreshService', | ||
__DIR__ | ||
); |