I’ve created a NextJS / Flask application that leverages Flask_JWT_Extended for authentication. The application uses a function called set_jwt_cookies to set an access_token and refresh_token as httpOnly cookies. The csrf_token is set as non-httpOnly to allow access via Javascript.
The application successfully sets these cookies upon login and they’re visible in the browser. The problem that I’ve encountered is that attempting to refresh the access_token results in an error message “CSRF double submit tokens do not match”. Despite several approaches, I’ve been unable to resolve this issue. I’ve simplified the code below.
# authentication/routes.py
@authentication.route('/refresh', methods=['POST'])
@jwt_required(refresh=True)
def refresh():
# Log the received CSRF token
received_csrf_token = request.headers.get('X-CSRF-TOKEN')
logging.info(f"Received CSRF token: {received_csrf_token}")
# Log the expected CSRF token
expected_csrf_token = request.cookies.get('csrf_token')
logging.info(f"Expected CSRF token (from cookie): {expected_csrf_token}")
if received_csrf_token != expected_csrf_token:
logging.error("CSRF double submit tokens do not match")
return jsonify({"msg": "CSRF double submit tokens do not match"}), 400
# Get the identity of the user from the refresh token
identity = get_jwt_identity()
# Create a new access token
access_token = create_access_token(identity=identity, expires_delta=timedelta(minutes=60))
# Create a response object
response = jsonify({'access_token': access_token})
# Set the access token as a cookie
set_access_cookies(response, access_token)
return response
// utils/getCookie.ts
export const getCookie = (name: string): string | undefined => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(";").shift();
};
// refresh/page.tsx
"use client";
import { useEffect, useState } from "react";
import { getCookie } from "@/utils/getCookie";
const HomePage = () => {
const [status, setStatus] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const refreshStatus = async () => {
try {
const apiUrl = process.env.NEXT_PUBLIC_FLASK_API_URL;
if (!apiUrl) {
console.error("API URL is not defined.");
return;
}
const csrfToken = getCookie("csrf_token");
console.log("CSRF Token:", csrfToken); // Debugging CSRF token
const response = await fetch(`${apiUrl}/refresh`, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": csrfToken || "",
},
});
console.log("Response Status:", response.status); // Debugging response status
const text = await response.text(); // Get the response as text to see what is returned
console.log("Response Text:", text); // Debugging response text
// Attempt to parse JSON if the content type is application/json
try {
const data = JSON.parse(text);
console.log("Response Data:", data); // Debugging response data
setStatus(data.user || "");
} catch (jsonError) {
console.error("Failed to parse JSON", jsonError);
setStatus(null);
}
} catch (error) {
console.error("An error occurred while refreshing tokens", error);
setStatus(null);
} finally {
setLoading(false);
}
};
useEffect(() => {
refreshStatus();
}, []);
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Home Page</h1>
{status ? (
<p>User Status: {status}</p>
) : (
<p>Failed to refresh user status. Please log in again.</p>
)}
</div>
);
};
export default HomePage;