res.session undefined graphql apollo-server-express - node.js

I am having trouble with sessions. For some reason, the req.session is undefined even though I'm using the
session middleware. I was trying to use redis but I couldn't make the connection work.
The strange part is that for some reason the cookie is registered in the graphql playground. So the reason must be in the way I pass the request, probably.
All the types are correct ( typescript isn't angry with anything ).
Here's the code from the server.ts
import express, { Request, Response } from "express";
import routes from "./routes";
import cors from "cors";
import "reflect-metadata";
import { createConnection } from "typeorm";
import { ApolloServer } from "apollo-server-express";
import { buildSchema } from "type-graphql";
import session from "express-session";
createConnection()
.then(async (connection) => {
console.log("Conexão feita com sucesso");
const app = express();
app.set("trust proxy", 1);
app.use(cors());
app.use(
session({
name: "qid",
secret: "keyboard cat",
resave: false,
saveUninitialized: true,
cookie: {
secure: false,
maxAge: 1000 * 60 * 60 * 24 * 365 * 10,
httpOnly: true,
},
})
);
const apolloServer = new ApolloServer({
schema: await buildSchema({
resolvers: [
],
validate: false, // Activate the validation with class-validation module.
}),
context: (req: Request, res: Response): Context => ({ req, res, session: req.session }),
playground: {
settings: {
'request.credentials': 'include',
},
},
});
apolloServer.applyMiddleware({ app });
app.use(express.json());
app.use(routes);
app.listen(3333);
})
.catch((error) => console.error(error));
And where I use the session.
#Mutation(() => UserResponse)
async login(
#Arg("info", () => UserLoginInputType) info: UserLoginInputType,
#Ctx(){req, res, session}: Context
): Promise<UserResponse> {
const user = await User.findOneOrFail({ where: { email: info.email } });
const valid = await argon2.verify(user.password, info.password);
if (valid) {
req.session.userId = user.id;
return {
user,
};
}
return {
errors: [
{
field: "password",
message: "Incorrect password",
},
],
};
}

I just forgot the curly brackets when passing res and req throw the context

you need to pass context something like this and you are good to go
context: ({ req, res }): Context => ({
req,
res,
session: req.session,
}),
also, it's better to do the configuration in the GraphQL config file to include the credentials.
graphql.config.ts
import { ApolloDriverConfig, ApolloDriver } from '#nestjs/apollo';
import { join } from 'path';
export const GraphQLConfig: ApolloDriverConfig = {
driver: ApolloDriver,
debug: true,
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
playground: {
settings: {
'editor.theme': 'light', // use value dark if you want a dark theme in the playground
'request.credentials': 'include',
},
},
};
and assign the config file to the module directory
user.module.ts
import { Module } from '#nestjs/common';
import { GraphQLModule } from '#nestjs/graphql';
import { TypeOrmModule } from '#nestjs/typeorm';
import { GraphQLConfig } from 'src/config/graphql.config';
import { UserEntity } from 'src/entity/user.entity';
import { UserResolver } from './user.resolver';
import { UserService } from './user.service';
#Module({
imports: [
TypeOrmModule.forFeature([UserEntity]),
GraphQLModule.forRoot(GraphQLConfig),
],
providers: [UserService, UserResolver],
})
export class UserModule {}
and it will automatically enable credentials value to be "include" from "omit"

Related

Error authenticating user using express-session and Typescript

I have a small app that allows registration and login, but I'm still trying to use session-express to persist the session.
Below is server.ts where I create the session, cors, etc...
import express, { json } from "express";
import { db } from "./models/db-connection";
import { router } from "./routes";
import session from "express-session";
var cors = require("cors");
const app = express();
app.use(
cors({
origin: "http://localhost:3000",
methods: ["POST", "GET"],
credentials: true,
})
);
app.use(json());
app.use(
session({
secret: "testtest",
resave: false,
saveUninitialized: false,
})
);
app.use(router);
app.listen(3001, async () => {
try {
await db.sync();
console.log("Connected to the database");
} catch (error) {
console.error("Failed to connect to the database", error);
}
});
In the routes.ts script I use the authenticate function which will only allow a new user to be registered if an user is already logged in.
But the problem is exactly here, req.session.authenticated is never true, it is always undefined, even when I set it to true as I will show in UserController.ts.
Below is routes.ts.
import express from "express";
import UserController from "./controllers/UserController";
import "./session-data";
function authenticate(req: express.Request, res: express.Response, next: express.NextFunction) {
console.log(req.session);
if (req.session.authenticated) {
next();
} else {
res.redirect("/login");
}
}
const router = express.Router();
router.post("/users", authenticate, UserController.create);
router.get("/users/login/:login", UserController.findLogin);
export { router };
As you can see below in UserController.ts, req.session.authenticated is true when we find a match, I put in a console.log just to confirm that req.session has the authenticated property at this point, and it does, but it looks like routes.ts can't see it.
UserController.ts
import express, { Request, Response } from "express";
import { UserModel } from "../models/UserModel";
import "../session-data";
const bcrypt = require("bcryptjs");
class UserController {
async findLogin(req: express.Request, res: express.Response) {
const email = req.query.email?.toString();
const password = req.query.password?.toString();
try {
const user: any = await UserModel.findOne({
where: {
email: email,
},
});
if (user) {
const match = await bcrypt.compare(password, user.password);
if (match) {
req.session.authenticated = true;
console.log(req.session);
return res.status(204).json(user);
} else {
req.session.authenticated = false;
return res.status(200).send("invalid password");
}
} else {
req.session.authenticated = false;
return res.status(201).send("User not found");
}
} catch (error: any) {
req.session.authenticated = false;
return res.send(error.message);
}
}
}
async create(req: Request, res: Response) {
try {
const { userName, email, password } = req.body;
const user = await UserModel.create({
userName,
email,
password,
});
return res.status(201).json(user);
} catch (error: any) {
console.error(error);
return res.send(error.message);
}
}
Since I'm using Typescript, I need to create a session-data.ts file to expand req.session
session-data.ts
declare module "express-session" {
interface SessionData {
authenticated: boolean;
}
}
export {};
In the session store, the session is never created either.
Could you help me please? I don't know why req.session.authenticated isn't working, I'm new to using typescript, I imagine there's something related to that.
Thanks!
Try simplifying your code as much as possible so that the problem is still reproducible. The following works for me:
declare module "express-session" {
interface SessionData {
authenticated: boolean;
}
}
function login(req: express.Request, res: express.Response, next: express.NextFunction) {
req.session.authenticated = true;
res.end("Logged in");
}
function authenticate(req: express.Request, res: express.Response, next: express.NextFunction) {
console.log(req.session);
if (req.session.authenticated) {
next();
} else {
res.end("Not logged in");
}
}
express()
.use(session({
secret: "Se$$ion",
resave: false,
saveUninitialized: false
}))
.get("/login", login)
.get("/auth", authenticate, function(req, res) {
res.end("Welcome");
})
.listen(3001);
GET /login returns "Logged in".
Then GET /auth return "Welcome" and the session is logged:
Session {
cookie: { path: '/', _expires: null, originalMaxAge: null, httpOnly: true },
authenticated: true
}

React Apollo used to set a cookie with userID

What I would like to do is:
create login mutation and return using it user id and username
add a cookie to a browser (chrome) with user id to check if user is logged in or not
Login mutation works if I do not assign userId to session, works means it returns requested data.
However when I add below line of code to add userId to session, it creates a cookie but I can't fetch data (userId and userName):
req.session!.userId = user.id;
I receive information i can't fetch data, please check your connection.
I am not sure when I am making mistake, any help appreciated.
Below I provide my setup:
index.ts
import "reflect-metadata"
import { MikroORM } from "#mikro-orm/core";
import { __prod__ } from "./constants";
import microConfig from "./mikro-orm.config";
import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import { buildSchema } from 'type-graphql';
import { HelloResolver } from "./resolvers/hello";
import { PostResolver } from "./resolvers/post";
import { UserResolver } from "./resolvers/user";
import session from 'express-session';
import IORedis from "ioredis";
import { MyContext } from "./types";
import { ApolloServerPluginLandingPageGraphQLPlayground } from "apollo-server-core";
const main = async () => {
//console.log("dirname: ", __dirname);
// connect db
const orm = await MikroORM.init(microConfig);
// run migrations
await orm.getMigrator().up();
const app = express();
// importing connect redis causing issue with session object
const RedisStore = require('connect-redis')(session)
//const RedisStore = connectRedis(session);
//const redisClient = redis.createClient();
const redisClient: IORedis.Cluster = new IORedis.Cluster([])
// https://github.com/tj/connect-redis/issues/300
// installed ioredis
app.use(
session({
name: "rjsession",
store: new RedisStore({
client: redisClient,
disableTouch: true,
}),
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 365 * 10, // 10 years
httpOnly: true,
sameSite: 'lax',
//secure: __prod__, //cookie works only in https, on dev good to run off
secure: false,
},
saveUninitialized: false,
secret: 'keyboard catadasdasdsdaasdadas',
resave: false,
})
)
const apolloServer = new ApolloServer({
schema: await buildSchema( {
resolvers: [HelloResolver, PostResolver, UserResolver],
validate: false,
}),
context: ({ req, res }): MyContext => ({ em: orm.em, req, res }),
// added plugin to force apollo server to use old graphql playground
plugins: [
ApolloServerPluginLandingPageGraphQLPlayground({
// options
})
],
});
await apolloServer.start();
apolloServer.applyMiddleware({
app,
cors: { credentials: true, origin: ["http://localhost:4000/"] },
});
app.listen(4000, () => {
console.log("server started on localhost: 4000")
})
app.get('/', (_, res) => {
res.send("hello");
})
};
// catch error and print it
main().catch(err => {
console.log(err);
});
login mutation definition:
#Mutation(() => UserResponse)
async login(
#Arg('options') options: UsernamePasswordInput,
#Ctx() { em, req }: MyContext
): Promise<UserResponse> {
// check if user registered
const user = await em.findOne(User, { username: options.username });
// if user null return password
if (!user) {
return {
errors: [
{
field: "username",
message: "User does not exist.",
},
],
};
}
// validate password against user input
const valid = await argon2.verify(user.password, options.password);
// return error if password failed validation
if (!valid){
return {
errors: [
{
field: "password",
message: "password incorrect.",
},
],
};
}
// asign user id in cookies
req.session!.userId = user.id;
console.log(req.session!.userId)
//req.session!.userId = user.id;
// if no errors return user
return {
user,
};
}
types.ts file:
import { EntityManager, IDatabaseDriver, Connection } from "#mikro-orm/core";
import { Response, Request} from 'express'
//import { Request } from 'express-serve-static-core'
export type MyContext = {
em: EntityManager<any> & EntityManager<IDatabaseDriver<Connection>>
//req: Request & {session: Express.Session} & { userId: number };
//req: Request & {session: Session};
req: Request; // & {session: Express.Session};
res: Response;
}
Based on your current code, you are using req.session!.userId only at one place, and you use you are getting error. That makes me think that you have an issue with redis.
When you login, your server is going to generate a session id which is stored on the Redis store. Since your redis is not running on your machine, you cannot store it so you are getting error.
Before you start your application, start the redis server first.

Express-session does not set cookie?

I'm following along with Ben Awad's 13-hour Fullstack React GraphQL TypeScript Tutorial and encountered a wall during the login cookie setting (aprx at 1:50:00).
I think I successfully connected to redis, set express-session and set req type but in graphql sandbox I don't see my cookie (named 'qid') at Inspect->Application.
index.ts
import { MikroORM } from "#mikro-orm/core";
import { __prod__ } from "./constants";
import microConfig from "./mikro-orm.config";
import express from "express";
import { ApolloServer } from "apollo-server-express";
import { buildSchema } from "type-graphql";
import { HelloResolver } from "./resolvers/hello";
import { PostResolver } from "./resolvers/post";
import { UserResolver } from "./resolvers/user";
import redis from "redis";
import session from "express-session";
import connectRedis from "connect-redis";
const main = async () => {
const orm = await MikroORM.init(microConfig);
await orm.getMigrator().up();
const app = express();
const RedisStore = connectRedis(session);
const redisClient = redis.createClient();
app.use(
session({
name: "qid",
store: new RedisStore({
client: redisClient,
disableTouch: true,
}),
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 365 * 10,
httpOnly: true,
sameSite: "none",
// secure: __prod__,
},
saveUninitialized: false,
secret: "dfhfdjkgfkbjktzkzf",
resave: false,
})
);
app.use(function (req, res, next) {
res.header(
"Access-Control-Allow-Origin",
"https://studio.apollographql.com"
);
res.header("Access-Control-Allow-Credentials", "true");
next();
});
const apolloServer = new ApolloServer({
schema: await buildSchema({
resolvers: [HelloResolver, PostResolver, UserResolver],
validate: false,
}),
context: ({ req, res }) => ({ em: orm.em, req, res }),
});
await apolloServer.start();
apolloServer.applyMiddleware({
app,
cors: {
credentials: true,
origin: new RegExp("/*/"),
},
});
app.listen(4000, () => {
console.log("server started on port 4000");
});
};
main();
types.ts
import { EntityManager, IDatabaseDriver, Connection } from "#mikro-orm/core";
import { Request, Response } from "express";
import { Session, SessionData } from "express-session";
export type MyContext = {
em: EntityManager<any> & EntityManager<IDatabaseDriver<Connection>>;
req: Request & {
session: Session & Partial<SessionData> & { userId: number };
};
res: Response;
};
and my userResolver (user.ts)
import { User } from "../entities/User";
import { MyContext } from "../types";
import {
Arg,
Ctx,
Field,
InputType,
Mutation,
ObjectType,
Query,
Resolver,
} from "type-graphql";
import argon2 from "argon2";
#InputType()
class UsernamePasswordInput {
#Field()
username: string;
#Field()
password: string;
}
#ObjectType()
class FieldError {
#Field()
field: string;
#Field()
message: string;
}
#ObjectType()
class UserResponse {
#Field(() => [FieldError], { nullable: true })
errors?: FieldError[];
#Field(() => User, { nullable: true })
user?: User;
}
#Resolver()
export class UserResolver {
#Mutation(() => UserResponse)
async login(
#Arg("options", () => UsernamePasswordInput) options: UsernamePasswordInput,
#Ctx() { em, req }: MyContext
): Promise<UserResponse> {
const user = await em.findOne(User, { username: options.username });
if (!user) {
return {
errors: [
{
field: "username",
message: "username does not exist",
},
],
};
}
const valid = await argon2.verify(user.password, options.password);
if (!valid) {
return {
errors: [
{
field: "password",
message: "incorrect password",
},
],
};
}
req.session.userId = user.id;
return {
user,
};
}
}
I tried setting up res.headers as graphql sandbox is asking but still to no avail. Would appreciate any help, thank you!
Okay I'm not sure what is happening, but I seemingly solved the issue.
My idea is that: GraphQL Playground is retired and localhost:port/graphql now redirects to Apollo GraphQL Sandbox to a different url and my guess is that the cookies do not get transfered to this location but the cookie is set at localhost.
So there is a way you can force Apollo to still use the Playground by adding:
import { ApolloServerPluginLandingPageGraphQLPlayground } from "apollo-server-core";
const apolloServer = new ApolloServer({
...,
plugins: [
ApolloServerPluginLandingPageGraphQLPlayground({
// options
}),
],
});
And this way Playground shows up and you can set
"request.credentials": "include",
in the settings and voila the cookie shows up at localhost:port.
I hope this helps anyone with this issue - however I'm still not exactly sure that this is a right solution.
Adding the old playground as a plugin probably works but, since they say it is being deprecated, if you want to make it work with the new Apollo Studio, here is how I managed to do it:
I added these three lines right after initializing the app:
app.set("trust proxy", !process.env.NODE_ENV === "production");
app.set("Access-Control-Allow-Origin", "https://studio.apollographql.com");
app.set("Access-Control-Allow-Credentials", true);
Here is how the configuration of my session looks like:
const RedisStore = connectRedis(session);
const redisClient = redis.createClient();
app.use(
session({
saveUninitialized: false,
store: new RedisStore({ client: redisClient }),
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 365 * 1, // 1 year
httpOnly: true,
sameSite: "none",
secure: true, // if true, studio works, postman doesn't; if false its the other way around
},
name: "qid",
secret: "keyboard cat",
resave: false,
}),
);
Then, over to Apollo Studio, go to Connection Settings -> Edit -> Include Cookies (this one was really hard to find):
Make sure to send this header with every request to login: x-forwarded-proto: https
spent some time on this one. Try this combined solution:
import { MikroORM } from "#mikro-orm/core";
import { __prod__ } from "./constants";
import microConfig from "./mikro-orm.config";
import express from "express";
import { ApolloServer } from "apollo-server-express";
import { buildSchema } from "type-graphql";
import { PostResolver } from "./resolvers/Post";
import { UserResolver } from "./resolvers/User";
import session from "express-session";
import connectRedis from "connect-redis";
import { createClient } from "redis";
const main = async () => {
try {
const orm = await MikroORM.init(microConfig);
orm.getMigrator().up();
const app = express();
app.set("trust proxy", process.env.NODE_ENV !== "production"); //a little fix here from another users codes--- actually think this made it works
app.set("Access-Control-Allow-Origin", "https://studio.apollographql.com");
app.set("Access-Control-Allow-Credentials", true);
let redisClient = createClient({ legacyMode: true });
redisClient.connect().catch(console.error);
let RedisStore = connectRedis(session);
const cors = {
credentials: true,
origin: "https://studio.apollographql.com",
};
app.use(
session({
name: "qid",
store: new RedisStore({ client: redisClient as any, disableTTL: true }),
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 365 * 10,
httpOnly: true,
secure: true,
sameSite: "none",
},
saveUninitialized: false,
secret: "keyboardcaxt",
resave: false,
})
);
const apolloServer = new ApolloServer({
schema: await buildSchema({
resolvers: [PostResolver, UserResolver],
validate: false,
}),
context: ({ req, res }) => ({ em: orm.em, req, res }),
});
await apolloServer.start();
apolloServer.applyMiddleware({ app, cors });
app.listen(4000, () => {
console.log("EXPRESS SERVER IS RUNNINGG");
});
} catch (error) {
console.log(error, "ERRR");
}
};
main();
Also dont forget to this
setup and hard-reset your https://studio.apollographql.com/sandbox/.
And this: add ENV to your root
Then you should be ready to go.

why req.logIn () doesn't store data when using angular?

I am working on angular and nodejs and their libraries, passport, passport-local, express, express-session. the req.logIn () does not save the data to the session, I tried the rest api with postman and it works correctly, and it saves the data, what is happening ?. could someone help please thank you in advance?
this is the code that according to passport closes the session:
app.get('/logout', (req, res) => {
req.logout();
res.redirect('logout');
});
add cors handling to be able to communicate with another port and activate the credentials for sending the cookie:
app.use(cors({origin:["http://localhost:4200"],credentials:true}));
const store = new session.MemoryStore; *//session configuration*
app.use(session({
secret:"SECRET",
resave:true,
saveUninitialized:true,
cookie:{
secure: false
},
store:store
}))
app.use(passport.initialize());*//initialize passport and passport sessions*
app.use(passport.session());
the login part is like this:
exports.Login = (req, res, next) => {
passport.authenticate('local', (err, usuario, info) => {
if (err) {
next(err)
}
if (!usuario) {
res.json(info)
}
else {
req.logIn(usuario, (err) => {
if (err) {
next(err)
}
else {
res.json("usuario existente"})
next()
}
})
}
})(req,res, next)
}
you tried with Postman and it worked and it means Express is working fine. so you need to check your Angular side.
because you are working with Session you need to set the correct setting:
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '#angular/common/http';
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs';
#Injectable()
export class SessionInterceptor implements HttpInterceptor {
constructor() {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const newRequest = request.clone({
withCredentials: true
});
return next.handle(newRequest);
}
}
import { HTTP_INTERCEPTORS, HttpClientModule } from '#angular/common/http';
#NgModule({
declarations: [],
imports: [],
providers: [ { provide: HTTP_INTERCEPTORS, useClass: SessionInterceptor, multi: true } ],
bootstrap: [AppComponent]
})
export class AppModule {}
more info:
https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials
Angular 6 httpClient Post with credentials

How to always validate JWT without guards decorator? (Nest.js + passport)

I using nest.js + passport + jwt + graphql in project.
If there is a token, then decoded information,
want to get undefined if don't have token.
Always must have Guards to receive decoded tokens.
Can I selectively generate 401 error?
#Module({
providers: [
JwtStrategy,
],
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret: 'hi',
signOptions: { expiresIn: '1d' },
}),
],
})
export class AuthModule {
}
export class GqlAuthGuard extends AuthGuard('jwt') {
getRequest(context: ExcutionContext) {
const ctx = GqlExecutionContext.create(context)
return ctx.getContext().req
}
}
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: config.jwt.secret,
})
}
validate(payload: JwtPayload): any {
return { user_id: +payload.user_id , username: payload.username }
}
}
#Resolver(() => Test)
export class TestResolver {
#UseGuards(GqlAuthGuard) // I want to get validated users without guards.
#Query(() => Test)
startTest(
#User() user: any, // req.user
) {
console.log(user)
}
}
I don't know if it's possible, but I want this code.
app.use((req, res, next) => {
if (req.headers.token) { // optional
try {
req.user = verify(req.headers.token)
} catch (err) {
req.user = undefined
}
}
next()
})
app.get((req, res, next) => {
if (req.user) {
...
}
})
You can apply global guards like so
async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule);
const reflector = app.get(Reflector);
app.useGlobalGuards(new GqlAuthGuard(reflector));
await app.listen(process.env.PORT);
}

Resources