Has anyone succesfully implemented an apollo express server (as the gateway) together with a federation of graphQL servers?
My means of authentication is present via a REST endpoint on my gateway so I am running an apollo-express server as a top layer, but when trying to query the data it doesn't return anything from the other services. When I switch to having the gateway operate as purely an Apollo server, then it functions and can get data from the services belonging to the apollo federation connected to it. But this means I have to disregard my implementation of authentication and other REST endpoints I am using with the service.
Current setup:
const main = async () => {
try {
const app = express();
app.use(function(_req, res, next) {
res.header("Access-Control-Allow-Origin", `${config.frontend_url}`);
res.header('Access-Control-Allow-Methods', "GET,HEAD,OPTIONS,POST,PUT");
res.header("Access-Control-Allow-Credentials", "true");
res.header('Access-Control-Allow-Headers', "Access-Control-Allow-Origin, Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers, Authorization");
next();
});
app.use(cookieParser());
app.use(cookieSession({
maxAge: 24 * 60 * 60 * 1000,
keys: [config.cookieKey]
}));
app.use('/auth', loginRouter);
const serviceList = [
{name: "service", url: config.service_url + "/graphql"},
{name: "service", url: config.service_url + "/graphql"},
]
const gateway = new ApolloGateway({
serviceList,
});
const { schema, executor } = await gateway.load();
// const schema = await createSchema();
app.use(
'/graphql',
checkToken,
graphqlHTTP(req => ({
schema,
graphiql: true,
context: req
}))
);
app.use('/', ((_req, res) => {
res.sendStatus(200);
}));
const apolloServer = new ApolloServer({ schema, executor, playground: true, tracing: false , subscriptions: false}) as any;
apolloServer.applyMiddleware({ app });
app.listen(config.port, () => {
console.log(`🚀 Server ready at api.backoffice.myos.co:${config.port}${apolloServer.graphqlPath}`)
});
} catch (e) {
Logger.error(">>> >>> >>> createSchema() error:", e);
}
};
Related
So I feel like I've tried everything.
Quick workflow breakdown. I have a React App deployed on Netlify that uses a Rest API backend hosted on Heroku with a Jaws MySQL instance and a Redis session store. There is an authorization workflow that uses a google0auth2.0 strategy and passport.js as well as the Redis to store each separate session which I only started using on the production build as my understanding was that express-sessions alone would cause memory leaks. That was my first step through this rabbit hole.
After some research, I added app.set('trust proxy', 1), added app.use(helmet.hsts()) to address headers issues and I attempted to secure the cookie with the code below:
app.use(
expressSession({
...
proxy: isProdEnv,
cookie: {
secure: isProdEnv
}
When checking the network tab in chrome dev tools, I see the cookie is attached to the callback however, it is not attached to the dashboard react page that the API redirects on the successful authorization.
My API index.js is below:
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const expressSession = require('express-session');
const {
createClient
} = require("redis");
let RedisStore = require('connect-redis')(expressSession);
require('dotenv').config();
const PORT = process.env.PORT || 5050;
const isProdEnv = process.env.NODE_ENV === 'production' ? true : false;
// Knex instance for DB managment
const knex = require('knex')(require('./knexfile.js')[process.env.NODE_ENV || 'development']);
const app = express();
app.use(express.json());
app.use(helmet());
app.use(helmet.hsts());
app.use(
cors({
origin: true,
credentials: true,
}),
);
let redisClient = createClient({
url: process.env.REDIS_URL,
lazyConnect: true,
showFriendlyErrorStack: true,
legacyMode: true,
retry_strategy: (options) => {
const {
error,
total_retry_time,
attempt
} = options;
if (error ? .code === 'ECONNREFUSED' || error ? .code === 'NR_CLOSED') {
return 5000;
}
if (total_retry_time > 1000 * 15) {
return undefined;
}
if (attempt > 10) {
return undefined;
}
return Math.min(options.attempt * 1000, 5000); //in ms
},
});
if (!redisClient.isOpen) {
redisClient.connect().catch(console.error);
console.info('connected to redis at', process.env.REDIS_URL);
}
redisClient.on('error', (err) => {
console.log('ⓘ on error:', err);
});
app.set('trust proxy', 1);
// Include express-session middleware (with additional config options required
// for Passport session)
app.use(
expressSession({
store: new RedisStore({
client: redisClient
}),
secret: process.env.SESSION_SECRET,
proxy: isProdEnv,
resave: false,
saveUninitialized: true,
name: 'lostnfound',
cookie: {
secure: isProdEnv
}
}),
);
// =========== Passport Config ============
// Initialize Passport middleware
app.use(passport.initialize());
app.use(passport.session());
passport.use(
new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK_URL,
scope: ['profile', 'email'],
},
(_accessToken, _refreshToken, profile, done) => {
const id = String(profile.id);
const profileId = Number(id.slice(-18));
// First let's check if we already have this user in our DB
knex('users')
.select('id')
.where({
google_id: profileId
})
.then((user) => {
if (user.length) {
// If user is found, pass the user object to serialize function
done(null, user[0]);
} else {
// If user isn't found, we create a record
knex('users')
.insert({
google_id: profileId,
avatar_url: profile._json.picture,
first_name: profile.name.givenName,
last_name: profile.name.familyName,
email: profile._json.email,
})
.then((userId) => {
// Pass the user object to serialize function
done(null, {
id: userId[0]
});
})
.catch((err) => {
console.log('Error creating a user', err);
});
}
})
.catch((err) => {
console.log('Error fetching a user', err);
});
},
),
);
// `serializeUser` determines which data of the auth user object should be stored in the session
// The data comes from `done` function of the strategy
// The result of the method is attached to the session as `req.session.passport.user = 12345`
passport.serializeUser((user, done) => {
console.log('serializeUser (user object):', user);
// Store only the user id in session
done(null, user.id);
});
// `deserializeUser` receives a value sent from `serializeUser` `done` function
// We can then retrieve full user information from our database using the userId
passport.deserializeUser((userId, done) => {
console.log('deserializeUser (user id):', userId);
// Query user information from the database for currently authenticated user
knex('users')
.where({
id: userId
})
.then((user) => {
// Remember that knex will return an array of records, so we need to get a single record from it
console.log('req.user:', user[0]);
// The full user object will be attached to request object as `req.user`
done(null, user[0]);
})
.catch((err) => {
console.log('Error finding user', err);
});
});
// Additional information on serializeUser and deserializeUser:
// https://stackoverflow.com/questions/27637609/understanding-passport-serialize-deserialize
// Import all route types for server functionality
const authRoutes = require('./routes/auth');
const postRoutes = require('./routes/post');
app.use('/auth', authRoutes);
app.use('/post', postRoutes);
app.listen(PORT, () => {
console.log(`🚀 Server listening on port ${PORT}.`);
});
And this is my google endpoint and google callback:
const express = require('express');
const router = express.Router();
const passport = require('passport');
require('dotenv').config();
const AuthController = require('../controller/auth');
// Create a login endpoint which kickstarts the auth process and takes user to a consent page
router.get('/google', passport.authenticate('google'));
// This is the endpoint that Google will redirect to after user responds on consent page
router.get(
'/google/callback',
passport.authenticate('google', {
failureRedirect: `${process.env.CLIENT_URL}/auth-fail`,
}),
(_req, res) => {
// Successful authentication, redirect to client-side application
res.redirect(`${process.env.CLIENT_URL}/dashboard`);
},
);
I will add, I'm on the free Heroku account and haven't set up any SSL. Is that what is holding me back? is there any third-party free SSL that I can work into the workflow?
It's hosted here if you want to get a first-hand look. Click on the login to google to go through the workflow and checkout the network tab after you're authorized (if you dare, lol)
I have my express server on a different port than my client-side nextjs project.
I know when you have a server on the same port you can use getRequestHandler with next that passes the user object to be accessible with getInitialProps in the client-side.
const express = require("express");
const next = require("next");
const app = next({ dev: true });
const handle = app.getRequestHandler();
app.prepare().then(() => {
const server = express();
// adds passport session
require("./middlewares").init(server);
const apolloServer = require("./graphql").createApolloServer();
apolloServer.applyMiddleware({ app: server });
server.all("*", (req, res) => {
return handle(req, res);
});
server.listen(port, (err) => {
if (err) throw err;
});
});
My passport implementation is as follows
const config = require("../config");
const session = require("express-session");
const passport = require("passport");
exports.init = (server, db) => {
require("./passport").init(passport);
const sess = {
name: "pid",
secret: config.SESSION_SECRET,
cookie: { maxAge: 2 * 60 * 60 * 1000 },
resave: false,
saveUninitialized: false,
store: db.initSessionStore(),
};
if (process.env.NODE_ENV === "production") {
server.set("trust proxy", 1);
sess.cookie.secure = true;
sess.cookie.httpOnly = true;
sess.cookie.sameSite = 'none';
sess.cookie.domain = process.env.DOMAIN;
}
server.use(session(sess));
server.use(passport.initialize());
server.use(passport.session());
};
And running the following on the express server, I can see req.user returning the user object.
app.use((req, res, next) => {
console.log(req.user);
next();
});
In a page in my nextjs app, in getInitialProps req.user is undefined
Home.getInitialProps = async (ctx) => {
const { req } = ctx;
const { user } = req;
console.log(user);
..........
};
Is there a way to either access the passport user object via SSR in nextjs or a different method to authorize and user on a page?
I do have a Github Repo with instructions on how to run the app in the README.md
Passport auth doesn't seems work across port. The solution is put a ngnix in front.
Local passport authorization on different ports
After adding authentication to our backend Graphql server the "Schema" and "Docs" are no longer visible in the Graphql Playground. Executing queries when adding a token to the "HTTP HEADERS" in the Playground does work correctly when authenticated and not when a user isn't authenticated, so that's ok.
We disabled the built-in Playground from Apollo-server and used the middleware graphql-playground-middleware-express to be able to use a different URL and bypass authentication. We can now browse to the Playground and use it but we can't read the "Schema" or "Docs" there.
Trying to enable introspection didn't fix this. Would it be better to call passport.authenticate() in the Context of apollo-server? There's also a tool called passport-graphql but it works with local strategy and might not solve the problem. I've also tried setting the token in the header before calling the Playground route, but that didn't work.
We're a bit lost at this. Thank you for any insights you could give us.
The relevant code:
// index/ts
import passport from 'passport'
import expressPlayground from 'graphql-playground-middleware-express'
const app = express()
app.use(cors({ origin: true }))
app.get('/playground', expressPlayground({ endpoint: '/graphql' }))
app.use(passport.initialize())
passport.use(bearerStrategy)
app.use(
passport.authenticate('oauth-bearer', { session: false }),
(req, _res, next) => { next() }
)
;(async () => {
await createConnections()
const server = await new ApolloServer({
schema: await getSchema(),
context: ({ req }) => ({ getUser: () => req.user, }),
introspection: false,
playground: false,
})
server.applyMiddleware({ app, cors: false })
app.listen({ port: ENVIRONMENT.port }, () => { console.log(`Server ready`) })
})()
// passport.ts
import { IBearerStrategyOptionWithRequest, BearerStrategy, ITokenPayload } from passport-azure-ad'
import { Account } from '#it-portal/entity/Account'
export const bearerStrategy = new BearerStrategy( config,
async (token: ITokenPayload, done: CallableFunction) => {
try {
if (!token.oid) throw 'token oid missing'
const knownAccount = await Account.findOne({ accountIdentifier: token.oid })
if (knownAccount) return done(null, knownAccount, token)
const account = new Account()
account.accountIdentifier = token.oid
account.name = token.name
account.userName = (token as any).preferred_username
const newAccount = await account.save()
return done(null, newAccount, token)
} catch (error) {
console.error(`Failed adding the user to the request object: ${error}`)
}
}
)
I figured it out thanks to this SO answer. The key was not to use passport as middleware on Express but rather use it in the Graphql Context.
In the example code below you can see the Promise getUser, which does the passport authentication, being used in the Context of ApolloServer. This way the Playground can still be reached and the "Schema" end "Docs" are still accessible when run in dev mode.
This is also the preferred way according to the Apollo docs section "Putting user info on the context".
// apollo.ts
passport.use(bearerStrategy)
const getUser = (req: Express.Request, res: Express.Response) =>
new Promise((resolve, reject) => {
passport.authenticate('oauth-bearer', { session: false }, (err, user) => {
if (err) reject(err)
resolve(user)
})(req, res)
})
const playgroundEnabled = ENVIRONMENT.mode !== 'production'
export const getApolloServer = async () => {
return new ApolloServer({
schema,
context: async ({ req, res }) => {
const user = await getUser(req, res)
if (!user) throw new AuthenticationError('No user logged in')
console.log('User found', user)
return { user }
},
introspection: playgroundEnabled,
playground: playgroundEnabled,
})
}
The best thing is that you only need two functions for this to work: passport.use(BearerStrategy) and passport.authenticate(). This is because sessions are not used so we don't need to add it as Express middleware.
// index/ts
const app = express()
app.use(cors({ origin: true }))
;(async () => {
await createConnections()
const server = await getApolloServer()
server.applyMiddleware({ app, cors: false })
app.listen({ port: ENVIRONMENT.port }, () => { console.log(`Server ready`) })
})()
I hope this helps others with the same issues.
I'm new to NUXT and SSR and I've been researching this for a few hours now and I can't seem to figure it out. I'm using JWT to authenticate users in my Nuxt app with a Bearer Token, which is working great until I hit refresh and lose my session.
Now I'm looking to persist sessions using express-session and connect-mongo. I can't get the cookie to set on the client to be included on future requests.
When a user is authenticated:
router.post('/login', function(req, res) {
User.findOne({
username: req.body.username
}, function(err, user) {
if (err) throw err;
if (!user) {
res.status(401).send({success: false, msg: 'Authentication failed. User not found.'});
} else {
// check if password matches
user.comparePassword(req.body.password, function (err, isMatch) {
if (isMatch && !err) {
// if user is found and password is right create a token
var token = jwt.sign(user.toJSON(), config.secret, { expiresIn: 604800 });
req.session.authUser = { 'user': 'Test User' }
return res.json({success: true, token: token, user: user});
} else {
res.status(401).send({success: false, msg: 'Authentication failed. Wrong password.'});
}
});
}
The console.log above shows the authUser in the session.
Session {
cookie:
{ path: '/',
_expires: 2018-04-03T18:13:53.209Z,
originalMaxAge: 60000,
httpOnly: true },
authUser: { user: 'Test User' } }
When I look at my chrome devtools application cookies a connect.ssid hasn't been set and when I console.log(req.session) on future requests the authUser is missing.
My server code is:
// Passport
var passport = require('passport');
var passportJWT = require("passport-jwt");
var ExtractJwt = passportJWT.ExtractJwt;
var JwtStrategy = passportJWT.Strategy;
// Config File
let config = require('./config/settings.js')
// Initialize Express
var app = express();
// CORS-ENABLE
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "http://127.0.0.1:1337");
res.header("Access-Control-Allow-Credentials", 'true');
next();
});
app.use(cors())
const dbPath = 'mongodb://blogUser:blogUserPassword#localhost:27017/blog'
// Express Session
app.use(session({
secret: 'super-secret-key',
resave: false,
saveUninitialized: false,
store: new MongoStore({ url: dbPath }),
cookie: { maxAge: 60000 }
}))
// File Upload
app.use(fileUpload());
// view engine setup
// app.set('views', path.join(__dirname, 'views'));
// app.set('view engine', 'jade');
// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
// Routes
var index = require('./routes/index');
var users = require('./routes/users');
app.use('/api', index);
app.use('/users', users);
// Passport Config
app.use(passport.initialize());
app.use(passport.session())
// mongoose
const options = {
autoIndex: true, // Don't build indexes
reconnectTries: Number.MAX_VALUE, // Never stop trying to reconnect
reconnectInterval: 500, // Reconnect every 500ms
poolSize: 10, // Maintain up to 10 socket connections
// If not connected, return errors immediately rather than waiting for reconnect
bufferMaxEntries: 0
};
console.log(options);
// Localhost Connect
mongoose.connect(dbPath, options).then(
() => { console.log("connected !!!"); },
err => { console.log(err); }
);
Any and all help is appreciated.
If you want to use the server you create the problem with the session is the express router, because change res and req vars so like recommend in nuxt use this.
const express = require('express');
// Create express router
const router = express.Router()
// Transform req & res to have the same API as express
const app = express()
router.use((req, res, next) => {
Object.setPrototypeOf(req, app.request)
Object.setPrototypeOf(res, app.response)
req.res = res
res.req = req
next()
})
You are missing this step
// Create express router
const router = express.Router()
// Transform req & res to have the same API as express
// So we can use res.status() & res.json()
router.use((req, res, next) => {
Object.setPrototypeOf(req, app.request)
Object.setPrototypeOf(res, app.response)
req.res = res
res.req = req
next()
})
The req, res parameters need to be interchanged on the client side
Once you do router.post('/login') and logout
app.use('/api', router)
And that will work perfectly
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
})));
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);
});
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
}))
);