From 1cca796d0782c9c926417a1960a42d9a780654a7 Mon Sep 17 00:00:00 2001 From: arhimede Date: Fri, 4 Sep 2026 22:31:49 +0300 Subject: [PATCH 1/3] Correct OAuth2 key generation and token revocation in v7 docs Keys are generated by php ./bin/generate-oauth2-keys.php, which skips when all three files in data/oauth exist, so updates no longer invalidate issued tokens (7.2.0, issue #503). UserService::revokeTokens() is private; document the repository methods instead. Adds a key rotation FAQ. Signed-off-by: arhimede --- docs/book/v7/security/oauth2-security.md | 61 +++++++++++++++++++----- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/docs/book/v7/security/oauth2-security.md b/docs/book/v7/security/oauth2-security.md index 92b2e4a..e6ddd93 100644 --- a/docs/book/v7/security/oauth2-security.md +++ b/docs/book/v7/security/oauth2-security.md @@ -2,7 +2,7 @@ ## Summary -The security steps to take before an OAuth2-protected Dotkernel API reaches production: remove or re-password the default `admin` and `frontend` OAuth clients, tune access and refresh token lifetimes, and understand how the JWT signing key pair is regenerated and where it must be kept. +The security steps to take before an OAuth2-protected Dotkernel API reaches production: remove or re-password the default `admin` and `frontend` OAuth clients, tune access and refresh token lifetimes, and understand how the JWT signing key pair is generated, why existing keys are preserved, and where they must be kept. ## Details @@ -22,19 +22,47 @@ The configuration for OAuth2 tokens can be edited in `config/autoload/local.php` By default, the lifetimes of the `access` and `refresh` tokens are set to one day and one month respectively. Make sure to adjust their values in accordance with your application's needs, with lower values being generally safer. -> If your application requires it, you can revoke user OAuth tokens before their expiration by making use of the `revokeTokens` method of `UserService`. +> If your application requires it, you can revoke a user's OAuth tokens before they expire. +> `UserService::revokeTokens()` is `private`, so it cannot be called from your own code; it runs as +> part of the public `UserService::deleteUser()`, which revokes the tokens and then anonymizes the +> account. +> +> To revoke tokens on their own, use the token repositories directly: fetch the user's tokens with +> `OAuthAccessTokenRepository::findAccessTokens($identity)`, then pass each token to +> `OAuthAccessTokenRepository::revokeAccessToken()` and +> `OAuthRefreshTokenRepository::revokeRefreshToken()`. > > Read more about the available [configuration options](https://docs.mezzio.dev/mezzio-authentication-oauth2/v1/intro/#configuration). ## Autogeneration of Cryptographic Keys -Dotkernel API makes use of the `./vendor/bin/generate-oauth2-keys` command from `mezzio-authentication-oauth2` to automatically regenerate the -public/private key pair used to verify the transmitted JWTs. -This process is done after each `composer update` (or `composer install` with no lock file), as specified in `composer.json` under the `scripts.post-update-cmd` key. +Dotkernel API runs its own `php ./bin/generate-oauth2-keys.php` script to create the public/private +key pair and the encryption key used to sign and verify the transmitted JWTs. +It is invoked after each `composer update` (or `composer install` with no lock file), as specified in +`composer.json` under the `scripts.post-update-cmd` key: + +```json +"post-update-cmd": [ + "php ./bin/generate-oauth2-keys.php", + "php ./bin/composer-post-install-script.php" +] +``` + +**Existing keys are never overwritten.** The script checks for `data/oauth/encryption.key`, +`data/oauth/private.key` and `data/oauth/public.key`; if all three are present it prints +`OAuth2 keys already exist. Skipping...` and stops. +Only when one is missing does it delegate to +`vendor/mezzio/mezzio-authentication-oauth2/bin/generate-oauth2-keys` to generate the set. + +> This guard matters in production: regenerating the keys invalidates every access token already +> issued. Preserving them across updates was added in Dotkernel API 7.2.0 +> ([issue #503](https://github.com/dotkernel/api/issues/503)). +> If you deliberately want to rotate the keys, delete the three files from `data/oauth` and run +> `composer update` — accepting that existing tokens stop working. While hidden to the VCS by default, keep in mind not to commit any local keys. -> Autogeneration of keys can be disabled by simply removing the `php ./vendor/bin/generate-oauth2-keys` command from the mentioned key. +> Key generation can be disabled by removing the `php ./bin/generate-oauth2-keys.php` entry from the mentioned key. > > While not related to Dotkernel API itself, do ensure that the directory containing the keys is properly secured. @@ -52,16 +80,25 @@ Defaults are one day for access tokens and one month for refresh tokens; shorter **Q: Can I invalidate a user's tokens before they expire?** -A: Yes, via the `revokeTokens` method of `UserService`. +A: Yes, but not via `UserService::revokeTokens()` — that method is `private`. +It runs as part of the public `UserService::deleteUser()`, which also anonymizes the account. +To revoke tokens on their own, use the repositories: `OAuthAccessTokenRepository::findAccessTokens($identity)` to list them, then `revokeAccessToken()` and `OAuthRefreshTokenRepository::revokeRefreshToken()` for each. + +**Q: When are the OAuth2 keys generated?** + +A: `php ./bin/generate-oauth2-keys.php` runs after every `composer update`, and after `composer install` when there is no lock file, via `scripts.post-update-cmd` in `composer.json`. +It only generates keys that are missing: if all three files in `data/oauth` exist it reports `OAuth2 keys already exist. Skipping...` and leaves them alone, so updating dependencies does not invalidate issued tokens. -**Q: When are the OAuth2 keys regenerated?** +**Q: How do I stop the keys from being generated?** -A: After every `composer update`, and after `composer install` when there is no lock file, through the `php ./vendor/bin/generate-oauth2-keys` script in `composer.json`. +A: Remove `php ./bin/generate-oauth2-keys.php` from the `scripts.post-update-cmd` key in `composer.json`. +Since 7.2.0 this is rarely necessary — the script already preserves existing keys, which is what protects issued tokens on a server. -**Q: How do I stop the keys from being regenerated?** +**Q: How do I deliberately rotate the keys?** -A: Remove `php ./vendor/bin/generate-oauth2-keys` from the `scripts.post-update-cmd` key in `composer.json`. -This matters on servers where regenerating keys would invalidate tokens already issued. +A: Delete `encryption.key`, `private.key` and `public.key` from `data/oauth`, then run `composer update`. +The script regenerates the missing set. +Every access token issued under the old keys stops working, so plan for clients to re-authenticate. **Q: Should the key pair be committed?** From 0f982f08bb40c0f4e7d119e3737e47ab6fb240c7 Mon Sep 17 00:00:00 2001 From: arhimede Date: Fri, 4 Sep 2026 22:37:03 +0300 Subject: [PATCH 2/3] Correct route:list output and filter behaviour in v7 docs Table now lists all 38 routes with the index column, including the two security routes split in 6.0. Filters use str_contains, so they are case-sensitive, not case-insensitive as documented. Signed-off-by: arhimede --- .../commands/display-available-endpoints.md | 96 ++++++++++--------- 1 file changed, 52 insertions(+), 44 deletions(-) diff --git a/docs/book/v7/commands/display-available-endpoints.md b/docs/book/v7/commands/display-available-endpoints.md index fed37a4..ee0d64d 100644 --- a/docs/book/v7/commands/display-available-endpoints.md +++ b/docs/book/v7/commands/display-available-endpoints.md @@ -2,7 +2,7 @@ ## Summary -The `route:list` CLI command inspects the application's routes at runtime and prints every endpoint's request method, route name and path. +The `route:list` CLI command inspects the application's routes at runtime and prints a numbered table of every endpoint's request method, route name and path. Results can be filtered by name, path or method. ## Usage @@ -14,51 +14,54 @@ php ./bin/cli.php route:list ``` The command runs through all routes and extracts endpoint information in realtime. +Rows are sorted by path, then by request method, and UUID route parameters are shown as `{id}` rather than the full regular expression they are declared with. +The count in the table header reflects method/path pairs, so a path answering three methods contributes three rows. + The output should be similar to the following: ```text -+-------------------- 37 Routes ------+-------------------------------------+ -| Request method | Route name | Route path | -+----------------+-------------------------------------+-------------------------------------+ -| GET | app::view-index | / | -| GET | admin::list-admin | /admin | -| POST | admin::create-admin | /admin | -| GET | admin::view-account | /admin/account | -| PATCH | admin::update-account | /admin/account | -| GET | admin::list-role | /admin/role | -| GET | admin::view-role | /admin/role/{id} | -| DELETE | admin::delete-admin | /admin/{id} | -| GET | admin::view-admin | /admin/{id} | -| PATCH | admin::update-admin | /admin/{id} | -| POST | app::create-error-report | /error-report | -| POST | security::token | /security/token | -| GET | user::list-user | /user | -| POST | user::create-user | /user | -| DELETE | user::delete-account | /user/account | -| GET | user::view-account | /user/account | -| PATCH | user::update-account | /user/account | -| POST | user::create-account | /user/account | -| POST | user::request-activate-account | /user/account/activate | -| PATCH | user::activate-account | /user/account/activate/{hash} | -| DELETE | user::delete-account-avatar | /user/account/avatar | -| GET | user::view-account-avatar | /user/account/avatar | -| POST | user::create-account-avatar | /user/account/avatar | -| POST | user::recover-account | /user/account/recover | -| POST | user::create-account-reset-password | /user/account/reset-password | -| GET | user::check-account-reset-password | /user/account/reset-password/{hash} | -| PATCH | user::update-account-reset-password | /user/account/reset-password/{hash} | -| GET | user::list-role | /user/role | -| GET | user::view-role | /user/role/{id} | -| DELETE | user::delete-user | /user/{id} | -| GET | user::view-user | /user/{id} | -| PATCH | user::update-user | /user/{id} | -| PATCH | user::activate-user | /user/{id}/activate | -| DELETE | user::delete-user-avatar | /user/{id}/avatar | -| GET | user::view-user-avatar | /user/{id}/avatar | -| POST | user::create-user-avatar | /user/{id}/avatar | -| PATCH | user::deactivate-user | /user/{id}/deactivate | ++------+----------------+-------------------- 38 Routes ------+-------------------------------------+ +| # | Request method | Route name | Route path | ++------+----------------+-------------------------------------+-------------------------------------+ +| 1 | GET | app::view-index | / | +| 2 | GET | admin::list-admin | /admin | +| 3 | POST | admin::create-admin | /admin | +| 4 | GET | admin::view-account | /admin/account | +| 5 | PATCH | admin::update-account | /admin/account | +| 6 | GET | admin::list-role | /admin/role | +| 7 | GET | admin::view-role | /admin/role/{id} | +| 8 | DELETE | admin::delete-admin | /admin/{id} | +| 9 | GET | admin::view-admin | /admin/{id} | +| 10 | PATCH | admin::update-admin | /admin/{id} | +| 11 | POST | app::create-error-report | /error-report | +| 12 | POST | security::generate-token | /security/generate-token | +| 13 | POST | security::refresh-token | /security/refresh-token | +| 14 | GET | user::list-user | /user | +| 15 | POST | user::create-user | /user | +| 16 | DELETE | user::delete-account | /user/account | +| 17 | GET | user::view-account | /user/account | +| 18 | PATCH | user::update-account | /user/account | +| 19 | POST | user::create-account | /user/account | +| 20 | POST | user::request-activate-account | /user/account/activate | +| 21 | PATCH | user::activate-account | /user/account/activate/{hash} | +| 22 | DELETE | user::delete-account-avatar | /user/account/avatar | +| 23 | GET | user::view-account-avatar | /user/account/avatar | +| 24 | POST | user::create-account-avatar | /user/account/avatar | +| 25 | POST | user::recover-account | /user/account/recover | +| 26 | POST | user::create-account-reset-password | /user/account/reset-password | +| 27 | GET | user::check-account-reset-password | /user/account/reset-password/{hash} | +| 28 | PATCH | user::update-account-reset-password | /user/account/reset-password/{hash} | +| 29 | GET | user::list-role | /user/role | +| 30 | GET | user::view-role | /user/role/{id} | +| 31 | DELETE | user::delete-user | /user/{id} | +| 32 | GET | user::view-user | /user/{id} | +| 33 | PATCH | user::update-user | /user/{id} | +| 34 | PATCH | user::activate-user | /user/{id}/activate | +| 35 | DELETE | user::delete-user-avatar | /user/{id}/avatar | +| 36 | GET | user::view-user-avatar | /user/{id}/avatar | +| 37 | POST | user::create-user-avatar | /user/{id}/avatar | +| 38 | PATCH | user::deactivate-user | /user/{id}/deactivate | +------+----------------+-------------------------------------+-------------------------------------+ - ``` ## Filtering results @@ -69,7 +72,11 @@ The following filters can be applied when displaying the route list: * Filter routes by path, using: `-p|--path[=PATH]` * Filter routes by method, using: `-m|--method[=METHOD]` -The filters are case-insensitive and can be combined. +The filters are matched as case-sensitive substrings and can be combined. +For example, `php ./bin/cli.php route:list -i avatar` lists only the six avatar routes, and adding `-m GET` narrows that to two. + +> Case matters. Route names and paths are lowercase and methods are uppercase, so `-i avatar` and +> `-m GET` match, while `-i Avatar` and `-m get` match nothing and print an empty table. Get more help by running this command: @@ -86,7 +93,8 @@ A: No. The command walks the application's registered routes in realtime, so it **Q: Which filters are available?** A: `-i|--name`, `-p|--path` and `-m|--method`. -They are case-insensitive and can be combined. +Each is a case-sensitive substring match, and they can be combined. +Use lowercase for names and paths and uppercase for methods — `-m get` matches nothing. **Q: Why do route names matter beyond documentation?** From ea4aca77a9d35f7f3a2a7428ea581de7b8e04db3 Mon Sep 17 00:00:00 2001 From: arhimede Date: Sat, 5 Sep 2026 18:14:29 +0300 Subject: [PATCH 3/3] Use one sentence per line in route list docs Splits a two-sentence line and unwraps the second. No wording changes. Signed-off-by: arhimede --- docs/book/v7/commands/display-available-endpoints.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/book/v7/commands/display-available-endpoints.md b/docs/book/v7/commands/display-available-endpoints.md index ee0d64d..da33f12 100644 --- a/docs/book/v7/commands/display-available-endpoints.md +++ b/docs/book/v7/commands/display-available-endpoints.md @@ -75,8 +75,8 @@ The following filters can be applied when displaying the route list: The filters are matched as case-sensitive substrings and can be combined. For example, `php ./bin/cli.php route:list -i avatar` lists only the six avatar routes, and adding `-m GET` narrows that to two. -> Case matters. Route names and paths are lowercase and methods are uppercase, so `-i avatar` and -> `-m GET` match, while `-i Avatar` and `-m get` match nothing and print an empty table. +> Case matters. +> Route names and paths are lowercase and methods are uppercase, so `-i avatar` and `-m GET` match, while `-i Avatar` and `-m get` match nothing and print an empty table. Get more help by running this command: