Авторизация - TupotaValentyn/MA.-Project GitHub Wiki

Для удобства пользователей предлагаю ввести несколько способов авторизации: обычная (почта + пароль) и авторизация через социальные сети. Из сетей предлагаю взять наиболее популярны и простые в разработке: Google и Facebook. Они дают возможность по API получить имя юзера и его почту для возможности отсылать электронные письма.
Так как на фронт-енде будет SPA, добавить поддержку обычных сессий не получится. Поэтому идентификация пользователя будет производиться посредством специального токена, который будет передан на front-end при успешной авторизации. На фронте нужно будет его сохранить куда-то для возможности дальнейшего использования, например, в localStorage и передавать его в хедерах при каждом запросе к апи. При использовании axios это удобно сделать при помощи interceptors:

const instance = axios.create({
    baseURL: 'http://localhost:3000',
});

instance.interceptors.request.use(config => {
    config.headers.authorization = `Bearer ${localStorage.getItem('token')}`;
    return config;
});

На сервере этот ключ будет проверяться на валидность и если с ним все ок, фронт получит запрашиваемые данные.
Если токен невалиден, в ответе придет HTTP статус 403. Если его срок действия истек, в ответе придет HTTP статус 401. В таких случаях нужно удалить старый токен из localStorage и отправить пользователя на страницу входа на сайт для повторной авторизации. При использовании axios это также удобно сделать при помощи interceptors:

instance.interceptors.response.use(
    response => response,
    error => {
        if (403 === error.response.status) {
            localStorage.removeItem('token');
            router.replace({ name: 'login' });
        } else {
            return Promise.reject(error);
        }
    },
);

Ну и в конце нужно повесить guard на роутер:

router.beforeEach((to, from, next) => {
    const isAuthorized = authService.isAuthorized();

    if (['login', 'register'].includes(to.name) && isAuthorized) {
        return next({ name: 'home' });
    }

    if (to.meta.requiresAuth) {
        if (isAuthorized) {
            next();
        } else {
            next({ name: 'login' });
        }
    } else {
        next();
    }
});

Локальная авторизация (электронная почта + пароль)

Регистрация

Ссылка на документацию - http://localhost:3000/api-docs/#/Auth/post_auth_register

Отправляем на сервер почту + пароль. В БД будет добавлен новый пользователь, но его статус будет "Не подтвержден" (флаг isConfirmed: будет false). Вместе с этим на почту будет выслано письмо с ссылкой для подтверждения почты.
Ответом на этот запрос фронт получит уникальный хеш-строку пользователя { userHash: 'blah-blah-blah' }. В нем закодирована информация о пользователе и он служит для дальнейшей идентификации юзера. Эти данные можно сохранить в localStorage для дальнейшего использования (при повторном входе на сайт если эти данные есть, сразу кидать на страничку оповещения о неподтвержденном email (см. дальше)).
На фронте нужно будет перебросить пользователя на страничку, где сказано, что на почту отправлено письмо для подтверждения + 2 кнопки: "Выслать повторно" и "Проверить подтверждение".
Клик на кнопку "Выслать повторно" отправляет запрос на ​/auth​/resend_confirmation​/{userHash} (документация - http://localhost:3000/api-docs/#/Auth/post_auth_resend_confirmation__userHash_)
Клик на кнопку "Проверить подтверждение" - запрос на ​/auth​/check_verification​/{userHash} (документация - http://localhost:3000/api-docs/#/Auth/get_auth_check_verification__userHash_). Если почта подтверждена, юзер может полноценно использовать систему.

Вход

Ссылка на документацию - http://localhost:3000/api-docs/#/Auth/post_auth_login

Отправляем на сервер почту + пароль. Если аккаунт пользователя не подтвержден, ответом на этот запрос фронт получит уникальный хеш-строку пользователя { userHash: 'blah-blah-blah' }. В таком случае нужно перенаправить пользователя на страницу оповещения о неподтвержденном email. Flow действий на этой странице см. выше в разделе "Регистрация"
Если почта уже подтверждена, фронт получит данные о токене авторизации вида { tokenData: TokenData }. В этом случае необходимо сохранить эти данные на фронте и перенаправить пользователя в систему.

Google

Ссылка на документацию - http://localhost:3000/api-docs/#/Auth/post_auth_google

Для получения возможности входа через Google необходимо создать проект и получить API ключ.
На сайт нужно подключить Google Auth SDK:

<script src="https://apis.google.com/js/platform.js" async defer></script>

После инициализации SDK можно повесить обработчик на клик на кнопку входа через Google:

window.gapi.load('auth2', () => {
    const instance = window.gapi.auth2.init({
        client_id: process.env.GOOGLE_CLIENT_ID,
    });

    instance.attachClickHandler(
        // Элемент, на который вешаем клик
        document.querySelector('.js-login-with-google'),

        // Объект опций (https://cutt.ly/qtfoSOy)
        {},

        // Callback, который выполнится после успешной авторизации пользователя. Объект 
        // googleUser содержит всю запрашиваемую инфу о юзере
        googleUser => {},
    
        // Callback, который выполнится после неудачной авторизации пользователя
        error => {},
    );
});

В обработчике успешного входа нужно получить id_token пользователя и отправить его на сервер:

async googleUser => {
    try {
        const idToken = googleUser.getAuthResponse().id_token;
        
        const response = await axios.post('/login/google', { idToken });
        const { tokenData } = response.data;

        // Сохраняем токен и публичные данные о пользователе в localStorage, например
        // ...

        this.$router.replace({ name: 'home' });
    } catch (error) {
        this.authError = error.message;
    }
}

Если в базе уже есть пользователь с таким же email, как в профиле Google, аккаунт Google будет привязан к этому пользователю.
В ответ фронт получит данные о токене { tokenData: TokenData }. После этого юзер может полноценно использовать систему.

См. доку чтобы получить больше инфы

Facebook

Ссылка на документацию - http://localhost:3000/api-docs/#/Auth/post_auth_facebook

Для получения возможности входа через Facebook необходимо создать проект и получить API ключ.
На сайт нужно подключить Facebook Auth SDK:

<script src="https://connect.facebook.net/en_US/sdk.js" async defer></script>

После загрузки скрипта нужно инициализировать модуль авторизации:

window.FB.init({
    appId: process.env.FACEBOOK_CLIENT_ID,
    cookie: true,
    xfbml: true,
    version: 'v6.0',
});

Далее можем навесить обработчик на кнопку входа через Facebook. В нем нужно получить токен пользователя и его ID и отправить эти данные на сервер:

window.FB.login(async response => {
    const accessToken = response.authResponse.accessToken;
    const userId = response.authResponse.userID;

    const res = await axios.post('/login/facebook', { accessToken, userId });
    const { tokenData } = res.data;

    // Сохраняем токен и публичные данные о пользователе в localStorage, например
    // ...

    this.$router.replace({ name: 'home' });
});

Если в базе уже есть пользователь с таким же email, как в профиле Facebook, аккаунт Facebook будет привязан к этому пользователю.
В ответ фронт получит данные о токене { tokenData: TokenData }. После этого юзер может полноценно использовать систему.

См. доку чтобы получить больше инфы

Logout

В модели UserPublicData есть поле authType, которое определяет, каким способом пользователь вошел на сайт. В зависимости от способа входа выход тоже будет разным.

Email + password

Нужно всего лишь удалить данные о пользователе и токене из localStorage

Google

Нужно вызвать метод .disconnect() и удалить данные о пользователе и токене из localStorage

window.gapi.load('auth2', () => {
    const instance = window.gapi.auth2.init({
        client_id: process.env.GOOGLE_CLIENT_ID,
    });

    instance.disconnect();
});      

Facebook

Нужно вызвать метод .logout() и удалить данные о пользователе и токене из localStorage

window.FB.logout(() => {
    console.log('User is logged out');
})     

Нюансы

Хранить токен в localStorage - небезопасно, поэтому есть несколько вариантов прохождения процесса авторизации:

  1. Забить на это и использовать jsonwebtoken как описано выше
  2. Заменить SPA на SSR. Это позволит ввести нормальные безопасные сессии через http-only cookies
  3. Заставить пользователя авторизироваться при каждом входе на сайт. После получения токена его можно сохранить в переменной внутри приложения, к которой не будет доступа у внешних скриптов.
⚠️ **GitHub.com Fallback** ⚠️