Cookie is not set in browser with express-session - docker

Setup
I run two docker containers locally. One express backend and one next.js frontend. My database is on a remote server.
Backend snippet:
const corsOptions = {
origin: '*',
credentials: true,
};
app.use(cors(corsOptions));
app.use(express.static("public"));
app.use(express.json()); // lets us interpret(parse) post requests and returns an Object
app.use(express.urlencoded({ extended: true }));
// import (own) database config
const database = require("./database");
// session middleware
const session = require('express-session');
const MySQLStore = require('express-mysql-session')(session);
const sessionStore = new MySQLStore({}, database);
sessionStore.all(function(error, sessions) {
console.log('all sessions:', sessions);
});
// setting up session middleware
app.use(session({
secret: 'asdasdcads',
createDatabaseTable: true,
store: sessionStore,
resave: false,
saveUninitialized: false,
expiration: 600000,
cookie: {
sameSite: 'lax',
domain: 'localhost'
},
secure: false
}));
When I open the site in localhost:80 (docker container mapped 80:3000), the sessions get created with IDs and when saveUninitialized is set to true the sessions are even stored in my db. The last step, setting the cookie in the browser fails. Instead, if I refresh the site, a new session is created.
I guess the session middleware is not adding a Set-Cookie to the response but I really wonder why and how I can look into it more deeply. I asked chatGPT and added a bunch of stuff to the cookie and session config to make it work, like you can see, but no success. ChatGPT also recommended to set cors after session but later reverted that.

Related

ClientConfigurationError Microsoft OAuth Flow

I am implementing the Microsoft Auth code flow but I am stuck with this error.
Based on this code example, here is how I am initializing the client:
const config = {
auth: {
clientId: process.env.MICROSOFT_CLIENT_ID,
authority: process.env.MICROSOFT_AUTHORITY,
clientSecret: process.env.MICROSOFT_CLIENT_SECRET,
},
};
const cca = new msal.ConfidentialClientApplication(config);
And later I want to create an authentication URL to redirect the user to:
const authCodeUrlParameters = {
scopes: ["user.read"],
redirectUri: "http://localhost:8080/oauth/microsoft",
state: 'state_here',
};
cca
.getAuthCodeUrl(authCodeUrlParameters)
.then((authCodeUrl) => {
return authCodeUrl;
})
.catch((error) => console.log(JSON.stringify(error)));
But I am getting this error: {"errorCode":"empty_url_error","errorMessage":"URL was empty or null.","subError":"","name":"ClientConfigurationError"}
Based on the docs about errors, it looks like it's thrown before requests are made when the given user config parameters are malformed or missing.
Anybody can spot where the configs are malformed?
The error is because of the missing configuration requirements in the application.
And most importantly , check the authorization request url for missing parameters like state and nonce and the redirect url.
Here request URL may require state and nonce parameters form cache as part of authCodeUrlParameters to construct the URL.
In authCodeUrlParameters see which of them is missed as they may lead to url to null.
You try to give your domain in knownAuthority
Ex:
auth: {
clientId: 'xxxx-xx-xx-xx-xxxxx',
authority: '<give authority>',
knownAuthorities: ['<domain here>']
redirectUri: 'https://localhost:8080'
},
cache: {
cacheLocation: "sessionStorage",
storeAuthStateInCookie: false,
secureCookies: false
},
Please make sure the redirect url is in correct format:
See Redirect URI (reply URL) restrictions - Microsoft Entra | Microsoft Learn
After setting the correct url, I could get proper response

How are cookie-http-only sessions supposed to work on a SPA with a separate API server?

When trying to figure out how to authenticate with Facebook/Google in the context of an SPA I'm building, someone pointed me to Stop using JWT for sessions.
I'm trying to give it a try, using HTTP-Only Cookies. My server is Ruby on Rails with Devise and my client is JavaScript with React, although the conceptual solution is independent of specific tech I believe.
My app gets loaded by going to projectx.lvh.me and then it makes a query to api.projectx.lvh.me to fetch the current user. At the beginning it's null because the user is not logged in. When a call request is made to sign in, the response from api.projectx.lvh.me contains the session cookie, hurra! But the next request that projectx.lvh.me makes to api.projectx.lvh.me doesn't carry the cookie, so, it seems the cookie is forever lost. Even opening api.projectx.lvh.me on another tab doesn't show the cookie. Is this supposed to work? What am I missing?
I thought this was blocked by third-party cookie blocking and that's why we can't use cookies in this scenario and we have to use jwt tokens (stored on a cookie, local storage or session storage).
I managed to get cookies working in this scenario by adding config/initializers/session_store.rb to my Rails app containing:
Rails.application.config.session_store :cookie_store, key: 'session', domain: :all
which caused the session cookie to not be for api.projectx.lvh.me but for .projectx.lvh.me.
On the frontend, the API calls needed to include withCredentials, which with Axios it was the withCredentials option set to true:
Axios.post(`${apiEndPoint()}/users`, { user: values }, { withCredentials: true })
and with fetch it was the credentials option set to "include":
fetch(`${apiEndPoint()}/graphql`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: "include",
body: JSON.stringify({
query: operation.text,
variables,
}),
})

Create React App inject environment variable

I'm new to react and started with the create-react-app template.
I am developing a SPA, that is consuming a REST API.
For developing purposes, i need to authenticate against this API using OAUTH2 (Access Token).
In the production environment, i don't need to authenticate (as it runs on the same machine).
In order to get this access token in the dev environment, I need to make a POST request to the authentication server (with client_id and client_secret) and then I receive the access token which I need for further request.
As the authentication server does not support cors, I cannot do this post request within the react app.
My solution was to write a node script, that does this post request and inject the token to the client App using environment variables.
In the package.json (I did an eject) I inserted this script (gettoken):
"start": "npm-run-all -p watch-css gettoken-js start-js"
In the gettoken.js file I make the post request for getting the access token and set (in the callback function):
process.env.REACT_APP_CRM_API_ACCESS_TOKEN = response.access_token;
Now I want to access this variable in the react app - but here process.env.REACT_APP_CRM_API_ACCESS_TOKEN is always undefinied.
What am I doing wrong?
Is there another way to inject the access_token to the client app?
Here is the getToken script:
var request = require('request');
const endpoint = "https://xxxx/oauth2/token";
const resource = "https://xxxyyy.com";
const params = [
{
name: 'userName',
value: 'myuser#user.com'
},
{
name: 'password',
value: '123'
},
{
name: 'grant_type',
value: 'password'
},
{
name: 'client_secret',
value: '11231'
},
{
name: 'client_id',
value: '123'
},
{
name: 'resource',
value: resource
}
];
const encodedParams= Object.keys(params).map((key) => {
return params[key].name + '=' + encodeURIComponent(params[key].value);
}).join('&');
request(
{
method: 'POST',
uri: endpoint,
headers: [
{
name: 'content-type',
value: 'application/x-www-form-urlencoded'
}
],
body: encodedParams + "&"
}
,
function (error, response, body) {
//throw new Error("asdfasdf");
if (error)
{
console.log('error', "ERROR GETTING ACCESS TOKEN FROM API: " + error);
}
else
{
let response = JSON.parse(body);
process.env.REACT_APP_CRM_API_ACCESS_TOKEN = response.access_token;
process.env.REACT_APP_CRM_API_ENDPOINT = resource;
console.log(process.env.REACT_APP_CRM_API_ACCESS_TOKEN);
}
}
);
You're bumping into a common issue with configuration in a Create React App.
The REACT_APP_ prefix is used at build time. Are you building after you put REACT_APP_CRM_API_ACCESS_TOKEN into the env? If not, the app doesn't have them.
If you're happy to have the token in the JS bundle then go with that.
Depending on how you plan to promote your build through various environments, you'll likely bump into other issues.
Here's one possible pipeline and the issue you'll bump into.
You have staging and production.
You build your app and end up with staging env variables built into the bundle.
You promote that bundle to production and the staging env vars are still in the bundle.
Two ways around it:
Rebuild on production so the prod vars are put into the bundle.
Use the web server to inject the vars from the environment into one of the files in your SPA.
I've gone through this 4 times now and tweaked my solution slightly each time. Each time was on an ejected CRA app and each solution wasn't very DRY.
I'm now trying to solve it for a non ejected CRA and again, trying to find a DRY solution is proving tricky.
Will update this answer if I find a nicer way.
Edit: Because you have ejected the app, you can change config/env.js to do whatever you need. Including the way the REACT_APP_ prefix works.

How to allow cookies and handle 302 redirects with workbox service worker tools?

I have a SSR based react app and at present implementing Workbox tools for precaching and offline capabilities into it. I ran into issues mainly because the site relies on cookies at server side and issues redirects based on these.
Initial load works fine, but once service worker(sw) is online and another refresh results in sw doing a fetch call with the url from within workbox source. During this time, the server doesn't find cookies(fetch doesn't carry credentials - link) and issues a redirect(302) to a different url(which lets you set some options into cookies and refreshes to old url).
This results in the following error on the client side The FetchEvent for "http://localhost:8080/" resulted in a network error response: a redirected response was used for a request whose redirect mode is not "follow".
The server issues redirect as 302 status, but the client results in:
site can’t be reached
The web page at http://localhost:8080/ might be temporarily down or it may have moved permanently to a new web address.
ERR_FAILED
Here is my service worker code and the assets are populated by workbox-webpack plugin.
/* global importScripts, WorkboxSW */
importScripts('/client/workbox-sw.prod.js')
// Create Workbox service worker instance
const workboxSW = new WorkboxSW({
clientsClaim: true,
cacheId: 'app-v3-sw',
})
// Placeholder array which is populated automatically by workboxBuild.injectManifest()
workboxSW.precache([])
// cache the offline html to be used as fallback navigation route.
workboxSW.router.registerRoute(
'/offline.html',
workboxSW.strategies.networkFirst({
networkTimeoutSeconds: 2,
cacheableResponse: { statuses: [0, 200] },
})
)
// cache google api requests.
workboxSW.router.registerRoute(
/\.googleapis\.com$/,
workboxSW.strategies.staleWhileRevalidate({
cacheName: 'v3-google-api-cache',
networkTimeoutSeconds: 2,
cacheableResponse: { statuses: [0, 200] },
})
)
// cache external requests.
workboxSW.router.registerRoute(
/(static\.clevertap\.com|www\.google-analytics\.com|wzrkt\.com|www\.googletagservices\.com|securepubads\.g\.doubleclick\.net|www\.googletagmanager\.com)/,
workboxSW.strategies.cacheFirst({
cacheName: 'v3-externals-cache',
cacheExpiration: {
maxEntries: 30,
},
cacheableResponse: { statuses: [0, 200] },
})
)
// Check if client can hit the url via network, if cannot then use the offline fallback html.
// https://github.com/GoogleChrome/workbox/issues/796
workboxSW.router.registerRoute(
({ event }) => event.request.mode === 'navigate',
({ url }) =>
// eslint-disable-next-line compat/compat
fetch(url.href).catch(() => caches.match('/offline.html'))
)
P.S.
This is my first try with workbox tools or service workers and I might have missed out or overseen some details. Kindly point me in some direction.
By default, fetch doesn't pass the cookies, so you will need to add the credentials in the options:
workboxSW.router.registerRoute(
({ event }) => event.request.mode === 'navigate',
({ url }) => fetch(url.href, {credentials: 'same-origin'}).catch(() => caches.match('/offline.html'))
)
More informations here: https://github.com/github/fetch#sending-cookies

Cross domain SignalR Authentication

I have a working .NET website with authentication up and running (Identity 2.0 via Owin & https://identityazuretable.codeplex.com/)
I would like to use websockets primarily for the SignalR transport, but because the domain is running on cloudflare (the free plan does not currently support websockets) I cannot use SignalR on the same domain and get websockets. So to work around this issue I have a subdomain websockets.example.com, which does not use cloudflare, but does point to the same application.
However, I now wish to authenticate the SignalR connections based on their forms authentication token in a cookie. However the cookie does not get sent when I do the below, or when SignalR connects to websockets.example.com
JQuery Request:
$.ajax({
url: "//websockets.example.com/signalr/hubs",
type: "POST",
xhrFields: {
withCredentials: true
}
});
Or:
$.connection.hub.url = '//websockets.example.com/signalr/';
$.connection.hub.start({ withCredentials: true });
Headers:
Accept:*/*
Accept-Encoding:gzip,deflate
Accept-Language:en-GB,en-US;q=0.8,en;q=0.6
Connection:keep-alive
Content-Length:0
Cookie:ARRAffinity=805c328533b85a33c6fdeb4870bd41f00e05fd898b5d784f2635656f9525466b
Host:websockets.example.com
Origin:http://example.com
Referer:http://example.com/Page
Response:
Access-Control-Allow-Credentials:true
Access-Control-Allow-Origin:https://example.com
EDIT: Owin Config:
app.Map("/signalr", map =>
{
map.UseCors(new CorsOptions
{
PolicyProvider = new CorsPolicyProvider
{
PolicyResolver = context =>
{
var corsPolicy = new CorsPolicy
{
AllowAnyHeader = true,
AllowAnyMethod = true,
SupportsCredentials = true,
AllowAnyOrigin = false,
};
corsPolicy.Origins.Add("http://example.com");
corsPolicy.Origins.Add("http://www.example.com");
corsPolicy.Origins.Add("http://websockets.example.com");
corsPolicy.Origins.Add("https://websockets.example.com");
corsPolicy.Origins.Add("https://example.com");
corsPolicy.Origins.Add("https://www.example.com");
return Task.FromResult(corsPolicy);
}
}
});
map.RunSignalR();
});
I think your issue is with the way your authentication cookie is set. This would explain why the cookie isn't sent to websockets.example.com via SignalR or a normal CORS request made via jQuery.ajax.
To ensure cookies set by a parent domain are sent to submdomains you need to explicitly define the domain with the Set-Cookie header:
Set-Cookie: name=value; domain=.mydomain.com
If you don't explicitly define the domain, cookies will only be sent back to the exact domain that set them.
http://erik.io/blog/2014/03/04/definitive-guide-to-cookie-domains/
https://stackoverflow.com/a/23086139/719967

Resources