keycloak-connect nodejs / meteor - Getting access denied at first login only and prod only - node.js

I have a meteor/nodeJs app that needs to connect to my client to authentify. I set up a connection access point as such (I just anonymized the various values):
import Keycloak from "keycloak-connect";
import { WebApp } from "meteor/webapp";
import express from "express";
import session from "express-session";
const app = express();
const memoryStore = new session.MemoryStore();
app.use(
session({
secret: "secret",
resave: false,
saveUninitialized: true,
store: memoryStore,
})
);
const kcConfig = {
clientId: "clientId",
serverUrl: "realmUrl",
realm: "clientName",
realmPublicKey: "publicKey",
};
const keycloak = new Keycloak({ store: memoryStore }, kcConfig);
app.use(keycloak.middleware());
app.get("/connect", keycloak.protect(), (req, res) => {
// doing my stuff here
res.writeHead(301, {
Location: "/connected",
});
res.end();
});
WebApp.connectHandlers.use(app);
The problem is:
When I run my server locally and go to the /connect link, I am redirected to the connection platform. I connect and I am sent back to my localhost:3000/connected => Everything works as intended
when I do exactly the same flow on the production environment I am getting an access denied (blank page with only access denied written) after trying to login for the first time. If I then manually go back to the /connect link I am getting directly connected (I guess I got the token properly and could connect again)
I don't know why the behaviour is different on both environment and why I am getting an access denied page when in prod.

As mentioned in comments I had an issue with my ROOT_URL, a trailing slash was left. Went better after removing it.

Related

Unable to access passport user in socket.io when using cors

I am creating a react app and I was adding functionality of registering users.
Everything was successful but I am unable to access Passport User property in socket I used the same code given in socket.io example
const session = require("express-session");
const passport = require("passport");
io.use(wrap(session({ secret: "cats" })));
io.use(wrap(passport.initialize()));
io.use(wrap(passport.session()));
io.use((socket, next) => {
if (socket.request.user) {
next();
} else {
next(new Error("unauthorized"))
}
});
This example works fine if domain is same but when I use CORS I am unable to access the passport property in session.
my react app domain is localhost:3000 and socket server domain is localhost:5000
Assuming that you are using same protocol and same domain but different ports it should still work fine if you setup your client and server with cors flags, e.g
// server-side
const io = new Server(httpServer, {
cors: {
origin: "https://example.com",
allowedHeaders: ["my-custom-header"],
credentials: true
}
});
// client-side
import { io } from "socket.io-client";
const socket = io("https://api.example.com", {
withCredentials: true,
extraHeaders: {
"my-custom-header": "abcd"
}
});
The sample above was taken from socket.io docs: https://socket.io/docs/v4/handling-cors/
However, the above configuration will work only if client/server are sharing the same top level domain and same protocol. e.g. client: https://example.com, server: https://server.example.com
I spent some time to figure out myself why:
client: http://127.0.0.1:3000 does not work with server: https://127.0.0.1:8000, notice the protocol difference.
With cors configurations in place, it works fine if I use http://127.0.0.1:8000 for server.
PS: If you need to use different top domains, be aware of SameSite policy that might be in place for your browser: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
This policy might restrict your cookies to be sent to server.
so... if different protocol or domains, you should make sure that you session cookie has SameSite flag set as 'none', via:
const session = require('express-session');
...
// Session setup
const sessionConfig = {
secret: 'secret', // Session secret
resave: false, //don't save session if unmodified
saveUninitialized: false, // don't create session until something stored
cookie: {
sameSite: 'none',
secure: true,
}
}
const sessionMiddleware = session(sessionConfig);
app.use(sessionMiddleware);
...
io.use(wrap(sessionMiddleware));
both sameSite and secure properties are needed if you are playing with https:// protocol

Very occasionally a user session is not deleted upon logout. Why might this happen?

Environment: Node.js, Express, Express-session, Redis, Digital Ocean Droplet
Background: My app allows users to log into and out of an account that they create.
Problem: Perhaps 1% or 2% of the time when a user logs out the session is not destroyed and the user can get back to their myAccount page by simply browsing to it. This only occurs on my Digital Ocean Droplet, never on my local Windows machine. My Windows box is only used for testing and uses the default session store provided with express-session.
I can confirm with redis-cli that occasionally sessions are not destroyed. For example just now,
$ redis-cli keys "*"
1) "sess:bVpK6dnOaMsF5ybU0fnnTsCXL14Y-fHh"
After logging out I ran redis-cli again and the session was still there. Although I had been redirected to my home page I could browse to the myAccount page without logging in again. I clicked log out and this time the session was destroyed and I could not browse to myAccount.
I should emphasize that this almost never happens. The session is usually destroyed. Why might this happen?
This is the setup for my droplet.
const session = require('express-session');
const redis = require('redis');
const redisClient = redis.createClient();
const RedisStore = require('connect-redis')(session);
app.set('trust proxy', 'loopback');
app.use(session({
name: process.env.SESSION_NAME,
proxy: true;
resave: true,
rolling: true,
saveUninitialized: true,
secret: process.env.SESSION_SECRET,
store: new RedisStore({ client: redisClient }),
unset: 'destroy',
cookie: {
maxAge: 2 * 60 * 60 * 1000,
sameSite: 'lax',
secure: true
}
}));
This is how I log users out.
router.get('/logout', redirectLogin, middlewareUserAccounts.logout);
exports.logout = wrapAsync(async function (req, res) {
req.session.destroy(function (err) {
if (err) {
throw new Error(err);
} else {
res.clearCookie(process.env.SESSION_NAME);
return res.redirect('/');
}
});
});
exports.redirectLogin = wrapAsync(async function (req, res, next) {
let doUserValuesExist = req.session.hasOwnProperty('userValues');
if (doUserValuesExist === false) return res.redirect('/login');
return next();
});
After many hours of research and testing I discovered the source of my problem.
router.get('/logout', redirectLogin, middlewareUserAccounts.logout);
I used a GET request instead of a POST request. Never use GET to log a client out of a server because GET may use the cache in the browser instead of hitting the server. In my case /logout redirected to / after the session was destroyed. Since / is in the cache it served that page up without hitting the server and the session was not destroyed. The reason this wasn't a problem on desktop was because it wasn't using the cache for local files. I'm not sure why this problem only occurred maybe 1% or 2% of the time on my Digital Ocean Droplet but that's the way it worked.

How to access HTTP Cookie in Node.JS - JWT

This might seem like a redundant question, but please hear me out first:
I'm working with a React Frontend and a Node Backend. I'm using JWT to deal with user authentication. Right now, I'm having trouble actually working with the JWT and performing the authentication. Here's where I'm stuck:
~ I try setting the token as an http cookie in my backend. If i work with postman, I see the token being set. However, when I use req.cookies.token to try and receive the token cookie to perform validation in the backend, I get an undefined value. Am I supposed to be sending the cookie from the frontend to the backend somehow? I feel like this is the part that I am missing.
Please advise!
SO I can give you an alternative solution to handling session making use of express-session and connect-mongodb-session this has tend to been the popular and somewhat secure solution for server session handling
Firstly you will need the following packages
npm i express-session connect-mongodb-session or yarn add express-session connect-mongodb-session
Now that we have packages that we need to setup our mongoStore and express-session middleware:
//Code in server.js/index.js (Depending on your server entry point)
import expressSession from "express-session";
import MongoDBStore from "connect-mongodb-session";
import cors from "cors";
const mongoStore = MongoDBStore(expressSession);
const store = new mongoStore({
collection: "userSessions",
uri: process.env.mongoURI,
expires: 1000,
});
app.use(
expressSession({
name: "SESS_NAME",
secret: "SESS_SECRET",
store: store,
saveUninitialized: false,
resave: false,
cookie: {
sameSite: false,
secure: process.env.NODE_ENV === "production",
maxAge: 1000,
httpOnly: true,
},
})
);
Now the session middleware is ready but now you have to setup cors to accept your ReactApp so to pass down the cookie and have it set in there by server
//Still you index.js/server.js (Server entry point)
app.use(
cors({
origin: "http://localhost:3000",
methods: ["POST", "PUT", "GET", "OPTIONS", "HEAD"],
credentials: true,
})
);
Now our middlewares are all setup now lets look at your login route
router.post('/api/login', (req, res)=>{
//Do all your logic and now below is how you would send down the cooki
//Note that "user" is the retrieved user when you were validating in logic
// So now you want to add user info to cookie so to validate in future
const sessionUser = {
id: user._id,
username: user.username,
email: user.email,
};
//Saving the info req session and this will automatically save in your mongoDB as configured up in sever.js(Server entry point)
request.session.user = sessionUser;
//Now we send down the session cookie to client
response.send(request.session.sessionID);
})
Now our server is ready but now we have to fix how we make request in client so that this flow can work 100%:
Code below: React App where you handling logging in
//So you will have all your form logic and validation and below
//You will have a function that will send request to server
const login = () => {
const data = new FormData();
data.append("username", username);
data.append("password", password);
axios.post("http://localhost:5000/api/user-login", data, {
withCredentials: true, // Now this is was the missing piece in the client side
});
};
Now with all this you have now server sessions cookies as httpOnly

Keycloak connecting to react and express project/ Cannot protect the routes according to roles

So I have an application that uses React in frontend and Express in backend. From an example I saw, I configured Keycloak in both of these sides. But I am trying to protect the "/" path with the realm role admin. Which means a user who doesn't have the role admin cannot acccess that route. However it doesn't work. I am thinking it's because frontend does the work (if you are a Keycloak user, you can access it wheter you have admin role or not) and backend doesn't get a chance.
In frontend index.js:
let initOptions = {
url: 'http://localhost:8080/auth', realm: '{realm_name}', clientId: '{client_id}', onLoad: 'login-required'
}
let keycloak = Keycloak(initOptions);
keycloak.init({ onLoad: initOptions.onLoad }).then(function(authenticated) {
ReactDOM.render(<SnackbarProvider anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}} maxSnack={3}><App /></SnackbarProvider>, document.getElementById('root'));
}).catch(function() {
alert('failed');
});
In backend app.js:
const Keycloak = require('keycloak-connect');
const session = require('express-session');
var memoryStore = new session.MemoryStore();
var keycloak = new Keycloak({ store: memoryStore });
app.use(session({
secret:'secret',
resave: false,
saveUninitialized: true,
store: memoryStore
}));
app.use(keycloak.middleware());
app.get('/', keycloak.protect('realm:admin'), function(req, res){
console.log("HERE");
});
Here console.log("HERE"); this doesn't write anything to the console. So I am guessing this app.get doesn't get executed.
I do not know how to configure Keycloak to both of them. Any help?

redirect based authentication flow in secured by keycloak node.js app behind application gateway

I'm getting access denied errors in secured node.js app which is an official keycloak example app
Secured app was dockerized and put behind application gateway which is itself dockerized.
The application gateway is node.js express application which uses http/https packages and routes incoming traffic to node.js secured app.
So, to access app url mapped urls were added to the gateway:
mappings:
- /:/
- /login:/login
- /logout:/logout
- /protected/resource:/protected/resource
Gateway does ssl offloading.
Keycloak was dockerized too and its /auth endpoint was mapped inside the gateway.
The app code is below:
var Keycloak = require('keycloak-nodejs-connect');
var hogan = require('hogan-express');
var express = require('express');
var session = require('express-session');
var app = express();
var server = app.listen(3005, function () {
var host = server.address().address;
var port = server.address().port;
console.log('Example app listening at http://%s:%s', host, port);
});
app.set('view engine', 'html');
app.set('views', require('path').join(__dirname, '/view'));
app.engine('html', hogan);
app.enable('trust proxy')
var memoryStore = new session.MemoryStore();
app.use(session({
secret: 'mySecret',
resave: false,
saveUninitialized: true,
store: memoryStore
}));
app.get('/', function (req, res) {
res.render('index');
});
var memoryStore = new session.MemoryStore();
app.use(session({
secret: 'mySecret',
resave: false,
saveUninitialized: true,
store: memoryStore
}));
// Additional configuration is read from keycloak.json file
// installed from the Keycloak web console.
var keycloak = new Keycloak({
store: memoryStore
});
app.use(keycloak.middleware({
logout: '/logout',
admin: '/',
protected: '/protected/resource'
}));
app.get('/login', keycloak.protect(), function (req, res) {
res.render('index', {
result: JSON.stringify(JSON.parse(req.session['keycloak-token']), null, 4),
event: '1. Authentication\n2. Login'
});
});
app.get('/protected/resource', keycloak.enforcer(['resource:view', 'resource:write'], {
resource_server_id: 'nodejs-apiserver'
}), function (req, res) {
res.render('index', {
result: JSON.stringify(JSON.parse(req.session['keycloak-token']), null, 4),
event: '1. Access granted to Default Resource\n'
});
});
keycloak.json is:
{
"realm" : "nodejs-example",
"realm-public-key" : "[public_key]",
"auth-server-url" : "https://[https://[gateway_url]]/auth",
"ssl-required" : "none",
"resource" : "nodejs-connect",
"public-client" : true
}
When https://[gateway_url]/ is accessed in the browser, KeyCloak redirects to login ui, user/password is entered in the login ui and after that access denied error is seen in the browser.
Below error is popped in the app logs:
Could not obtain grant code error: { Error: self signed certificate
in certificate chain
So basically the app fails to exchange authorization code for access token.
What i tried:
1) Accessing Keycloak token endpoint with curl as follows succeeds (Access/Refresh token is returned):
curl -k --key [keypath] --cert [certpath:passphrase] -d "grant_type=authorization_code&client_id=nodejs-connect&redirect_uri=https://[gw_url]/login?auth_callback=1&client_session_state=[client_state]&code=[authz_code]
-X POST 'https://[gw_url]/auth/realms/nodejs-example/protocol/openid-connect/token'
2) changing "auth-server-url" to "https://[gateway_url]:8080/auth" in keycloak.json helped too. Access token is returned. 8080 is published port of Keycloak docker container.
So, i guess the issue is that node.js adapter in the app doesn't present ssl ceritificate to gateway when it wants to replace the authz code with access token. So i tried to change auth-server-url to relative /auth. However
Could not obtain grant code error: { Error: connect ECONNREFUSED
127.0.0.1:80
is popped inside the logs of the app.
How to configure keycloak node.js adapter correctly to secure services behind the application gateway?
Hey I just had the same error and fixed by putting in the LAN ip Address in the keycloak.json instead of the host name.

Resources