I am working on a project that can fetch posts from Facebook using meta API. The facebook callback function returns the token, however I am having trouble to save it in the session. The session seems to be created, but it does not add the access token.
export const facebookCallback = async (req, res) => {
const { code } = req.query;
if (!code) {
console.error("Authorization code missing in callback");
return res.status(400).json({ message: "Authorization code not provided" });
}
try {
// Step 1: Get short-lived token
const tokenResponse = await axios.get(
`https://graph.facebook.com/v15.0/oauth/access_token`,
{
params: {
client_id: process.env.FACEBOOK_APP_ID,
redirect_uri: process.env.FACEBOOK_REDIRECT_URL,
client_secret: process.env.FACEBOOK_APP_SECRET,
code,
},
}
);
const shortLivedToken = tokenResponse.data.access_token;
if (!shortLivedToken) {
console.error("Failed to retrieve short-lived access token");
return res.status(500).json({ message: "Failed to retrieve short-lived access token" });
}
// Step 2: Exchange for long-lived token
const longLivedToken = await exchangeForLongLivedToken(shortLivedToken);
console.log('Long lived token:', longLivedToken);
// Step 3: Save token in session and set the cookie explicitly
req.session.accessToken = longLivedToken;
console.log('Session accessToken set:', req.session.accessToken); // Explicitly log session token
// Ensure the cookie is set properly
req.session.cookie.maxAge = 3600000; // Optional: ensure session is valid for 1 hour
req.session.cookie.httpOnly = true; // To prevent client-side access
req.session.cookie.secure = process.env.ENVIRONMENT === "production"; // Set secure flag for production
// Save session
req.session.save((err) => {
if (err) {
console.error('Error saving session:', err);
return res.status(500).json({ message: 'Failed to save session' });
}
console.log('Session saved successfully:', req.session);
// Send the cookie with the response
res.cookie('connect.sid', req.sessionID, {
httpOnly: true,
secure: process.env.ENVIRONMENT === "production", // Use secure cookies in production
sameSite: 'lax',
maxAge: 3600000, // 1 hour cookie
});
// Redirect to client-side
const clientUrl =
process.env.ENVIRONMENT === 'production'
? process.env.PRODUCTION_CLIENT_URL
: process.env.LOCAL_CLIENT_URL;
return res.redirect(`${clientUrl}/admin/projects/new/facebook`);
});
} catch (error) {
console.error('Error during Facebook callback:', error.response?.data || error.message);
return res.status(500).json({ message: 'Failed to complete Facebook login' });
}
};
Here is how the session is set up:
app.use(
session({
name: "connect.sid", // Default session cookie name
secret: process.env.JWT_SECRET,
resave: true, // Allow the session to be re-saved even if it was not modified
saveUninitialized: false, // Do not save empty sessions
cookie: {
httpOnly: true,
secure: process.env.ENVIRONMENT === "production", // Set secure cookies in production
sameSite: "lax", // Helps prevent CSRF
maxAge: 3600000, // 1 hour
},
store: MongoStore.create({
mongoUrl: process.env.MONGO_URI, // MongoDB URI for storing sessions
ttl: 14 * 24 * 60 * 60, // 14 days TTL for session
autoRemove: "native", // Automatically clean up expired sessions
}),
})
);
So when I make request from the front-end to fetch facebook pages it cannot extract the token from the session.
fetchFacebookPages: async () => {
try {
console.log("Fetching Facebook pages...");
const response = await http.get("/admin/facebook/pages");
console.log("Pages fetched:", response.data.pages);
return response.data.pages;
} catch (error) {
console.error("Error fetching pages:", error.response?.data || error.message);
throw error;
}
},
here are my logs:
Session saved successfully: Session {
cookie: {
path: '/',
_expires: 2024-12-06T13:12:33.062Z,
originalMaxAge: 3600000,
httpOnly: true,
secure: false,
sameSite: 'lax'
},
accessToken: '********'
Validating token:
Session in Middleware: Session {
cookie: {
path: '/',
_expires: 2024-12-06T13:12:34.096Z,
originalMaxAge: 3600000,
httpOnly: true,
secure: false,
sameSite: 'lax'
}
}
}
Access token missing from session
My http interceptor:
import axios from "axios";
// Function to fetch the client's public IP address
async function fetchIPAddress() {
try {
const response = await axios.get('https://api.ipify.org?format=json');
return response.data.ip;
} catch (error) {
console.error('Failed to fetch IP address:', error);
return null;
}
}
// Create an Axios instance
const http = axios.create({
baseURL: process.env.REACT_APP_API_URL
? `${process.env.REACT_APP_API_URL}/api`
: "http://localhost:3001/api", // Default to localhost if environment variable is not set
headers: {
"Content-Type": "application/json",
},
withCredentials: true, // Send cookies with requests
});
// Request interceptor to add additional headers
http.interceptors.request.use(
async (config) => {
const ipAddress = await fetchIPAddress(); // Fetch public IP
if (ipAddress) {
config.headers['X-Client-IP'] = ipAddress; // Add IP to request headers
}
console.log("Request Headers:", config.headers); // Log headers for debugging
return config;
},
(error) => {
console.error("Request Error:", error);
return Promise.reject(error);
}
);
// Response interceptor to handle success and error responses
http.interceptors.response.use(
(response) => {
console.log("Response Data:", response.data); // Log response data
return response;
},
(error) => {
console.error("Response Error:", error.response?.data || error.message);
return Promise.reject(error); // Pass error to the calling function
}
);
export default http;
And this is the component that is supposed to fetch pages:
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router";
import adminServices from "../../../API/adminServices";
import styles from "./FacebookFetch.module.css";
import arrow from "../../../Assets/arrow.svg";
import AdminLoader from "../../../Shared/AdminLoader/AdminLoader";
const FacebookFetch = ({ setDescription, setLinks }) => {
const [pages, setPages] = useState([]);
const [posts, setPosts] = useState({});
const [activePageId, setActivePageId] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const navigate = useNavigate();
useEffect(() => {
if (window.location.hash === "#_=_") {
window.history.replaceState({}, document.title, window.location.pathname);
}
const autoLoginAndFetchPages = async () => {
setLoading(true);
setError(null);
try {
const fetchedPages = await adminServices.fetchFacebookPages();
setPages(fetchedPages);
} catch (err) {
if (err.response?.status === 401 && !localStorage.getItem("facebookRetry")) {
console.log("No valid token found, redirecting to Facebook login...");
localStorage.setItem("facebookRetry", "true");
handleFacebookLogin();
} else {
console.error("Error fetching pages:", err.message);
setError(err);
}
} finally {
setLoading(false);
}
};
autoLoginAndFetchPages();
}, []);
const fetchPages = async () => {
setLoading(true);
setError(null);
try {
const fetchedPages = await adminServices.fetchFacebookPages();
setPages(fetchedPages);
} catch (err) {
console.error("Error fetching Facebook pages:", err.message);
setError(err);
} finally {
setLoading(false);
}
};
const fetchPosts = async (pageId) => {
setLoading(true);
setError(null);
try {
setPosts({});
setActivePageId(null);
const selectedPage = pages.find((page) => page.id === pageId);
const fetchedPosts = await adminServices.fetchFacebookPagePosts(
pageId,
selectedPage.access_token
);
setPosts({
[pageId]: fetchedPosts,
});
setActivePageId(pageId);
} catch (err) {
console.error("Error fetching posts:", err.message);
setError(err);
} finally {
setLoading(false);
}
};
const handleFacebookLogin = () => {
adminServices.redirectToFacebookLogin();
};
const selectPost = async (post) => {
setLoading(true);
setError(null);
try {
const selectedPage = pages.find((page) => page.id === activePageId);
if (!selectedPage?.access_token) {
throw new Error("Page access token is missing.");
}
if (!post.message && !post.attachments) {
setError("This post has no content to fetch.");
return;
}
const postData = await adminServices.downloadPostMedia(post.id, selectedPage.access_token);
const description = postData.description || "";
// Extract only non-media links from the description
const linkRegex = /(https?://[^s]+)/g;
const extractedLinks = description.match(linkRegex) || [];
const cleanedDescription = description.replace(linkRegex, "").trim();
// Combine API links and extracted non-media links
const combinedLinks = [...(postData.links || []), ...extractedLinks].filter(
(link) => !postData.media.includes(link) // Exclude media URLs
);
// Set description and cleaned links
setDescription(cleanedDescription);
setLinks(combinedLinks);
// Navigate and pass the appropriate data
navigate("/admin/projects/new", {
state: {
description: cleanedDescription,
links: combinedLinks,
media: postData.media, // Separate field for media
},
});
} catch (err) {
console.error("Error selecting post:", err.message);
setError(err.message);
} finally {
setLoading(false);
}
};
return (
<div className={styles.container}>
{loading && <AdminLoader />} {/* Show loader during loading */}
<h1 className={styles.title}>
Fetch From <span className={styles.facebook}>Facebook</span>
</h1>
<div className={styles.actions}>
<button className={`primary__btn`} onClick={handleFacebookLogin}>
Configure
</button>
<button className="primary__btn" onClick={fetchPages}>
Refresh Pages
</button>
</div>
{error && <p className={styles.error}>Error: {error.message || "Unknown error"}</p>}
<div className={styles.content}>
<div className={styles.pages}>
<h2>Pages</h2>
{pages.length > 0 ? (
<ul className={styles.pageList}>
{pages.map((page) => (
<li key={page.id} className={styles.pageItem}>
<button className={styles.pageButton} onClick={() => fetchPosts(page.id)}>
{page.name}
<img className={styles.arrow} src={arrow} alt="arrow" />
</button>
</li>
))}
</ul>
) : (
<p>No pages found or not logged in.</p>
)}
</div>
<div className={styles.posts}>
<h2>Posts</h2>
{Object.keys(posts).length > 0 ? (
Object.entries(posts).map(([pageId, pagePosts]) => (
<div className={styles.posts__list} key={pageId}>
{pagePosts.map((post, index) => (
<div key={index} className={styles.post}>
<div className={styles.wrapper}>
<p className={styles.postMessage}>
{post.message || "No message or content provided."}
</p>
{post.message && post.message.match(/https?://[^s]+/g) && (
<div className={styles.postLinks}>
<strong>Links:</strong>
{post.message
.match(/https?://[^s]+/g)
.map((link, i) => (
<a
href={link}
key={i}
target="_blank"
rel="noopener noreferrer"
className={styles.link}
>
{link}
</a>
))}
</div>
)}
<button
className={`${styles.selectPostButton}`}
onClick={() => selectPost(post)}
>
Select Post
</button>
</div>
{post.full_picture && (
<img
src={post.full_picture}
alt="Post content"
className={styles.postImage}
/>
)}
</div>
))}
</div>
))
) : (
<p>Select a page to view posts.</p>
)}
</div>
</div>
</div>
);
};
export default FacebookFetch;
Here are the back-end routes:
// Facebook routes
router.get("/facebook/login", facebookLogin);
router.get("/facebook/login/callback", facebookCallback);
router.get("/facebook/pages", ensureValidAccessToken, getUserPages);
router.get("/facebook/posts/:pageId", ensureValidAccessToken, getPagePosts);
router.post('/facebook/post/download', ensureValidAccessToken, downloadPostMedia);
This is the validation function that throws the error
export const ensureValidAccessToken = async (req, res, next) => {
console.log("Session in Middleware:", req.session);
if (!req.session.accessToken) {
console.error("Access token missing from session");
return res.status(401).json({ message: "No token provided!" });
}
next();
};
I tried debugging a lot, but I cannot find out why it is not saving the token.