{"id":18780512,"url":"https://github.com/wunderwerkio/drupal-verification","last_synced_at":"2026-04-26T16:32:41.631Z","repository":{"id":150666334,"uuid":"623005997","full_name":"wunderwerkio/drupal-verification","owner":"wunderwerkio","description":"Foundation for Verification Providers to verify a request operation.","archived":false,"fork":false,"pushed_at":"2025-01-04T18:54:26.000Z","size":59,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"1.0.x","last_synced_at":"2025-05-21T06:39:34.882Z","etag":null,"topics":["decoupled-drupal","drupal","drupal-10-module","drupal-9-module","drupal-module"],"latest_commit_sha":null,"homepage":"https://www.drupal.org/project/verification","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/wunderwerkio.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-04-03T14:00:37.000Z","updated_at":"2025-01-04T18:54:30.000Z","dependencies_parsed_at":"2023-04-09T19:16:57.885Z","dependency_job_id":null,"html_url":"https://github.com/wunderwerkio/drupal-verification","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/wunderwerkio/drupal-verification","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wunderwerkio%2Fdrupal-verification","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wunderwerkio%2Fdrupal-verification/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wunderwerkio%2Fdrupal-verification/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wunderwerkio%2Fdrupal-verification/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/wunderwerkio","download_url":"https://codeload.github.com/wunderwerkio/drupal-verification/tar.gz/refs/heads/1.0.x","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wunderwerkio%2Fdrupal-verification/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32305035,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-26T09:34:17.070Z","status":"ssl_error","status_checked_at":"2026-04-26T09:34:00.993Z","response_time":129,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["decoupled-drupal","drupal","drupal-10-module","drupal-9-module","drupal-module"],"created_at":"2024-11-07T20:26:43.329Z","updated_at":"2026-04-26T16:32:41.613Z","avatar_url":"https://github.com/wunderwerkio.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Important Notice\n\nThe version control was moved to Drupal's GitLab instance!  \nSee https://www.drupal.org/project/verification for more info!\n\n---\n\n# Drupal Verification API\n\n[![Lint \u0026 Test](https://github.com/wunderwerkio/drupal-verification/actions/workflows/main.yml/badge.svg)](https://github.com/wunderwerkio/drupal-verification/actions/workflows/main.yml)\n[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=wunderwerkio_drupal-verification\u0026metric=alert_status)](https://sonarcloud.io/summary/new_code?id=wunderwerkio_drupal-verification)\n\nThis drupal module provides the foundation to implement verification for various operations (e.g. reset password, change email or passwordless logins) that a drupal user can do.\nThat is done by having multiple `VerificationProvider` plugins verifying a request.\n\n**Table of Contents:**\n\n- [Motivation](#motivation)\n- [Concept](#concept)\n  - [Diagram](#diagram)\n- [Example](#example)\n- [Verification Providers](#verification-providers)\n  - [Hash Provider](#hash-provider)\n  - [Implement your own provider](#implement-your-own-provider)\n\n## Motivation\n\nIn decoupled scenarious, some operations like updating a user email, resetting the password or cancelling a user account need additional verification as an additional security measure.\n\nAdditionally some operations must also be preceeded by a login, because the user might not be able to login (e.g. when performing a password reset).\nThe verification must then be able to verify the login AND the operation (e.g. the password reset).\n\nThis module tries to solve this problem by providing the foundation for a sophisticated verification system.\n\n## Concept\n\nThe Verification API revolves around having **Plugins** that implement the `VerificationProviderBase` class. The plugins job is to verify a given request and return a `VerificationResult`.\n\nGiven that a login may preceed the actual operation, the verification is split into two parts:\n\n1. (optional) Verify if the verification method is eligible for a login\n2. Verify if the verification method is eligible for the operation\n\n*The verification method MUST be independendtly invalidated for login and the operation!*\n\nTherefore each plugin must implement the `verifyLogin` and `verifyOperation` methods.\n\n\u003e **Note**\n\u003e Each plugin is responsible to invalidate the verification method once it has been used, and to implement appropriate security measures, like prohibiting brute force attacks!\n\nThe verification is strictly tied to the following aspects:\n\n- **Operation** - A string describing what operation should be made\n- **User** - The drupal user that the operation is performed on\n- **Email** - If a different email address then the user's should be used\n\nIf these change between verification start and finish, the verification MUST fail.\n\n### Diagram\n\n```mermaid\nflowchart LR\n    A[Incoming Request] --\u003e|Needs Verification| Verification-API\n\n    subgraph Verification-API\n        C[Request Verifier] --\u003e |Calls all Verification Providers| Verification-Provider\n\n        subgraph Verification-Provider\n            direction TB\n            D{Verification\\nProvider} --\u003e|Valid Verification Data| E[Ok]\n            D --\u003e|Invalid Verification Data| F[Err]\n            D --\u003e|No Verification Data| G[Unhandled]\n        end\n\n        Verification-Provider --\u003e H[Create\\naggregated\\nResult]\n    end\n\n    Verification-API --\u003e  R{Check\\nResult}\n\n    R --\u003e |Ok| X[Proceed with\\nRequest Handling]\n    R --\u003e |Error| Z[Return Error]\n    R --\u003e |Unhandled| Z[Return Error]\n```\n\n## Example\n\n```php\n\u003c?php\n\nuse Drupal\\Core\\Controller\\ControllerBase;\nuse Drupal\\verification\\Service\\RequestVerifier;\nuse Symfony\\Component\\HttpFoundation\\JsonResponse;\nuse Symfony\\Component\\HttpFoundation\\Request;\nuse Symfony\\Component\\HttpFoundation\\Response;\n\nclass MyController extends ControllerBase {\n\n  public function __construct(\n    // This is the 'verification.request_verifier' service.\n    protected RequestVerifier $verifier,\n  ) {}\n\n  public function handleUpdatePassword(Request $request): Response {\n    $user = $this-\u003ecurrentUser();\n\n    $payload = $request-\u003egetContent();\n    $data = Json::decode($payload);\n\n    $mail = $data['email'];\n\n    // Verify the operation.\n    $result = $this-\u003everifier-\u003everifyOperation($request, 'update-password', $user, $mail);\n\n    // Check if OK.\n    if ($result-\u003eok) {\n      // The verification was successful! Yay!\n\n      // More information MAY be found by checking the code.\n      echo $result-\u003ecode;\n    }\n\n    // Check if unhandled.\n    if ($result-\u003eunhandled) {\n      // The verification was not handled.\n      // This is the case if none of the registered verification providers\n      // could relevant verification data in the request.\n    }\n\n    // Check if error.\n    if ($result-\u003eerr) {\n      // The verification was handled, but resulted in an error.\n\n      // An error code is always set.\n      // Possible error codes depend on the verification provider.\n      echo $result-\u003ecode;\n    }\n\n    // For convenience, the result can also generate a JSON:API schema conform\n    // error message, if the result was not OK, otherwise returns NULL.\n    //\n    // @see https://github.com/wunderwerkio/jsonapi-error\n    if ($response = $result-\u003etoErrorResponse()) {\n      return $response;\n    }\n  }\n\n}\n```\n\n## Verification Providers\n\nHere is a curated list of verification providers that utilize the Verification API:\n\n- [Hash (built-in)](#hash-provider)\n- [Magic Code](https://github.com/wunderwerkio/drupal-magic-code)\n\n*If you want your verification provider listed, feel free to open an issue.*\n\n### Hash Provider\n\n**Install the `verification_hash` module.**\n\nThe `Hash` provider is a built-in verification provider that uses a hash and timestamp from the `user_pass_rehash` function to verify an operation.\n\nThis provider expects the incoming Request to have the following header:\n\n`X-Verification-Hash: \u003chash\u003e$$\u003ctimestamp\u003e`\n\nThe hash and timestamp are separated with two dollar signs.\n\nFor comparison, a new hash is generated by the provider for the given user account.\nThe generated hash and passed hash are then compared if they match.\n\nIn addition, the hash is only valid for the configured password\nreset timeout in `user.settings.password_reset_timeout`.\n\n### Implement your own provider\n\nTo implement a custom Verification Provider, create a new `VerificationProvider` Plugin that\nis annotated with the `VerificationProvider` annotation, extends the `VerificationProviderBase` class\nand implements the `VerificationProviderInterface` interface.\n\n\u003e **Note**\n\u003e Do not forget to invalidate data, so that a verification method cannot be re-used.\n\nA real implementation can be found at `./modules/verification_hash/src/Plugin/VerificationProvider/Hash.php`.\n\n**This is a crude example**\n\n```php\n\u003c?php\n\ndeclare(strict_types=1);\n\nnamespace Drupal\\verification_hash\\Plugin\\VerificationProvider;\n\nuse Drupal\\Core\\Plugin\\ContainerFactoryPluginInterface;\nuse Drupal\\Core\\Session\\AccountInterface;\nuse Drupal\\verification\\Plugin\\VerificationProviderBase;\nuse Symfony\\Component\\HttpFoundation\\Request;\nuse Drupal\\verification\\Result\\VerificationResult;\n\n/**\n * Custom verification provider plugin.\n *\n * @VerificationProvider(\n *   id = \"custom_provider\",\n *   label = @Translation(\"Custom Provider\"),\n * )\n */\nclass CustomProvider extends VerificationProviderBase implements ContainerFactoryPluginInterface {\n\n  /**\n   * {@inheritdoc}\n   */\n  public function verifyLogin(Request $request, string $operation, AccountInterface $user, ?string $email = NULL): VerificationResult {\n    // Implement login verification here...\n\n    if ($verification_data_not_found) {\n      return VerificationResult::unhandled();\n    }\n\n    // Invalidate verification method for `login` here if it was handled.\n\n    if ($verification_was_successful) {\n      return VerificationResult::ok();\n    }\n\n    if ($verification_has_failed) {\n      return VerificationResult::err('some_error_code');\n    }\n  }\n\n  /**\n   * {@inheritdoc}\n   */\n  public function verifyOperation(Request $request, string $operation, AccountInterface $user, ?string $email = NULL): VerificationResult {\n    // Implement operation verification here...\n\n    if ($verification_data_not_found) {\n      return VerificationResult::unhandled();\n    }\n\n    if ($verification_was_successful) {\n      return VerificationResult::ok();\n    }\n\n    if ($verification_has_failed) {\n      return VerificationResult::err('some_error_code');\n    }\n  }\n\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwunderwerkio%2Fdrupal-verification","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwunderwerkio%2Fdrupal-verification","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwunderwerkio%2Fdrupal-verification/lists"}