Objectifs
- Cognito User Pool pour faire un système d’authentification.
- Vue 3 pour le UI.
- AWS Amplify pour communiquer facilement avec Cognito directement à partir du frontend.
- Vuex pour sauvegarder les states et centraliser la logique applicative.
- Bootstrap 5 pour le CSS.
- Authorization Code Flow, car c’est l’approche recommandée pour le type d’application utilisé.
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:
- auto sign in (après un refresh de la SPA)
- sign in
- sign up
- lost password
- recover password
- lost activation code
- verify account
- sign out
- signin with facebook
- get current user information
- change password
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.