Passwordless Authentication With Slim and Vue.js | TeleSign

TeleSign’s self-service portal makes it easy for anyone to freely experiment with our platform. As a developer here, this is really exciting and something I’m happy to be part of. To help showcase how our platform and self-service portal can be utilized, I’m going to walk you through building a web app with a passwordless sign-in system.

By “passwordless,” we mean there will be no need for an end-user to enter a password at account sign-in. Instead, a one-time passcode (OTP) will be sent to the end-user’s mobile device. Let’s dig in to the requirements a little more.

  • The app will need a sign-in page with an input field for the end-user’s phone number and a big sign-in button. The sign-in button can also be swapped for a sign-up button, depending on the end-user’s account stage. We won’t be needing any other information from the end-user, other than their phone number. The phone number will be their unique ID.
  • Just to make things a bit simpler and to the point, the user won’t be creating any data, he/she will only need access to a restricted area of the app with, for instance, a secret list of music albums.
  • Once the user has submitted the form, the app needs to send an SMS to his/her phone, containing the OTP.
  • The app also needs to present to the end-user a form to re-enter the OTP and complete the sign-in by submitting the form.
  • Finally, he/she should be presented with the secret list.

Let’s call this app, “Passwordless Demo.”

Scope

Since this is just a demo, we won’t cover topics like encrypting HTTP traffic, logging, monitoring, testing, application security and UX, which are all necessary for any app to be production-ready. We did however recently publish a great post on UX, focusing specifically on phone verification forms. It includes our recommendations and references to examples of implementations.

Security

If you are reading this post because you’re interested in setting up a passwordless authentication system yourself, there are a few more aspects of going passwordless to consider. Phones get stolen and their ownership can be spoofed. Passwords, on the other hand, can be stored securely and have no natural intermediary. Passwordless alone is a trade-off between usability and security, with passwordless being conceptually less secure and having greater usability compared to passwords. Unless you are adding an extra layer of security on top of sent OTPs, you are trading security for usability and you should not do that in case the data behind the authentication system fits into any of these categories:

  • It contains PII, including PHI
  • Its breach could cause reputational or monetary harm
  • Its breach would trigger national or local breach notification laws

Each website is a different story, so as an example, if you are building a photo sharing website where people come to get inspired and spark off new ideas, chances are they would only need an account handle. Since there’s no PII involved other than the telephone number for signing in, there’s no additional PII to disclose or take advantage of. Therefore, it’s fine to use passwordless. On the other hand, if you are building an e-banking portal or a doctor’s website, a hacked account could cause greater damage and you should opt for a more secure authentication mechanism.

We’ll be publishing a sequel to this article in which we will guide you through implementing two-factor authentication to help you make an informed decision for yourself (follow us on Twitter to get a notification when we make it public).

The Stack

We’ve picked two magnificent frameworks, Vue.js and Slim. The logic behind these picks is to quickly prototype the client and build it independently—to be able to serve it as a static website and declare a clear http interface for the actions that will have to happen on a web server. Along the way, we will use a handful of libraries that you’ll hopefully find as rewarding as the two frameworks.

TL;DR

If you are more interested in skimming through to the final result, go here for the service and here for the client.

Scaffolding the HTTP Service

OK, the fun part. Get yourself a machine with PHP 5.6 or above and Composer installed. And let’s start with an empty directory, mkdir passwordless && cd passwordless, and declare dependency on Slim with composer require slim/slim. Then let’s make a file for the list of music albums, touch albums.json and another one for the front controller, and basically the whole of our http service, mkdir public && touch public/app.php. We will be running our service with the PHP’s built-in webserver, with php -S 0.0.0.0:8081 -t public public/app.php, so let’s add a custom Composer command to composer.json, the file that has been generated by the require command we’ve run.

  "scripts": {
    "start": "php -S 0.0.0.0:8081 -t public public/app.php"
  },
  "config": {
    "process-timeout": 0
  }

That will allow us to start our service by typing only composer start. The config part tells Composer not to impose timeouts on its commands and so he won’t be stopping us from running the service for as long as we want to.

Let’s open app.php in an editor, require the autoloader script and instantiate Slim with this sensible configuration while we are developing.

<?php

require __DIR__ . '/../vendor/autoload.php';

$app = new SlimApp([
  'settings' => [
    'displayErrorDetails' => true
  ]
]);

Let’s also immediately add this bit of boilerplate code that will allow us to call the service from a website served from a different host.

$app->add(function (Request $request, Response $response, callable $next) {
  return $next($request, $response)
    ->withHeader('Access-Control-Allow-Origin', 'http://localhost:8080')
    ->withHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, Origin, Authorization')
    ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
});

$app->options('/{routes:.+}', function (Request $request, Response $response) {
  return $response;
});

If you are looking for more material on CORS, there’s a good article at MDN.

Let’s also add a test route to stop for a while and make sure everything works so far.

$app->get('/ping', function (Request $request, Response $response) {
  return $response->write('pong');
});

And then call run(), to say we are done with defining our service.

$app->run();

Starting the service with composer start and calling curl http://localhost:8081/app.php/ping should respond with “pong”.

If that works for you too, let’s continue with defining other routes.

$app->group('/api/authenticate', function () use ($app) {
  $app->get('/send_otp', function ($request, $response) {});
  $app->get('/verify_otp', function ($request, $response) {});
});

$app->group('/api/protected', function () use ($app) {
  $app->get('/albums', function ($request, $response) {});
});

That’s three new routes, /api/authenticate/send_otp, /api/authenticate/verify_otp and /api/protected/albums, grouped such that we can later apply request authorization routines specific to those groups in form of Slim middleware.

Send OTP Routine

This one is the callback function for the /app/authenticate/send_otp route and it will expect one query parameter, phone number, that the user enters from the sign-in page – remember the requirements. Variable $request implements a standards-based PSR-7 interface, so we can collect the phone number with:

    $params = $request->getQueryParams();
    $phone = $params['phone'];

At this point, let’s require TeleSign SDK for PHP, composer require telesign/telesign and make an alias for its MessagingClient and randomWithNDigits, right after the PHP’s open tag:

use telesignsdkmessagingMessagingClient;
use function telesignsdkutilrandomWithNDigits;

And because we’ll be dealing with a client ID and a secret key, let’s also do composer require vulcas/phpdotenv, make a .env file in our project root and store those two parameters inside the .env as:

TELESIGN_CUSTOMER_ID="telesign_customer_id"
TELESIGN_SECRET_KEY="telesign_secret_key"

To obtain your own customer id and secret key, go to sign up for an account, here.

Now let’s add use DotenvDotenv; to the aliases list and load those parameters by adding the anticipated middleware to the /api/authenticate group:

})->add(function (Request $request, Response $response, callable $next) {
  $dotenv = new Dotenv(__DIR__ . '/..');
  $dotenv->load();
  return $next($request, $response);
});

Inside of our route callback, let’s generate an OTP with 5 digits and send it to the provided phone number.

    $otp = randomWithNDigits(5);

    $telesign_response = (
      new MessagingClient(getenv('TELESIGN_CUSTOMER_ID'), getenv('TELESIGN_SECRET_KEY'))
    )->message($phone, "Your OTP is $otp.", 'OTP', [
      'account_lifecycle_event' => 'sign-in'
    ]);

$telesign_response is supposed to give us response code 200 and a reference ID, that is a unique number of the request we are making, so let’s return an error if that doesn’t happen:

    if ($telesign_response->status_code != 200 or !isset($telesign_response->json['reference_id'])) {
      return $response->withJson([
        'error' => "$telesign_response->status_code: $telesign_response->body"
      ]);
    }

The withJson() method belongs to the Slim’s Response object. It will do all the necessary heavy lifting for us. It’s enough that we give it a payload in an appropriate PHP type. So, let’s agree the response in case of an error will contain property error and its value will be an error message.

We have to store the OTP somewhere, so let’s use Redis. Install it on a reachable machine and run it with e.g. redis-server --protected-mode no, but then consider whether you want protected mode off or not. Also make sure the default port 6379 is not blocked for you with a firewall.

Let’s require predis/predis, alias its Client class with use PredisClient as PredisClient;, add REDIS_URL="redis_url" to .env and store $otp in Redis with:

    try {
      $redis = new PredisClient(getenv('REDIS_URL'));
      $redis->set($telesign_response->json['reference_id'], $otp);
      $redis->expire($telesign_response->json['reference_id'], 60);
    }
    catch (Exception $e) {
      return $response->withJson([
        'error' => 'Problem accessing storage'
      ]);
    }

We’ve used reference ID as key for the OTP and set its ttl to 1 minute. That’s the time interval in which the OTP will make sense to us.

All is now left to do is return reference ID to the client – it could have been an internal reference that maps uniquely to the value of $otp. Let’s also wrap it in a signed JWT, so no one but us can make it valid. We’ll need to add one more property to .env, SECRET_KEY="secret_key", require firebase/php-jwt, add another alias use FirebaseJWTJWT; and add at the end of our route callback:

    $jwt = JWT::encode([
      'sub' => $phone,
      'reference_id' => $telesign_response->json['reference_id'],
      'exp' => strtotime('1 minute')
    ], getenv('SECRET_KEY'));

    return $response->withJson([
      'authorization_code' => $jwt // Anyone w/o this won't be able to sign in
    ]);

Its expiration time will roughly match expiration time of the OTP. In case the authorization code has expired, we’ll consider the OTP has expired too, and there will be no need to check for that in turn.

The authorization code will also play the role of a client-side session token. In the subsequent call from the client we will read the sub property to identify the user.

As a final touch, we can add this try-catch block to our middleware function, right before we call $next:

  try {
    $dotenv->required('SECRET_KEY')->notEmpty();
    $dotenv->required('REDIS_URL')->notEmpty();
  }
  catch (Exception $e) {
    return $response->withJson([
      'error' => $e->getMessage()
    ]);
  }

That middleware would basically say, load .env, and if there’s no SECRET_KEY or REDIS_KEY or if any of them is empty, return a response with an error message to the client immediately, otherwise proceed with the route callback.

Verify OTP Routine

In this callback for route /api/authorization/verify_otp we’ll expect a user-supplied OTP and authorization code that the user obtained by sending its phone number to our previously-defined endpoint, both as query parameters.

    $params = $request->getQueryParams();
    $user_supplied_otp = $params['otp'];

    try {
      $decoded = JWT::decode($params['authorization_code'], getenv('SECRET_KEY'), ['HS256']);
    }
    catch (Exception $e) {
      return $response->withJson([
        'error' => 'Bad authorization code'
      ]);
    }

There’s nothing much new here, except that the firebase/php-jwt library wants us to list allowed encryption algorithms. And because we didn’t pick an algorithm at the time we were creating authorization code, HS256, the default was used. We could have made it unencrypted, that would also work fine.

Let’s go to Redis to get the original OTP:

    try {
      $otp = (new PredisClient(getenv('REDIS_URL')))->get($decoded->reference_id);
    }
    catch (Exception $e) {
      return $response->withJson([
        'error' => 'Problem accessing storage'
      ]);
    }

Then check if the OTPs match:

    if ($otp === null or $user_supplied_otp != $otp) {
      return $response->withJson([
        'error' => "Could not verify your OTP"
      ]);
    }

Then make a JWT for the authorization header of all subsequent requests to the protected group of endpoints.

    $jwt = JWT::encode([
      'sub' => $decoded->sub,
      'exp' => strtotime('5 minutes')
    ], getenv('SECRET_KEY'));

    return $response->withJson([
      'id_token' => $jwt,
      'token_type' => 'Bearer',
      'phone' => $decoded->sub
    ]);

Returning Albums

There’s one more route to implement /api/protected/albums.

Let’s first check the authorization header for the whole protected group of routes, by adding a middleware to it:

})->add(function (Request $request, Response $response, callable $next) {
  (new Dotenv(__DIR__ . '/..'))->load();

  $authorization_header = $request->getHeader('Authorization');

  if (count($authorization_header) == 0) {
    return $response->withJson([
      'error' => 'Missing authorization header'
    ]);
  }

  $id_token = preg_replace('/^Bearer (.+)$/', '$1', $authorization_header[0]);

  try {
    JWT::decode($id_token, getenv('SECRET_KEY'), ['HS256']);
  }
  catch (Exception $e) {
    return $response->withJson([
      'error' => 'Bad authorization header'
    ]);
  }

  return $next($request, $response);
});

Then, in our route callback we can just return the list:

  return $response
    ->withHeader('Content-Type', 'application/json')
    ->write(file_get_contents(__DIR__ . '/../albums.json'));

Now, we only need the list. Let’s make it follow this format:

{
  "data": [
    {
      "title": "Thriller",
      "author": "Michael Jackson",
      "year": "1982",
      "label": "Epic"
    }
  ]
}

You can copy and paste this into albums.json.

The Website

Now that we are clear with what exactly the service will do, making a client for it is straightforward.

Let’s start with another empty directory, separately from the service, mkdir passwordless-website && passwordless-website. And let’s open a new file touch index.html with the following boilerplate content.

<!doctype html>
<html lang=en>
<head>
  <title></title>
  <meta charset=utf-8>
  <meta name=viewport content=width=device-width,minimum-scale=1,initial-scale=1,user-scalable=yes>
  <style>
  body {
    padding: 50px 0;
  }
  </style>
</head>
<body>
  <div id=app>
    <router-view></router-view>
  </div>
</body>
</html>

Let’s give our page a title by the name of the project, <title>Passwordless Demo</title>.

Let’s link the Vue.js framework, by appending <script src=//unpkg.com/vue@2.1.10/dist/vue.js></script> to the head element. This means there will be no installing dependencies and packaging our client, but instead we will have only this file to deal with. We will be using the unpkg CDN to load other libraries as well.

Actually, we’ll need vue-router right away, so append <script src=//unpkg.com/vue-router@2.1.3/dist/vue-router.js></script> too to the bottom of the head element. Vue-router is a plugin for Vue. Once included, it will look for Vue and add new features to it.

As for the webserver, any that serves static HTML will do. If you need a suggestion, try http-server – we love it!

Website Routes

Now let’s define some routes, or pages of our website. Add a script tag to the bottom of the body element with the following content.

  const APP_SERVICE_URL = 'http://localhost:8081/app.php/api';

  const router = new VueRouter({
    routes: [
      {
        path: '/',
        component: {
          template: '#home-page-template'
        }
      },
      {
        path: '/enter_otp',
        component: {
          template: '#enter-otp-template'
        }
      },
      {
        path: '/secret',
        component: {
          template: '#secret-page-template'
        }
        children: [
          {
            path: '',
            redirect: 'albums'
          },
          {
            path: 'albums',
            component: {
              template: '#albums-template'
            }
          }
        ]
      }
    ]
  });

This gives us effectively three routes, /, /enter_otp and /secret/albums. For each of them we’ll add its own HTML that will be rendered from the template of its corresponding component and in place of the router-view element you’ve seen in the HTML boilerplate.

Each template in turn can have its own router-view element that is a placeholder for rendering nested routes, declared with the children property. We will use this feature of vue-router in order to make a shared header for other possible secret pages such as /secret/albums is.

Vue Templates

There are several ways to define templates and we’ll be using the X-Templates. They are script elements of type text/x-template which id matches the value of a component’s template property, the CSS selector.

Let’s add the template for the / route to the body. We are going to need Bootstrap, so let’s also include <link rel=stylesheet href=//unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css>. The template can now look something like this:

<script type=text/x-template id=home-page-template>
  <form class=form-centered @submit.prevent=send_otp>
    <h2>Passwordless Demo</h2>
    <div class=input-group>
      <span class=input-group-addon>
        <span class="glyphicon glyphicon-phone" aria-hidden=true></span>
      </span>
      <input type=tel class=form-control placeholder="Phone number" name=phone>
    </div>
    <button class="btn btn-primary btn-block">Send an OTP</button>
  </form>
</script>

We’ve used Bootstrap’s input groups and buttons to style this page and added custom form-centered class. There’s Vue’s v-on directive, actually its shorthand “@” notation, which registers handler called send_otp to submit events of the form. The .prevent is a directive modifier which will disable default form submission by the browsers, so we can handle it on our own. There’s Vue’s v-model directive for 2-way binding between the form element’s value and the corresponding component’s phone property.

Because we will start making requests to our HTTP service that we’ve created, let’s include vue-resource with <script src=//unpkg.com/vue-resource@1.0.3/dist/vue-resource.min.js></script>.

Vue Components

We can now switch back to the component, adding the property and the submit event handler:

          data: function () {
            return {
              phone: ''
            };
          },
          methods: {
            send_otp: function (event) {
              this.$http.get(`${APP_SERVICE_URL}/authenticate/send_otp`, {
                params: {
                  phone: this.phone
                }
              }).then(function (response) {
                if (response.body.error) {
                  alert(response.body.error);
                  return;
                }

                localStorage.setItem('authorization_code', response.body.authorization_code);
                this.$router.push('/enter_otp');
              }, function (response) {
                console.error(response);
              });
            }
          }

This handler will read phone from the component’s data and Vue will make sure it’s the same as the value of the bound input field. Then it will send it to the HTTP service. When response is returned, it will store the expected authorization code that keeps reference ID of the request to the TeleSign API, remember? And it will navigate user to the /enter_otp page.

Let’s also add two more rules to the style element, to make the page look prettier.

button {
  margin-top: 6px;
}
.form-centered {
  max-width: 330px;
  padding: 15px;
  margin: auto;
}

Form Validation

Now comes the part that you’ll especially appreciate if you are new to Vue. It’s our pick of a form validation library. Let’s include Vuelidate with <script src=//unpkg.com/vuelidate@0.2.0/dist/vuelidate.min.js></script> and its accompanying validators with <script src=//unpkg.com/vuelidate@0.2.0/dist/validators.min.js></script>.

These are our own validators for the phone number:

  const required = window.validators.required;
  const phone = function (value) {
    return !required(value) || !!value.match(/^[ds()+-]+$/);
  };
  const digitsBetween = function (min, max) {
    return function (value) {
      const numChars = value.match(/d/g);
      return !required(value) || (!!numChars && numChars.length >= min && numChars.length <= max);
    }
  };
  const phoneDigitsBetween = function (min, max) {
    return function (value) {
      return !phone(value) || digitsBetween(min, max)(value);
    }
  };

Vuelidate mandates that we tell Vue to use it explicitly, so let’s also say Vue.use(window.vuelidate.default);.

The component will need the validations property:

          validations: {
            phone: {
              required,
              phone,
              phoneDigitsBetween: phoneDigitsBetween(6, 15)
            }
          },

The template, it can grow into something like this:

  <script type=text/x-template id=home-page-template>
    <form class=form-centered @submit.prevent=send_otp novalidate>
      <h2>Passwordless Demo</h2>
      <div :class="{'input-group-error':$v.phone.$error}">
        <div class=input-group>
          <span class=input-group-addon>
            <span class="glyphicon glyphicon-phone" aria-hidden=true></span>
          </span>
          <input type=tel class=form-control placeholder="Phone number"
              v-model=phone name=phone @blur=$v.phone.$touch()>
        </div>
        <span class=input-group-message v-if=!$v.phone.required>Please enter your phone number</span>
        <span class=input-group-message v-if=!$v.phone.phone>This is not a phone number</span>
        <span class=input-group-message v-if=!$v.phone.phoneDigitsBetween>We only accept 6 to 15 digit phone numbers</span>
      </div>
      <button class="btn btn-primary btn-block">Send an OTP</button>
    </form>
  </script>

with a few additional CSS rules:

  .input-group-message {
    display: none;
    color: #a94442;
  }
  .input-group-error .input-group-message {
    display: inline;
  }
  .input-group-error input {
    border-color: #a94442;
  }
  .input-group-error input:focus {
    box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(169,68,66,.6);
    border-color: #a94442;
  }

That will hide/show error messages as appropriate and make our page look more correct by building on the Bootstrap’s focus state.

We’ve got two more of Vue’s directives, v-if and v-bind, the latter with its shorthand “:”.

The only a bit more complex part is that .input-group-message and .input-group-error .input-group-message rules, together with Vuelidate’s $error property of the field’s validation state and Vue’s conditional class application will hide error messages until the input field becomes dirty and invalid, that is, until the user actually enters an invalid value.

In our submit handler, we should check if the phone is valid and return immediately if it’s not:

              if (this.$v.$invalid) {
                this.$v.$touch();
                return;
              }

Submitting OTP

The /enter_otp page is very similar, with its template:

  <script type=text/x-template id=enter-otp-template>
    <form class=form-centered @submit.prevent=authenticate novalidate>
      <h2>Passwordless Demo</h2>
      <p class=text-info>Please confirm your identity with your OTP.</p>
      <div :class="{'input-group-error':$v.otp.$error}">
        <div class=input-group>
          <span class=input-group-addon>
            <span class="glyphicon glyphicon-lock" aria-hidden=true></span>
          </span>
          <input type=text class=form-control placeholder=OTP
            v-model=otp name=otp @blur=$v.otp.$touch()>
        </div>
        <span class=input-group-message v-if=!$v.otp.required>Please enter your OTP</span>
      </div>
      <button class="btn btn-primary btn-block">Sign In</button>
    </form>
  </script>

and its component:

        component: {
          template: '#enter-otp-template',
          data: function () {
            return {
              otp: ''
            };
          },
          validations: {
            otp: {
              required
            }
          },
          methods: {
            authenticate: function (event) {
              if (this.$v.$invalid) {
                this.$v.$touch();
                return;
              }

              this.$http.get(`${APP_SERVICE_URL}/authenticate/verify_otp`, {
                params: {
                  otp: this.otp,
                  authorization_code: localStorage.getItem('authorization_code')
                }
              }).then(function (response) {
                if (response.body.error) {
                  alert(response.body.error);
                  return;
                }

                localStorage.setItem('userData', JSON.stringify(response.body));
                localStorage.removeItem('authorization_code');
                this.$router.push('/secret/albums');
              }, function (response) {
                console.error(response);
              });
            }
          }
        }

Only this time we send value of the otp property and the authorization code from the prior response, and we expect user data and store it for subsequent use on the /secret/albums page.

Let’s make that last page. Honestly, congratulations on reading through this far. We are almost done.

Fetching Albums

The /secret‘s template, containing a router-view that we’ve spoken of:

  <script type=text/x-template id=secret-page-template>
    <div>
      <nav class="navbar navbar-default navbar-fixed-top">
        <div class=container>
          <div class=navbar-header>
            <router-link class=navbar-brand to=/secret/albums>Passwordless Demo</router-link>
          </div>
          <div class="collapse navbar-collapse">
            <ul class="nav navbar-nav">
              <router-link to=/secret/albums tag=li active-class=active>
                <router-link to=/secret/albums>Albums</router-link>
              </router-link>
            </ul>
            <p class="navbar-text navbar-right"><a href="/" @click.prevent=signOut><small>Sign Out</small></a></p>
            <p class="navbar-text navbar-right">You're signed in with <strong>{{ userData.phone }}</strong>.</p>
          </div>
        </div>
      </nav>
      <div class=container>
        <router-view></router-view>
      </div>
    </div>
  </script>

What’s new here is that we’ve used Bootstrap’s navbar, vue-router’s declarative navigation and Vue’s text interpolation. You can notice how Vue plays nicely with Bootstrap with this example.

The /secret‘s component, with nothing new other than the created lifecycle hook:

    component: {
      template: '#secret-page-template',
      data: function () {
        return {
          userData: {}
        };
      },
      created: function () {
        this.userData = JSON.parse(localStorage.getItem('userData'));
      },
      methods: {
        signOut: function () {
          localStorage.clear();
          this.$router.push('/');
        }
      }
    }

Let’s add a template for our nested albums route:

  <script type=text/x-template id=albums-template>
    <div class=albums>
      <div class=list-group v-for="a in albums">
        <div class=list-group-item>
          <h4 class=list-goup-item-heading><strong>{{ a.title }}</strong> by <em>{{ a.author }}</em></h4>
          <p class=list-group-item-text>{{ a.label }} ★ {{ a.year }}</p>
        </div>
      </div>
    </div>
  </script>

that differs by the use of the v-for directive, and its component:

            component: {
              template: '#albums-template',
              data: function () {
                return {
                  userData: {},
                  albums: []
                };
              },
              created: function () {
                this.userData = JSON.parse(localStorage.getItem('userData'));
                this.getAlbums();
              },
              methods: {
                getAlbums: function () {
                  this.$http.get(`${APP_SERVICE_URL}/protected/albums`, {
                    headers: {
                      'Authorization': `${this.userData.token_type} ${this.userData.id_token}`
                    }
                  }).then(function (response) {
                    if (response.body.error) {
                      alert(response.body.error);
                      return;
                    }

                    this.albums = response.body.data;
                  }, function (response) {
                    console.error(response);
                  });
                }
              }
            }

And this one is different because we’ve used a named pattern, fetching after navigation, when requesting the albums from our HTTP service. And because we’ve set the authorization header with the id token that we’ve got back after submitting the correct OTP. For how we’ve defined our HTTP service, we would be doing that, setting the header the same way, for every request to any other service’s routes under the /api/protected group.

And lastly, let’s tell Vue we are done with configuration and it can render the app.

  new Vue({
    el: '#app',
    router
  });

Run the app and try it out!

Further Reading

There are other good features of our application stack that we wish we could’ve touched in this article, so here’s some recommended reading for that:

Get Started Now
Try Our API

GET STARTED WITH TELESIGN

Integrate our products seamlessly into your user experience.
TALK TO AN EXPERT