Webux Lab

Par Studio Webux

Cognito User Pool, Vue 3 et amplify

TG
Tommy Gingras Studio Webux S.E.N.C 2022-03-12

Objectifs

Prérequis

npm init -y

vue create test-auth-app

cd test-auth-app

npm i aws-amplify bootstrap vue-router vuex

Les versions utilisées:

"dependencies": {
    "aws-amplify": "^4.3.16",
    "bootstrap": "^5.1.3",
    "core-js": "^3.6.5",
    "vue": "^3.0.0",
    "vue-router": "^4.0.14",
    "vuex": "^4.0.2"
  }

Comment créer un component custom (une librairie)

J’ai tout simplement suivi cet article: https://blog.logrocket.com/building-vue-3-component-library/

En peu de temps, nous pouvons obtenir un UI réutilisable pour le système d’authentification.

.
├── dist
│   ├── library.js
│   └── library.mjs
├── package-lock.json
├── package.json
├── rollup.config.js
└── src
    ├── components
    │   ├── ChangePassword.vue
    │   ├── LostPassword.vue
    │   ├── RecoverPassword.vue
    │   ├── ResendCode.vue
    │   ├── SignIn.vue
    │   ├── SignUp.vue
    │   └── VerifyAccount.vue
    ├── components.js
    └── index.js

3 directories, 14 files

Aucune logique ne fait partie du component. Les props vont être utilisées pour passer les fonctions requises.

Je ne vais pas aller plus loin pour cette partie, l’article référé explique très bien les étapes.


Vuex et le module pour l’authentification

Pour configurer Vuex, j’utilise cette structure:

src/
  store/
    modules/
      authentication.js
    index.js

Le fichier index.js fait seulement le lien entre tous les modules.

Le fichier authentication.js contient toute la logique qui se rapporte à l’authentification,

Voici les méthodes implémentées:

Toutes ces commandes utilisent Amplify pour simplifier le développement. J’utilise seulement le module d’authentification de amplify, donc il n’est pas requis de suivre leur documentation pour générer tous les outils. Le Cognito User Pool et le Client sont générés avec CloudFormation pour garder la flexibilité et éviter de dépendre d’un autre outil (Amplify CLI dans ce cas)


Le code

index.js

import { createStore } from 'vuex';
import authentication from './modules/authentication';

const debug = process.env.NODE_ENV !== 'production';

const store = createStore({
  modules: {
    authentication,
  },
  strict: debug,
});

export default store;

authentication.js

const { Auth } = require('aws-amplify');
const router = require('../../router').default;

Auth.configure({
  region: process.env.VUE_APP_COGNITO_REGION,
  userPoolId: process.env.VUE_APP_COGNITO_USER_POOL,
  userPoolWebClientId: process.env.VUE_APP_COGNITO_CLIENT_ID,
  oauth: {
    domain: process.env.VUE_APP_COGNITO_DOMAIN,
    scope: process.env.VUE_APP_COGNITO_SCOPE ? process.env.VUE_APP_COGNITO_SCOPE.split(',') : [],
    redirectSignIn: process.env.VUE_APP_COGNITO_REDIRECT_SIGN_IN,
    redirectSignOut: process.env.VUE_APP_COGNITO_REDIRECT_SIGN_OUT,
    responseType: 'code',
  },
});

const state = {
  profile: null,
  tokens: null,
  isAuthenticated: false,

  info: null,
};

const getters = {
  isAuthenticated: (state) => {
    return state.isAuthenticated;
  },

  getUserInfo: (state) => {
    return state.profile;
  },

  idToken: (state) => {
    console.log(state.tokens);
    return state.tokens?.idToken;
  },

  status: (state) => {
    return state.info;
  },
};

const actions = {
  async autoSignIn({ commit }) {
    let response = await Auth.currentSession();
    console.debug('autoSignIn', response);
    commit('SIGN_IN', response);
    return response;
  },

  async signIn({ commit }, creds) {
    try {
      let response = await Auth.signIn(creds.email, creds.password);

      console.debug('signIn', response);

      commit('SIGN_IN', response);
      return response;
    } catch (e) {
      if (e.code === 'UserNotConfirmedException') {
        router.push('/verify-account');
        return;
      }
      throw e;
    }
  },

  async signOut({ commit }, global = false) {
    await Auth.signOut({ global: global });
    commit('SIGN_OUT');

    router.push('/signin');
  },

  async loadCurrentUser({ commit }) {
    let response = await Auth.currentAuthenticatedUser().catch((e) => {
      console.debug('Unable to load the current user, ', e);
    });

    console.debug('Current user info', await Auth.currentUserInfo());

    if (response) {
      commit('CURRENT_USER', { attributes: response.attributes, username: response.username });
    }
    return response;
  },

  async signUp({ commit }, creds) {
    let response = await Auth.signUp({
      username: creds.email,
      password: creds.password,
      attributes: {
        email: creds.email,
        name: creds.name,
      },
    }).catch((e) => {
      throw e;
    });

    commit('SIGN_UP', response.username);
    return response;
  },

  async verifyAccount({ commit }, creds) {
    try {
      let response = await Auth.confirmSignUp(creds.email, creds.code);
      commit('ACCOUNT_VERIFIED', response.username);
      router.push('/signin');
    } catch (e) {
      if (e.code === 'CodeMismatchException' || e.code === 'ExpiredCodeException') {
        router.push('/resend-code');
        return;
      }

      if (e.code === 'NotAuthorizedException') {
        router.push('/signin');
        return;
      }

      throw e;
    }
  },

  async resendCode({ commit }, creds) {
    let response = await Auth.resendSignUp(creds.email).catch((e) => {
      throw e;
    });

    commit('CODE_RESENT', response.username);
    return response;
  },

  async signInWithFacebook({ commit }) {
    let response = await Auth.federatedSignIn({ provider: 'Facebook' });

    commit('SIGN_IN', 'FB');
    return response;
  },

  async changePassword({ commit }, creds) {
    const user = await Auth.currentAuthenticatedUser();
    const response = await Auth.changePassword(user, creds.currentPassword, creds.newPassword);

    commit('CURRENT_USER', user);
    return response;
  },

  async lostPassword({ commit }, username) {
    const info = await Auth.forgotPassword(username);
    commit('STATUS', info);
  },

  async recoverPassword({ commit }, creds) {
    const info = await Auth.forgotPasswordSubmit(creds.email, creds.code, creds.password);

    commit('STATUS', info);
    return info;
  },
};

const mutations = {
  SIGN_IN(state, response) {
    state.username = response.username;
    state.tokens = {
      idToken: response.signInUserSession
        ? response.signInUserSession.idToken.jwtToken
        : response.idToken.jwtToken,
    };
    state.isAuthenticated = true;
  },

  SIGN_OUT(state) {
    state.profile = null;
    state.tokens = {};
    state.isAuthenticated = false;
  },

  CURRENT_USER(state, user) {
    state.profile = { username: user.username, ...user.attributes };
  },

  SIGN_UP(state, username) {
    state.username = username;
  },

  ACCOUNT_VERIFIED(state, username) {
    state.username = username;
  },

  CODE_RESENT(state, username) {
    state.username = username;
  },

  STATUS(state, info) {
    state.info = info;
  },
};

export default {
  namespaced: true,
  state,
  getters,
  actions,
  mutations,
};


Connecter la configuration avec l’application

Voici la configuration du fichier main.js.

Vous devez ajouter tous les modules et créer l’application Vue.

main.js

import { createApp } from 'vue';
import router from './router'; // non couvert ici
import store from './store';

import App from './App.vue';

import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap';

import Authentication from '../../../../modules/login/dist/library'; // Le component custom

createApp(App).use(Authentication).use(router).use(store).mount('#app');

Exemple pour utiliser les components et les lier à Vuex

Le fichier App.vue va être utilisé dans ce cas pour simplifier la documentation.

Le code

App.vue

<template>
  <div>
    <div class="w-50 mb-5">
      <webuxlab-change-password :changePassword="changePasswordFn" />
    </div>

    <div class="w-50 mb-5">
      <webuxlab-recover-password :recoverPassword="recoverPasswordFn" />
    </div>

    <div class="w-50 mb-5">
      <webuxlab-lost-password :lostPassword="lostPasswordFn" />
    </div>

    <div class="w-50 mb-5">
      <webuxlab-sign-in :signIn="signInFn" />
    </div>

    <div class="w-50 mb-5">
      <webuxlab-sign-up :signUp="signUpFn" />
    </div>

    <div class="w-50 mb-5">
      <webuxlab-resend-code :resendCode="resendCodeFn" />
    </div>

    <div class="w-50 mb-5">
      <webuxlab-verify-account :verifyAccount="verifyAccountFn" />
    </div>

    <div class="whoami mb-5 mt-5">
      <h5>User Info</h5>
      <pre>{{ getUserInfo }}</pre>
    </div>
  </div>
</template>

<script>
import { mapGetters, mapActions } from 'vuex';

export default {
  name: 'App',

  computed: {
    ...mapGetters('authentication', ['getUserInfo']),
  },

  async mounted() {
    await this.autoSignIn();
    await this.loadCurrentUser();
  },

  methods: {
    ...mapActions('authentication', [
      'signIn',
      'signUp',
      'verifyAccount',
      'resendCode',
      'loadCurrentUser',
      'changePassword',
      'lostPassword',
      'recoverPassword',
      'autoSignIn',
    ]),

    async recoverPasswordFn(ev) {
      await this.recoverPassword(ev);
    },

    async lostPasswordFn(ev) {
      await this.lostPassword(ev.email);
    },

    async signInFn(ev) {
      await this.signIn(ev);
      await this.loadCurrentUser();
    },

    async signUpFn(ev) {
      await this.signUp(ev);
    },

    async verifyAccountFn(ev) {
      await this.verifyAccount(ev);
    },

    async resendCodeFn(ev) {
      await this.resendCode(ev);
    },

    async changePasswordFn(ev) {
      await this.changePassword(ev);
    },
  },
};
</script>

Tous les components sont utilisés sur la même page pour simplifier les tests et la vérification du UI.


Configuration de Cognito

À la racine du projet, vous devez créer un fichier .env; dans ce cas-ci vous pouvez le commit, car l’information de ce .env n’est pas sensible. Faites toujours attention.

# Cognito

VUE_APP_COGNITO_REGION=""
VUE_APP_COGNITO_USER_POOL=""
VUE_APP_COGNITO_CLIENT_ID=""
VUE_APP_COGNITO_DOMAIN=""
VUE_APP_COGNITO_SCOPE=""
VUE_APP_COGNITO_REDIRECT_SIGN_IN=""
VUE_APP_COGNITO_REDIRECT_SIGN_OUT=""

NODE_ENV="development"

Vous devez utiliser les informations qui sont disponibles sur l’interface de Cognito.

Je vous recommande fortement d’utiliser Cloudformation pour créer le Cognito User Pool et toutes les autres ressources nécessaires. Cette stratégie simplifie et documente automatiquement votre infrastructure.



Recherche