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.
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.
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.
Registration Token
Realm Name
Client Application Name
Broker URL (where to be sent to complete a password recovery)
// 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.exportdefaultnewVuex.Store({ state: { toznyClient:false, name:'' }, mutations: {SET_TOZNY_CLIENT(state, payload){state.toznyClient = payload },SET_NAME(state, token) {constbase64Url=token.split('.')[1]constbase64=base64Url.replace('-','+').replace('_','/')constclaims=JSON.parse(window.atob(base64))state.name = claims['preferred_username']; }, }, actions: {asyncregister({commit}, payload){try{constres=awaittozId.register(payload.email,payload.pass, registrationToken,payload.email)localStorage.setItem('toznyClient',JSON.stringify(res.serialize()))commit('SET_TOZNY_CLIENT', res)consttoken=awaitres.token()commit('SET_NAME', token) }catch(err){return err; } }, }, getters: {loggedIn: state =>!!state.toznyClient }})
// 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.exportdefaultnewVuex.Store({ state: { toznyClient:false, name:'' }, mutations: {SET_TOZNY_CLIENT(state, payload){state.toznyClient = payload },SET_NAME(state, token) {constbase64Url=token.split('.')[1]constbase64=base64Url.replace('-','+').replace('_','/')constclaims=JSON.parse(window.atob(base64))state.name = claims['preferred_username']; }, }, actions: {asynclogin({commit}, payload){try{constres=awaittozId.login(payload.email,payload.pass)localStorage.setItem('toznyClient',JSON.stringify(res.serialize()))commit('SET_TOZNY_CLIENT', res)consttoken=awaitres.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.
// 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.exportdefaultnewVuex.Store({ actions: {asyncrequestReset({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)constres=awaittozId.initiateRecovery(payload.email) }catch(err){return err; } }, }, getters: {loggedIn: state =>!!state.toznyClient }})
// 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.exportdefaultnewVuex.Store({ actions: {asynccompleteRecovery({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 changeconstres=awaittozId.completeRecovery(payload.otp,payload.noteId)awaitres.changePassword(payload.pass)return; }catch(err){return err; } }, }, getters: {loggedIn: state =>!!state.toznyClient }})
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).
import Vue from'vue'import Vuex from'vuex'import Tozny from'tozny-browser-sodium-sdk'constrealmName=process.env.VUE_APP_REALM_NAME;constappName=process.env.VUE_APP_APP_NAME;constbrokerUrl=process.env.VUE_APP_BROKER_URL;constregistrationToken=process.env.VUE_APP_REGISTRATION_TOKEN;constrealmConfig= {"realmName": realmName,"appName": appName,"brokerTargetUrl": brokerUrl}consttozIDConfig=Tozny.Identity.Config.fromObject(realmConfig)consttozId=newTozny.Identity(tozIDConfig)Vue.use(Vuex)exportdefaultnewVuex.Store({ state: { toznyClient:false, name:'' }, mutations: {SET_TOZNY_CLIENT(state, payload){state.toznyClient = payload },SET_NAME(state, token) {constbase64Url=token.split('.')[1]constbase64=base64Url.replace('-','+').replace('_','/')constclaims=JSON.parse(window.atob(base64))state.name = claims['preferred_username']; },LOGOUT(state){deletelocalStorage.clear(); } }, actions: {asyncsetToznyClient({commit}, payload){commit('SET_TOZNY_CLIENT', payload) },logout({commit}){commit('LOGOUT') },asyncrehydrateTozny({commit}){constclient=tozId.fromObject(localStorage.getItem('toznyClient'))commit('SET_TOZNY_CLIENT', client)consttoken=awaitclient.token()commit('SET_NAME', token) },asynclogin({commit}, payload){try{constres=awaittozId.login(payload.email,payload.pass)localStorage.setItem('toznyClient',JSON.stringify(res.serialize()))commit('SET_TOZNY_CLIENT', res)consttoken=awaitres.token()commit('SET_NAME', token) }catch(err){console.log("Bad password")return err; } },asyncrequestReset({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)constres=awaittozId.initiateRecovery(payload.email) }catch(err){return err; } },asynccompleteRecovery({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 changeconstres=awaittozId.completeRecovery(payload.otp,payload.noteId)awaitres.changePassword(payload.pass)return; }catch(err){return err; } },asyncregister({commit}, payload){try{constres=awaittozId.register(payload.email,payload.pass, registrationToken,payload.email)localStorage.setItem('toznyClient',JSON.stringify(res.serialize()))commit('SET_TOZNY_CLIENT', res)consttoken=awaitres.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)exportdefaultnewRouter({ 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 dashboardasyncfunctionauthRedirect (to, from, next) {if(!store.state.toznyClient &&localStorage.getItem('toznyClient')){awaitstore.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 storeasyncfunctionrequireAuth (to, from, next) {if(!store.state.toznyClient &&localStorage.getItem('toznyClient')){awaitstore.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 actionimport store from'./store'exportdefault {asyncgetToken () {// 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 neededtry{consttoken=awaitstore.state.toznyClient.token()return token; }catch(err){console.log("Unable to get token")returnfalse; } },}
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.