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

Creating your TozID Realm

Click Manage Realm to access the below screens

Creating a Client Application in your Realm

Required Settings for You Application

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:

Ending project directory structure

Environment Variables

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

Registration Token Realm Name Client Application Name 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

Register.vue vuex.js Register.vue < 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 > ​ vuex.js ​ 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' ] ; } , } , actions : { 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 } } ) ​

Login

Login.vue vuex.js Login.vue < 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 > ​ vuex.js ​ 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' ] ; } , } , actions : { 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 ; } } , } , getters : { loggedIn : state => ! ! state . toznyClient } } ) ​

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.

request reset vuex.js ResetPassword.vue complete reset vuex.js request reset vuex.js ​ export default new Vuex . Store ( { ​ actions : { ​ async requestReset ( { commit } , payload ) { try { ​ ​ ​ const res = await tozId . initiateRecovery ( payload . email ) } catch ( err ) { return err ; } } , ​ } , getters : { loggedIn : state => ! ! state . toznyClient } } ) ​ ResetPassword.vue <template> <div> <h2>Reset Password</h2> <form @submit.prevent="submitReset" autocomplete="off" v-if="!error"> <label> New Password <input v-model="pass" type="password"> </label> <br> <button type="submit">Reset Password</button> </form> <p v-if="error" class="error">Unable to verify the reset link.</p> </div> </template> ​ <script> import { mapActions, mapGetters } from "vuex"; export default { data () { return { email: '', pass: '', error: false, email_otp: '', note_id: '' } }, computed: { }, mounted(){ this.error = false; if(this.$route.query.email_otp && this.$route.query.note_id){ this.email_otp = this.$route.query.email_otp; this.note_id = this.$route.query.note_id; }else{ this.error = true; } }, methods: { ...mapActions(['completeRecovery']), async submitReset () { try{ await this.completeRecovery({otp: this.email_otp, noteId: this.note_id, pass: this.pass}) this.error = false; this.$router.replace('/login') }catch(err){ this.error = true; } } } } </script> ​ <style> .error { color: red; } </style> ​ complete reset vuex.js ​ export default new Vuex . Store ( { actions : { async completeRecovery ( { commit } , payload ) { try { const res = await tozId . completeRecovery ( payload . otp , payload . noteId ) await res . changePassword ( payload . pass ) return ; } catch ( err ) { return err ; } } , } , getters : { loggedIn : state => ! ! state . toznyClient } } ) ​

App.vue App.vue < 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 : # 2 c3e50 ; } ​ #app { padding : 0 20 px ; } ​ ul { line - height : 1.5 em ; padding - left : 1.5 em ; } ​ a { color : # 7 f8c8d ; text - decoration : none ; } ​ a : hover { color : # 4 fc08d ; } < / style > ​

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).

Dashboard.vue Dashboard.vue < 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 : 400 px ; display : flex ; flex - direction : column ; } ​ < / style > ​

Vuex Store

store/index.js store/index.js 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 { ​ ​ ​ const res = await tozId . initiateRecovery ( payload . email ) } catch ( err ) { return err ; } } , async completeRecovery ( { commit } , payload ) { try { 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

router/index.js router/index.js 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 ( ) ) } } ] } ) ​ ​ 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' } ) } } ​ ​ 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

auth.js auth.js ​ import store from './store' ​ export default { ​ async getToken ( ) { ​ 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​