machineghost
machineghost

Reputation: 35725

How can I authenticate a GraphQL endpoint with Passport?

I have a GraphQL endpoint:

app.use('/graphql', graphqlHTTP(request => ({
  graphiql: true,
  schema
})));

I also have a Passport route for logging in (and handling the callback, since I'm using Google OAuth2):

this.app.get('/login', passport.authenticate('google'));
this.app.get('/auth/callback/google', ....

Passport add a user to the request, and all of the articles I can find online recommend authenticating in each of my GraphQL resolvers using that:

resolve: (root, args, { user }) => {
  if (!user) throw new NotLoggedInError();

However it doesn't make sense to have to add that logic to every resolver when it applies to all of them, so I was hoping to somehow authenticate the entire endpoint.

The problem is that I'm not sure how to combine middleware. I tried the following but it just broke the endpoint:

app.use('/graphql',  passport.authenticate('google'), graphqlHTTP(request => ({
  graphiql: true,
  schema
})));

Upvotes: 3

Views: 3575

Answers (2)

vbranden
vbranden

Reputation: 5996

I have the following working. Some issues I had were around making sure my google API was enabled and the proper scopes were enabled. I am also only using the passport middleware on the auth endpoints and using an isAuthenticated middleware to check if the session is authenticated and if not redirect to the auth endpoint. also putting the request object into the context so that it can be used by the resolver to potentially authorize the user. You would of course need to update the user lookup as I am just passing mock data.

import express from "express";
import graphqlHTTP from "express-graphql";
import passport from "passport";
import cookieParser from "cookie-parser";
import session from "express-session";
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
import { buildSchema } from "graphql";

const PORT = 5000;

const data = [
  { id: "1", name: "foo1" },
  { id: "2", name: "foo2" },
  { id: "3", name: "foo3" },
];

const def = `
type Foo {
  id: String!
  name: String
}
type Query {
  readFoo(id: String!): Foo
}
schema {
  query: Query
}
`;
const schema = buildSchema(def);
const fieldMap = schema.getType("Query").getFields();
fieldMap.readFoo.resolve = (source, args) => {
  return data.filter(({ id }) => id === args.id)[0] || null;
};

passport.serializeUser((user, done) => {
  done(null, user);
});

passport.deserializeUser((obj, done) => {
  done(null, obj);
});

passport.use(
  new GoogleStrategy(
    {
      clientID: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      callbackURL: `http://localhost:${PORT}/auth/google/callback`,
    },
    (accessToken, refreshToken, profile, cb) => {
      return cb(null, {
        id: "1",
        username: "foo@bar.baz",
        googleId: profile.id,
      });
    }
  )
);

function isAuthenticated(req, res, next) {
  return req.isAuthenticated() ? next() : res.redirect("/auth/google");
}

const app = express();
app.use(cookieParser());
app.use(
  session({
    secret: "sauce",
    resave: false,
    saveUninitialized: false,
  })
);
app.use(passport.initialize());
app.use(passport.session());

app.get("/auth/fail", (req, res) => {
  res.json({ loginFailed: true });
});

app.get(
  "/auth/google",
  passport.authenticate("google", { scope: ["profile"] })
);

app.get(
  "/auth/google/callback",
  passport.authenticate("google", { failureRedirect: "/auth/fail" }),
  (req, res) => {
    res.redirect("/graphql");
  }
);

app.use(
  "/graphql",
  isAuthenticated,
  graphqlHTTP((req) => ({
    schema,
    graphiql: true,
    context: req,
  }))
);

app.listen(PORT, () => {
  console.log("Started local graphql server on port ", PORT);
});


Upvotes: 6

machineghost
machineghost

Reputation: 35725

vbranden's answer was excellent, and it is the basis of this answer. However, his answer has a lot of other code which obfuscates the solution a bit. I didn't want to mess with it, since it offers a more complete view of things, but hopefully this answer will be helpful in its own way by being more direct. But again, all credit for this solution belongs to vbranden (please upvote his answer accordingly).

If you make an isAuthenticated function with the appropriate signature (request, response, next) you can then "chain" that function in when you setup your GraphQL endpoint:

function isAuthenticated(req, res, next) {
  return req.isAuthenticated() ?
    next() :
    res.redirect('/auth/google');
}

app.use(
  '/graphql',
  isAuthenticated,
  graphqlHTTP(req => ({
    schema,
    graphiql: true,
    context: req
  }))
);

Upvotes: 1

Related Questions