diff --git a/docs/_sidebar.md b/docs/_sidebar.md
index b41d3604..80a4a973 100755
--- a/docs/_sidebar.md
+++ b/docs/_sidebar.md
@@ -67,7 +67,9 @@
- [`healthcheck`](/commands/system/healthcheck)
- **User Commands**
- [`user:create`](/commands/user/user-create)
- - [`user:token:create`](/commands/user/user-token-create)
+ - **Token Commands**
+ - [`token:create`](/commands/token/token-create)
+ - [`token:revoke`](/commands/token/token-revoke)
- **Backup Commands**
- [`backup:list`](/commands/backup/backup-list)
- [`backup:fetch`](/commands/backup/backup-fetch)
diff --git a/docs/commands/assets/user-token-create-help.html b/docs/commands/assets/token-create-help.html
old mode 100755
new mode 100644
similarity index 98%
rename from docs/commands/assets/user-token-create-help.html
rename to docs/commands/assets/token-create-help.html
index 45751cc9..0cc2b84b
--- a/docs/commands/assets/user-token-create-help.html
+++ b/docs/commands/assets/token-create-help.html
@@ -42,7 +42,7 @@
Creates a new token for an user.
Usage:
- user:token:create <identifier> <password>
+ token:create <identifier> <password>
Arguments:
identifier Unique identifier of the user (email) or the user's UUID
diff --git a/docs/commands/assets/user-token-create.html b/docs/commands/assets/token-create.html
old mode 100755
new mode 100644
similarity index 97%
rename from docs/commands/assets/user-token-create.html
rename to docs/commands/assets/token-create.html
index 57b5ab30..e0b6658e
--- a/docs/commands/assets/user-token-create.html
+++ b/docs/commands/assets/token-create.html
@@ -43,7 +43,7 @@
▄ ▀ ▀ ▄ Ember Nexus API
▀ █ ▀ v0.0.37, production mode
- User Token Create
+ Token Create
Found user with identifier 'command-test@localhost.dev ', user's UUID is 3ae8f319-f585-40bf-8bdf-3d3f990e62e6 .
diff --git a/docs/commands/assets/token-revoke-help.html b/docs/commands/assets/token-revoke-help.html
new file mode 100644
index 00000000..1e5cd1fa
--- /dev/null
+++ b/docs/commands/assets/token-revoke-help.html
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+stdin
+
+
+
+
+
+Description:
+ Creates a new token for an user.
+
+Usage:
+ token:create <identifier> <password>
+
+Arguments:
+ identifier Unique identifier of the user (email) or the user's UUID
+ password Password of the user
+
+Options:
+ -h, --help Display help for the given command. When no command is given display help for the list command
+ -q, --quiet Do not output any message
+ -V, --version Display this application version
+ --ansi|--no-ansi Force (or disable --no-ansi) ANSI output
+ -n, --no-interaction Do not ask any interactive question
+ -e, --env=ENV The Environment name. [default: "prod"]
+ --no-debug Switch off debug mode.
+ -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
+
+
+
diff --git a/docs/commands/assets/token-revoke.html b/docs/commands/assets/token-revoke.html
new file mode 100644
index 00000000..734e7010
--- /dev/null
+++ b/docs/commands/assets/token-revoke.html
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+stdin
+
+
+
+
+
+
+ ▄
+ ▄ ▀ ▀ ▄ Ember Nexus API
+ ▀ █ ▀ v0.0.36, prod mode
+
+ Token Create
+
+ Found user with identifier 'command-test@localhost.dev ', user's UUID is a0c8fe7b-f5be-4c08-9dff-44582081ea7c .
+
+▶ Successfully created new token: secret-token:TBOdmNurTLRk0WNrOi0Qdt .
+
+
+
+
diff --git a/docs/commands/token/token-create.md b/docs/commands/token/token-create.md
new file mode 100644
index 00000000..9706c8c8
--- /dev/null
+++ b/docs/commands/token/token-create.md
@@ -0,0 +1,17 @@
+# `token:create`
+
+Help Output
+
+```bash
+php bin/console token:create --help
+```
+
+[](../assets/token-create-help.html ':include :type=html')
+
+Example Command
+
+```bash
+php bin/console token:create command-test@localhost.dev 1234
+```
+
+[](../assets/token-create.html ':include :type=html')
diff --git a/docs/commands/token/token-revoke.md b/docs/commands/token/token-revoke.md
new file mode 100644
index 00000000..b9edd4de
--- /dev/null
+++ b/docs/commands/token/token-revoke.md
@@ -0,0 +1,17 @@
+# `token:revoke`
+
+Help Output
+
+```bash
+php bin/console token:revoke --help
+```
+
+[](../assets/token-revoke-help.html ':include :type=html')
+
+Example Command
+
+```bash
+php bin/console token:revoke
+```
+
+[](../assets/token-revoke.html ':include :type=html')
diff --git a/docs/commands/user/user-token-create.md b/docs/commands/user/user-token-create.md
deleted file mode 100644
index 244a2240..00000000
--- a/docs/commands/user/user-token-create.md
+++ /dev/null
@@ -1,17 +0,0 @@
-# `user:token:create`
-
-Help Output
-
-```bash
-php bin/console user:token:create --help
-```
-
-[](../assets/user-token-create-help.html ':include :type=html')
-
-Example Command
-
-```bash
-php bin/console user:token:create command-test@localhost.dev 1234
-```
-
-[](../assets/user-token-create.html ':include :type=html')
diff --git a/lib/EmberNexusBundle/src/Service/EmberNexusConfiguration.php b/lib/EmberNexusBundle/src/Service/EmberNexusConfiguration.php
index 8aeccb5f..53b98d63 100644
--- a/lib/EmberNexusBundle/src/Service/EmberNexusConfiguration.php
+++ b/lib/EmberNexusBundle/src/Service/EmberNexusConfiguration.php
@@ -241,6 +241,9 @@ public function setRegisterUniqueIdentifier(string $registerUniqueIdentifier): E
if (0 === strlen($registerUniqueIdentifier)) {
throw new Exception('Unique identifier can not be an empty string.');
}
+ if (!preg_match("/^[A-Za-z0-9_]+$/", $registerUniqueIdentifier)) {
+ throw new Exception("Unique identifier must only contain alphanumeric characters and '_'.");
+ }
$this->registerUniqueIdentifier = $registerUniqueIdentifier;
return $this;
diff --git a/src/Command/UserTokenCreateCommand.php b/src/Command/TokenCreateCommand.php
old mode 100755
new mode 100644
similarity index 97%
rename from src/Command/UserTokenCreateCommand.php
rename to src/Command/TokenCreateCommand.php
index f2efbaf3..ebdf42fa
--- a/src/Command/UserTokenCreateCommand.php
+++ b/src/Command/TokenCreateCommand.php
@@ -22,8 +22,8 @@
/**
* @psalm-suppress PropertyNotSetInConstructor $io
*/
-#[AsCommand(name: 'user:token:create', description: 'Creates a new token for an user.')]
-class UserTokenCreateCommand extends Command
+#[AsCommand(name: 'token:create', description: 'Creates a new token for an user.')]
+class TokenCreateCommand extends Command
{
private OutputStyle $io;
diff --git a/src/Command/TokenRevokeCommand.php b/src/Command/TokenRevokeCommand.php
new file mode 100644
index 00000000..c5c0695f
--- /dev/null
+++ b/src/Command/TokenRevokeCommand.php
@@ -0,0 +1,318 @@
+addOption(
+ self::OPTION_FORCE,
+ 'f',
+ InputOption::VALUE_NEGATABLE,
+ 'If enabled, command will not ask for manual confirmation.',
+ false
+ );
+ $this->addOption(
+ self::OPTION_DRY_RUN,
+ null,
+ InputOption::VALUE_NEGATABLE,
+ 'Lists tokens which would be affected by revocation. Does not apply said revocation.',
+ false
+ );
+ // command filters
+ $this->addOption(
+ self::OPTION_USER,
+ 'u',
+ InputOption::VALUE_REQUIRED,
+ 'Token revocation is only applied to given user. User can be specified by its UUID (takes precedent) or its identifier.',
+ null
+ );
+ $this->addOption(
+ self::OPTION_GROUP,
+ 'g',
+ InputOption::VALUE_REQUIRED,
+ 'Token revocation is only applied to users of given group. Group must be specified by its UUID.',
+ null
+ );
+ $this->addOption(
+ self::OPTION_ISSUED_BEFORE,
+ 'b',
+ InputOption::VALUE_REQUIRED,
+ 'Token revocation is only applied to tokens issued before a given datetime. Datetime must be in the format "YYYY-MM-DD HH:MM" (UTC).',
+ null
+ );
+ $this->addOption(
+ self::OPTION_ISSUED_AFTER,
+ 'a',
+ InputOption::VALUE_REQUIRED,
+ 'Token revocation is only applied to tokens issued after a given datetime. Datetime must be in the format "YYYY-MM-DD HH:MM" (UTC).',
+ null
+ );
+ $this->addOption(
+ self::OPTION_ISSUED_WITHOUT_EXPIRATION_DATE,
+ null,
+ InputOption::VALUE_NEGATABLE,
+ 'Token revocation is only applied to tokens with no explicit expiration date.',
+ false
+ );
+ $this->addOption(
+ self::OPTION_ISSUED_WITH_EXPIRATION_DATE,
+ null,
+ InputOption::VALUE_NEGATABLE,
+ 'Token revocation is only applied to tokens with explicit expiration date set.',
+ false
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->io = new EmberNexusStyle($input, $output);
+
+ $this->io->title('Token Revocation');
+
+ $filters = [];
+ $arguments = [];
+
+ if (
+ $input->getOption(self::OPTION_ISSUED_WITH_EXPIRATION_DATE) === true &&
+ $input->getOption(self::OPTION_ISSUED_WITHOUT_EXPIRATION_DATE) === true
+ ) {
+ throw new Exception(sprintf(
+ "Using both %s and %s is not possible.",
+ self::OPTION_ISSUED_WITH_EXPIRATION_DATE,
+ self::OPTION_ISSUED_WITHOUT_EXPIRATION_DATE
+ ));
+ }
+ if ($input->getOption(self::OPTION_ISSUED_WITH_EXPIRATION_DATE) === true) {
+ $filters[] = 't.expirationDate IS NOT NULL';
+ }
+ if ($input->getOption(self::OPTION_ISSUED_WITHOUT_EXPIRATION_DATE) === true) {
+ $filters[] = 't.expirationDate IS NULL';
+ }
+
+
+ $optionIssuedBefore = null;
+ if ($input->getOption(self::OPTION_ISSUED_BEFORE) !== null) {
+ $optionIssuedBefore = DateTime::createFromFormat('Y-m-d H:i', $input->getOption(self::OPTION_ISSUED_BEFORE));
+ $filters[] = 't.created < $issuedBefore';
+ $arguments['issuedBefore'] = $optionIssuedBefore;
+ }
+
+ $optionIssuedAfter = null;
+ if ($input->getOption(self::OPTION_ISSUED_AFTER) !== null) {
+ $optionIssuedAfter = DateTime::createFromFormat('Y-m-d H:i', $input->getOption(self::OPTION_ISSUED_AFTER));
+ $filters[] = 't.created > $issuedAfter';
+ $arguments['issuedAfter'] = $optionIssuedAfter;
+ }
+
+ if ($optionIssuedBefore && $optionIssuedAfter) {
+ if ($optionIssuedBefore->getTimestamp() < $optionIssuedAfter->getTimestamp()) {
+ throw new Exception(sprintf(
+ "%s can not be before %s.",
+ self::OPTION_ISSUED_BEFORE,
+ self::OPTION_ISSUED_AFTER
+ ));
+ }
+ }
+
+ if ($input->getOption(self::OPTION_USER) !== null) {
+ $userIdentifier = $input->getOption(self::OPTION_USER);
+ if (preg_match(Regex::UUID_V4, $userIdentifier)) {
+ $filters[] = 'u.id = $userIdentifier';
+ } else {
+ $filters[] = sprintf(
+ "u.%s = \$userIdentifier",
+ $this->emberNexusConfiguration->getRegisterUniqueIdentifier()
+ );
+ }
+ $arguments['userIdentifier'] = $userIdentifier;
+ }
+
+ if ($input->getOption(self::OPTION_GROUP) !== null) {
+ $filters[] = '(t)<-[:OWNS]-(:User)-[:IS_IN_GROUP*1..]->(:Group {id: $groupIdentifier})';
+ $arguments['groupIdentifier'] = $input->getOption(self::OPTION_GROUP);
+ }
+
+ $joinedFilters = join("\nAND ", $filters);
+ $finalQuery = sprintf(
+ "MATCH (t:Token)<-[:OWNS]-(u:User)\n".
+ "%s%s%s".
+ "RETURN t.id, t.created, t.expirationDate, u.id, u.%s as userUniqueIdentifier\n".
+ "ORDER BY t.created ASC, t.id ASC",
+ count($filters) > 0 ? 'WHERE ' : '',
+ $joinedFilters,
+ count($filters) > 0 ? "\n" : '',
+ $this->emberNexusConfiguration->getRegisterUniqueIdentifier()
+ );
+
+// $this->io->writeln("----------------------------");
+//
+// foreach ($filters as $filter) {
+// $this->io->writeln(' '.$filter);
+// }
+//
+// $this->io->writeln("----------------------------");
+
+ $res = $this->cypherEntityManager->getClient()->runStatement(
+ new Statement($finalQuery, $arguments)
+ );
+
+
+ $countTokensToBeRevoked = count($res);
+ if ($countTokensToBeRevoked === 0) {
+ $this->io->finalMessage('No tokens found.');
+ return Command::SUCCESS;
+ }
+
+ $this->io->writeln(sprintf(
+ " %d tokens are affected of revocation%s",
+ $countTokensToBeRevoked,
+ $countTokensToBeRevoked > 10 ? '. First 10 are shown:' : ':'
+ ));
+ $this->io->newLine();
+
+ $rows = [];
+ foreach ($res as $i => $tokenResult) {
+ $tokenCreated = $tokenResult['t.created'];
+ if ($tokenCreated instanceof DateTimeZoneId) {
+ $tokenCreated = $tokenCreated->toDateTime()->format('Y-m-d H:i:s');
+ }
+ $tokenExpires = $tokenResult['t.expirationDate'];
+ if ($tokenExpires instanceof DateTimeZoneId) {
+ $tokenExpires = $tokenExpires->toDateTime()->format('Y-m-d H:i:s');
+ }
+ $rows[] = [
+ $tokenResult['t.id'],
+ $tokenResult['u.id'],
+ $tokenResult['userUniqueIdentifier'],
+ $tokenCreated,
+ $tokenExpires ?? '-'
+ ];
+ if (count($rows) >= 10) {
+ break;
+ }
+ }
+
+ $table = $this->io->createCompactTable();
+ $table->setHeaders([
+ 'Token UUID',
+ 'User UUID',
+ 'User identifier',
+ 'Created',
+ 'Expires on'
+ ]);
+ $table->setRows($rows);
+ $table->render();
+ $this->io->newLine();
+
+
+ /**
+ * @var QuestionHelper $helper
+ */
+ $helper = $this->getHelper('question');
+ $question = new ConfirmationQuestion(sprintf(
+ " Are you sure you want to revoke all %d tokens? [y/N]: ",
+ $countTokensToBeRevoked
+ ), false);
+ if (!$helper->ask($input, $output, $question)) {
+ $this->io->finalMessage('Aborted revoking tokens.');
+
+ return self::FAILURE;
+ }
+ $this->io->newLine();
+
+// foreach ($res as $tokenResult) {
+// $this->io->writeln(sprintf(
+// "Token: %s, owned by %s",
+// $tokenResult['t.id'],
+// $tokenResult['userUniqueIdentifier']
+// ));
+// }
+
+
+
+
+
+
+
+
+
+// $normalizedArguments = [];
+// foreach ($arguments as $i => $argument) {
+// if ($argument instanceof \DateTimeInterface) {
+// $argument = 'datetime("'.$argument->format('c').'")';
+// } else {
+// $argument = \Safe\json_encode($argument, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
+// }
+// $normalizedArguments[] = sprintf(
+// " %s: %s,",
+// $i,
+// $argument
+// );
+// }
+//
+// $finalArguments = sprintf(
+// ":params\n{\n%s\n}",
+// join("\n", $normalizedArguments)
+// );
+//
+// $this->io->writeln($finalArguments);
+// $this->io->newLine();
+// $this->io->writeln($finalQuery);
+
+ $this->io->finalMessage('Successfully revoked tokens.');
+
+ return Command::SUCCESS;
+ }
+}
diff --git a/src/Security/TokenGenerator.php b/src/Security/TokenGenerator.php
index b04d8516..7e618c1d 100755
--- a/src/Security/TokenGenerator.php
+++ b/src/Security/TokenGenerator.php
@@ -71,6 +71,7 @@ public function createNewToken(UuidInterface $userUuid, array $data = [], int $l
->addProperties([
'hash' => $hash,
'expirationDate' => (new DateTime())->add(new DateInterval(sprintf('PT%sS', $lifetimeInSeconds))),
+ 'state' => ''
]);
$this->elementManager->create($tokenNode);
$this->elementManager->flush();
diff --git a/src/Type/TokenStateType.php b/src/Type/TokenStateType.php
new file mode 100644
index 00000000..2270f998
--- /dev/null
+++ b/src/Type/TokenStateType.php
@@ -0,0 +1,17 @@
+