Vue with TozID

Introduction

In this guide we'll show you how use both TozID and TozStore to securely authenticate and store data using end to end encryption in a Vue based application. We'll keep things simple and use the Vue CLI to bootstrap our application and only use the Tozny SDK as our other dependency. Once we're done this application will let you do the following: 1. Secure login with zero knowledge authentication powered by TozID 2. End to end encrypted reads and writes using TozStore This tutorial assumes you have some basic knowledge of Vue but we'll walk through all of the steps. As always the complete source code is linked below.

Project Initialization

We'll assume you're using Node 12 (but we've tested this back to Node 8 without issue). Please be sure you have the Vue CLI tools installed. If you don't just run this command from your terminal.

npm install -g @vue/cli
# OR
yarn global add @vue/cli

Now lets create our actual project. Vue CLI walks us through that process when we initialize a new project. Here are the configuration steps we've chosen:

vue create tozny-vue-auth-example

Selecting presets from the CLI

We'll use Vuex, Vue Router, and Babel.

The remaining configurations are left as the defaults, using history router and dedicated config files. Once that process has finished, we're ready to start coding. You can start your blank project by issuing the following commands:

cd tozny-vue-auth-example //or whatever you named the project
npm run serve

If successful you should get the standard Vue initialization app that looks like the image below.

If you're seeing the webpage above when you navigate to the URL indicated in your terminal then we're in the right spot. Let's get our Tozny account configured and we'll be ready to code.

Setting Up Tozny

Setting up your Tozny account is straight forward. We'll need to complete two steps, creating an account and creating a realm. Both are free - get started by heading over to the Tozny Dashboard and either logging in or registering an account.

Be sure to use an email address you have access to. You need to verify your email before writes to the platform are enabled.

A Realm defines where your users authenticate against. Once you create a realm you can then create your applications that users can authenticate against - in this tutorial we're using the Javascript SDK to authenticate against a realm. Realm names must be unique.

We need to complete a few things in our Tozny account:

  • Create a realm in TozID

  • Create an client application in the realm

  • Set the application to allow * for web origins

Click Manage Realm to access the below screens

Remember to set the redirect URI and web origins allowed and your redirect to localhost

Authentication with TozID

Authentication in TozID happens a little differently than most authentication providers. We use cryptographic operations to derive keys from a username and password and issue signed requests to our Identity platform. If the request is successful we return encrypted data which contains your keys and your derived keys are then used to decrypt those encryption keys. We know, that sounds confusing, but it's all handled automatically by our SDK.

Check out your network requests, your plain text passwords never leave your browser. We think that's pretty awesome. Tozny never stores them because we never see them.

Using our SDK you'll issue a standard looking login request and get back two important pieces of data. A tozStore credentials object and a standard JWT. You can use the JWT for authentication against any tranditional API backend - such as Laravel, Express, etc.

import Tozny from 'tozny-browser-sodium-sdk'
const realmName = process.env.VUE_APP_REALM_NAME;
const appName = process.env.VUE_APP_APP_NAME;
const brokerUrl = process.env.VUE_APP_BROKER_URL;
const registrationToken = process.env.VUE_APP_REGISTRATION_TOKEN;

const realmConfig = {
    "realmName": realmName,
    "appName": appName,
    "brokerTargetUrl" : brokerUrl
}
const tozIDConfig = Tozny.Identity.Config.fromObject(realmConfig)
const tozId = new Tozny.Identity(tozIDConfig)

Application Tutorial

Application Scaffolding

Lets dive into the main coding exercise needed to make our application work. Since we used Vue CLI much of the boilerplate is already written and we can focus on the components we need to write. First off we'll need to install the JS Tozny SDK. Do that by going into your project directory and running the installation command.

npm install tozny-browser-sodium-sdk@2.0.0-alpha.1

We'll also create a few files to let users navigate our note taking app. The main files will be: 1. Login Page 2. Registration Page 3. Notes Dashboard (Authenticated) 4. Password Recovery Page 5. Router, store, and auth components as detailed below.

The end result of our project tree should resemble this:

Environment Variables

Create a file named .env.local at your base directory which contains the following entries.

  1. Registration Token

  2. Realm Name

  3. Client Application Name

  4. Broker URL (where to be sent to complete a password recovery)

VUE_APP_REALM_NAME=
VUE_APP_APP_NAME=
VUE_APP_BROKER_URL=http://localhost:8080/reset
VUE_APP_REGISTRATION_TOKEN=

Registration

<template>
  <div>
    <h2>Register</h2>
    <form @submit.prevent="submitRegister" autocomplete="off">
      <label>
        Email
      <input v-model="email">
      </label>
      <br>
      <label>
        Password
        <input v-model="pass" type="password">
      </label><br>
      <button type="submit">Register</button>
      <p v-if="error" class="error">Bad registration information or password.</p>
    </form>
  </div>
</template>

<script>
  import { mapActions, mapGetters } from "vuex";
  export default {
    data () {
      return {
        email: '',
        pass: '',
        error: false
      }
    },
    computed: {
      ...mapGetters([
        'loggedIn',
      ])
    },
    methods: {
      ...mapActions(['register']),
      async submitRegister () {
        this.error = false;
        if(!this.email || !this.pass){
          this.error = true;
          return;
        }
        try{
          await this.register({email: this.email, pass: this.pass})
          this.error = false;
          this.$router.replace(this.$route.query.redirect || '/dashboard')
        }catch(err){
          this.error = true;
        }

      }
    }
  }
</script>

<style>
  .error {
    color: red;
  }
</style>

Login

<template>
  <div>
    <h2>Login</h2>
    <p v-if="$route.query.redirect">
      You need to login first.
    </p>
    <form @submit.prevent="submitLogin" autocomplete="off">
      <label>
        Email
        <input v-model="email" >
      </label>
      <br>
      <label>
        Password
        <input v-model="pass"  type="password">
      </label>
      <br>
      <button type="submit">login</button>
      <p v-if="error" class="error">Bad login information</p>
    </form>
    <button @click="resetPw">Forgot Password</button>
    <p>{{message}}</p>
  </div>
</template>

<script>
  import { mapActions, mapGetters } from "vuex";
  export default {
    data () {
      return {
        email: '',
        pass: '',
        error: false,
        message: ""
      }
    },
    computed: {
      ...mapGetters([
        'loggedIn',
      ])
    },
    methods: {
      ...mapActions(['login','requestReset']),
      async submitLogin () {
        try{
          await this.login({email: this.email, pass: this.pass})
          this.error = false;
          this.$router.replace(this.$route.query.redirect || '/dashboard')
        }catch(err){
          this.error = true;
        }
      },
      async resetPw(){
        this.message = "";
        try{
          await this.requestReset({email:this.email});
          this.message = "Please check your email";
        }catch(err){
          
        }
      }
    }
  }
</script>

<style>
  .error {
    color: red;
  }
</style>

Forgot Password

Note that in order to use password recovery managed by Tozny you must grant us access to act as a broker and deliver the password reset email. If you'd like to manage this yourself just toggle the setting to off in the Dashboard. When you have this setting off Tozny is cryptographically isolated from your data and cannot recover or release your data.

// We've removed all of the unused methods from the registration
// flow here just to make it easier to follow.  The entire
// vuex store file is available in the Github repo that
// is linked below.

export default new Vuex.Store({

  actions: {

    async requestReset({commit}, payload){
      try{

        // Requesting a reset from the TozID service and since
        // our realm is configured to let TozID broker the
        // reset our email will be delivered by Tozny 

        // (if you toggle OFF the setting in your realm settings then this will fail)

        const res = await tozId.initiateRecovery(payload.email)
      }catch(err){
        return err;
      }
    },

    
  },
  getters: {
    loggedIn: state => !!state.toznyClient
  }
})
<template>
  <div id="app">
    <h1>Auth Flow</h1>
    <ul>
      <li>
        <router-link v-if="loggedIn" to="/logout">Log out</router-link>
        <router-link v-if="!loggedIn" to="/login">Log in</router-link>
      </li>
      <li v-if="!loggedIn">
        <router-link v-if="!loggedIn" to="/register">Register</router-link>
      </li>
      <li>
        <router-link to="/dashboard">Dashboard</router-link>
        (authenticated)
      </li>
    </ul>
    <template v-if="$route.matched.length">
      <router-view></router-view>
    </template>
    <template v-else>
      <p>You are logged {{ loggedIn ? 'in' : 'out' }}</p>
    </template>
  </div>
</template>

<script>
  import { mapState, mapGetters } from "vuex";
  export default {
    data () {
      return {
       
      }
    },
    computed: {
      ...mapGetters([
        'loggedIn',
      ])
    },
  }
</script>
<style>
  html, body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
    color: #2c3e50;
  }

  #app {
    padding: 0 20px;
  }

  ul {
    line-height: 1.5em;
    padding-left: 1.5em;
  }

  a {
    color: #7f8c8d;
    text-decoration: none;
  }

  a:hover {
    color: #4fc08d;
  }
</style>

Dashboard.vue

Here is where we're actually reading and writing encrypted data from Tozny. One of the key benefits of Tozny's platform is that Tozny never has access to key material so there is no ability to leak decrypted data - all of the encryption and decryption operations happen client side (in your browser in this case).

<template>
  <div>
    <h2>Dashboard</h2>
    <p>You're logged in {{name}}!</p>

    <h3 v-if="notes.length > 0">Your Notes</h3>
    <ul>
    <li v-for="item in notes" :key="item.meta.recordId">
      {{item.data.note}}
    </li>
    </ul>
    <h3>Write New Secret Note</h3>
    <div class="note-area">
      <textarea v-model="note" />
      <button @click="writeNote">Write Note</button>
    </div>
    <div>
      <button @click="showToken">Show JWT</button>
      <div>{{token}}</div>
    </div>
  </div>
</template>
<script>
  import auth from '../auth'
  import { mapState } from "vuex";
  export default {
    data () {
      return {
        notes: [],
        loading: false,
        token: "",
        note: ""
      }
    },
    computed: {
        ...mapState(["toznyClient", "name"])
    },
    mounted(){
      
      this.getNotes();
      
    },
    methods: {
      async getNotes(){
         let records = await this.toznyClient.storageClient.query(true, null, null, 'note').next();
         this.notes = records;
      },
      async writeNote(){
        await this.toznyClient.storageClient.write('note', {'note': this.note});
        this.getNotes();
      },
      async showToken(){
        const token = await auth.getToken();
        this.token = token;
      }
    }
  }
</script>
<style scoped>
  .note-area{
    width: 400px;
    display: flex;
    flex-direction: column;
  }

</style>

Vuex Store

import Vue from 'vue'
import Vuex from 'vuex'
import Tozny from 'tozny-browser-sodium-sdk'
const realmName = process.env.VUE_APP_REALM_NAME;
const appName = process.env.VUE_APP_APP_NAME;
const brokerUrl = process.env.VUE_APP_BROKER_URL;
const registrationToken = process.env.VUE_APP_REGISTRATION_TOKEN;

const realmConfig = {
    "realmName": realmName,
    "appName": appName,
    "brokerTargetUrl" : brokerUrl
}
const tozIDConfig = Tozny.Identity.Config.fromObject(realmConfig)
const tozId = new Tozny.Identity(tozIDConfig)

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    toznyClient: false,
    name: ''

  },
  mutations: {
    SET_TOZNY_CLIENT(state, payload){
      state.toznyClient = payload
    },
    SET_NAME(state, token) {
      const base64Url = token.split('.')[1]
      const base64 = base64Url.replace('-', '+').replace('_', '/')
      const claims = JSON.parse(window.atob(base64))
      state.name = claims['preferred_username'];
    },
    LOGOUT(state){
      delete localStorage.clear();
    }
    
  },
  actions: {
    async setToznyClient({commit}, payload){
      commit('SET_TOZNY_CLIENT', payload)
      
    },
    logout({commit}){
      commit('LOGOUT')
    },
    async rehydrateTozny({commit}){

        const client = tozId.fromObject(localStorage.getItem('toznyClient'))
        commit('SET_TOZNY_CLIENT', client)
        const token = await client.token()
        commit('SET_NAME', token)
    },
    async login({commit}, payload){
      try{
        const res = await tozId.login(payload.email, payload.pass)
        localStorage.setItem('toznyClient',JSON.stringify(res.serialize()))
        commit('SET_TOZNY_CLIENT', res)
        const token = await res.token()
        commit('SET_NAME', token)
        

      }catch(err){
        console.log("Bad password")
        return err;
      }
    },
    async requestReset({commit}, payload){
      try{

        // Requesting a reset from the TozID service and since
        // our realm is configured to let TozID broker the
        // reset our email will be delivered by Tozny 

        // (if you toggle OFF the setting in your realm settings then this will fail)

        const res = await tozId.initiateRecovery(payload.email)
      }catch(err){
        return err;
      }
    },
    async completeRecovery({commit}, payload){
      try{
        // First we verify the query parameters from the recovery link
        // and pass them to the complete recovery method and then
        // the next function performs the password change
        const res = await tozId.completeRecovery(payload.otp, payload.noteId)
        await res.changePassword(payload.pass)
        return;
      }catch(err){
        return err;
      }
    },
    async register({commit}, payload){
      try{
        const res = await tozId.register(payload.email, payload.pass, registrationToken, payload.email)
        localStorage.setItem('toznyClient',JSON.stringify(res.serialize()))
        commit('SET_TOZNY_CLIENT', res)
        const token = await res.token()
        commit('SET_NAME', token)

      }catch(err){
        return err;
      }
    },
    
  },
  getters: {
    loggedIn: state => !!state.toznyClient
  }
})

Vue Router

import Vue from 'vue'
import Router from 'vue-router'
import Dashboard from '@/components/Dashboard.vue'
import Login from '@/components/Login.vue'
import ResetPassword from '@/components/ResetPassword.vue'
import Register from '@/components/Register.vue'
import store from '../store'
import auth from '../auth'

Vue.use(Router)

export default new Router({
  mode: 'history',
  base: __dirname,
  routes: [
    { path: '/dashboard', component: Dashboard, beforeEnter: requireAuth },
    { path: '/login', component: Login, beforeEnter: authRedirect },
    { path: '/reset', component: ResetPassword },
    { path: '/register', component: Register, beforeEnter: authRedirect },
    { path: '/logout',
      beforeEnter (to, from, next) {
        store.dispatch("logout").then(
          location.reload()
        )
        
      }
    }
  ]
})

// If a user attempts to access login or register let's check if they are
// already authenticated and properly route them back to the dashboard

async function authRedirect (to, from, next) {
  if(!store.state.toznyClient && localStorage.getItem('toznyClient')){
    await store.dispatch('rehydrateTozny')
  }
 
  if (!store.state.toznyClient) {
    next()
  } else {
    next({
      path: '/dashboard'
    })
  }
}

// Before we allow access to protected routes the application should
// validate that the user is properly authenticated use our vuex 
// getter that is defined in the store

async function requireAuth (to, from, next) {
  if(!store.state.toznyClient && localStorage.getItem('toznyClient')){
    await store.dispatch('rehydrateTozny')
  }
 
  if (!store.state.toznyClient) {
    next({
      path: '/login',
      query: { redirect: to.fullPath }
    })
  } else {
    next()
  }
}

TozID Auth

// This auth file is intentionally kept simple and designed to only
// return a standard JWT on demand.  The hydration from from
// local storage is done in our vuex store as an action

import store from './store'

export default {

  async getToken () {
    // Call this to get a token to set as a global bearer token when calling your own API
    // Tozny will handle automatically refreshing this token for you if needed

    try{
      const token = await store.state.toznyClient.token()
      return token;
    }catch(err){
      console.log("Unable to get token")
      return false;
    }
  },
}

Wrapping Up

Putting all of this together should result in you being able to login, register, and read and write notes that only your user can decrypt! If you run into any questions just shoot us an email at support@tozny.com and we'll be happy to help.

Grab the full source code for this project on our github - https://github.com/tozny/tozny-vue-auth-example

Last updated