keycloak logout doesn't invalidate token when call a rest api - jwt

I've a React app that uses Keycloak as a authentication service. Also I've a Nodejs rest api with endpoints secured by keycloak, so the React app sends JWT when needs call an api. In Keycloak admin console I created 1 public client with users and roles.
All works fine, but the only problems is when a I logout through admin console, or
from my React application berfore that expiration time, I still can call to my app with these token.
Why my backend app doesn't validate the token with server?
My node app uses keycloak-node-connect adapter and my keycloak.json is:
{
"client-id": "my-public-client",
"bearer-only": true,
"auth-server-url": "http://localhost:8180/auth",
"realm": "my-realm"
}

Solved
I can solved my probleam like suggested in Keycloak: Access token validation end point
keycloak.config.js
var session = require('express-session');
var Keycloak = require('keycloak-connect');
var request = require('request');
const createError = require('http-errors');
let _keycloak;
var memoryStore = new session.MemoryStore();
function initKeycloak() {
if (_keycloak) {
console.log("Trying to init Keycloak again!");
return _keycloak;
}
else {
console.log("Initializing Keycloak...");
_keycloak = new Keycloak({ store: memoryStore });
return _keycloak;
}
}
function getKeycloak() {
if (!_keycloak) {
console.error('Keycloak has not been initialized. Please called init first.');
}
return _keycloak;
}
async function validateTokenKeycloak(req, res, next) {
if (req.kauth && req.kauth.grant) {
console.log('--- Verify token ---');
try {
var result = await _keycloak.grantManager.userInfo(req.kauth.grant.access_token);
//var result = await _keycloak.grantManager.validateAccessToken(req.kauth.grant.access_token);
if(!result) {
console.log(`result:`, result);
throw Error('Invalid Token');
}
} catch (error) {
console.log(`Error: ${error.message}`);
return next(createError.Unauthorized());
}
}
next();
}
module.exports = {
memoryStore,
initKeycloak,
getKeycloak,
validateTokenKeycloak
};
app.js
const express = require('express');
const createError = require('http-errors');
const dotenv = require('dotenv').config();
const session = require('express-session');
const keycloakConfig = require('./config/keycloak.config');
const app = express();
// Keycloak
app.use(session({
secret: 'secret',
resave: false,
saveUninitialized: true,
store: keycloakConfig.memoryStore
}));
const keycloak = keycloakConfig.initKeycloak();
app.use(keycloak.middleware());
app.use(keycloakConfig.validateTokenKeycloak);
app.use("/health", require('./routes/health.route'));
// 404 handler and pass to error handler
app.use((req, res, next) => {
next(createError(404, 'Not found'));
});
// Error Handler
app.use((err, req, res, next) => {
res.status(err.status || 500);
res.send({
error : {
status : err.status || 500,
message : err.message
}
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server starter on port ${PORT}`);
});

Related

Axios response interceptor for refreshing token keeps firing in Vue 3

I'm trying to implement a refresh token with Vue 3 and Java for backend. It is working but interceptor keeps firing.
The logic: On every request there's a JWT Authorization header that authenticates the user. If that expires, there's a cookie endpoint in place ready to refresh the JWT.
I am using axios and interceptor response to check if the client gets a 401 to try and refresh the JWT. The cookie may be valid or not.
The problem is that the interceptor to refresh the JWT never stops firing, and I think I have something wrong with the synchronization of the requests. Below is my code:
Api.js:
import axios from "axios";
const instance = axios.create({
baseURL: "MY_URL",
});
export default instance;
token.service.js:
class TokenService {
getLocalRefreshToken() {
const user = JSON.parse(localStorage.getItem("user"));
return user?.refreshToken;
}
getLocalAccessToken() {
const user = JSON.parse(localStorage.getItem("user"));
return user?.accessToken;
}
updateLocalAccessToken(token) {
let user = JSON.parse(localStorage.getItem("user"));
user.accessToken = token;
localStorage.setItem("user", JSON.stringify(user));
}
getUser() {
return JSON.parse(localStorage.getItem("user"));
}
setUser(user) {
// eslint-disable-next-line no-console
console.log(JSON.stringify(user));
localStorage.setItem("user", JSON.stringify(user));
}
removeUser() {
localStorage.removeItem("user");
}
}
export default new TokenService();
setupInterceptors.js:
import axiosInstance from "./api";
import TokenService from "./token.service";
const setup = (store) => {
axiosInstance.interceptors.request.use(
(config) => {
const token = TokenService.getLocalAccessToken();
if (token) {
config.headers["Authorization"] = 'Bearer ' + token;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
axiosInstance.interceptors.response.eject()
axiosInstance.interceptors.response.use(
(res) => {
return res;
},
async (err) => {
const originalConfig = err.config;
if (originalConfig.url !== "/auth/login" && err.response) {
// Access Token was expired
if (err.response.status === 401 && !originalConfig._retry) {
originalConfig._retry = true;
try {
const rs = await axiosInstance.post("/auth/refreshtoken", {
refreshToken: TokenService.getLocalRefreshToken(),
});
const { accessToken } = rs.data;
store.dispatch("auth/refreshToken", accessToken);
TokenService.updateLocalAccessToken(accessToken);
return axiosInstance(originalConfig);
} catch (_error) {
return Promise.reject(_error);
}
}
}
return Promise.reject(err);
}
);
};
export default setup;
try this out and make sure you use another instance of Axios for the refresh token request
// to be used by the interceprot
firstAxiosInstance = axios.create({ baseURL: MY_URL });
//to be used by the refresh token API call
const secondAxiosInstance = axios.create({ baseURL: MY_URL});
firstAxiosInstance.interceptors.response.use(
(res) => {
return res;
},
async (err) => {
// this is the original request that failed
const originalConfig = err.config;
// decoding the refresh token at this point to get its expiry time
const decoded = jwt.decode(localStorage.getItem('refreshToken'));
// check if the refresh token has expired upon which logout user
if (decoded.exp < Date.now() / 1000) {
store.commit('logout');
router.push('/');
}
// get new access token and resend request if refresh token is valid
if (decoded.exp > Date.now() / 1000) {
if (err.response.status === 401) {
originalConfig._retry = true;
try {
const rs = await requestService.post('/api-v1/token/refresh/', {
refresh: localStorage.getItem('refreshToken'),
});
store.commit('update_aceess_token', rs.data);
err.config.headers.Authorization = `Bearer ${rs.data.access}`;
return new Promise((resolve, reject) => {
requestService
.request(originalConfig)
.then((response) => {
resolve(response);
})
.catch((e) => {
reject(e);
});
});
} catch (_error) {
return Promise.reject(_error);
}
}
}
return Promise.reject(err);
},
);
try clean el token authorization before send request refresh, by example
in mutations(vuex)
clearAccessToken(state) {
state.access_token = ''
TokenService.removeAccessTokenApi();
},
For me it was fixed by not using the same axios instance for the refresh token request.

Socket.io, Mongodb returning undefined to frontend

I want to use socekt.io for a new project I am building. I am using socket.io for a login component and will be using socket.io in the future to update pages like a chat app. I am also using mongoose to handle my mongodb connection. I am taking in a username, and returning a password to my front end to be bcryptjs compareSync hashed. The problem I am having is that whatever is returned to the front end is undefined. When I print out what is returned to the front end, it prints out the value I am looking for though. Something is going on between the backend emitting something, and the frontend receiving something but I don't know what is it exactly. Here is my code for the back end:
const express = require('express')
const socket = require('socket.io');
const http = require('http');
const router = require('./router');
const mongoose = require('mongoose');
let Player = require('../models/player.model');
require('dotenv').config();
const PORT = process.env.PORT || 5000;
const app = express();
const server = http.createServer(app);
const uri = process.env.ATLAS_URI;
mongoose.connect(uri, {useNewUrlParser: true, useCreateIndex: true,
useUnifiedTopology: true });
const connection = mongoose.connection;
connection.once('open',() => {
console.log('MongoDB database connection established successfully')
});
const io = socket(server);
io.on('connection', (socket) => {
console.log('We have a new connection');
socket.on('login', ({ username }, callback) => {
console.log(username);
Player.find({"username": username}, function (err, player) {
if(err) {
console.log("there has been an error"), {player: null}
}
socket.emit('id', { password: player[0]['password'].toString(), id : player[0]['_id']})
}) })})
app.use(router);
server.listen(PORT, () => console.log('Server is working'))
Here is my code for the front end:
const ENDPOINT = 'localhost:5000';
async function submitAccount (e) {
e.preventDefault();
socket.emit('login', { username });
socket.on("id", (response) => {
setPassword2(String(response['password']));
id = response['id']; console.log(id);
console.log(password2)
});
try {
if (bcrypt.compareSync(password, password2) == true) {
props.setCookie("id", id);
setAccess(true);
access2 = true;
console.log(access2)
console.log('works')
}
else {
setErrorType('Invalid Password')
setErrorMsg('There is an issue with your password. Please try again')
setOpenModal(true)
console.log(password);
console.log(password2);
}
}
catch {
setErrorType('Invalid Username')
setErrorMsg('This username does not exist. Please try another')
setOpenModal(true)
}
Thanks for the help!
When you do the socket.on, it should include the whole statement you are looking to change with the socket.io output. See below:
async function submitAccount (e) {
e.preventDefault();
socket.emit('login', { username });
socket.on("id", (response) => {
setPassword2(String(response['password']));
id = response['id']; console.log(id);
console.log(password2)
if (password2 != undefined) {
try {
if (bcrypt.compareSync(password, password2) == true) {
props.setCookie("id", id);
setAccess(true);
access2 = true;
console.log(access2)
console.log('works')
}
}

Create mongoDB Session with Socket.io connection only

I need to create a session on MongoDB when the socket connection start, I have the following code:
var express = require('express');
var session = require('express-session');
var sharedsession = require("express-socket.io-session");
var MongoDBStore = require('connect-mongodb-session')(session);
var app = express();
var http = require('http').createServer(app);
var io = require('socket.io')(http);
var store = new MongoDBStore({
uri: 'CONNECTION TO MONGODB',
collection: 'sessions',
connectionOptions: {
useNewUrlParser: true
}
});
// Catch errors
store.on('error', function(error) {
console.log(error);
});
let sessionConfig = require('express-session')({
secret: 'This is a secret',
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 7 // 1 week
},
store: store,
httpOnly: false,
resave: true,
saveUninitialized: true
});
app.use(sessionConfig);
io.use(sharedsession(sessionConfig, {
autoSave:true
}));
app.get('/', function(req, res) {
res.send('<html><head><script src="http://localhost:3001/socket.io/socket.io.js"></script><script>var socket = io();</script></head><body>Test</body></html>');
});
io.on('connection', (socket) => {
console.log('a user connected');
console.log('socket.handshake',socket.handshake.session);
console.log(socket.handshake.xdomain);
console.log("A USER LOGGED IN WITH ID: ", socket.request.user);
// Accept a login event with user's data
socket.on("login", function(userdata) {
socket.handshake.session.userdata = userdata;
socket.handshake.session.save();
});
socket.on("logout", function(userdata) {
if (socket.handshake.session.userdata) {
delete socket.handshake.session.userdata;
socket.handshake.session.save();
}
});
});
http.listen(3001, () => {
console.log('listening on *:3000');
});
server = app.listen(3000);
If I access to , the session is created successfully in MongoDB, I understand that the configuration with express is ok, and the Socket Share that session correctly when I try to connect from the same domain.
The problem is, that I have a mobile app with ionic v4, that only access from a socket connection and the session is never created on MongoDB.
Any thoughts?
UPDATE 1:
I try to add passport.socketio to get a proper way to add authentication to my app, and the output of this code, is Not Session found, its right, the socket connection never add the session to the MongoDB:
var express = require('express');
var session = require('express-session');
var sharedsession = require("express-socket.io-session");
let passport = require('passport');
let passportSocketIo = require("passport.socketio");
let cors = require('cors')
var MongoDBStore = require('connect-mongodb-session')(session);
var app = express();
var http = require('http').createServer(app);
var io = require('socket.io')(http);
var store = new MongoDBStore({
uri: 'MONGO DB STRING CONNECTION',
collection: 'sessions',
connectionOptions: {
useNewUrlParser: true
}
});
// Catch errors
store.on('error', function(error) {
console.log(error);
});
app.use(cors());
app.use(passport.initialize());
app.use(passport.session());
let config = {
passport : passport,
secret: 'This is a secret',
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 7 // 1 week
},
store: store,
httpOnly: false,
resave: true,
saveUninitialized: true,
success: onAuthorizeSuccess, // *optional* callback on success - read more below
fail: onAuthorizeFail
};
let sessionConfig = require('express-session')(config);
app.use(sessionConfig);
io.origins('*:*');
io.use(sharedsession(sessionConfig, passportSocketIo.authorize(config)));
app.get('/', function(req, res) {
res.send('<html><head><script src="http://localhost:3001/socket.io/socket.io.js"></script><script>var socket = io();</script></head><body>Test</body></html>');
});
io.on('connection', (socket) => {
console.log('a user connected');
console.log('socket.handshake',socket.handshake.session);
console.log(socket.handshake.xdomain);
console.log("A USER LOGGED IN WITH ID: ", socket.request.user);
// Accept a login event with user's data
socket.on("login", function(userdata) {
socket.handshake.session.userdata = userdata;
socket.handshake.session.save();
});
socket.on("logout", function(userdata) {
if (socket.handshake.session.userdata) {
delete socket.handshake.session.userdata;
socket.handshake.session.save();
}
});
});
http.listen(3001, () => {
console.log('listening on *:3000');
});
server = app.listen(3000);
function onAuthorizeSuccess(data, accept){
console.log('successful connection to socket.io');
// accept(); //Let the user through
}
function onAuthorizeFail(data, message, error, accept){
console.log("Errror",error);
if(error) accept(new Error(message));
console.log('failed connection to socket.io:', message);
// accept(null, false);
}
Try
//Before io.on('connection....
io​.​engine​.​generateId​ ​=​ (​req​) ​=>​ {
//Try to get the cookie out from
//req.rawHeaders and return it
//so the socket ID will equal to the session Id
});
//The connection event
​io​.​on​(​'​connection​'​, ​socket​ ​=>​ { ....
//You can then do a mongoose search query using the socket Id here to match the user in the db

Heroku: My routes still pointing to localhost when deployed

I set up my NODE_ENV to production under APP>Settings>Config Vars on Heroku and set up a mongo.json file in my root directory:
{
"development": {
"host": "127.0.0.1:27017",
"dbName": "hillfinder"
},
"production": {
"PRODUCTION_DB_DSN": "mongodb+srv://***credentials***#hillfinder-qjxuo.mongodb.net/production?retryWrites=true&w=majority"
}
}
But it's still pointing to localhost:
This is the my server/index.js:
const express = require('express');
require('dotenv').config();
const nextJS = require('next');
var cookieParser = require('cookie-parser');
var session = require('express-session');
var MongoStore = require('connect-mongo')(session);
var bodyParser = require('body-parser');
var auth = require('./lib/auth');
var cors = require('cors');
var morgan = require('morgan');
var HttpStatus = require('http-status-codes');
var PORT = process.env.PORT || 8016;
const { isBlockedPage, isInternalUrl } = require('next-server/dist/server/utils');
function NODE_ENVSetter(ENV) {
var environment,
environments = {
production: () => {
environment = process.env.PRODUCTION_DB_DSN;
console.log(`We are currently in the production environment: ${environment}`);
return environment;
},
test: () => {
environment = process.env.TEST_DB_DSN;
console.log(`We are currently in the test environment: ${environment}`);
return environment;
},
default: () => {
environment = process.env.DEVELOPMENT_DB_DSN;
console.log(`We are currently in the development environment: ${environment}`);
return environment;
}
};
(environments[ENV] || environments['default'])();
return environment;
}
var db = NODE_ENVSetter('production');
var mongoose = require('mongoose');
function errorHandler(err, req, res, next) {
// Set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// Log error
console.error(err.stack);
// Render the error page
res.status(err.status || 500);
// Default error message by HTTP code
res.render('error', {
title: HttpStatus.getStatusText(err.status),
message: HttpStatus.getStatusText(err.status)
});
}
async function start() {
const dev = process.env.NODE_ENV !== 'production';
const app = nextJS({ dev });
const server = express();
await app
.prepare()
.then(() => {
mongoose.connect(db, { useNewUrlParser: true });
mongoose.Promise = global.Promise;
mongoose.connection
.on('connected', () => {
console.log(`Mongoose connection open on ${db}`);
})
.on('error', err => {
console.log(`Connection error: ${err.message}`);
});
})
.catch(err => {
console.error(err);
});
server.set('view engine', 'html');
server.use('/uploads', express.static(__dirname + '/uploads'));
server.use(bodyParser.urlencoded({ limit: '50mb', extended: false }));
server.use(bodyParser.json({ limit: '50mb' }));
server.use(morgan('dev'));
server.use(cookieParser());
server.use(
session({
secret: 'very secret 12345',
resave: false,
saveUninitialized: false,
store: new MongoStore({ mongooseConnection: mongoose.connection })
})
);
server.use(auth.initialize);
server.use(auth.session);
server.use(auth.setUser);
console.log('auth.setUser ', auth.setUser);
server.use(cors());
server.use('/users', require('./users'));
server.use('/images', require('./images'));
// Redirect all requests to main entrypoint pages/index.js
server.get('/*', async (req, res, next) => {
try {
const pathName = req.originalUrl;
if (isInternalUrl(req.url)) {
return app.handleRequest(req, res, req.originalUrl);
}
if (isBlockedPage(pathName)) {
return app.render404(req, res, req.originalUrl);
}
req.locals = {};
req.locals.context = {};
const html = await app.renderToHTML(req, res, '/', {});
// Handle client redirects
const context = req.locals.context;
if (context.url) {
return res.redirect(context.url);
}
// Handle client response statuses
if (context.status) {
return res.status(context.status).send();
}
// Request was ended by the user
if (html === null) {
return;
}
app.sendHTML(req, res, html);
} catch (e) {
next(e);
}
});
// error handler
server.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.errorStatus = err.status;
res.locals.errorMessage = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
console.log('err.status ', err.status);
res.status(err.status).send(err.message);
});
server.listen(PORT, err => {
if (err) throw err;
console.log(`> Ready and listening on http://localhost:${PORT}`);
});
}
start();
How would I fix that so I can log on to MongoDB atlas?
Thank you in advance!

node js and passport validation miss passport session

I have app.js as below
var express = require('express'),
path = require('path'),
favicon = require('serve-favicon'),
logger = require('morgan'),
cookieParser = require('cookie-parser'),
bodyparser = require('body-parser'),
db = require('./model/db');
var app=express();
var dbConfig = require('./db.js');
var mongoose = require('mongoose');
mongoose.Promise = global.Promise;
mongoose.createConnection(dbConfig.url);
var passport = require('passport');
app.use(bodyparser.urlencoded({
extended:true
}));
var expressSession = require('express-session');
var flash = require('connect-flash');
app.use(expressSession({
secret: 'crackalackin',
resave: true,
saveUninitialized: true,
cookie : { secure : false, maxAge : (4 * 60 * 60 * 1000) }, // 4 hours
}));
app.use(passport.initialize());
app.use(passport.session());
// Using the flash middleware provided by connect-flash to store messages in session
// and displaying in templates
var flash = require('connect-flash');
app.use(flash());
// Initialize Passport
var initPassport = require('./passport/init');
initPassport(passport);
var routes = require('./routes/index')(passport);
app.use('/', routes);
/// catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
app.listen(6061,function(){
console.log("started on port 6061");
});
/passport/init.js
var login = require('./login');
var signup = require('./signup');
var User = require('../models/user');
module.exports = function (passport) {
// Passport needs to be able to serialize and deserialize users to support persistent login sessions
passport.serializeUser(function (user, done) {
done(null, user._id);
});
passport.deserializeUser(function (id, done) {
try {
User.getById(id, function (err, user) {
done(err, user);
});
} catch (ex) {
}
});
// Setting up Passport Strategies for Login and SignUp/Registration
login(passport);
signup(passport);
}
when I write console.log(req.session) in /passport/login.js at validation function.
I get output like
Session {
cookie:
{ path: '/',
_expires: 2017-02-01T14:10:38.523Z,
originalMaxAge: 14400000,
httpOnly: true,
secure: false } }
I miss the
passport: {}
so I can login but can not generate passport session.
So isAuthenticated function say "login plase".
how can I manage session here.