Testing authenticated route with sinon never goes through to controller - express

I'm trying to unit test a expressjs route that should just check if the response has status with value 200 and has property 'success' set to true.
This response should be returned from the last method in middleware chain: UserController.findOne.
router.get('/:email', ifAuthenticated, UserController.findOne);
The request has to go through passportjs middleware ifAuthenticated for authentication check.
The middleware in question:
export const ifAuthenticated = (req, res, next) => {
if (req.isAuthenticated()) return next();
return ReE(res, { 'message': 'Not authorized' }, 401);
}
The test case with relevant code:
beforeEach(() => {
authenticate = sinon.stub(passport, 'authenticate').returns(() => { });
});
afterEach(() => {
authenticate.restore();
});
describe.only('Find user api tests', () => {
const result = {
"success": true,
"user": {
"id": "58557283-610d-4879-b65b-0125db36d763",
"firstName": "Mac",
"lastName": "Ole",
"email": "test#a.pl",
"phone": 123456789,
}
}
beforeEach(() => {
query = sinon.stub(userController, 'findOne').resolves(result);
authenticate.yields(null, { email: 'test#a.pl' });
sinon.stub(authMiddleware, 'ifAuthenticated').callsFake((req, res, next) => next());
});
afterEach(() => {
query.restore();
authenticate.restore();
});
it('Should return one user', (done) => {
chai.request(server)
.get('/v1/users/test#a.pl')
.end(function (err, res) {
if (err) return done(err);
res.should.have.status(200);
res.body.should.have.property('success').eq(true);
done();
});
})
})
The issue is that no matter what I try, the test always fails and returns from the ifAuthenticated middleware with status 401 even though it is stubbed:
sinon.stub(authMiddleware, 'ifAuthenticated').callsFake((req, res, next) => next());
Test result:
Uncaught AssertionError: expected Response{ domain: null, …(35), …(1) } to have status code 200 but got 401
What I'd like to achieve is for the stubbed middleware to return next() and go through to controller.
It is possible I'm missing something trivial but don't really know anymore where to look, so any advice is much appreciated.
Do you have any idea what am I missing?

Related

Cant catch axios error in promise, response works fine

I am trying to catch an error whilst the user tries to access a page without an authentication token.
axios.js?v=012beb2f:840 POST http://localhost:3001/api/get-user 422 (Unprocessable Entity)
Uncaught (in promise) AxiosError {message: 'Request failed with status code 422', name: 'AxiosError', code: 'ERR_BAD_REQUEST', config: {…}, request: XMLHttpRequest, …}
router.beforeEach((to, from, next) => {
const store = useUserStore()
if(to.meta.requiresAuth)
{
try
{
const response = axios.post('/api/get-user', {}, {
headers: {
Authorization: `Bearer ${store.user.token}`
}
})
.then(response => {
console.log(response)
next()
})
}
catch(error)
{
console.log(error)
next('/login')
}
}
else
{
next()
}
})
Thats the code that makes the request to the server. If the token is correct it works fine. However incorrect token throws the error mentioned above. I would like it to redirect to /login page if token is incorrect.
This is the code on server side.
router.post('/get-user', signupValidation, (req, res, next) => {
if(
!req.headers.authorization ||
!req.headers.authorization.startsWith('Bearer') ||
!req.headers.authorization.split(' ')[1]
){
return res.status(422).json({
message: "Please provide the token",
});
}
const theToken = req.headers.authorization.split(' ')[1];
const decoded = jwt.verify(theToken, 'the-super-strong-secrect');
db.query('SELECT * FROM users where id=?', decoded.id, function (error, results, fields) {
if (error) throw error;
return res.send({ error: false, data: results[0], message: 'Fetch Successfully.' });
});
});
Change the synchronous try/catch...
try
{
somePromise.then(...)
}
catch(error)
{
console.log(error)
next('/login')
}
...to instead use the catch() provided by the promise:
const headers = { Authorization: `Bearer ${store.user.token}` };
axios.post('/api/get-user', {}, { headers })
.then(response => {
console.log(response)
next()
})
.catch(error => {
console.log(error)
next('/login')
}}
Note, also, that the OP code incorrectly assigned the axios.post promise to an unused variable called "response".
Alternatively, use the synchronous try/catch style with async/await:
router.beforeEach(async (to, from, next) => {
const store = useUserStore()
if(to.meta.requiresAuth)
{
try
{
const headers = { Authorization: `Bearer ${store.user.token}` };
const response = await axios.post('/api/get-user', {}, { headers });
console.log(response);
next();
}
catch(error)
{
console.log(error)
next('/login')
}
}
else
{
next()
}
})

setting cookie after google passport callback

I am using facebook and google oauth2 login using passport js, with this flow
User clicked the login button
Redirects to facebook/google auth page (depending on what login the user chooses)
The auth page redirects back to a callback page (/auth/callback/[provider])
A passport express middleware will catch it to parse some data and then send it to a remote api of myown to sign the user in
The auth remote api will send a response back consisting the user token
A custom express middleware will catch the response to set cookie on the server
the express chain ends by route it to /profile (cookie with token is set on the browser)
/profile will then checks if there is a token, if there is not: it will redirect to /
Doing this flow on facebook login is fine, the user is successfully redirected to /profile, with all of its data and token, the google oauth2 login however seems to be doing the redirect to /profile then setting the token (step #7 then #6), so everytime the user is using google oauth2 login, its always gonna be redirected back to / since by the time it arrives at /profile, it doesnt have the token
here's the code on the above's flow
#./server.js
const express = require('express')
const next = require('next')
const Passport = require('./server/middleware/passport')
const Api = require('./server/api')
const port = parseInt(process.env.PORT, 10)
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
app
.prepare()
.then(() => {
const server = express()
// ... other unrelated things
server.use(Passport.initialize())
Api.passport.facebook(server)
Api.passport.facebookCallback(server)
Api.passport.google(server)
Api.passport.googleCallback(server)
// ... other unrelated things
server.all('*', (req, res) => handle(req, res))
server.listen(port, (error) => {
if (error) throw error
// ... other unrelated things
})
})
#./server/api.js
const Passport = require('middleware/passport')
function setCookie(req, res, next) {
res.cookie('token', req.user.auth.token, {
httpOnly: true,
sameSite: 'strict',
path: '/',
secure: process.env.NODE_ENV !== 'development',
})
next()
}
function facebook(app) {
return app.get('/auth/facebook', (req, res, next) => {
Passport.authenticate('facebook', {
scope: ['email', 'public_profile']
})(req, res, next)
})
}
function facebookCallback(app) {
return app.get(
'/auth/callback/facebook',
Passport.authenticate('facebook', { session: false, failureRedirect: '/' }),
setCookie,
(req, res) => {
res.redirect('/profile')
},
)
}
function google(app) {
return app.get('/auth/google', (req, res, next) => {
Passport.authenticate('google', {
scope: [
'https://www.googleapis.com/auth/userinfo.email ',
'https://www.googleapis.com/auth/userinfo.profile ',
],
prompt: 'consent',
authType: 'rerequest',
accessType: 'offline',
})(req, res, next)
})
}
function googleCallback(app) {
return app.get(
'/auth/callback/google',
Passport.authenticate('google', { failureRedirect: '/', session: false }),
setCookie,
(req, res) => {
res.redirect('/profile')
},
)
}
module.exports = {
passport: {
facebook,
facebookCallback,
google,
googleCallback,
}
}
#./server/middleware/passport.js
const axios = require('axios')
const passport = require('passport')
const GoogleStrategy = require('passport-google-oauth20').Strategy
const FacebookStrategy = require('passport-facebook').Strategy
passport.serializeUser((user, done) => {
done(null, user)
})
passport.deserializeUser((obj, done) => {
done(null, obj)
})
function verifyCallback(req, ... , done) {
process.nextTick(async () => {
try {
const options = {
baseURL: baseUrl, // My remote api url
method: 'POST',
url: '/auth/signin',
headers: {
'Content-Type': 'application/json',
},
data: JSON.stringify({
// email, fullname, etc
}),
}
const response = await axios(options)
return done(null, response.data)
} catch (error) {
const { response } = error
return done(JSON.stringify(response.data, null, 2), null)
}
})
}
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: callbackURLGoogle,
passReqToCallback: true,
}, verifyCallback))
passport.use(new FacebookStrategy({
clientID: process.env.FACEBOOK_CLIENT_ID,
clientSecret: process.env.FACEBOOK_CLIENT_SECRET,
callbackURL: callbackURLFacebook,
enableProof: true,
profileFields: ['id', 'name', 'email', 'picture.type(large)'],
passReqToCallback: true,
}, verifyCallback))
module.exports = passport
I console.log() things, just to figure out if it falls to the correct sequence of flow, the console doesn't seem to log anything suspicious, is there's something i am missing here?
PS: i am also using next js with custom server
I was facing the same problem and was able to send cookies by using custom callback.
router.get('/google/callback', (req, res) => {
passport.authenticate('google', {session: false, failureRedirect:'/auth/google/failure'},
async(err, user) => {
// You can send cookies and data in response here.
})(req, res)
})
Please refer custom callback section in documentation for explanation.

Cookie blocked by Chrome

I am trying to set up a simple register/login form with vuejs in the front and a server with express js using the passport library to setup a local and social media startegy.
But I can't seem to pass cookies to the front end when I login with the local strategy.
Also when I sign in with google I get the cookie on the front end but it is not sent with the next API call but this is a subject for another question.
I was confused by this so I made a simple project just to receive and send cookies and it works. Here is the back end:
//headers in app.js
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'http://localhost:8080');
res.header('Access-Control-Allow-Credentials', true);
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
next();
});
// index file
router.get('/cookie', function (req, res, next) {
res.cookie("token", "mytoken");
res.send("cookie sent");
});
router.get('/info', function (req, res, next) {
cookies = req.cookies;
console.log(cookies);
res.cookie("token", "mytoken");
res.send("cookie sent");
});
And here is my front end methods that call the API:
methods: {
async getCookie() {
await axios.get("http://localhost:3000/cookie",{withCredentials:true}).then((response) => {
console.log(response);
}).catch((e) => {
console.log(e);
});
},
async sendCookie() {
await axios.get("http://localhost:3000/info",{withCredentials:true}).then((response) => {
console.log(response);
}).catch((e) => {
console.log(e);
});
}
}
With that I have no problem passing the cookie in the requests and receiving it.
Now on my real project I have this on the back end
//Headers just like the other project
router.post('/users/login', function (req, res, next) {
passport.authenticate('local', { session: false }, function (err, user, info) {
if (err) { return next(err); }
if (user) {
res.cookie('token', 'mytoken');
return res.json({ user: user.toAuthJSON() });
} else {
return res.status(401).json(info);
}
})(req, res, next);
});
Front end call:
// Service file to call the api
axios.defaults.baseURL = "http://127.0.0.1:3000/api/";
axios.defaults.withCredentials = true;
const ApiService = {
get(resource, slug = "") {
return axios.get(`${resource}/${slug}`).catch(error => {
throw new Error(`ApiService ${error}`);
});
},
...
}
export default ApiService;
//actual call in authetification module file
[LOGIN](context, credentials) {
return new Promise(resolve => {
ApiService.post("users/login", { email: credentials.email, password: credentials.password })
.then(({ data }) => {
context.commit(SET_AUTH, data.user);
resolve(data);
})
.catch(({ response }) => {
context.commit(SET_ERROR, response.data.errors);
});
});
},
//
The request works but the cookie is blocked by Chrome:
I don't see what is the difference in my two projects that would trigger this warning on Chrome for the last one.
EDIT: In my original post axios.defaults.baseURL was not set to my actual value.
After last update of chrome browser I also started to get this error. A solution to solve this go to chrome://flags and disable SameSite by default cookies and disable Enable removing SameSite=None cookies. This will solve your problem. I think another way to solve this changing your cookie settings(samesite attribute) when you are creating your session.
Find the solution thank to #skirtle. I set axios.defaults.baseURL to "http://127.0.0.1:3000/api/" but localhost and 127.0.0.1 are not interchangeable and considered two different domains. I switched to axios.defaults.baseURL = "http://localhost:3000/api" and it fixed the problem.

Express Passportjs Authenticate not being reached in router callback

If I pass passport.authenticate("local") as middleware into my route, it executes. But this way I do not have access to res so I can send a message back to my front end. However, if I attempt to call it in the route callback function, it is not firing.
router.post("/login", function(req, res, next) {
passport.authenticate("local", function(err, user, info) {
console.log("Unreached"); // This is not logging
});
})
Here is my passport.use inside app.js
passport.use(new LocalStrategy({
usernameField: "portalId"
}, function(portalId, enteredPassword, done) {
var params = {
TableName: "MyTableName",
KeyConditionExpression : "PortalID = :portalID",
ExpressionAttributeValues : {
":portalID" : Number(portalId)
}
}
docClient.query(params, function(err, user) {
if (err) throw err;
let realPassword = user.Items[0].password;
bcrypt.compare(enteredPassword, realPassword, function(err, res) {
if (err) throw err;
if (res) {
return done(null, user);
}
if (!res) {
return done(null, false, { message: "Invalid Credentials" });
}
})
})
}));
Saw in some other post a snippet of code using the custom callback and he had (req, res, next) right after the passport.authenticate function. I added this and my code was being fired now.

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
})));
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
}))
);