В Части 1 мы создали модуль офлайн-аутентификации с использованием Web Crypto API.

Теперь мы рассмотрим подключение этого к пользовательскому интерфейсу с помощью Vue 3 и API композиции.

Код закончился в Github, если вы хотите пропустить его и сразу перейти к нему.



Сфера

Чтобы сделать это приложение простым, мы будем использовать только 2 компонента. Наше основное приложение и компонент с проверкой подлинности, которые будут обрабатывать аутентификацию и логику отображения с проверкой подлинности.

Мы не будем создавать пользовательский интерфейс для создания новой пары ключей. Вместо этого я установил токен localStorage вверху index.html, который будет представлять ключ пользователя.

index.html

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

В реальном потоке аутентификации мы позволим пользователю загружать или извлекать этот ключ из своего собственного хранилища, как мы это делаем в альфа-версии Concords Boards.



App.vue

Есть 3 состояния, которые нам нужно учитывать для контента: аутентифицированный контент; неаутентифицированный контент; и контент, который должен отображаться независимо от состояния аутентификации. Мы будем использовать именованные слоты для обработки контента для аутентифицированных пользователей и слот по умолчанию для контента, который будет отображаться в обоих состояниях.

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

Аутентифицированный.vue

В нашем компоненте Authenticated нам нужно предоставить наши 3 слота, а также обработать поток аутентификации, а также отобразить пользовательский интерфейс для входа в систему.

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

Мы абстрагируем нашу логику аутентификации в составные функции многократного использования, которые затем просто нужно настроить в нашем компоненте Authenticated.

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

Мы будем использовать локальное хранилище браузера для хранения нашего ключа и токена JWK. Для входа в систему у нас есть компонуемая функция useStorageCredentials, которая хранит ссылку на реактивное представление наших учетных данных для входа.

использоватьStorageCredentials.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
});

Чтобы войти в систему, мы можем использовать методы setStorageCredentials для обновления состояния нашего локального хранилища. У нас есть наблюдатель для синхронизации этих значений с состоянием localStorage в браузере.

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

Вход в систему

Хранения ключа и JWK недостаточно для аутентификации пользователя, теперь нам нужно использовать модуль аутентификации, который мы создали в Части 1.

useSigningKey.js

Затем мы передаем наш ключ и JWK методу getSigningKey, который затем вызывает нашу функцию входа в систему аутентификации, используя API WebCrypto для создания ключа подписи из комбинации открытого ключа и JWK.

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
});

Этот ключ подписи является нашим идентификатором аутентификации. Только совпадающий ключ и комбинация JWK создадут ключ подписи, поэтому теперь мы можем предположить, что наш пользователь аутентифицирован.

useAuthentication.js

Теперь мы собираем все это вместе в единую компонуемую функцию аутентификации, экспортируя необходимое состояние и методы для входа и выхода из аутентифицированного контента. Наблюдая за нашим signingKey, чтобы обновить нашу реактивную ссылку isAuthenticated, которую мы будем использовать в нашем приложении.

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

Демо: https://offline-authentication.concords.app/

Источник: https://github.com/teamconcords/offline-authentication