---
title: 'Authentification Express / React'
disqus: hackmd
---
Authentification Express / React
===
**Sommaire**
[TOC]
# Côté back
## Librairies à installer
* `passport` pour la protection des routes
* `passport-jwt` pour la protection par token
* `jsonwebtoken` pour créer et décoder les token
* `bcrypt` pour hasher le mot de passe
## Créer une paire clef privée/clef publique
On a besoin de créer une paire de clefs pour pouvoir créer et vérifier nos JWT. On peut le faire avec ces commandes, en considérant qu'on a bien un dossier `config`
```bash=linux
ssh-keygen -t rsa -P "" -b 4096 -m PEM -f config/id_rsa
ssh-keygen -e -m PEM -f config/id_rsa > config/id_rsa.pub
```
On récupérera ces clefs dans le code en utilisant `fs`
```javascript
const privateKey = fs.readFileSync('config/id_rsa');
const publicKey = fs.readFileSync('config/id_rsa.pub');
```
## Configurer passport
On peut créer une fonction de configuration de passport dont l'objectif est de lui ajouter une stratégie d'authentification par token
```javascript
export function configurePassport() {
passport.use(new Strategy({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: publicKey,
algorithms: ['RS256']
}, async (payload, done) => {
try {
const user = await UserRepository.findByEmail(payload.email);
if(user) {
return done(null, user);
}
return done(null, false);
} catch (error) {
console.log(error);
return done(error, false);
}
}))
}
```
Il ne faudra pas oublier de lancer cette fonction de configuration au lancement de l'application, par exemple dans le server.js (et on dit également à express d'utiliser passport)
```javascript
configurePassport();
export const server = express();
server.use(passport.initialize());
```
## Fonction de création du token
On peut se créer une petite fonction dont le but sera de créer le token. On peut modifier le temps de validiter par défaut des token qui seront créés
```javascript
export function generateToken(payload, expire = 60*60) {
const token = jwt.sign(payload, privateKey,{algorithm: 'RS256', expiresIn: expire });
return token;
}
```
(l'algorithme de création du token et du decoding du token côté stratégie doivent correspondre. Ils sont lié à l'algorithme utilisé pour créer nos clés privée/publique)
## Inscription et hashing du mot de passe
Les étapes importantes sont les suivantes :
Vérifier si un user avec cet email ou username existe déjà
```javascript
const exists = await UserRepository.findByEmail(req.body.email);
if(exists) {
res.status(400).json({error: 'Email already taken'});
return;
}
```
Assigner les valeurs par défaut (ici par exemple le role du nouveau user) et hasher le mot de passe avec bcrypt avant de faire persister
```javascript
newUser.role = 'user';
newUser.password = await bcrypt.hash(newUser.password, 11);
await UserRepository.add(newUser);
```
Renvoyer le user et éventuellement un token si on souhaite l'authentifier à l'inscription (ne marche que si on a pas de validation du user par email)
```javascript
res.status(201).json({
user:newUser,
token: generateToken({
email: newUser.email,
id:newUser.id,
role:newUser.role
})
});
```
## Login et envoie du token
Le login sera presque toujours similaire entre les applications
```javascript
const user = await UserRepository.findByEmail(req.body.email);
if(user) {
const samePassword = await bcrypt.compare(req.body.password, user.password);
if(samePassword) {
res.json({
user,
token: generateToken({
email: user.email,
id:user.id,
role:user.role
})
});
return;
}
}
res.status(401).json({error: 'Wrong email and/or password'});
```
On va chercher le user par son identifiant (ici l'email, mais ça pourrait être autre chose) dans la base de donnée.
Si le user existe, on compare le mot de passe envoyé avec le mot de passe hashé stocké en bdd
Si tout est ok, on renvoie le user et surtout le token en lui mettant comme contenu n'importe quoi qui nous permettra d'identifier le user (un id peut suffire, un email peut suffire, un username peut suffire)
# Côté Front
## Librairies utilisées
* `react` comme framework front
* `axios` pour les requêtes http
* `@reduxjs/toolkit` et `react-redux` pour gérer les states globaux. Alternativement si c'est juste pour le user, on pourrait utiliser les `useContext` natif à react
## Mise en place de redux
### La slice redux pour le user
Permet de stocker le user de manière globale dans l'application.
Les parties feedback ne sont nécessaire que si on gère également les requêtes AJAX depuis redux.
```javascript
const authSlice = createSlice({
name: 'auth',
initialState: {
user: null,
loginFeedback: '',
registerFeedback: ''
},
reducers: {
login(state, {payload}) {
state.user = payload;
},
logout(state) {
state.user = null;
localStorage.removeItem('token');
},
updateLoginFeedback(state, {payload}) {
state.loginFeedback = payload
},
updateRegisterFeedback(state, {payload}) {
state.registerFeedback = payload
}
}
});
export const {login,logout, updateLoginFeedback, updateRegisterFeedback} = authSlice.actions;
export default authSlice.reducer;
```
### La création du store
On créer le store en lui assignant toutes les slices qu'on aura créée (un seule store par appli, si on a plusieurs slices, on les met dans le même store)
```javascript
export const store = configureStore({
reducer: {
auth: authSlice
}
});
```
### L'assignation du store à l'application react
Pour que les states du store et les actions soient accessibles depuis les component de notre application, il faut entouré notre application d'un Provider auquel on fourni le store. (dans le index.js par exemple)
```jsx
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
```
### Utilisation dans un component
Pour récupérer la valeur du user depuis le store, on pourra utiliser
```javascript
const user = useSelector(state => state.auth.user)
```
depuis n'importe quel component.
Si jamais on en a besoin hors d'un component (dans un service par exemple) on peut utiliser le store
```javascript
const user = store.getState().auth.user`
```
(la quasi totalité du temps, on utilisera le userSelector)
Pour muter la valeur/déclencher une action ou un thunk, on utilisera
```javascript
const dispatch = useDispatch();
dispatch(login(values))
```
### Les thunks
On peut définir des thunks qui sont "juste" des actions asynchrones de redux.
Ici, on a les 3 thunks principaux pour l'authentification, un qui nous login à partir d'un token, un qui nous login à partir d'un email/mot de passe et un qui nous permet de s'inscrire
```javascript
export const loginWithToken = () => async (dispatch) => {
const token = localStorage.getItem('token');
if(token) {
try {
const response = await axios.get('http://localhost:8000/api/user/account');
dispatch(login(response.data));
} catch(error) {
dispatch(logout());
}
}
}
export const register = (body) => async (dispatch) => {
try {
const response = await axios.post('http://localhost:8000/api/user', body);
const {user, token} = response.data;
localStorage.setItem('token', token);
dispatch(updateRegisterFeedback('Registration successful'));
dispatch(addUser(user));
dispatch(login(user));
// dispatch(loginWithCredentials(body));
} catch (error) {
dispatch(updateRegisterFeedback(error.response.data.error));
}
}
export const loginWithCredentials = (credentials) => async (dispatch) => {
try {
const response = await axios.post('http://localhost:8000/api/user/login', credentials);
localStorage.setItem('token', token);
dispatch(updateLoginFeedback('Login successful'));
dispatch(login(response.data));
} catch (error) {
dispatch(updateLoginFeedback('Email and/or password incorrect'));
}
}
```
Le `loginWithToken` devra être dispatch au lancement de l'application afin d'authentifier le user si on a un token stocké en localStorage.
Le `loginWithCredentials` et le `register` devront tous deux être dispatch au moment de la validation d'un formulaire (respectivement de login et d'inscription)
## Les intercepteurs axios
axios permet de créer ce qu'on appelle des intercepteurs qui, comme leur nom l'indiquent, vont permettre d'intercepter les requêtes et les réponses de toutes les requêtes AJAX faites avec axios.
Dans notre cas, on s'en servira pour injecter le token dans toutes les headers de toutes nos requêtes sortantes ainsi que pour logout le user si jamais le token a expiré (et donc qu'une requête nous renvoie un 401)
```javascript
axios.interceptors.request.use((config) => {
const token =localStorage.getItem('token');
if(token) {
config.headers.authorization = 'bearer '+token;
}
return config;
});
axios.interceptors.response.use((response) => {
if(response.status === 401) {
store.dispatch(logout());
}
return response;
})
```
###### tags: `React` `Express` `Authentification` `Passport` `JWT`