How to use subscriptions-transport-ws with passport and express-session - passport.js

I'm using a passport local strategy that works well with express:
passport.use(localStrategy);
passport.serializeUser((user, done) => done(null, JSON.stringify(user)));
passport.deserializeUser((obj, done) => done(null, JSON.parse(obj)));
app.use(passport.initialize());
app.use(passport.session());
This localStrategy is doing a Mongoose call to get the user based on his pubKey and I guess that request.user is populated by this way.
I setup my graphql endpoint like this:
app.use('/graphql', bodyParser.json(), graphqlExpress(request => ({
debug: true,
schema,
context: {
user: request.user,
req: request,
},
formatError: (e) => {
console.log(JSON.stringify(e, null, 2));
return e;
},
})));
And my subscriptions this way:
const ws = createServer(app);
// Run the server
ws.listen(settings.APP_PORT, () => {
console.log(`App listening on port ${settings.APP_PORT}!`);
// Set up the WebSocket for handling GraphQL subscriptions
new SubscriptionServer({
execute,
subscribe,
schema,
onConnect: (connectionParams, webSocket) => {
console.log(webSocket.upgradeReq);
return { user: connectionParams };
},
}, {
server: ws,
path: '/subscriptions',
});
});
My session is working well on graphql queries and mutations. But not with my subscriptions.
My goal is to have access to my user session in my subscription resolver context. I may need to access something like request.user in onConnect to populate the context, but I don't know how to do.

So after some tinkering I've figured out that what you need to do is basically run the everything that recreates the passport for the socket's upgradeReq by re-running the middleware:
// Start with storing the middleware in constants
const passportInit = passport.initialize();
const passportSession = passport.session();
app.use(passportInit);
app.use(passportSession);
...
// Now populate the request with the upgradeReq using those constants
onConnect: (connectionParams, webSocket) => new Promise((resolve) => {
passportInit(webSocket.upgradeReq, {}, () => {
passportSession(webSocket.upgradeReq, {}, () => {
resolve(webSocket.upgradeReq);
});
});
}),
If you are using an express-session you need to add that to the above.

For graphql-ws it works in similar fashion to #Max Gordon answer, just different values must be passed to the calls. Attaching code snippet for expressSession + passport usage. If you don't need express session simply remove this call from onConnect callback.
const sessionInstance = expressSession({
// options
});
app.use(sessionInstance);
const passportInitialize = passport.initialize();
const passportSession = passport.session();
app.use(passportInitialize);
app.use(passportSession);
const serverCleanup = useServer(
{
schema,
context: async (ctx) => {
return {
user: ctx.extra.request.user
};
},
onConnect: async (ctx) =>
new Promise((resolve) => {
// Is stored sessionMiddleware() from above
sessionInstance(ctx.extra.request, {}, () => {
// Is stored passport.initialize() from above
passportInitialize(ctx.extra.request, {}, () => {
// Is stored passport.session() from above
passportSession(ctx.extra.request, {}, () => {
resolve(true); // always allow connecting (or check if user is in request already and deny if not, depends what you want to achieve)
});
});
});
}),
},
wsServer,
);

Take a look at graphql-passport. It essentially does the same thing that Max mentions in his answer, but in a simplified package.
Note that this currently works for subscriptions-transport-ws, which is now considered deprecated. I have, as of yet, not found a solution while using the newer replacement, graphql-ws, as that works in a different way to the legacy socket system, and doesn't seem to have access to any kind of request object.

Related

How to make through Google's IAP in Cypress?

I come with a question because I have an application, it has IAP as a proxy before seeing the rest of the page. I want to test this page, however, IAP is standing in my way. I'm trying to wade through the docs, but I don't understand which way of authentication I may need to use.
I tried with Application Default Credentials as I have that JSON auth file and env pointing at that, and after going to the site, I get a 403 that I can't access the site. Maybe I'm doing something wrong in general?
cypress.config.ts
import { GoogleAuth } from 'google-auth-library';
const GOOGLE_PROJECT_ID = 1234567;
async setupNodeEvents(on, config): Promise<Cypress.PluginConfigOptions> {
// ... other functions, working with async/await
on('task', {
getIAPToken: async () => {
console.log('🔥 Initialized GoogleAuth');
const auth = new GoogleAuth({
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
});
const client = await auth.getClient();
const projectId = GOOGLE_PROJECT_ID;
const iapUrl = `https://iap.googleapis.com/projects/${projectId}/iap_web/auth`;
console.log('🔥 Requesting IAP');
const response = await client.request({ url: iapUrl });
return response;
},
});
return config;
},
test.cy.ts
describe('Smoke tests', () => {
it('content is not visible', () => {
cy.visit("https://some-page-behind-iap.com");
cy.contains("Content!").should(content => expect(content).to.not.be.visible;
});
it('content is visible', () => {
cy.visit("https://some-page-behind-iap.com");
cy.task('getIAPToken').then((data) => {
console.log("loaded:", data);
});
cy.contains("Content!").should(content => expect(content).to.be.visible;
});
});

Secure a GraphQL API with passport + JWT's or sessions? (with example)

To give a bit of context: I am writing an API to serve a internal CMS in React that requires Google login and a React Native app that should support SMS, email and Apple login, I am stuck on what way of authentication would be the best, I currently have an example auth flow below where a team member signs in using Google, a refresh token gets sent in a httpOnly cookie and is stored in a variable in the client, then the token can be exchanged for an accessToken, the refresh token in the cookie also has a tokenVersion which is checked before sending an accessToken which does add some extra load to the database but can be incremented if somebody got their account stolen, before any GraphQL queries / mutations are allowed, the user's token is decoded and added to the GraphQL context so I can check the roles using graphql-shield and access the user for db operations in my queries / mutations if needed
Because I am still hitting the database even if it's only one once on page / app load I wonder if this is a good approach or if I would be better off using sessions instead
// index.ts
import "./passport"
const main = () => {
const server = fastify({ logger })
const prisma = new PrismaClient()
const apolloServer = new ApolloServer({
schema: applyMiddleware(schema, permissions),
context: (request: Omit<Context, "prisma">) => ({ ...request, prisma }),
tracing: __DEV__,
})
server.register(fastifyCookie)
server.register(apolloServer.createHandler())
server.register(fastifyPassport.initialize())
server.get(
"/auth/google",
{
preValidation: fastifyPassport.authenticate("google", {
scope: ["profile", "email"],
session: false,
}),
},
// eslint-disable-next-line #typescript-eslint/no-empty-function
async () => {}
)
server.get(
"/auth/google/callback",
{
preValidation: fastifyPassport.authorize("google", { session: false }),
},
async (request, reply) => {
// Store user in database
// const user = existingOrCreatedUser
// sendRefreshToken(user, reply) < send httpOnly cookie to client
// const accessToken = createAccessToken(user)
// reply.send({ accessToken, user }) < send accessToken
}
)
server.get("/refresh_token", async (request, reply) => {
const token = request.cookies.fid
if (!token) {
return reply.send({ accessToken: "" })
}
let payload
try {
payload = verify(token, secret)
} catch {
return reply.send({ accessToken: "" })
}
const user = await prisma.user.findUnique({
where: { id: payload.userId },
})
if (!user) {
return reply.send({ accessToken: "" })
}
// Check live tokenVersion against user's one in case it was incremented
if (user.tokenVersion !== payload.tokenVersion) {
return reply.send({ accessToken: "" })
}
sendRefreshToken(user, reply)
return reply.send({ accessToken: createAccessToken(user) })
})
server.listen(port)
}
// passport.ts
import fastifyPassport from "fastify-passport"
import { OAuth2Strategy } from "passport-google-oauth"
fastifyPassport.registerUserSerializer(async (user) => user)
fastifyPassport.registerUserDeserializer(async (user) => user)
fastifyPassport.use(
new OAuth2Strategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: "http://localhost:4000/auth/google/callback",
},
(_accessToken, _refreshToken, profile, done) => done(undefined, profile)
)
)
// permissions/index.ts
import { shield } from "graphql-shield"
import { rules } from "./rules"
export const permissions = shield({
Mutation: {
createOneShopLocation: rules.isAuthenticatedUser,
},
})
// permissions/rules.ts
import { rule } from "graphql-shield"
import { Context } from "../context"
export const rules = {
isAuthenticatedUser: rule()(async (_parent, _args, ctx: Context) => {
const authorization = ctx.request.headers.authorization
if (!authorization) {
return false
}
try {
const token = authorization.replace("Bearer", "")
const payload = verify(token, secret)
// mutative
ctx.payload = payload
return true
} catch {
return false
}
}),
}
To answer your question directly, you want to be using jwts for access and that's it. These jwts should be created tied to a user session, but you don't want to have to manage them. You want a user identity aggregator to do it.
You are better off removing most of the code to handle user login/refresh and use a user identity aggregator. You are running into common problems of the complexity when handling the user auth flow which is why these exist.
The most common is Auth0, but the price and complexity may not match your expectations. I would suggest going through the list and picking the one that best supports your use cases:
Auth0
Okta
Firebase
Cognito
Authress
Or you can check out this article which suggests a bunch of different alternatives as well as what they focus on

How to use authenticate web sockets using node, ws and passport with JWT?

I have a web server written in node/express which uses passport to authenticate users with JSON Web Tokens (JWT).
For regular HTTP methods this is what I use:
app.get('/api/stuff', isLoggedIn(), (req, res) => {
//get stuff
res.send( whatever );
});
Where isLoggedIn is this function:
function isLoggedIn() {
return passport.authenticate('local-jwt', { session: false });
}
The authentication itself is handled by the 'local-jwt' configuration in passport using a JWTStrategy object and it works as expected.
Now I need to add web sockets to this app. I'm using the ws library. Here is what I have so far:
const wss = new WebSocket.Server({
port: 8080,
verifyClient: async (info, done) => {
// ???
done( result );
}
});
wss.on('connection', ws => {
// web socket events
});
How do I use the passport authentication to only allow clients with the correct token to connect to the web socket server?
You can sign the token with the user's id or anything you are using to differentiate users
var today = new Date();
var exp = new Date(today);
exp.setDate(today.getDate() + 60);
jwt.sign({
id: user._id,
exp: parseInt(exp.getTime() / 1000),
}, secret);
then pass the token to the frontend and use it to initialize your Websocket connection with it appended to the url then take and decode it in the server like this
const wss = new WebSocket.Server({
verifyClient: async (info, done) => {
console.log(info.req.url);
console.log('------verify client------');
const token = info.req.url.split('/')[1];
var decoded = jwt.verify(token, secret);
info.req.user = await User.findById(decoded.id).exec();
/*info.req.user is either null or the user and you can destroy the connection
if its null */
done(info.req);
},
server
});
This is one of many ways. There is a way with sessions but I see you don't want to use them. Also I don't know if you use mongo but it's the same for any database you just have to change some code.

JWT authentication in Node.js + express-graphql + passport

I'm writing a fullstack application using MERN and I need to provide authentication using JWT-tokens. My code looks like:
router.use(GraphQLHTTP(
(req: Request, res: Response): Promise<any> => {
return new Promise((resolve, reject) => {
const next = (user: IUser, info = {}) => {
/**
* GraphQL configuration goes here
*/
resolve({
schema,
graphiql: config.get("isDev"), // <- only enable GraphiQL in production
pretty: config.get("isDev"),
context: {
user: user || null,
},
});
};
/**
* Try to authenticate using passport,
* but never block the call from here.
*/
passport.authenticate(['access'], { session: false }, (err, loginOptions) => {
next(loginOptions);
})(req, res, next);
})
}));
I want to provide a new generation of tokens and through GraphQL. In doing so, I need to check whether the user has used the correct method of authentication. For example, to get a new access token, you need a refresh token, you need to log in using the password and e-mail for the refresh token. But using a passport implies that after authentication I will simply have a user.
How should I proceed?

How to pass request headers through to graphql resolvers

I have a graphql endpoint which is authorised by a JWT. My JWT strategy verifies the JWT then adds the user object to the request object.
In a restful route, I would access my users' data like so:
router.get('/', (req, res, next) => {
console.log('user', req.user)
}
I want to access req.user object within my graphql resolver in order to extract the users' ID. However, when I try log the context variable, it is always empty.
Do I need to configure my graphql endpoint to pass through the req data to the resolver?
My app.js has my graphql set up like this:
import { graphqlExpress, graphiqlExpress } from 'apollo-server-express';
app.use('/graphql', [passport.authenticate('jwt', { session: false }), bodyParser.json()], graphqlExpress({ schema }));
Then I have my resolvers like so:
const resolvers = {
Query: {
user: async (obj, {email}, context) => {
console.log('obj', obj) // undefined
console.log('email', email) // currently passed through in graphql query but I want to replace this with the user data passed in req / context
console.log('context', context) // {}
return await UserService.findOne(email)
},
};
// Put together a schema
const schema = makeExecutableSchema({
typeDefs,
resolvers,
});
How can I access my JWT user data in my resolvers?
Apparently you need to pass through context manually like so:
app.use('/graphql', [auth_middleware, bodyParser.json()], (req, res) => graphqlExpress({ schema, context: req.user })(req, res) );
Found the answer here if anybody is interested:

Resources