Обновление JWT с помощью промежуточного ПО Next.js через куки

Вопрос или проблема

Недавно я отказался от Next Auth и начал реализовывать свою собственную сессию и аутентификацию на основе JWT cookies. У меня сейчас проблемы с файлом middleware в next.js, где мой JWT не обновляется, что бы я ни пробовал. Я могу передать refreshToken на мой endpoint обновления, но data.newAuthToken в моей функции middleware является неопределенным.

import { NextResponse } from 'next/server';
import { jwtVerify } from 'jose';

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

async function refreshAuthToken(refreshToken) {
    const response = await fetch(`${process.env.URL}/api/auth/refresh-token`, {
        method: 'POST',
        headers: { Authorization: `Bearer ${refreshToken}` },
    });

    if (response.ok) {
        const data = await response.json();
        return data.newAuthToken; // НЕОПРЕДЕЛЕНО ЗДЕСЬ
    }

    return null; // Вернуть null, если обновление не удалось
}

export async function middleware(req) {
    const authToken = req.cookies.get('authToken')?.value;
    const refreshToken = req.cookies.get('refreshToken')?.value;

    if (authToken) {
        try {
            // Попытка проверить `authToken`
            await jwtVerify(authToken, secret);
            return NextResponse.next();
        } catch (error) {
            // Если authToken истек, попытка его обновить
            if (error.code === 'ERR_JWT_EXPIRED' && refreshToken) {
                const newAuthToken = await refreshAuthToken(refreshToken);
                if (newAuthToken) {
                    // Установить новый `authToken` в cookies и продолжить
                    const response = NextResponse.next();
                    response.cookies.set('authToken', newAuthToken, { httpOnly: true, path: "https://stackoverflow.com/" });
                    return response;
                }
            }
        }
    }

    // Перенаправить на страницу входа, если нет действительного auth token или обновление не удалось
    return NextResponse.redirect(new URL("https://stackoverflow.com/", req.url));
}

// Конфигурация маршрутизатора для применения middleware только к определенным маршрутам
export const config = {
    matcher: [
        '/my-account',
        '/messages'
    ],
};

Вот мой endpoint обновления:

import { jwtVerify, SignJWT } from 'jose';
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { connectToDB } from '@utils/database';
import User from '@models/User';
import { headers } from 'next/headers'; // Импортировать заголовки, чтобы получить доступ к заголовку Authorization

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

//api/auth/refresh-token
export async function POST() {
    try {
        // Извлечь refresh token из cookies
        var refreshToken = cookies().get('refreshToken')?.value;

        if (!refreshToken) {
            // ОБРАБОТАТЬ ДАННЫЕ, ПЕРЕДАННЫЕ ИЗ MIDDLEWARE
            const authHeader = headers().get('Authorization');
            refreshToken = authHeader?.split(' ')[1]; // Предполагая формат "Bearer <token>"
            if(!refreshToken) {
                return new Response("Unauthorized", { status: 401 });
            }
        }

        // Проверить refresh token
        const { payload } = await jwtVerify(refreshToken, secret);
        const id = payload.userId;

        // Опционально, проверить базу данных, если нужна более актуальная информация
        await connectToDB();
        const user = await User.findById(id);

        if (!user) {
            return new Response("User not found", { status: 404 });
        }

        // Создать новый токен доступа с актуальной информацией о пользователе
        const newAuthToken = await new SignJWT({
            id: user._id.toString(),
            username: user.username,
            image: user.image,
            admin: user.admin,
            blocked: user.blocked,
            email_verified: user.email_verified,
        })
            .setProtectedHeader({ alg: 'HS256' })
            .setIssuedAt()
            .setExpirationTime('1h') // Установить срок действия для токена доступа
            .sign(secret);

        // Опционально, обновить refresh token, если вы хотите продлить его срок жизни
        const newRefreshToken = await new SignJWT({ userId: user._id.toString() })
            .setProtectedHeader({ alg: 'HS256' })
            .setIssuedAt()
            .setExpirationTime('30d') // Установить срок действия refresh token
            .sign(secret);

        // Создать NextResponse и установить новые токены в HTTP-only cookies
        const response = NextResponse.json({ message: 'Token refreshed successfully' });
        response.cookies.set('authToken', newAuthToken, { httpOnly: true, path: "https://stackoverflow.com/" });
        response.cookies.set('refreshToken', newRefreshToken, { httpOnly: true, path: "https://stackoverflow.com/" });

        return response;

    } catch (error) {
        console.error("Ошибка при обновлении токена:", error);
        return new Response(JSON.stringify({ error: 'Server error' }), { status: 500 });
    }
}

Ответ или решение

Обновление JWT с помощью промежуточного программного обеспечения Next.js через куки

При создании системы аутентификации с использованием JWT и куки в приложении на Next.js может возникнуть множество сложностей. В данном случае вы сталкиваетесь с проблемой обновления токена JWT в промежуточном программном обеспечении, где ценная информация (новый токен аутентификации) оказывается недоступной. Ниже вы найдете подробный анализ вашего кода и рекомендации по устранению ошибок.

Проблема с получением нового токена

Исходная проблема заключается в том, что переменная data.newAuthToken в функции refreshAuthToken возвращает undefined. Это может произойти по нескольким причинам:

  1. Некорректный endpoint или ошибка сервера: Убедитесь, что ваш endpoint для обновления токена настроен правильно и возвращает необходимые данные.
  2. Неправильная форма ответа: Убедитесь, что ваш endpoint возвращает JSON в правильном формате. Вам следует проверить, что возвращаемая структура данных соответствует data.newAuthToken.
// Проверьте что ваше API действительно возвращает данные в нужной форме
const data = await response.json();
console.log(data); // Это поможет увидеть, что возвращает ваш сервер
return data.newAuthToken;
  1. Обработка ошибок: Убедитесь, что ваш серверный код корректно обрабатывает все возможные ошибки и всегда возвращает JSON-ответ с соответствующей структурой.

Корректировка кода Middleware

Рассмотрим ваш код для промежуточного программного обеспечения и предложим несколько исправлений. Ваша функция middleware должна проверять наличие токена и осуществлять попытку его обновления.

export async function middleware(req) {
    const authToken = req.cookies.get('authToken')?.value;
    const refreshToken = req.cookies.get('refreshToken')?.value;

    if (authToken) {
        // Проверка на действительность токена
        try {
            await jwtVerify(authToken, secret);
            return NextResponse.next();
        } catch (error) {
            // Если токен истек, пытаемся его обновить
            if (error.code === 'ERR_JWT_EXPIRED' && refreshToken) {
                const newAuthToken = await refreshAuthToken(refreshToken);
                if (newAuthToken) {
                    // Установка нового токена
                    const response = NextResponse.next();
                    response.cookies.set('authToken', newAuthToken, { httpOnly: true, path: "/" }); // Используйте корень, если не нужно использовать конкретные домены
                    return response;
                }
            }
        }
    }

    // Перенаправление на страницу логина, если токены не действительны
    return NextResponse.redirect(new URL("/login", req.url));
}

Обновление серверной логики

В вашем коде для обновления токена, убедитесь, что вы правильно проверяете токены и формируете ответ:

export async function POST() {
    try {
        const refreshToken = cookies().get('refreshToken')?.value || headers().get('Authorization')?.split(' ')[1];

        if (!refreshToken) {
            return new Response("Unauthorized", { status: 401 });
        }

        // Проверьте refreshToken
        const { payload } = await jwtVerify(refreshToken, secret);
        const userId = payload.userId;

        // Работа с БД для получения пользователя
        await connectToDB();
        const user = await User.findById(userId);
        if (!user) {
            return new Response("User not found", { status: 404 });
        }

        // Создайте новый токен аутентификации
        const newAuthToken = await new SignJWT({ id: user._id.toString(), ... })
                                .setProtectedHeader({ alg: 'HS256' })
                                .setIssuedAt()
                                .setExpirationTime('1h')
                                .sign(secret);

        // Вернем новый токен в куки
        const response = NextResponse.json({ newAuthToken });
        response.cookies.set('authToken', newAuthToken, { httpOnly: true, path: "/" }); // Укажите правильный путь для куки

        return response;

    } catch (error) {
        console.error("Error refreshing token:", error);
        return new Response("Server error", { status: 500 });
    }
}

Заключение

Применение JWT для управления сессиями и аутентификацией требует тщательной настройки как на стороне клиента, так и на сервере. Ошибки могут проявляться как на уровне API, так и в коде промежуточного программного обеспечения. Рекомендуется тщательно следить за логи каждого вызова, а также пояснениями, что помогает быстрее находить и устранять ошибки. Используйте опыт из этой ситуации, чтобы укрепить ваши знания в области работы с JWT и интеграции API в Next.js. С правильным подходом, вы сможете создать надежную систему аутентификации для вашего приложения.

Оцените материал
Добавить комментарий

Капча загружается...