{"id":22085644,"url":"https://github.com/nexmo/todo-web","last_synced_at":"2025-03-23T22:16:11.899Z","repository":{"id":66173067,"uuid":"43412710","full_name":"Nexmo/ToDo-Web","owner":"Nexmo","description":"Example ToDo list app, showing use of the Verify API.","archived":false,"fork":false,"pushed_at":"2021-02-02T13:24:10.000Z","size":66,"stargazers_count":1,"open_issues_count":2,"forks_count":3,"subscribers_count":39,"default_branch":"main","last_synced_at":"2025-01-29T05:25:07.491Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Nexmo.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":"2015-09-30T04:55:53.000Z","updated_at":"2023-09-13T13:58:45.000Z","dependencies_parsed_at":"2023-02-20T23:15:25.795Z","dependency_job_id":null,"html_url":"https://github.com/Nexmo/ToDo-Web","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nexmo%2FToDo-Web","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nexmo%2FToDo-Web/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nexmo%2FToDo-Web/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nexmo%2FToDo-Web/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Nexmo","download_url":"https://codeload.github.com/Nexmo/ToDo-Web/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245175480,"owners_count":20572787,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":[],"created_at":"2024-12-01T01:15:41.743Z","updated_at":"2025-03-23T22:16:11.873Z","avatar_url":"https://github.com/Nexmo.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# [How-To] Adding Verify to a Web App\n\n## Introduction\nAlmost every web developer has built a login page, and probably more than one. To show how to protect a login with phone \nnumber powered second factor authentication (2FA), we'll walk through the process of adding Nexmo's Verify API to an \nexisting web application.\n\nOur web application is a simple todo list. All the application data, and even the login, is powered by Parse. That \nwill allow us to focus on the code that powers 2FA and abstract away the code needed to persist todo items and store \npasswords. It also means running this example app yourself only requires PHP and a Parse account.\n\nIf you're comfortable with modern PHP frameworks, you should be able to follow this how-to in your own development \nenvironment. Once you clone the master branch of the repository, use [`composer install`][composer] to include the \ndependencies. If you want to skip to a specific section, just checkout the associated git tag.\n\nIf PHP is not your language of choice, no problem. The concepts in general are common to any web application. You'll \nfind how to add second verification to a web application even when not following along on your own.\n\nAll service credentials are stored in a [local config file][local_config] which can be created by hand or by running\n[`$php public/index.php setup config`][config_setup]\n\nOur Parse database structure is pretty simple, we have a `User` class with `username` and `password`, as well as a \n`ToDo` class with a `todoItem` and a pointer to the `user` that owns the item. To set up your own copy, you can use \n[this dump of the Parse schema][schema] or run [`$php public/index.php setup parse`][parse_setup] after you've setup a \nParse application.\n\nThe application itself is built as a rather standard Zend Framework 2 module based on the Action Controller concept. How \nthe application is configured isn't too relevant to this example; however, the bulk of that is in the [module \nconfiguration][module_config], where you'll see the application's routes defined.\n\nThere are only two controllers, one for the application itself (the [`AppController`][app]) and one for creating \naccounts and authenticating (the [`AuthController`][auth]). Parse is really doing all the hard work for the \n[`AuthController`][auth], creating users:\n\n    $user = new ParseUser();\n    $user-\u003esetUsername($email);\n    $user-\u003esetPassword($password);\n    \n[*View in Context*][parse_create]\n\nLogging them in:\n\n    $user = ParseUser::logIn($this-\u003erequest-\u003egetPost('email'), $this-\u003erequest-\u003egetPost('password'));\n    $_SESSION['todo']['user'] = $user-\u003egetUsername();\n    \n\n[*View in Context*][parse_login]\n\nThen redirecting to the `AppController`:\n\n     $this-\u003eredirect()-\u003etoRoute('app');\n     \n[*View in Context*][redirect]\n     \nYou can take a look at the entire [`AuthController`][auth] before 2FA is added.\n\nFor any requests to the Nexmo API we'll be using Philip Shipley's client library. It's a simple wrapper [built on Guzzle\nand its Web Service Clients][client]. We can add a factory to create and configure the client library when needed. That \nmeans we also need to add Nexmo API credentials to our [local config file][local_config] along with the Parse \ncredentials.\n\n## Adding to Signup\nBefore enabling second factor when signing in, the ToDo list application needs to have the user's phone number. The \neasiest way to ensure we have that - and confirm that it is really the user's number - is use the Verify API to make \nnumber confirmation part of the signup process as well.\n\nThis also helps avoid spoof accounts, as a user must provide their phone number when they signup and we can force that\nnumber to be unique per user.\n\nFirst we need to add a phone number field to our database. Since we're using Parse, we can go to the Parse dashboard and\nadd a `phoneNumber` string to the `User` class. But we don't even have to do that, as setting the field will \nautomatically create it in Parse.\n\nWe also need to add that field to the signup form, an easy addition to [the signup template][signup_template]. Following\nthe bootstrap borrowed markup, we just add another input element:\n\n    \u003clabel for=\"phone\" class=\"sr-only\"\u003ePhone Number\u003c/label\u003e\n    \u003cinput type=\"text\" id=\"phone\" name=\"phone\" class=\"form-control\" placeholder=\"Phone Number\" required\u003e\n\n[*View in Context*][signup_template]\n\nNow in the [`AuthController`][auth_commit] we need to delay creating the user until they've verified the phone number they \nprovided, and we've checked that the number is unique to their account. Verifying ownership of the phone number is where\nwe start using Nexmo's Verify API. \n\nVerifying a number takes two steps. First our application makes a [verfy request][verify_request] to the API and gets a \n`request_id` in response. This starts a process where the user is sent a numeric code by SMS (or should SMS not be \nsuccessful, by a voice call).\n\nOnce the user provides that code to our application, an API request is made to the [verify check][verify_check] endpoint\nto verify that the user provided the correct code. If they did, they've confirmed ownership of the device that was sent\nthe code.\n\nYou can take a look at Nexmo's [Verify Quickstart][verify_quickstart] or the [full Verify docs][verify_ref] for more \ninformation. \n\nTo keep things simple, we'll keep everything signup related submitting to the controller's [`signupAction()`][signup_action]. \nThat means the method needs to be aware of two potential requests: the initial POST where the user provides their\nemail, password, and phone number, and the follow up POST where the user provides the verification code they received \non their phone.\n\nChecking the POST data for a `code` parameter is a simple way to determine which kind of request we're handling. If \nthere is no `code` parameter, we'll assume this is the initial request with all the user data. \n\n    if(!$this-\u003erequest-\u003egetPost('code')){\n    \n[*View in Context*][signup_conditional]\n\nBecause we're delaying the creation of the user until the number is verified the user's data can be stored in the \n`$_SESSION` for now.\n    \n    $_SESSION['signup']['email'] = $this-\u003erequest-\u003egetPost('email');\n    $_SESSION['signup']['password'] = $this-\u003erequest-\u003egetPost('password');\n\n[*View in Context*][signup_session]\n\nWe need to ensure that the number is in international format before verifying it. We can use Nexmo's Number Insight API \nto do that, so we'll grab the NI client library from the service locator. Nexmo's Number Insight API makes it easy to \nfind the international format of any number. We need to provide a default country code as well, and to keep things simple\nwe'll set that to `US`; however, it could be dynamically determined based on the IP address of the user.\n\n    $ni = $this-\u003egetServiceLocator()-\u003eget('Nexmo\\Insight');\n    $result = $ni-\u003ebasic([\n        'number'  =\u003e $this-\u003erequest-\u003egetPost('phone'),\n        'country' =\u003e 'US'\n    ]);\n\n    if(isset($result['international_format_number'])){\n        $_SESSION['signup']['phone'] = $result['international_format_number'];\n    } else {\n        $_SESSION['signup']['phone'] = $this-\u003erequest-\u003egetPost('phone');\n    }\n\n[*View in Context*][signup_ni]\n\nIf for some reason we don't get an internationally formatted number back from the Number Insight API, we'll fallback to \nthe number the user provided.\n\nOnce we have a well formatted number we can start the verification process. Making a request to the verify API requires\ntwo parameters, the number you want to verify and the 'brand' to display to the user. \n\n    $response = $this-\u003everify-\u003everify([\n        'number' =\u003e  $number,\n        'brand' =\u003e 'ToDo List'\n    ]);\n\nLike the Number Insight client, we pull `$this-\u003everify` from the service locator. Because we'll use it more than once, \nwe pull it from the service locator when the request is dispatched by overriding the `onDispatch()` method. Using a \n[client library][client] makes calling the API simple. However, behind the scenes it's just turning those parameters \ninto HTTP parameters, adding the credentials the client library was initialized with, and parsing the API response JSON \ninto an array.\n\nWe can check the `status` property of the response to confirm that the verification process started. If `status` is \nanything other than `0`, there was a problem with the request. In that case we'll take the `error_text` from the \nresponse, and notify the user using the `flashMessenger()` helper in ZF2. The view is [already setup][flash_messenger] \nto display any messages to the user.\n\n    if($response['status'] != 0){\n        $this-\u003eflashMessenger()-\u003eaddErrorMessage($response['error_text']);\n        return;\n    }\n\nIf the verification process was successful, then we just need to set a `$_SESSION` variable. `request` will store the \ncurrent verification request ID, which we'll use to verify that the user has provided us with the right code.  \n\n    $_SESSION['verify']['request'] = $response['request_id'];\n\nNow we need to prompt the user for the code. But before we do that, let's take a step back. This is a process that will\nhappen more than once. We'll be doing a verification during signup, a verification during signin, and potentially a \nverification for other critical actions.\n \nSo it makes sense to abstract this whole process into something reusable. Since we're using PHP, that'll be a method\non a trait so we can use it in any other class. In other languages you might some other form of mixin, regular class \ninheritance, or wrap it in a standalone object that can be reused.\n\n    class AuthController extends AbstractActionController\n    {\n        use VerifyTrait;\n        //...\n\n[*View in Context*][use_trait]\n\nTo start things off, we'll move the code that fetches a Verify client library in the `onDispatch()` method. If you're \nfollowing along, just [grab the full trait code][trait] and remove all but the [`onDispatch()` method][trait_dispatch].\n\nWe'll create a `startVerification()` to start the verification process, and a `verifyPrompt()` method to \ndisplay the confirmation form. `startVerification()` is just the code we had added to the `signupAction()`. But since we \ncan use this in multiple controllers, we may need to change what URL the verification prompt submits the code to. That \nwill only be relevant for a single verification request, so we allow an optional `url` to be passed to \n`startVerification()` and store it in the `$_SESSION`.\n\n    protected function startVerification($number, $url = null)\n    {\n        $response = $this-\u003everify-\u003everify([\n            'number' =\u003e  $number,\n            'brand' =\u003e 'ToDo List'\n        ]);\n\n        if($response['status'] != 0){\n            $this-\u003eflashMessenger()-\u003eaddErrorMessage($response['error_text']);\n        }\n\n        $_SESSION['verify']['url'] = $url;\n        $_SESSION['verify']['request'] = $response['request_id'];\n    }\n    \n[*View in Context*][startVerification]\n\nThe `verifyPrompt()` method is rather simple. By default, ZendFramework just renders the view template that matches \nthe name of the controller's action. `verifyPrompt()` sets up a custom template that [prompts the user for a \ncode][verify_template]. It can also allow us to customize the prompt.\n\n    protected function verifyPrompt($prompt)\n    {\n        $view = new ViewModel([\n            'prompt' =\u003e $prompt,\n            'url'    =\u003e $_SESSION['verify']['url'],\n        ]);\n\n        $view-\u003esetTemplate('verify');\n        return $view;\n    }\n    \n[*View in Context*][verifyPrompt]\n\nNow that we've abstracted the common functionality, we can add a few lines to the `signupAction()`, starting the \nverification process, and prompting the user for the verification code they were sent.\n\n    $this-\u003estartVerification($_SESSION['signup']['phone']);\n    return $this-\u003everifyPrompt('Please Verify Your Number');\n\nBut there's still an issue if the user hits refresh, sending the same data again, the application trying to start the\nverification process a second time. While Nexmo's Verify API won't allow a concurrent verification request for the same \nnumber, avoiding that accidental refresh entirely creates a much better user experience.\n\nRedirecting the user after submitting a form is a common technique used to avoid that situation. In this case, using \nthat pattern will also simplify handling incorrect verification codes. \n   \nSo back in our trait, we add a rather verbosely named `showVerifyIfNeeded()` method that will - in case you can't \nguess it - show a verification prompt if needed. The logic is rather straightforward. If the `$_SESSION` has a request\nID, and if that request is currently in progress, return the verification prompt. \n\n    protected function showVerifyIfNeeded($prompt = 'Please Verify Your Number')\n    {\n        //check if we're in the middle of a verification\n        if(isset($_SESSION['verify']['request'])){\n            $response = $this-\u003everify-\u003esearch([\n                'request_id' =\u003e $_SESSION['verify']['request']\n            ]);\n            \n            if(isset($response['status']) AND 'IN PROGRESS' == $response['status']){\n                return $this-\u003everifyPrompt($prompt);\n            }\n        }\n    }\n    \n[*View in Context*][showVerifyIfNeeded]\n\nLike `verifyPrompt()` we allow a custom prompt. We also use the Verify API to search for a verification by ID, so we \ncan check the status. The verification prompt will only be returned for verifications that are in progress. Those are \nverifications where the code can still be checked. Any verification that has expired due to the time lapsed, or has\nfailed due to too many invalid codes, will not return the verification prompt.\n\nWe can now use `showVerifyIfNeeded()` in our `signupAction()`. At the start of the method we already check to see if \nthe request was a POST. We return if it was not as there's nothing to do except show the default view template with the\nsignup form. Now we return the verification prompt if we're in the middle of a verification.\n\n    if(!($this-\u003erequest instanceof Request) OR !$this-\u003erequest-\u003eisPost()){\n        return $this-\u003eshowVerifyIfNeeded();\n    }\n    \n[*View in Context*][signup_show]\n    \nOn the initial POST to the `signupAction()` to create the user, once the verification process is started we redirect \nback to the same action. If the verification was successfully started the prompt will be displayed. If it was not, the \nerror was passed to the user in the flash messenger, and will be displayed after the redirect. \n    \n    $this-\u003estartVerification($_SESSION['signup']['phone']);\n    return $this-\u003eredirect()-\u003etoRoute('auth', ['action' =\u003e 'signup']);\n    \n[*View in Context*][signup_verify]\n    \nNow that we have a pretty robust and reusable way to start the verification process, we need to check the code the user\nsubmits and if it is correct, create the user's account. The conditional code we added to start the verification process\nis skipped when the user submits a `code`.\n\n    if(!$this-\u003erequest-\u003egetPost('code')){\n        //...\n        return $this-\u003eredirect()-\u003etoRoute('auth', ['action' =\u003e 'signup']);\n    }\n    \n[*View in Context*][signup_start]\n\nThe original code that created the Parse user has to be updated to use the data we stored in `$_SESSION`, and to add the\nverified phone number to the user's account.\n \n    $email = $_SESSION['signup']['email'];\n    $password = $_SESSION['signup']['password'];\n    $phone = $_SESSION['signup']['phone'];\n    \n    $user = new ParseUser();\n    $user-\u003esetUsername($email);\n    $user-\u003esetPassword($password);\n    $user-\u003eset('phoneNumber', $phone);\n\n[*View in Context*][signup_data]\n\nThe rest of the user creation stays the same. But we need to ensure this code is only reached when the user submits the \ncorrect `code`, not just any code. \n\nChecking the user provided code is something we'll do everywhere, so we'll add a `checkCode()` method to our trait. \nSince we stored the request ID in `$_SESSION` we only need to pass the method the user's code. \n\n    protected function checkCode($code)\n    {\n        $response = $this-\u003everify-\u003echeck([\n            'request_id' =\u003e $_SESSION['verify']['request'],\n            'code' =\u003e $code\n        ]);\n\n        if($response['status'] != 0){\n            $this-\u003eflashMessenger()-\u003eaddErrorMessage($response['error_text']);\n            return false;\n        }\n        return true;\n    }\n    \n[*View in Context*][checkCode]\n    \nIf the API response is unsuccessful, we'll add the error as a flash message. The calling code can use the return value\nof the method to determine if the check was successful or not. Now in our `signupAction()` we just add a call to \n`checkCode()` before creating the user.\n\n    if(!$this-\u003echeckCode($this-\u003erequest-\u003egetPost('code'))){\n        return $this-\u003eredirect()-\u003etoRoute('auth', ['action' =\u003e 'signup']);\n    }\n    \n[*View in Context*][signup_check]\n\nAt this point we can start to see the advantage of `showVerifyIfNeeded()`. Should a user submit the incorrect \nverification code, they'll be redirected back to the `signupAction()`, and if the verification is still active they'll \nbe prompted again. If they've entered an incorrect code too many times, or of the verification has timed out, they'll \nsee the signup form and can start again. In either case the error will also be displayed due to the flash message.\n\n## Protecting Signin \nNow that users have a verified phone number on their account, we can enable second factor authentication on signin. \nSince we're using the `signinAction()` as a [cheap way to do sign users out][signout] of the application, we have to relocate the \nrather aggressive `ParseUser::logout()` so it doesn't logout users that are in the middle of a verification process. \nIf there's a Parse user and `$_SESSION['todo']['user']` has the same username, we need to sign the user out. Any other \ncondition is an inprocess signin.\n\n    if($user = ParseUser::getCurrentUser() AND $_SESSION['todo']['user'] == $user-\u003egetUsername()){\n        ParseUser::logOut();\n        $_SESSION['todo']['user'] = null;\n    }\n\n[*View in Context*][signout_conditional]\n\nUsing the same pattern as we did in the `signupAction()` we'll take any non-POST and show a [verification prompt if \nneeded][auth_signin]. And the same conditional handles starting the verification process on the initial signin request.\n\n    if(!($this-\u003erequest instanceof Request) OR !$this-\u003erequest-\u003eisPost()){\n        return $this-\u003eshowVerifyIfNeeded();\n    }\n    \n    if(!$this-\u003erequest-\u003egetPost('code')){\n    //...\n    \n[*View in Context*][signin_conditional]    \n\nWe'll put the original signin code in that conditional. Any incorrect username and password will just redirect to the \nsignin form, and the error will be passed in the flash messenger. But now we'll make sure the `$_SESSION` key we use to \ntrack the signed in user is set to null, as we don't want just a correct username and password to signin the user.\n\n    try {\n        $user = ParseUser::logIn($this-\u003erequest-\u003egetPost('email'), $this-\u003erequest-\u003egetPost('password'));\n        $_SESSION['todo']['user'] = null;\n    } catch (ParseException $e) {\n        $this-\u003eflashMessenger()-\u003eaddErrorMessage($e-\u003egetMessage());\n        return $this-\u003eredirect()-\u003etoRoute('auth', ['action' =\u003e 'signin']);\n    }\n    \n[*View in Context*][signin_auth]\n    \nIf the login is successful, then we'll start a verification process, just like the `signupAction()`. Only now the number\nwill come from the authenticated user. Like before, regardless of the outcome of the verification process we'll redirect\nto the `signinAction()` and let `showVerifyIfNeeded()` handle the verification prompt. \n\n    $this-\u003estartVerification($user-\u003eget('phoneNumber'));\n    return $this-\u003eredirect()-\u003etoRoute('auth', ['action' =\u003e 'signin']);\n    \n[*View in Context*][signin_verify]\n    \nIf the user provides a 'code', and there's a logged in user in the Parse session, the conditional is skipped. The code \nis checked, and if invalid the user is redirected to the `signinAction()` where they are prompted again if the \nverification process is still active. If it is not, they have to start the login process again.\n\n    if(!$this-\u003echeckCode($this-\u003erequest-\u003egetPost('code'))){\n        return $this-\u003eredirect()-\u003etoRoute('auth', ['action' =\u003e 'signin']);\n    }\n    \n    $user = ParseUser::getCurrentUser();\n    $_SESSION['todo']['user'] = $user-\u003egetUsername();\n    $this-\u003eredirect()-\u003etoRoute('app');\n    \n[*View in Context*][signin_check]\n    \nIf the code is valid, the login success is stored in the `$_SESSION` just like before, and the user redirected to the \n`AppController`. \n\nAbstracting the verification process made enabling second factor on the signin a simple change. But we can take it one\nstep further.\n\n## Second Factor for Delete\nOur ToDo list items are pretty important. Should we walk away from our computer and not lock it, we wouldn't want some\nrandom co-worker to stop by and delete them all. We can prevent that catastrophic possibility by [adding second factor\nauthentication to the delete process of the ToDo list application][delete_commit].\n\nSince this is the first time we're doing a verification process in the [`AppController`][app] we need to add a use \nstatements for the [`VerifyTrait`][trait]\n\n    class AppController extends AbstractActionController\n    {\n        use VerifyTrait;\n        //...\n\n[*View in Context*][app_trait]\n\nOur `deleteAction()` expects a POST with the ID of the todo item, queries Parse to find that item, and then destroys it.\nOnce done, it redirects to the main `AppController` action which just renders a list of items to do.\n\n    $query = new ParseQuery(self::PARSE_CLASS);\n    try {\n        $item = $query-\u003eget($this-\u003erequest-\u003egetPost('id'));\n        $item-\u003edestroy();\n    } catch (ParseException $e) {\n        $this-\u003eflashMessenger()-\u003eaddErrorMessage($e-\u003egetMessage());\n    }\n    $this-\u003eredirect()-\u003etoRoute('app');\n\n[*View in Context*][delete] \n \nFollowing the pattern of signup and signin, we add a conditional to check if the user POSTed a `code`. If they didn't,\nthe id is stored in the `$_SESSION`, we start a verification, then we redirect to the main app.\n\n    if(!$this-\u003erequest-\u003egetPost('code')){\n        $_SESSION['todo']['delete'] = $this-\u003erequest-\u003egetPost('id');\n        $this-\u003estartVerification($this-\u003euser-\u003eget('phoneNumber', '/app/delete'));\n        $this-\u003eredirect()-\u003etoRoute('app');\n    }\n    \n[*View in Context*][delete_conditional] \n\nThe only thing different this time is that we provide `startVerification()` with a URL that sets where the form is \nsubmitted. This allows us to always redirect the `deleteAction()` back to the main `indexAction()`, but still have the \nverification code submitted to the `deleteAction()`.\n\nWhen a `code` is sent to the `deleteAction()` it's checked and if it's valid the item is deleted.\n\n    $code = $this-\u003erequest-\u003egetPost('code');\n    if($this-\u003echeckCode($code)){\n        $query = new ParseQuery(self::PARSE_CLASS);\n        try {\n            $item = $query-\u003eget($_SESSION['todo']['delete']);\n            $item-\u003edestroy();\n            $_SESSION['todo']['delete'] = null;\n        } catch (ParseException $e) {\n            $this-\u003eflashMessenger()-\u003eaddErrorMessage($e-\u003egetMessage());\n        }\n    }\n    \n    $this-\u003eredirect()-\u003etoRoute('app');\n    \n[*View in Context*][delete_check]\n\nSince there's never any view for the `deleteAction()`, every request is redirected to the main app action \n`indexAction()`. It's there we add `showVerifyIfNeeded()` to handle prompting the user for a verification code.\n\n    if($view = $this-\u003eshowVerifyIfNeeded()){\n        return $view;\n    }\n    \n[*View in Context*][delete_show]\n\n## Next Steps\nAdding second factor authentication to your application is not a complex process with Nexmo's Verify API. At this point \nwe have a nicely abstracted way to add phone number powered second factor to any part of our application. However, there \nare things that are obviously missing or could be better. Here are a couple big ones to keep in mind.\n\nOur application is simple, and there's no place to update the user's phone number (or even password). When adding \nphone numbers as a second factor, you'll also need to add a way for the user to update that phone number. And there's \nno way to have users add a phone number, if they signed up *before* support for second factor login was added.\n\nError handling in this example is at best simplistic. If it's not possible to get the international format of the \nnumber from the Number Insight API, that's a signal that there's probably something wrong with the number the user \nprovided. And just echoing the error message from the Verify API to the user makes things simple, but it's definitely \nbetter to check the error code and provide the user with a message crafted to help them know what to do next.\n\nYou'll also find a few anti-patterns, like accessing the service locator from the controller instead of injecting all \ndependencies, sharing the $_SESSION global across all the controllers, or using a trait that is directly tied to an \nabstract class. All can be avoided with proper dependency injection; but that would make this example a bit harder to \nfollow. \n\nNow that you've seen how easy it is to add second factor authentication to signin or any other important part of \nyour application, add it to your application today and keep your user's accounts secure.\n\n[composer]: https://getcomposer.org/doc/00-intro.md\n[local_config]: https://github.com/Nexmo/ToDo-Web/blob/master/config/autoload/local.php.dist\n[config_setup]: https://github.com/Nexmo/ToDo-Web/blob/master/module/Todo/src/SetupController.php#L40-L53\n[schema]: https://github.com/Nexmo/ToDo-Web/blob/master/schema.json\n[parse_setup]: https://github.com/Nexmo/ToDo-Web/blob/master/module/Todo/src/SetupController.php#L88-L107\n[module_config]: https://github.com/Nexmo/ToDo-Web/blob/master/module/Todo/config/module.config.php\n[app]: https://github.com/Nexmo/ToDo-Web/blob/master/module/Todo/src/AppController.php\n[auth]: https://github.com/Nexmo/ToDo-Web/blob/master/module/Todo/src/AuthController.php\n[parse_create]:https://github.com/Nexmo/ToDo-Web/blob/master/module/Todo/src/AuthController.php#L23-L25\n[parse_login]: https://github.com/Nexmo/ToDo-Web/blob/master/module/Todo/src/AuthController.php#L50-L51\n[redirect]: https://github.com/Nexmo/ToDo-Web/blob/master/module/Todo/src/AuthController.php#L52\n[client]: http://www.phillipshipley.com/2015/04/creating-a-php-nexmo-api-client-using-guzzle-web-service-client-part-1/\n[signup_template]: https://github.com/Nexmo/ToDo-Web/blob/verify-signup/module/Todo/view/signup.phtml#L6-L7\n[auth_commit]: https://github.com/Nexmo/ToDo-Web/commit/verify-signup#diff-5a5461e66ce9d6b08b64f818e3a65f6c\n[verify_request]: https://docs.nexmo.com/api-ref/verify\n[verify_check]: https://docs.nexmo.com/api-ref/verify/check\n[verify_quickstart]: https://developers.nexmo.com/Quickstarts/verify/verify/\n[verify_ref]: https://docs.nexmo.com/api-ref/verify\n[signup_action]: https://github.com/Nexmo/ToDo-Web/blob/verify-signup/module/Todo/src/AuthController.php#L16-L67\n[signup_conditional]: https://github.com/Nexmo/ToDo-Web/blob/verify-signup/module/Todo/src/AuthController.php#L23\n[signup_session]: https://github.com/Nexmo/ToDo-Web/blob/verify-signup/module/Todo/src/AuthController.php#L25-L26\n[signup_ni]: https://github.com/Nexmo/ToDo-Web/blob/verify-signup/module/Todo/src/AuthController.php#L29-L39\n[flash_messenger]: https://github.com/Nexmo/ToDo-Web/blob/master/view/layout/layout.phtml#L36-L43\n[use_trait]: https://github.com/Nexmo/ToDo-Web/blob/verify-signup/module/Todo/src/AuthController.php#L8-L10\n[trait]: https://github.com/Nexmo/ToDo-Web/blob/verify-signup/module/Todo/src/VerifyTrait.php\n[trait_dispatch]: https://github.com/Nexmo/ToDo-Web/blob/verify-signup/module/Todo/src/VerifyTrait.php#L9-L24\n[startVerification]: https://github.com/Nexmo/ToDo-Web/blob/verify-signup/module/Todo/src/VerifyTrait.php#L26-L39\n[verify_template]: https://github.com/Nexmo/ToDo-Web/blob/verify-signup/module/Todo/view/verify.phtml\n[verifyPrompt]: https://github.com/Nexmo/ToDo-Web/blob/verify-signup/module/Todo/src/VerifyTrait.php#L41-L50\n[showVerifyIfNeeded]: https://github.com/Nexmo/ToDo-Web/blob/verify-signup/module/Todo/src/VerifyTrait.php#L52-L64\n[signup_show]: https://github.com/Nexmo/ToDo-Web/blob/verify-signup/module/Todo/src/AuthController.php#L18-L20\n[signup_verify]: https://github.com/Nexmo/ToDo-Web/blob/verify-signup/module/Todo/src/AuthController.php#L41-L42\n[signup_start]: https://github.com/Nexmo/ToDo-Web/blob/verify-signup/module/Todo/src/AuthController.php#L23-L43\n[signup_data]: https://github.com/Nexmo/ToDo-Web/blob/verify-signup/module/Todo/src/AuthController.php#L50-L57\n[checkCode]: https://github.com/Nexmo/ToDo-Web/blob/verify-signup/module/Todo/src/VerifyTrait.php#L66-L79\n[signup_check]: https://github.com/Nexmo/ToDo-Web/blob/verify-signup/module/Todo/src/AuthController.php#L46-L48\n[signout]: https://github.com/Nexmo/ToDo-Web/blob/master/module/Todo/src/AuthController.php#L43\n[signout_conditional]: https://github.com/Nexmo/ToDo-Web/blob/verify-signin/module/Todo/src/AuthController.php#L75-L78\n[auth_signin]: https://github.com/Nexmo/ToDo-Web/commit/verify-signin\n[signin_conditional]: https://github.com/Nexmo/ToDo-Web/blob/verify-signin/module/Todo/src/AuthController.php#L80-L84\n[signin_auth]: https://github.com/Nexmo/ToDo-Web/blob/verify-signin/module/Todo/src/AuthController.php#L85-L91\n[signin_verify]: https://github.com/Nexmo/ToDo-Web/blob/verify-signin/module/Todo/src/AuthController.php#L93-L94\n[signin_check]: https://github.com/Nexmo/ToDo-Web/blob/verify-signin/module/Todo/src/AuthController.php#L97-L103\n[delete_commit]: https://github.com/Nexmo/ToDo-Web/commit/verify-delete#diff-9468e6b6d5f46e314916c360ca4e33dc\n[app_trait]: https://github.com/Nexmo/ToDo-Web/blob/verify-delete/module/Todo/src/AppController.php#L12-L14\n[delete]: https://github.com/Nexmo/ToDo-Web/blob/master/module/Todo/src/AppController.php#L89-L97\n[delete_conditional]: https://github.com/Nexmo/ToDo-Web/blob/verify-delete/module/Todo/src/AppController.php#L95-L99\n[delete_check]: https://github.com/Nexmo/ToDo-Web/blob/verify-delete/module/Todo/src/AppController.php#L101-L113\n[delete_show]: https://github.com/Nexmo/ToDo-Web/blob/verify-delete/module/Todo/src/AppController.php#L48-L50","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnexmo%2Ftodo-web","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnexmo%2Ftodo-web","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnexmo%2Ftodo-web/lists"}