Allow User Data and Cookies Cross Domain
-
After spending several hours combing through documentation, API references, and various search results, I couldnโt find a straightforward way to access user data externally from discussions.codenamejessica.com when using my codenamejessica.com website. This led me down a rabbit hole of researching CORS policies, cookie documentation, and related topicsโit was anything but enjoyable. Along the way, I managed to break my site at least a hundred times, an experience rivaled only by my frustrations with Azure at work. To save others from the same ordeal, Iโve created a concise guide to simplify this process.
This document provides a detailed guide to resolving CORS issues when using Nginx as a reverse proxy for NodeBB, ensuring proper handling of credentials (cookies) and cross-origin requests.
Prerequisites
- Access to your Nginx server configuration: Ensure you can edit the Nginx configuration files.
- NodeBB installed and running: NodeBB should be accessible locally at
http://127.0.0.1:4567
. - SSL Certificate: Ensure your server uses HTTPS with a valid SSL certificate (e.g., managed by Certbot).
Problem Overview
The issue occurs because:
- The
Access-Control-Allow-Credentials
header is missing or improperly set. - Cross-origin resource sharing (CORS) policies block requests when credentials (cookies) are included.
- The
OPTIONS
preflight request is not properly handled.
Steps to Fix
Step 1: Edit the Nginx Configuration
Locate your Nginx configuration file for
**domain or subdomain NodeBB points too**
. This is typically located in/etc/nginx/sites-available
or/etc/nginx/conf.d
.Edit the file to include the following configuration:
server { listen 80; server_name forum.example.com; # Global CORS Headers add_header Access-Control-Allow-Origin https://example.com always; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always; add_header Access-Control-Allow-Headers "Authorization, Content-Type" always; add_header Access-Control-Allow-Credentials true always; location / { # Handle OPTIONS preflight requests if ($request_method = OPTIONS) { add_header Access-Control-Allow-Origin https://example.com always; add_header Access-Control-Allow-Credentials true always; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always; add_header Access-Control-Allow-Headers "Authorization, Content-Type" always; return 204; # No Content } proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_set_header X-NginX-Proxy true; proxy_pass http://127.0.0.1:4567; proxy_redirect off; # Socket.IO Support proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } location /assets/uploads/ { add_header Cross-Origin-Resource-Policy cross-origin always; add_header Access-Control-Allow-Origin "https://example.com" always; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always; add_header Access-Control-Allow-Headers "Authorization, Content-Type" always; } location ~ ^/api/(categories|topic|posts|users|login|groups|admin|email|flags|notifications|search|tags|post|outgoing)$ { # Add CORS headers add_header Access-Control-Allow-Origin https://example.com always; add_header Access-Control-Allow-Credentials true always; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always; add_header Access-Control-Allow-Headers "Authorization, Content-Type" always; # Handle OPTIONS preflight requests if ($request_method = OPTIONS) { add_header Access-Control-Allow-Origin https://example.com always; add_header Access-Control-Allow-Credentials true always; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always; add_header Access-Control-Allow-Headers "Authorization, Content-Type" always; return 204; # No Content } proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_pass http://127.0.0.1:4567; } # HTTPS Configuration listen 443 ssl; # managed by Certbot ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot } # Redirect HTTP to HTTPS server { if ($host = forum.example.com) { return 301 https://$host$request_uri; } # managed by Certbot listen 80; server_name forum.example.com; return 404; # managed by Certbot }
Step 2: Restart Nginx
Save the changes and restart Nginx to apply the new configuration:
sudo systemctl restart nginx
Step 3: Clear Browser Cache
Clear your browser cache and cookies to ensure no stale configurations interfere with testing.
Step 4: Test the Configuration
-
Open Browser Developer Tools:
- Go to the Network Tab.
- Trigger a cross-origin request (e.g., from
https://example.com
tohttps://forum.example.com
).
-
Inspect the Request and Response Headers:
- Ensure the following headers are present in the response:
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
- Ensure the following headers are present in the response:
-
Verify Preflight (
OPTIONS
************) Requests:- Confirm that the
OPTIONS
requests return a204 No Content
response with the correct CORS headers.
- Confirm that the
-
Test API Calls:
- Use an API client like
curl
to verify the headers:curl -X OPTIONS https://forum.example.com/api/users/ \ -H "Origin: https://example.com" \ -H "Access-Control-Request-Method: GET" \ -H "Access-Control-Request-Headers: Authorization, Content-Type" -v
- Use an API client like
Step 5: Debugging
If the issue persists:
-
Check Nginx Logs:
sudo tail -f /var/log/nginx/error.log
-
Verify the Backend (NodeBB):
- Ensure NodeBB is not overriding or omitting the required headers.
-
Repeat Testing:
- Retest using both browser tools and
curl
.
- Retest using both browser tools and
This configuration should resolve the CORS issue and allow cross-origin requests with credentials. If further issues arise, revisit the headers and proxy configuration for adjustments.
Changing Cookie Settings in NodeBB
To ensure cookies work correctly across domains (e.g.,
example.com
andforum.example.com
), configure the following settings in NodeBB:-
Edit the
config.json
file in the NodeBB root directory:Add or modify the
cookieDomain
andcookie
properties as follows:{ "cookieDomain": ".example.com", "cookie": { "sameSite": "None", "secure": true } }
cookieDomain
: Ensures cookies are shared across the main domain and subdomains.sameSite
: Set toNone
to allow cross-origin requests with credentials.secure
: Set totrue
to ensure cookies are only sent over HTTPS.
-
Restart NodeBB:
Apply the changes by restarting NodeBB:
./nodebb restart
-
Verify Cookies:
Use your browser's developer tools to check that the cookies are:
- Accessible on both
example.com
andforum.example.com
. - Marked with the correct domain,
Secure
, andSameSite
attributes.
- Accessible on both
With these changes, cookies should work seamlessly across your domains, ensuring proper authentication and session handling for cross-origin requests.
Additional Steps: Configuring Cookies in the NodeBB Admin Control Panel
In addition to editing the
config.json
file, update the cookie settings in the NodeBB Admin Control Panel:-
Access the Admin Control Panel:
- Navigate to
Admin -> Settings -> Cookies
.
- Navigate to
-
Set the Cookie Domain:
- Add
.example.com
as the cookie domain.
- Add
-
Save Changes:
- Ensure you click "Save" to apply the changes.
-
Restart NodeBB:
- Restart NodeBB to ensure all settings take effect:
./nodebb restart
- Restart NodeBB to ensure all settings take effect:
This ensures that cookies are properly configured for cross-origin requests and shared between the primary domain and subdomains.
Codename: Jessica
Linux Enthusiast | Adventurer | Smart Ass
My Site | Join the Forum
-
Now I have come across the unknown and I believe currently unsolvable question. I have a logout button on codenamejessica.com, and I have tried:
logoutUser() { const csrfToken = window.config?.csrf_token; // Retrieve CSRF token if (!csrfToken) { console.error('CSRF token is missing. Cannot log out.'); alert('Logout failed. Please refresh and try again.'); return; } axios.post('https://discussions.codenamejessica.com/logout', {}, { withCredentials: true, // Include cookies headers: { 'x-csrf-token': csrfToken, // Add CSRF token }, }) .then(response => { // Trigger logout hooks if needed // For example: hooks.fire('action:app.loggedOut', response.data); // Redirect or reload based on server response if (response.data.next) { window.location.href = response.data.next; } else { window.location.reload(); } }) .catch(error => { console.error('Logout failed:', error); alert('Failed to log out. Please try again.'); }); }
axios.get('https://discussions.codenamejessica.com/api/me', { withCredentials: true }) .then(response => { const { uid, sessionId } = response.data; // Example structure this.revokeSession(uid, sessionId); }) .catch(error => console.error('Failed to fetch user details:', error)); revokeSession(uid, sessionId) { axios.delete(`https://discussions.codenamejessica.com/api/users/${uid}/sessions/${sessionId}`, { withCredentials: true, headers: { 'Authorization': `Bearer <TOKEN>`, 'Content-Type': 'application/json', }, }) .then(() => { alert('Logged out successfully'); window.location.href = 'https://codenamejessica.com/'; }) .catch(error => console.error('Failed to revoke session:', error)); }
and even:
logoutUser() { const apiToken = '<YOUR_API_TOKEN>'; // Replace with your actual API token axios.post('https://discussions.codenamejessica.com/logout', {}, { withCredentials: true, // Include cookies headers: { 'Authorization': `Bearer ${apiToken}`, // Add the Bearer token 'Content-Type': 'application/json', }, }) .then(() => { alert('You have been logged out!'); // Redirect to your homepage or another page after successful logout window.location.href = 'https://codenamejessica.com/'; }) .catch(error => { console.error('Error logging out:', error); alert('Failed to log out. Please try again.'); }); }
Nothing seems to be working, I am unable to obtain the csrf has anyone actually got this to work without importing nodebb files into their app?
-
@codenamejessica Thanks for this. I'm actually looking to do something similar where I am building a portal at work and need to embed the kb (nodebb) in it. They both share the same root domain, but I cannot get this to work. The smartest thing would be to pass this using a reverse proxy, but that doesn't seem to work either, which is odd.
The reverse proxy would resolve all of the issues, but I can't get it to work - it seems to load the site again rather than the remote site.
-
@phenomlab Which part are you referring to the API data on the TLD, or the logout feature on the TLD?
If it is getting API data, I have mine with nginx using the proxy
@codenamejessica said in Allow User Data and Cookies Cross Domain:
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_set_header X-NginX-Proxy true;
If it is about the Logout button, I haven't thought about putting this in the proxy. The TLD is on the same server as nodebb. Maybe I am having a hard time seeing the forest through the trees.
Let me think on that, I may have an idea, although ultimately it should have a proper API using DELETE, or PUT so someone can logout correctly with the API.
-
Actually, figured it out! I would like to tell you what I did, but I really don't know. I broke my CSRF tokens, then in pure panic fixed it by messing around with the nginx config.
But let me show you what is working:
The nginx config above (I edited it with the new one)
This code here for the Logout:updateUserNavForLoggedInUser(userData) { const navContainer = document.getElementById('elUserNav'); const userPicture = userData.picture ? `<a href="https://discussions.codenamejessica.com/user/${userData.username}" rel="nofollow" class="nodebbUserPhoto nodebbUserPhoto--fluid nodebbUserNav__link" title="Go to ${userData.username}'s profile"> <img src="${userData.picture}" alt="${userData.username}" class="nodebbUserNav__avatar"> </a>` : `<a href="https://discussions.codenamejessica.com/user/${userData.username}" rel="nofollow" data-nodebb-hook="userNoPhotoWithUrl" class="nodebbUserPhoto nodebbUserPhoto--fluid nodebbUserNav__link" title="Go to ${userData.username}'s profile"> <div class="nodeBBNavNoPhotoTile"> ${this.getInitial(userData.username)} </div> </a>`; navContainer.innerHTML = ` <li data-el="profile"> ${userPicture} </li> <li data-el="logout"> <button class="nodebbUserNav__link" id="logoutButton"> <i class="fa-solid fa-right-from-bracket" aria-hidden="true"></i> <span class="nodebbUserNav__text">Log Out</span> </button> </li> `; // Attach the logout functionality to the button document.getElementById('logoutButton').addEventListener('click', this.logoutUser); }, logoutUser() { axios.get('https://discussions.codenamejessica.com/api/config', { withCredentials: true, // Include cookies }) .then(response => { const csrfToken = response.data.csrf_token; // Extract CSRF token return axios.post('https://discussions.codenamejessica.com/logout', {}, { withCredentials: true, // Include cookies headers: { 'x-csrf-token': csrfToken, // Use retrieved CSRF token }, }); }) .then(() => { window.location.href = 'https://codenamejessica.com/'; }) .catch(error => { console.error('Error logging out:', error); alert('Failed to log out. Please try again.'); }); },
Ultimately It took getting the user's x-csrf token, which I acquired by adding the x-csrf items and adding the logout to the api section of the nginx config. Then I was able to acquire the x-csrf from the
const csrfToken = response.data.csrf_token;
. Sending that in with a post, and redirect back to the website.AGGHHH! That was hard! Don't judge me, I see your eyes giving me those looks.