In Part 1 we created an offline authentication module using the Web Crypto API

Now we will go through hooking this up to a UI, with Vue 3 and the composition API.

The code is over in Github if you want to skip ahead and jump straight into it.

Scope

To keep this app simple, we’ll only be using 2 components. Our main App and an Authenticated component, which will handle authentication and authenticated display logic.

We won’t build up a UI to create a new key-pair. Instead, I’ve gone and set a localStorage token at the top of the index.html which will represent the users key.

index.html

<script>
localStorage.setItem('key', '{"crv": "P-384","ext": true,"kty": "EC","x": "rg9BzHryeKvuHln38btrKjxDpOaDVQz8Eczxwy2xnuL1deWCTthEbplser7NW3n_","y": "suob7sbqEuo285FhTDz4FE_hjhgu7ZyXToCzHkBTPegiodmzISXaFx1t9bcaHsyq"}');
</script>

In a real authentication flow, we will allow the user to upload or fetch this key from their own storage location, as we do in the Concords Boards alpha.

App.vue

There are 3 states we need to address for content: authenticated content; un-authenticated content; and content which should display regardless of the authenticated state. We’ll use named slots to handle content for authenticated users, and a default slot for the content which will show in both states.

<template>
<authenticated>
<h1>Offline Authenticaion</h1>
<template #logged-in>
<p>
🎉 You found the Key
</p>
</template>
<template #not-logged-in>
Sorry, you must know the key.
</template>
</authenticated>
</template>
<script>
import { defineComponent} from 'vue';
import Authenticated from '@/components/Authenticated.vue';
export default defineComponent({
name: 'App',
components: {
Authenticated
}
});
</script>

Authenticated.vue

In our Authenticated component, we need to provide our 3 slots, and also handle the authentication flow, along with displaying a UI to login.

<template>
<slot/>
<slot name="logged-in" v-if="isAuthenticated"/>
<div v-else>
<slot name="not-logged-in"/>
<p>
<input type="password" v-model="jwk">
<button @click="login">Login</button>
</p>
</div>
</template>

We’ll abstract our authentication logic into reusable composable functions that then just needs setting up in our Authenticated component.

import { defineComponent, ref, unref, watch } from 'vue'; 
import useAuthentication from '@/composables/useAuthentication';
export default defineComponent({
name: 'Authenticated',
setup() {
const {
isAuthenticated,
sessionLogin,
login,
storageCredentials
} = useAuthentication();
const jwk = ref(null); function userLogin() {
const { key } = storageCredentials;
login(key, unref(jwk))
}
sessionLogin(); return {
isAuthenticated,
login: userLogin,
jwk
};
},
});

Vue 3 Composable Functions

We will be using the browsers localStorage to store both our key and JWK token. To log in, we have a useStorageCredentials composable function, that stores reference to a reactive representation of our login credentials.

useStorageCredentials.js

import { reactive, watch } from 'vue';const storageCredentials = reactive({
key: null,
jwk: null,
});
watch(storageCredentials, ({ key, jwk }) => {
localStorage.setItem('key', JSON.stringify(key));
localStorage.setItem('jwk', JSON.stringify(jwk));
});
function setStorageCredentials(key, jwk) {
storageCredentials.key = key;
storageCredentials.jwk = jwk;
}
function getStorageCredentials() {
storageCredentials.key = JSON.parse(localStorage.getItem('key'));
storageCredentials.jwk = JSON.parse(localStorage.getItem('jwk'));
}
export default () => ({
getStorageCredentials,
setStorageCredentials,
storageCredentials
});

To log in, we can use the setStorageCredentials methods to update our local storage state. We have a watcher to sync these values with the localStorage state in the browser.

const login = async (key, jwk) =>
setStorageCredentials(key, jwk);
const logout = async () =>
setStorageCredentials(storageCredentials.key, null);

Logging In

Storing the key and JWK isn’t enough to authenticate a user, we now need to use the authentication module we built in Part 1.

useSigningKey.js

We then pass our key and JWK to the getSigningKey method, which then calls our authentication login function, using the WebCrypto API to generate the signing key from the Public Key and JWK combination.

import { ref } from 'vue';
import { login } from '../platform/authentication';
const signingKey = ref(null);async function getSigningKey(key, jwk) {
if (key && jwk) {
signingKey.value = await login(jwk, key);
} else {
signingKey.value = false;
}
}
export default () => ({
signingKey,
getSigningKey
});

This signing key is our authentication identifier. Only the matching key and JWK combination will create a signing key, so we can now assume our user is authenticated.

useAuthentication.js

Now we piece it all together into a single authentication composable function, exporting the necessary state and methods to log in and out of authenticated content. Watching our signingKey to update our reactive isAuthenticated ref we will use in our app.

import { watch, ref } from 'vue';
import useStorageCredentials from './useStorageCredentials';
import useSigningKey from './useSigningKey';
import { create } from '../platform/authentication';
const {
storageCredentials,
setStorageCredentials,
getStorageCredentials
} = useStorageCredentials();
const { signingKey, getSigningKey } = useSigningKey();
const isAuthenticated = ref(false);
watch(storageCredentials, ({ key, jwk }) =>
getSigningKey(key, jwk));
watch(signingKey, (newSigningKey = {}) => {
isAuthenticated.value = newSigningKey.type === 'private';
});
const login = async (key, jwk) =>
setStorageCredentials(key, jwk);
const logout = async () =>
setStorageCredentials(storageCredentials.key, null);
export default () => ({
login,
logout,
sessionLogin: getStorageCredentials,

storageCredentials,
isAuthenticated,
signingKey
})

Demo: https://offline-authentication.concords.app/

Source: https://github.com/teamconcords/offline-authentication

--

--