apollographql / apollo-server

🌍  Spec-compliant and production ready JavaScript GraphQL server that lets you develop in a schema-first way. Built for Express, Connect, Hapi, Koa, and more.
https://www.apollographql.com/docs/apollo-server/
MIT License
13.77k stars 2.03k forks source link

User is undefined in context after authenticate with Passport #1487

Closed HBCharles closed 6 years ago

HBCharles commented 6 years ago

I'm trying to access user in Apollo Server 2 context but got undefined. The req.user is correctly defined in the express middleware.

You can find a minimal reproduction here

server.js

import { ApolloServer, gql } from 'apollo-server-express'

import { typeDefs, resolvers } from './api/schema'
import express from 'express'
import session from 'express-session'
import bodyParser from "body-parser"
import passport from 'passport'
import './api/passport'

const app = new express()

app.use(express.static("public"));
app.use(session({
    secret: "cats",
    resave: true,
    saveUninitialized: false
}));
app.use(bodyParser.urlencoded({ extended: false }));
app.use(passport.initialize());
app.use(passport.session());

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req }) => {
        // req.user is undefined
    console.log("req:", req.user)
  }
});

app.post('/login',
  passport.authenticate('local', { session: true }),
  function(req, res) {
        // req.user defined
    console.log('in login:', req.user)
    res.redirect('/ok');
    }
);

app.get('/ok',
  function(req, res) {
        // req.user defined
    console.log('in ok:', req.user)
    res.json(req.user)
    }
);

server.applyMiddleware({ app });

app.listen({ port: 3000 }, () =>
  console.log(`🚀 Server ready at http://localhost:3000${server.graphqlPath}`)
)

passport.js

import passport from 'passport'
import { Strategy as LocalStrategy } from 'passport-local'
import User from './db'

passport.serializeUser(function(user, done) {
  console.log("=============================SerializeUser called")
  done(null, user.id);
});

passport.deserializeUser(function(id, done) {
  console.log("=============================DeserializeUser called on: ", id)
  User.findById(id, function(err, user) {
    done(err, user);
  });
});

passport.use('local', new LocalStrategy(
  function(username, password, done) {
    User.findOne({ username: username }, function (err, user) {
      if (err) { return done(err); }
      if (!user) {
        return done(null, false, { message: 'Incorrect username.' });
      }
      if (!user.validPassword(password)) {
        return done(null, false, { message: 'Incorrect password.' });
      }
      return done(null, user);
    });
  }
));
tux-tn commented 6 years ago

It looks like you are not calling the passport.authenticate() middleware in your /graphql route

HBCharles commented 6 years ago

Could you show me where to add the middleware ? Do I need to modify the applyMiddleware function ?

tux-tn commented 6 years ago

You just need to call passport middleware before server.applyMiddleware.

app.post('/login',
  passport.authenticate('local', { session: true }),
  function(req, res) {
        // req.user defined
    console.log('in login:', req.user)
    res.redirect('/ok');
    }
);

app.get('/ok',
  function(req, res) {
        // req.user defined
    console.log('in ok:', req.user)
    res.json(req.user)
    }
);
// default endpoint path is /graphql 
app.use('/graphql',  passport.authenticate('local', { session: true }));

server.applyMiddleware({ app });

app.listen({ port: 3000 }, () =>
  console.log(`🚀 Server ready at http://localhost:3000${server.graphqlPath}`)
)
HBCharles commented 6 years ago

It looks like the graphql server is not running anymore when I add the middleware. I got a 400 error code.

bmfs commented 6 years ago

@HBCharles did you solve this?

bmfs commented 6 years ago

Faced similar problem, but in the end the issue was my lack of understand on how passportjs lib works. Had to customize passport.authenticate callback function to bypass the authentication error and manually set the req.user value

rosshadden commented 6 years ago

I don't understand the significance of this line:

app.use('/graphql',  passport.authenticate('local', { session: true }));

No examples I have seen explicitly add any passport auth middlewares to the /graphql endpoint. Also what if you have many passport providers, like Steam and Google? Do you need to add all of them as middleware to the /graphql endpoint?

This might be what is causing the issue I just posted (#1657), but I'm confused because I have never seen examples (neither official nor unofficial) that explicitly mount middleware over gql.

rosshadden commented 6 years ago

Furthermore, if I add a middleware on /graphql like you suggest @tux-tn, it sends me to auth with the provider (Steam in the case I tested), which sends me back to my loginUrl, and then when I go back to /graphql it does the same thing. Over and over. So I don't see how mounting middleware like that is supposed to help.

bakhaa commented 5 years ago

If you testing on playground, u need aded playground: { settings: { 'request.credentials': 'include', }, }, in ApolloServer options. Example use passport-local & graphQL you can see here https://github.com/bakhaa/react-express/blob/master/api/resolvers/user.js

cadenzah commented 5 years ago

On the extension from @bakhaa 's solution, if you are using apollo-link-http in frontend, you should add credentials option with 'include' to attach your session cookie on your request to GraphQL server, otherwise there will be nothing with passport on req.session, including req.user.

const httpLink = createHttpLink({
  uri: `${API_URL}/graphql`,
  credentials: 'include'
})
bionicles commented 4 years ago

please improve documentation for passport / oauth / session with context in apollo server

josuevalrob commented 4 years ago

I am having the same issue...

This is my GraphQL local strategy

passport.use(
    new GraphQLLocalStrategy((email, password, next) => {
        console.log(`🎫  ${JSON.stringify(User)} 🚔  👮‍♂`)
        User.findOne({ email })
            .then(user => !user
                ? next(null, false, 'Invalid email or password')
                : user.checkPassword(password) //bcrypt
                        .then(match => !match
                            ? next(null, false, 'Invalid email or password')
                            : next(null, user)
                        )
            )
            .catch(error => next(error))
    }),
);

and the apolloServer configuration:

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req, res }) => buildContext({ req, res, User }),
  playground: {
    settings: {
      'request.credentials': 'same-origin',
    },
  },
})

Where do I have to apply the passport.authenticate(?) @tux-tn Did you find a solution @HBCharles