Currently I handle my authentication as following :
function login(req, res, next) {
// fetch user from the db
User.findOne(req.body)
.exec() // make query a Promise
.then((user) => {
const token = jwt.sign({ username: user.username }, config.jwtSecret);
return res.json({ token, username: user.username });
})
.catch(() => {
const err = new APIError('Authentication error', httpStatus.UNAUTHORIZED, true);
return Promise.reject(err);
});
}
I am trying to standardize my errors with a common APIError class
import httpStatus from 'http-status';
/**
* #extends Error
*/
class ExtendableError extends Error {
constructor(message, status, isPublic) {
super(message);
this.name = this.constructor.name;
this.message = message;
this.status = status;
this.isPublic = isPublic;
this.isOperational = true; // This is required since bluebird 4 doesn't append it anymore.
Error.captureStackTrace(this, this.constructor.name);
}
}
/**
* Class representing an API error.
* #extends ExtendableError
*/
class APIError extends ExtendableError {
/**
* Creates an API error.
* #param {string} message - Error message.
* #param {number} status - HTTP status code of error.
* #param {boolean} isPublic - Whether the message should be visible to user or not.
*/
constructor(message, status = httpStatus.INTERNAL_SERVER_ERROR, isPublic = false) {
super(message, status, isPublic);
}
}
export default APIError;
How can I test the Promise.reject in my test ?
describe('# POST /api/v1/auth/login', () => {
it('should return Authentication error', () => {
return request(app)
.post('/api/v1/auth/login')
.send(invalidUserCredentials)
// following lines are not valid anymore with Promise.reject ..
.expect(httpStatus.UNAUTHORIZED)
.then((res) => {
expect(res.body.message).to.equal('Authentication error');
});
});
You aren't handling the error/rejection at all. You need to send the error Back. I would recommend to add a error handling middleware at the end of your routes in express and then use next(err) to pass to it.
// at the end of your routes
app.use(function(err, req, res, next) {
// do whatever you want, but at least send status and message:
res.status(err.status).json({
message: err.message,
});
});
now handle the error in your routes through:
.catch(() => {
const err = new APIError('Authentication error', httpStatus.UNAUTHORIZED, true);
return next(err);
});
It's running fine now , I had an issue in my express.js error handling.. as the APIError type checking was always false... extending the es6-error package rather than the Error solve this Babel issue...
APIError.js
import ExtendableError from 'es6-error'; // solve Babel issue w isInstanceOf()
import httpStatus from 'http-status'
class MyExtendableError extends ExtendableError {
constructor(message, status, isPublic) {
super(message);
this.name = this.constructor.name;
this.message = message;
this.status = status;
this.isPublic = isPublic;
this.isOperational = true; // This is required since bluebird 4 doesn't append it anymore.
Error.captureStackTrace(this, this.constructor.name);
}
}
/**
* Class representing an API error.
* #extends MyExtendableError
*/
class APIError extends MyExtendableError {
constructor(message, status = httpStatus.INTERNAL_SERVER_ERROR, isPublic = false) {
super(message, status, isPublic);
}
}
export default APIError;
Express.js
// catch 404 and forward to error handler
/* istanbul ignore next */
app.use((req, res, next) => {
const err = new APIError('API not found', httpStatus.NOT_FOUND);
return next(err);
});
// if error is not an instance Of APIError, convert it.
app.use((err, req, res, next) => {
if (err instanceof expressValidation.ValidationError) {
// validation error contains errors which is an array of error each containing message[]
const unifiedErrorMessage = err.errors.map((error) => {
return error.messages.join('. ');
}).join(' and ');
const error = new APIError(unifiedErrorMessage, err.status, true);
res.status(error.status).json({
message: err.isPublic ? err.message : httpStatus[err.status],
stack: (config.env === 'test' || config.env === 'development' ) ? err.stack : {}
});
} else if (!(err instanceof APIError)) {
const apiError = new APIError(err.message, err.status, err.isPublic);
res.status(apiError.status).json({
message: err.isPublic ? err.message : httpStatus[err.status],
stack: (config.env === 'test' || config.env === 'development' ) ? err.stack : {}
});
}else {
res.status(err.status).json({
message: err.isPublic ? err.message : httpStatus[err.status],
stack: (config.env === 'test' || config.env === 'development' ) ? err.stack : {}
});
}
});
auth.route.js
import express from 'express';
import validate from 'express-validation';
import expressJwt from 'express-jwt';
import paramValidation from '../../../config/param-validation';
import authCtrl from '../controllers/auth.controller';
import config from '../../../config/config';
const router = express.Router();
/** POST /api/auth/login - Returns token if correct username and password is provided */
router.route('/login')
.post(validate(paramValidation.login), authCtrl.login);
auth.controller.js
function login(req, res, next) {
// fetch user from the db
User.findOne(req.body)
.exec() // make query a Promise
.then((user) => {
const token = jwt.sign({ username: user.username }, config.jwtSecret);
return res.json({ token, username: user.username });
})
.catch(() => {
const err = new APIError('Authentication error', httpStatus.UNAUTHORIZED, true);
return next(err);
});
}
auth.test.js
..
it('should return Authentication error', () => {
return request(app)
.post('/api/v1/auth/login')
.send(invalidUserCredentials)
.expect(httpStatus.UNAUTHORIZED)
.then((res) => {
expect(res.body.message).to.equal('Authentication error');
});
});
...
Related
I am new to typescript, I am trying to learn how to use JSON Web Token to authenticate a typescript API but I am getting this error whenever I do npm start.
I am getting unexpected token at Function
How can I fix this error and I can I get JSON auth on this test API?
I am sure that most of my errors are in between the //start of test and //end of test comment
export class UsersController {
constructor(
#inject(UsersBindings.USERS_SERVICE)
public usersService: UsersService,
) { }
// Start of test
#get('/api', generateSpec('Get Api list'))
async apiList() {
return ("This is a sample gold API");
}
#post('/api/posts', verifyToken, (req, res) => {
jwt.verify(req.token, 'secretkey', (err, authData) => {
if(err) {
res.sendStatus(403);
} else {
res.json({
message: 'Post created now...',
});
}
});
});
function verifyToken(req, res, next) {
const bearerHeader = req.headers['authorization'];
if(typeof bearerHeader !== 'undefined') {
const bearer = bearerHeader.split(' ');
const bearerToken = bearer[1];
req.token = bearerToken;
next();
} else {
res.sendStatus(403);
}
}
// End of test
#get('/users/{id}', generateSpec('Get user profile'))
async getUserProfile(#param.path.string('id') id: string): Promise<Users> {
return this.usersService.getUser(id);
}
#post('/user', generateSpec('Create new user'))
async createUser(
#requestBody(generateRequestBody(TeacherProfile))
userRegisterCredentials: IRegisterUserCredentials): Promise<Users> {
return await this.usersService.createUser(userRegisterCredentials);
}
}
Test
it('should fail trying to GET bookmarks with false user id',async () => {
try {
const response = await request(app)
.get(baseApiUrlUnderTest + 'false_user_id/bookmarks')
.set('Authorization', bearerToken);
} catch (e) {
console.log(e); //it doesn't reach this point
expect(e.httpStatus).to.equal(HttpStatus.UNAUTHORIZED);
}
});
The relevant part of the method under test:
/* GET bookmark of user */
personalBookmarksRouter.get('/', keycloak.protect(), wrapAsync(async (request, response) => {
userIdTokenValidator.validateUserIdInToken(request);
...
}));
where wrapAsync makes sure the error is passed to the custom error handler:
let wrapAsync = function (fn) {
return function(req, res, next) {
// Make sure to `.catch()` any errors and pass them along to the `next()`
// middleware in the chain, in this case the error handler.
fn(req, res, next).catch(next);
};
}
The validateUserIdInToken method which causes the method under test to throw an exception:
const AppError = require('../models/error');
const HttpStatus = require('http-status-codes');
let validateUserIdInToken = function (request) {
const userId = request.kauth.grant.access_token.content.sub;
if ( userId !== request.params.userId ) {
throw new AppError(HttpStatus.UNAUTHORIZED, 'Unauthorized', ['the userId does not match the subject in the access token']);
}
}
module.exports.validateUserIdInToken = validateUserIdInToken;
and the custom error handler in the root middleware:
app.use(function(err, req, res, next) {
if (res.headersSent) {
return next(err)
}
if(err instanceof AppError) { //execution lands here as expected and the test stops...
res.status(err.httpStatus);
return res.send(err);
} else {
res.status(err.status || HttpStatus.INTERNAL_SERVER_ERROR);
res.send({
message: err.message,
error: {}
});
}
});
I think you may be approaching this incorrectly. Invalid auth should not raise errors in the app - it's not an error really, is a validation issue.
If the auth fails, simply send the relevant http error code - 401 back to the client.
res.send(HttpStatus.UNAUTHORIZED, 'a message if you want'); // 401
In your route handler:
personalBookmarksRouter.get('/', keycloak.protect(), wrapAsync(async (request, response) => {
const userId = request.kauth.grant.access_token.content.sub;
if ( userId !== request.params.userId ) {
return response.send(HttpStatus.UNAUTHORIZED);
}
...
}));
In your test, check the for status 401:
chai.request(server)
.get('/false_user_id/bookmarks')
.end((err, result) => {
if (err) {
return callback(err);
}
result.should.have.status(401);
});
Thanks to #laggingreflex's comment I missed debugging that the response actually returned with the expected status and error message
The adjusted test case now looks like this:
it('should fail trying to GET bookmarks with false user id',async () => {
const response = await request(app)
.get(baseApiUrlUnderTest + 'false_user_id/bookmarks')
.set('Authorization', bearerToken);
expect(response.status).to.equal(HttpStatus.UNAUTHORIZED);
});
Testing newbie, learning the ropes.
I'm trying to test my error handler controller. After a few hours, I have managed to get it working but need some feedback on the implementation.
Something feels wrong when I start changing the NODE_ENV but I'm not sure how to get around it.
I've written tests separate tests for sendErrorDev and sendErrorProd, so here I just need to test to see if they were called?
errorController.js:
const AppError = require('../utils/appError');
const handleValidationErrorDB = err => {
const errors = Object.values(err.errors).map(el => el.message);
const message = `Invalid input data. ${errors.join('. ')}`;
return new AppError(message, 400);
};
const sendErrorDev = (err, req, res) => {
return res.status(err.statusCode).json({
status: err.status,
error: err,
message: err.message,
stack: err.stack
});
};
const sendErrorProd = (err, req, res) => {
if (err.isOperational) {
return res.status(err.statusCode).json({
status: err.status,
message: err.message
});
}
return res.status(500).json({
status: 'error',
message: 'Something went very wrong!'
});
};
const handleGlobalErrors = (err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
if (process.env.NODE_ENV === 'development') {
helpers.sendErrorDev(err, req, res);
} else if (process.env.NODE_ENV === 'production') {
let error = { ...err };
error.message = err.message;
if (error.name === 'ValidationError')
error = helpers.handleValidationErrorDB(error);
helpers.sendErrorProd(error, req, res);
}
};
const helpers = {
sendErrorDev,
sendErrorProd,
handleGlobalErrors,
handleValidationErrorDB
};
module.exports = helpers;
errorController.test.js:
const { mockRequest, mockResponse } = require('../express-mocks');
const AppError = require('../../src/utils/appError');
const errorController = require('../../src/controllers/errorController');
describe('errorController', () => {
describe('handleGlobalErrors', () => {
let req;
let res;
let newError;
beforeEach(() => {
req = mockRequest();
res = mockResponse();
newError = new AppError('Some error', 500);
});
afterEach(() => {
process.env.NODE_ENV = 'test';
});
it('should call sendErrorDev in development', () => {
process.env.NODE_ENV = 'development';
const spy = jest
.spyOn(errorController, 'sendErrorDev')
.mockImplementation(jest.fn());
errorController.handleGlobalErrors(newError, req, res);
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
it('should call sendErrorProd in production', () => {
process.env.NODE_ENV = 'production';
const spy = jest
.spyOn(errorController, 'sendErrorProd')
.mockImplementation(jest.fn());
errorController.handleGlobalErrors(newError, req, res);
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
});
});
You are passing null as the first parameter to .call which is supposed to be the object you want to invoke the function as a method of. Try removing the .call and just invoke it as:
errorController.handleGlobalErrors(newError, req, res);
I am sending an HTTP request from angular to a node/express API. How do I get the actual error message I send from the node/express API
I am handling my exceptions well when they are thrown in the API but the moment I try to read the message in Angular I only get the name of the type of error I throw. For example, if I throw a 409 the error received by angular is just "Conflict" and does not contain the details I send. Please look at my code below.
I am sending my request as below
register(user: UserAccount) {
return this.http
.post(`${config.apiUrl}/users/register`, user)
.pipe(
map((res: HttpResponse<Response>) => {
return res;
}))
.pipe(catchError(err => this.errorHandler.handleError(err)));
}
My handle error is as below:
handleError(error: HttpErrorResponse) {
console.log(error);
if (error) {
let errMessage = '';
try {
errMessage = error.message;
} catch (error) {
errMessage = error.statusText;
}
return throwError(errMessage || error || 'Server error');
}
return throwError(error.error || error || 'Server error');
}
This how I am throwing my error when I occurs in my Node/Express API
registerUser (req, res) {
debug(chalk.blue(`*** insert user`))
userRepo
.create(req.body)
.then(user => {
debug(chalk.green(`*** Insert User ok!`))
res.status(200).json({
status: true,
error: null,
user: user
})
})
.catch(err => {
debug(chalk.red(`*** insertUser error: ${util.inspect(err)}`))
if (err['type'] && err['type'] === '409') {
res.status(409).json({
status: false,
error: err['message'],
user: null
})
} else {
res.status(400).json({
status: false,
error: err,
user: null
})
}
})
}
I want to be able to receive the json object with the information about the error but all I am getting when I access the error item is, for example, in the case of raising a 409, I only get 'Conflict'
The response data in case of error is inside the error property of HttpErrorResponse. So you may need to modify the handleError method as below.
handleError(error: HttpErrorResponse) {
console.log(error);
if (error) {
let errMessage = '';
try {
// error.error holds the json response sent from API.
errMessage = JSON.stringify(error.error);
} catch (error) {
errMessage = error.statusText;
}
return throwError(errMessage || error || 'Server error');
}
return throwError(error.error || error || 'Server error');
}
Server-side express code
registerUser(req, res) {
debug(chalk.blue(`*** insert user`))
userRepo
.create(req.body)
.then(user => {
/**
* SUCCESS TO-DO
* .
* .
* .
* .
*/
return res.send(200).json(user)
})
.catch(err => {
/**
* error handler in server
* FAILURE TO-DO
* .
* .
* .
*/
if (err['type'] && err['type'] === '409') {
res.status(409).json({
status: false,
error: 'some 409 type error',
user: null
})
} else {
res.status(400).json({
status: false,
error: 'some 400 type error',
user: null
})
}
})
}
Your angular code
handleError(error: HttpErrorResponse) {
console.log(error);
if (error) {
let errMessage = '';
try {
errMessage = error.message; // <- here is the problem
} catch (error) {
errMessage = error.statusText;
}
return throwError(errMessage || error || 'Server error');
}
return throwError(error.error || error || 'Server error');
}
Firstly, if I may rename your HttpErrorResponse object as http_error as it is too generic. However, the trick lies in reading from the error key of the http_error object, and not from the message key.
Client-side error handler(angular)
handleError(http_error: HttpErrorResponse) {
// {status: false, error: 'some 409/400 type error', user: null }
const errMessage = http_error.error;
/* TO-DO STUFF */
return throwError(errMessage || 'Server error');
}
In my main express.js config file, I use two custom error middleware functions:
const error = require('../middlewares/error');
// catch 404 and forward to error handler
app.use(error.notFound);
// if error is not an instanceOf APIError, convert it.
app.use(error.converter);
I use boom to unify error messages. this is my error middleware:
module.exports = {
/**
* Error responder. Send stacktrace only during development
* #public
*/
responder: (err, req, res, next) => {
res.status(err.output.payload.statusCode);
res.json(err.output.payload);
res.end();
},
/**
* If error is not a Boom error, convert it.
* #public
*/
converter: (err, req, res, next) => {
if (env !== 'development') {
delete err.stack;
}
if (err.isBoom) {
return module.exports.responder(err, req, res);
}
if (err.name === 'MongoError' && err.code === 11000) {
const boomedError = boom.conflict('This email already exists');
boomedError.output.payload.stack = err ? err.stack : undefined;
return module.exports.responder(boomedError, req, res);
}
const boomedError = boom.boomify(err, { statusCode: 422 });
return module.exports.responder(boomedError, req, res);
},
/**
* Catch 404 and forward to error responder
* #public
*/
notFound: (req, res) => {
const err = boom.notFound('Not Found');
return module.exports.responder(err, req, res);
},
};
My problem is, when I make a "register" action with an existing email, the responder() is executed three times. One for my boom.conflict error, but then also one for "not found". (even though I've done res.end().
This is the register logic:
exports.register = async (req, res, next) => {
try {
validationResult(req).throw();
const user = new User(req.body);
const token = generateTokenResponse(user, user.token());
const userTransformed = user.transform();
user.tokens.push({ kind: 'jwt', token });
user.activationId = uuidv1();
await user.save();
res.status(httpStatus.CREATED);
sendValidationEmail(user.activationId);
return res.json({ user: userTransformed });
} catch (err) {
return next(converter(err, req, res, next));
}
};
user.save() triggers this by the way:
userSchema.pre('save', async function save(next) {
try {
if (!this.isModified('password')) return next();
const rounds = env === 'test' ? 1 : 10;
const hash = await bcrypt.hash(this.password, rounds);
this.password = hash;
return next();
} catch (err) {
return next(converter(err));
}
});
Calling res.end() just tells the response stream that you're done sending data. You don't need that in this case because calling res.json() will do it for you.
However, that isn't the same as telling Express that you're done with handling the request. Just because you've sent a response doesn't necessarily mean you've no work left to do.
The way you tell Express that you've finished is by not calling next(). Express assumes you've finished by default and will only continue executing the middleware/routing chain if you call next().
So this line:
return next(converter(err, req, res, next));
should just be:
converter(err, req, res, next);
Likewise your other call to converter() shouldn't be calling next() either.