node, session store remove expired sessions - session

I am trying to implement a session store for an express.js node app
My question are :
How do you delete cookie that have a browser session lifetime (marked with expires = false according to connect doc)
Should I store session data as a json string or directly as an object
this is the coffee script I came up with so far, using mongoose as I this is the orm I chose for the app
express = require 'express'
mongoose = require "mongoose"
util = require "util"
# define session schema
SessionSchema = new mongoose.Schema
sid : { type: String, required: true, unique: true }
data : { type: String, default: '{}' }
expires : { type: Date, index: true }
module.exports =
class SessionStore extends express.session.Store
constructor: (options) ->
console.log "creating new session store"
options ?= {}
# refresh interval
options.interval ?= 60000
options.url ?= "mongodb://localhost/session"
# create dedicated session connection
connection = mongoose.createConnection options.url
# create session model
#Session = connection.model 'Session', SessionSchema
# remove expired session every cycles
removeExpires = => #Session.remove expires: { '$lte': new Date() }
setInterval removeExpires, options.interval
get: (sid, fn) ->
#Session.findOne sid: sid, (err, session) ->
if session?
try
fn null, JSON.parse session.data
catch err
fn err
else
fn err, session
set: (sid, data, fn) ->
doc =
sid: sid
data: JSON.stringify data
expires: data.cookie.expires
try
#Session.update sid: sid, doc, upsert: true, fn
catch err
fn err
destroy: (sid, fn) ->
#Session.remove { sid: sid }, fn
all: (fn) ->
#Session.find { expires: { '$gte': new Date() } }, [ 'sid' ], (err, sessions) ->
if sessions?
fn null, (session.sid for session in sessions)
else
fn err
clear: (fn) -> #Session.drop fn
length: (fn) -> #Session.count {}, fn

I'm pretty new to node so take this with a grain of salt.
While not directly session oriented the remember me tutorial on dailyjs will help a bit I think. Specifically the last bit of code where he verifies login tokens.
Also, I believe it's better to parse the JSON and store as an object. Should be easier to get access to the different cookie bits that way if you take care of the parse up front.

Related

Cookie value is returned as undefined

I'm using cookie storage session to hold user's token which is received from authentication. When I'm trying to set it after login and call it from the root.tsx's Loader Function, the userId is returned as undefined.
My loader function is:
export let loader: LoaderFunction = async({request, params}) => {
let userId = await getUserId(request);
console.log(userId);
return (userId ? userId : null);
}
The function which I receive the userId getUserId is defined as:
export async function getUserId(request: Request){
let session = await getUserSession(request);
let userId = session.get("userId");
if (!userId || typeof userId !== "string") return null;
return userId;
}
The getUserSession function is as:
export async function getUserSession(request: Request){
return getSession(request.headers.get('Cookie'));
}
I receive the getSession from destructring createCookieSessionStorage.
I'm creating a cookie with createUserSession function which is like:
export async function createUserSession(userId: string, redirectTo: string){
let session = await getSession();
session.set("userId", userId);
return redirect(redirectTo, {
headers: {
"Set-Cookie": await commitSession(session),
},
});
}
I also receive the commitSession from destructing createCookieSessionStorage. I used the same code from the Jokes demo app.
let { getSession, commitSession, destroySession } = createCookieSessionStorage({
cookie: {
name: "RJ_session",
secure: true,
secrets: [sessionSecret],
sameSite: "lax",
path: "/",
maxAge: 60 * 60 * 24 * 30,
httpOnly: true,
},
});
You configured the cookie to have secure: true, this makes the cookie only work with HTTPS, while some browsers allow secure cookies in localhost most don't do it so that may be causing your cookie to not be saved by the browser, making subsequent requests not receive the cookie at all.

Passing a unique session ID to Winston logger

I am trying to figure out if there is a way to use a unique (per request) session ID in all of the winston logger calls when an HTTP request is made.
Elaboration on the issue:
Given a scenario that several hundred requests hit a website per minute and each request passes through different functions which log out various messages.
My goal is to log messages including a unique session ID per request using winston logger, until the response is sent.
I generate a unique session ID for the request using app.use(session(...)) from express-session library.
Using morgan, the HTTP logs are printed with a unique session ID like so:
logger = winston.createLogger(...);
const myStream = {
write: (text: string) => {
logger.info(text);
}
}
morgan.token('sessionid', function (req, res) { return req['sessionID'] });
app.use(morgan(':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent" ["SESSION_ID :sessionid"]', { stream: myStream }));
However, I also want to use the same session ID in other logger.* functions elsewhere in the code. I am able to do that but as the number of simulataneous requests (using k6 load test) increases, the session ID gets overwritten by a new session ID of another request.
My code for using the session ID in request in a winston transport is:
public static initializeLogger(appInstance: express.Application) {
if (!appInstance) throw new Error(`Cannot initialize logger. Invalid express.Application instance passed. Logging may not be available`);
appInstance.use((req, res, next) => {
//this.m_sessionID = req["sessionID"];
this.m_logger.clear();
this.m_logger = winston.createLogger({
level: LOG_LEVEL,
levels: winston.config.syslog.levels,
format: winston.format.json(),
transports: [
new winston.transports.Console({ format: winston.format.simple() }),
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'debug.log', level: 'debug' }),
new WinstonCloudWatch({
logGroupName: CLOUDWATCH_LOG_GROUP_NAME,
logStreamName: function () {
let date = new Date().toISOString().split('T')[0];
return 'k-server-logs-' + date;
},
awsRegion: AWS_REGION,
awsAccessKeyId: process.env.AWS_ACCESS_KEY_ID,
awsSecretKey: process.env.AWS_SECRET_ACCESS_KEY,
retentionInDays: process.env.CLOUDWATCH_LOG_RETENTION_DAYS ? Number(process.env.CLOUDWATCH_LOG_RETENTION_DAYS) : 30,
messageFormatter: (log) => {
return `${JSON.stringify({
message: log.message,
sessionID: req["sessionID"],
level: log.level
})}`
}
})
],
});
next();
});
}
I was hoping putting the winston logger in app.use(...) middleware would set up the cloudwatch transport for the winston logger along with using the req.sessionID as each request comes in.
However, this setup isn't working. If I send even 10 simultaneous requests, this code breaks and the sessionID is incorrectly stamped on logger.* messages and/or duplicated across multiple messages.
I reviewed other implementations such as https://solidgeargroup.com/en/express-logging-global-unique-request-identificator-nodejs/ but could not get it to work.
Hoping for some advice - I am sure my setup is off.
Thank you in advance.
Key hint from https://solidgeargroup.com/en/express-logging-global-unique-request-identificator-nodejs/
Use express-http-context which has a set and get function that will ensure that the unique session ID is available throughout your code.
import httpContext from 'express-http-context';
...
...
logger.add(new WinstonCloudWatch({
level:LOG_LEVEL,
logGroupName: CLOUDWATCH_LOG_GROUP_NAME,
logStreamName: function () {
let date = new Date().toISOString().split('T')[0];
return `${process.env.CLOUDWATCH_LOG_FILE_NAMEPREFIX}-logs-${date}`;
},
awsRegion: AWS_REGION,
awsAccessKeyId: process.env.AWS_ACCESS_KEY_ID,
awsSecretKey: process.env.AWS_SECRET_ACCESS_KEY,
retentionInDays: process.env.CLOUDWATCH_LOG_RETENTION_DAYS ? Number(process.env.CLOUDWATCH_LOG_RETENTION_DAYS) : 30,
messageFormatter: (log) => {
return `${JSON.stringify({
message: log.message,
**sessionID: httpContext.get('reqId')**,
level: log.level
})}`
}
}));

Flutter For Web Cookie/Token Sessions and Authentcation

I am working on a full stack app using NodeJS and Flutter For Web, at the moment i don't understand how to make safe cookie/token sessions.
The answer i need is how to make an authentication system with Flutter For Web like other Social Networks or Stackoverflow itself.
Importing dart.html directly doesn't support from flutter 1.9 : Reference
I came across the package universal_html while digging in for the solution, and its working fine for me. Below is my helper class to store key-value pair locally on web:
import 'package:universal_html/prefer_universal/html.dart';
class WebStorage {
//Singleton
WebStorage._internal();
static final WebStorage instance = WebStorage._internal();
factory WebStorage() {
return instance;
}
String get sessionId => window.localStorage['SessionId'];
set sessionId(String sid) => (sid == null) ? window.localStorage.remove('SessionId') : window.localStorage['SessionId'] = sid;
}
To read,
WebStorage.instance.sessionId;
To write,
WebStorage.instance.sessionId = 'YOUR_CREDENTIAL';
Example:
fetchPost(params, "CMD_USERREGISTRATION").then((result) {
...
APIResponse response = APIResponse(xmlString: result.body);
if (!response.isSuccess()) {
...
return;
}
var sid = response.getSessionId();
if (kIsWeb) {
WebStorage.instance.sessionId = sid;
}
}
main.dart:
#override
Widget build(BuildContext context) {
if (kIsWeb) {
isLogin = WebStorage.instance.sessionId != null;
} else {
isLogin = //check from SharedPreferences;
}
return isLogin ? dashboardPage() : loginPage();
}
UPDATE:
shared_preferences now support web from the version 0.5.6. See also shared_preferences_web
This is an old question but the chosen answer is not totally safe.
Using web storage for sensitive information is not safe, for the web.
You should use http-only cookies. Http-only cookies cannot be read via Javascript but the browser automatically sends it to the backend.
Here is example of Nodejs-Express-TypeScript Code;
In this example there two cookies one is http-only, the other one is not.
The non-http-only cookie is for checking logged-in situation, if it's valid client assumes the user is logged in. But the actual control is done by the backend.
PS: There is no need to store or send any token, because browser handles it (cookies) automatically.
const cookieConfig = {
httpOnly: true,
secure,
maxAge: 30 * 24 * 60 * 60 * 1000,
signed: secure,
}
const cookieConfigReadable = {
httpOnly: false,
secure,
maxAge: 30 * 24 * 60 * 60 * 1000,
signed: secure,
}
function signToken(unsignedToken: any) {
const token = jwt.sign(unsignedToken, privatekey, {
algorithm: 'HS256',
expiresIn: jwtExpirySeconds,
})
return token
}
const tokenObj = {
UID: userId,
SID: sessionId,
}
const token = signToken(tokenObj)
// sets session info to the http-only cookie
res.cookie('HSINF', token, cookieConfig)
// sets a cookie with expires=false value for client side check.
res.cookie('expired', false, cookieConfigReadable)
But in this approach there is a challenge during debugging, because NodeJS and Flutter Web serves on different ports, you should allow CORS for dev environment.
if (process.env.NODE_ENV === 'dev') {
app.use(
cors({
origin: [
'http://localhost:8080',
'http://127.0.0.1:8080',
],
credentials: true,
}),
)
}
And in Flutter Web you cannot use http.get or http.post directly during debugging, because flutter disables CORS cookies by default.
Here is a workaround for this.
// withCredentials = true is the magic
var client = BrowserClient()..withCredentials = true;
http.Response response;
try {
response = await client.get(
Uri.parse(url),
headers: allHeaders,
);
} finally {
client.close();
}
Also in debugging there is another challenge; many browsers does not allow cookies for localhost, you should use 127.0.0.1 instead. However Flutter Web only runs for certain url and certain port, so here is my VsCode configuration for Flutter to run on 127.0.0.1
{
"name": "project",
"request": "launch",
"type": "dart",
"program": "lib/main.dart",
"args": [
"-d",
"chrome",
"--web-port",
"8080",
"--web-hostname",
"127.0.0.1"
]
}
These were for setting and transferring cookie, below you can find my backend cookie check
const httpOnlyCookie = req.signedCookies.HSINF
const normalCookie = req.signedCookies.expired
if (httpOnlyCookie && normalCookie === 'false') {
token = httpOnlyCookie
}
if (token) {
let decoded: any = null
try {
decoded = jwt.verify(token, privatekey)
} catch (ex) {
Logger.error(null, ex.message)
}
if (decoded) {
//Cookie is valid, get user with the session id
}
}
You can encrypt it as you want then use getStorage to store it & it supports all platforms, example:
GetStorage box = GetStorage();
write it:
box.write('jwt', 'value');
read it:
if (box.hasData('jwt')) {
box.read('jwt');
}

graphql throws unknown argument error

It seems like I'm not getting something fundamental with graphql.
I am trying to get a user by it's id which is in turn the result of another query. In this case a query on some session data.
I don't understand why the error occurs.
Here's my code:
{
session(key: "558fd6c627267d737d11e758f1ae48cae71fc9b584e2882926ad5470c88d7c3ace08c9c7") {
userId
expires
user(id: userId) {
name
}
}
}
And I get
Unknown argument "id" on field "user" of type "Session"
My schema looks like this:
type Session {
userId: String,
expires: String,
user: User
}
type User {
_id: String
name: String
email: String
firstName: String
lastName: String
}
type Query {
session(key: String!): Session
user(id: String!): User
}
Addendum Feb 23 2017
I apologize that I wasn't sufficiently explicit about the corresponding resolvers in my initial post. Yes, I my resolvers are defined and e. g. the query works for session if I don't add users.
Here's my root:
{
Query: {
async user(parentValue, args, req) {
let user = await adapters.users.byId(args.id);
return user;
},
async session(parentValue, args, req) {
let session = await adapters.session(args.key);
let userId = session.session.userId;
let expires = session.expires;
return {userId: userId, expires: expires};
}
}
}
You need to create some resolver function on field user in type Session, because GraphQL does not know how to return the user. In graphql-js it would look like that
const Session = new GraphQLObjectType({
name: 'Session',
fields: {
userId: { type: GraphQLString },
expires: { type: GraphQLString },
user: {
type: User, // it is your GraphQL type
resolve: function(obj, args, context){
// obj is the session object returned by ID specified in the query
// it contains attributes userId and expires
// however graphql does not know you want to use the userId in order to retrieve user of this session
// that is why you need to specify the value for field user in type Session
return db.User.findById(obj.userId).then(function(user){
return user;
});
}
}
}
});
It is just a simple example from Node.js to show you how you could return the user instance. You have the possibility of specifying how to retrieve values of every field in each GraphQL type (each field can have it's own resolve function).
I suggest you read the GraphQL documentation concerning root and resolvers
EDIT
I will show you an example how you can deal with such a situation. Let's consider two types: User and Session
type User {
_id: String
name: String
}
type Session {
expires: String
user: User
}
If you want your query to return Session object together with it's User, you do not need to include the userId field in Session type.
You can solve this situation in two ways. First one is to use single resolver for the session query.
{
Query: {
async session(parentValue, args, req) {
let session = await adapters.session(args.key);
// now you have your session object with expires and userId attributes
// in order to return session with user from query, you can simply retrieve user with userId
let user = await adapter.users.byId(session.userId);
return { expires: session.expires, user: user }
}
}
}
This is your first option. With that solution you can return session object with user assigned to it via single query and without extra resolve methods on any fields of Session type. The second solution is the one I have shown previously, where you use resolve method on field user of Session type - so you simply tell GraphQL how it should obtain the value for this field.

In Electron, is the default session persistent?

I'm using Electron (v1.2.7) and I need session cookies to persist between app restarts (for auth).
Is it possible? I know in Chrome when you set "Continue where you left off", the session is kept but not sure if this works the same in Electron.
As a workaround, I tried storing the session cookies also as non session cookies but it failed with a generic error.
Clarification: I'm not setting the session cookies they are set by other webpages during authentication.
The default session is persistent, but if you set cookies using session.defaultSession.cookies.set() you must set the expiration date in order for the cookie to be persisted.
You can persist cookies setting the session and a expirationDate
This example was made on Angularjs
var session = require('electron').remote.session;
var ses = session.fromPartition('persist:name');
this.put = function (data, name) {
var expiration = new Date();
var hour = expiration.getHours();
hour = hour + 6;
expiration.setHours(hour);
ses.cookies.set({
url: BaseURL,
name: name,
value: data,
session: true,
expirationDate: expiration.getTime()
}, function (error) {
/*console.log(error);*/
});
};
PD: A problem that i had was if i didn't persist them, after a fews reloads my cookies get lost. So in this example i'd persist them 6 hours
If you are loading an external webpage, you should be using Electrons <webview> tag to disable nodeintegration and for other security reasons. Using a webview will give you easy access to the Partition attribute which can be set to persist (ex: persist:mypage). You can also set the partition attribute on an Electron window if needed.
I was loading an external webpage and the configuration below worked for me. By default the webpage is configured to use "session cookie" and thats why I change it to "persistent cookie" with expiration date of 2 weeks:
// Modules to control application life and create native browser window
const {app, BrowserWindow} = require('electron')
const path = require('path')
const util = require('util')
function createWindow () {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 700,
height: 500,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
partition: 'persist:infragistics'
},
icon: __dirname + '/assets/favicon.ico',
show:false
})
let cookies = mainWindow.webContents.session.cookies;
cookies.on('changed', function(event, cookie, cause, removed) {
if (cookie.session && !removed) {
let url = util.format('%s://%s%s', (!cookie.httpOnly && cookie.secure) ? 'https' : 'http', cookie.domain, cookie.path);
console.log('url', url);
cookies.set({
url: url,
name: cookie.name,
value: cookie.value,
domain: cookie.domain,
path: cookie.path,
secure: cookie.secure,
httpOnly: cookie.httpOnly,
expirationDate: new Date().setDate(new Date().getDate() + 14)
}, function(err) {
if (err) {
log.error('Error trying to persist cookie', err, cookie);
}
});
}
});
Note: Its important to ensure that you've set the "partition" webPreferences property as well.
A String that sets the session used by the page. If partition starts with persist:, the page will use a persistent session available to all pages in the app with the same partition. if there is no persist: prefix, the page will use an in-memory session
Origin source.

Resources