Come testare l'autenticazione con jwt all'interno di un cookie con supertest, passport e JEST
Ehi ragazzi, sto attualmente cercando di fare qualcosa di simile a quanto pubblicato qui: Come autenticare le richieste Supertest con Passport?
poiché vorrei testare altri endpoint che richiedono l'autenticazione ma in aggiunta devono passare un jwt. In questo momento, l'ho testato su POSTMAN e sul browser e sembra che funzioni bene, ma i miei casi di test continuano a rompersi. Ho un percorso POST di accesso configurato in questo modo:
AccountService.js
// Login POST route
router.post('/account_service/login', (req, res, next) => {
passport.authenticate('local-login', (err, user, info) => {
try {
if (err) {
const error = new Error('An Error occurred: Cannot find user');
return next(error);
} else if (!user) {
return res.redirect('/account_service/login');
}
req.login(user, { session: false }, (error) => {
if (error) {
return next(error);
}
const email = req.body.email;
const role = req.user[0].role;
const id = req.user[0].id;
const user = {
email: email,
role: role,
id: id
};
const accessToken = jwt.sign(user, config.ACCESS_TOKEN_SECRET, {
expiresIn: 28800 // expires in 8 hours
});
const cookie = req.cookies.cookieName;
if (cookie === undefined) {
// set a new cookie
console.log('setting new cookie');
res.cookie('jwt', accessToken, { maxAge: 900000, httpOnly: true });
res.send({ token: accessToken });
} else {
// cookie was already present
console.log('cookie exists', cookie);
}
res.redirect('/account_service/profile');
});
} catch (error) {
return next(error);
}
})(req, res, next);
});
Dopo che l'utente è stato autenticato, assegno un token Web JSON all'utente e lo inserisco nel cookie in modo che venga memorizzato nelle intestazioni per le richieste autorizzate. Ecco un esempio:
AccountService.js
// Get all users
router.get('/account_service/all_users', passport.authenticate('jwt', { session: false }), (req, res, next) => {
const sql = 'select * from user';
const params = [];
db.all(sql, params, (err, rows) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
res.json({
message: 'success',
data: rows
});
});
});
Uso passport.authenticate per assicurarmi che il jwt sia valido. Questa richiesta GET funziona solo dopo aver effettuato l'accesso con l'account utente amministratore.
All'interno del mio file del passaporto l'ho configurato in questo modo:
passport.js
const LocalStrategy = require('passport-local').Strategy;
const db = require('../database.js');
const bcrypt = require('bcrypt');
const config = require('../config/config.js');
const JwtStrategy = require('passport-jwt').Strategy;
const cookieExtractor = function (req) {
var token = null;
if (req && req.cookies) token = req.cookies.jwt;
return token;
};
module.exports = function (passport) {
passport.serializeUser(function (user, done) {
done(null, user);
});
passport.deserializeUser(function (user, done) {
done(null, user);
});
passport.use('local-login', new LocalStrategy({
usernameField: 'email',
passwordField: 'password',
passReqToCallback: true
}, (req, email, password, done) => {
try {
const sql = `select * from user WHERE email = "${email}"`; const params = []; db.all(sql, params, (err, row) => { if (err) { return done(err); } if (!row.length || !bcrypt.compareSync(password, row[0].password)) { return done(null, false, req.flash('loginMessage', 'Inavalid username/password combination. Please try again.')); } return done(null, row); }); } catch (error) { return done(error); } })); const opts = {}; opts.jwtFromRequest = cookieExtractor; // check token in cookie opts.secretOrKey = config.ACCESS_TOKEN_SECRET; // eslint-disable-next-line camelcase passport.use(new JwtStrategy(opts, function (jwtPayload, done) { try { const sql = `select * from user WHERE email = "${jwtPayload.email}"`;
const params = [];
db.all(sql, params, (err, row) => {
if (err) {
return done(err);
}
if (!row.length || !bcrypt.compareSync('admin', jwtPayload.role)) {
return done(null, false, { message: '403 Forbidden' });
}
return done(null, row);
});
} catch (error) {
return done(error);
}
}));
};
È qui che mi confondo quando i miei casi di test si interrompono. Sto provando ad accedere prima dei miei casi di test per consentire l'esecuzione degli altri casi di test, ma finisco per ricevere un errore 401. Ecco i miei casi di test:
accountservice.test.js
const app = require('../../app');
const supertest = require('supertest');
const http = require('http');
describe('Account Service', () => {
let server;
let request;
beforeAll((done) => {
server = http.createServer(app);
server.listen(done);
request = supertest.agent(server);
request.post('/account_service/login')
.send({ email: '[email protected]', password: 'admin' })
.end(function (err, res) {
if (err) {
return done(err);
}
console.log(res);
done();
});
});
afterAll((done) => {
server.close(done);
});
it('Test request all users endpoint | GET request', async done => {
const response = await request.get('/account_service/all_users');
expect(response.status).toBe(200);
expect(response.body.message).toBe('success');
expect(response.body.data.length).toBe(3);
done();
});
});
Ma i miei casi di test falliscono perché ricevo un errore 401 quando si aspetta un codice di successo 200.
Ho provato a pensare a un modo per estrarre il jwt da un cookie dopo la chiamata di accesso in modo da poter impostare le intestazioni per il codice di richiesta GET / account_service / all_users ma non sono riuscito a trovare un modo utilizzando Supertest. Ho visto questo post: Il test di percorsi autenticati con JWT non riesce utilizzando Mocha + supertest + passport, ma ho visto che ottiene il token dal corpo.
Risposte
Dopo aver giocato con il mio codice, ho finito per avere problemi con l'archiviazione in memoria e l'esecuzione di funzioni db.run asincrone che chiamavano ogni volta che eseguivo il mio server. Quindi ho usato un file per memorizzare i miei dati e ho eseguito di nuovo i miei test e ha finito per funzionare!
Ecco il codice difettoso:
const sqlite3 = require('sqlite3').verbose();
const md5 = require('md5');
const DBSOURCE = ':memory:';
const db = new sqlite3.Database(DBSOURCE, (err) => {
if (err) {
// Cannot open database
console.error(err.message);
throw err;
} else {
db.run(`CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name text,
email text UNIQUE,
password text,
status text,
comments text,
photos text,
CONSTRAINT email_unique UNIQUE (email)
)`,
(err) => {
if (err) {
// Table already created
console.log('Table already created');
} else {
// Table just created, creating some rows
const insert = 'INSERT INTO user (name, email, password, status, comments, photos) VALUES (?,?,?,?,?,?)';
db.run(insert, ['user_delete', '[email protected]', md5('admin123456'), 'pending_deleted', 'comment1,comment2', 'https://giphy.com/gifs/9jumpin-wow-nice-well-done-xT77XWum9yH7zNkFW0']);
db.run(insert, ['user_no_delete', '[email protected]', md5('user123456'), 'active', 'comment1', 'https://giphy.com/gifs/cartoon-we-bare-bears-wbb-NeijdlusjcduU']);
db.run(insert, ['mikey', '[email protected]', md5('mikey123'), 'pending_deleted', 'comment1', 'https://giphy.com/gifs/wwe-shocked-vince-mcmahon-gdKAVlnm3bmKI']);
}
});
}
});
module.exports = db;
Ho semplicemente memorizzato questi dati all'interno di un file e ho usato invece questo codice:
const sqlite3 = require('sqlite3').verbose();
const DBSOURCE = 'mockdb.sqlite';
// Data inserted inside file
/*
db.run(insert, ['user_delete', '[email protected]', bcrypt.hashSync('admin123456', saltRounds), 'pending_deleted', 'comment1,comment2', 'https://giphy.com/gifs/9jumpin-wow-nice-well-done-xT77XWum9yH7zNkFW0', bcrypt.hashSync('user', saltRounds)]);
db.run(insert, ['user_no_delete', '[email protected]', bcrypt.hashSync('user123456', saltRounds), 'active', 'comment1', 'https://giphy.com/gifs/cartoon-we-bare-bears-wbb-NeijdlusjcduU', bcrypt.hashSync('user', saltRounds)]);
db.run(insert, ['mikey', '[email protected]', bcrypt.hashSync('mikey123', saltRounds), 'pending_deleted', 'comment1', 'https://giphy.com/gifs/wwe-shocked-vince-mcmahon-gdKAVlnm3bmKI', bcrypt.hashSync('user', saltRounds)]);
db.run(insert, ['admin', '[email protected]', bcrypt.hashSync('admin', saltRounds), 'active', 'admincomments', 'adminphoto', bcrypt.hashSync('admin', saltRounds)]);
console.log('last hit in database');
});
*/
const db = new sqlite3.Database(DBSOURCE, (err) => {
if (err) {
// Cannot open database
console.error(err.message);
throw err;
}
console.log('Connection successful!');
});
module.exports = db;
Ho anche finito per usare supertest.agent.
const app = require('../../app');
const supertest = require('supertest');
const http = require('http');
const db = require('../../database/database.js');
describe('Account Service', () => {
let server;
let request;
// Find cookie management option.
beforeAll(async (done) => {
server = http.createServer(app);
server.listen(done);
request = supertest.agent(server);
done();
});
E ha finito per funzionare e risolvere con successo il mio problema!