Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add typed error handling #29

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,31 @@ jwt.setNotbefore(); // Remove the exp claim
```


## Error Handling

If an error occurs then one of the following error types will either be thrown
or passed as the first argument in a callback.

Type | Description
----------------------------------------|--------------------------------------
JwtError | Base type. All errors implement this.
JwtParseError | Jwt cannot be parsed.
UnsupportedSigningAlgorithmJwtError | Unsupported signing algorithm.
SigningKeyRequiredJwtError | Signing key is required.
NotActiveJwtParseError | Jwt not active.
ExpiredJwtParseError | Jwt is expired.
SignatureAlgorithmMismatchJwtParseError | Unexpected signature algorithm.
SignatureMismatchJwtParseError | Signature verification failed.

To handle a specific error, simply compare the instance of the error received to one of
the error types above, as shown in the example below:

```javascript
if (err instanceof nJwt.JwtParseError) {
console.log('Unable to parse the provided jwt.');
}
```

## Supported Algorithms

"alg" Value | Algorithm used
Expand Down
69 changes: 69 additions & 0 deletions errors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
var util = require('util');

function JwtError(message) {
Error.captureStackTrace(this, this.constructor);
this.name = 'JwtError';
this.message = this.userMessage = message;
}
util.inherits(JwtError, Error);

function JwtParseError(jwtString, parsedHeader, parsedBody, innerError) {
this.name = 'JwtParseError';
this.message = this.userMessage = 'Jwt cannot be parsed';
this.jwtString = jwtString;
this.parsedHeader = parsedHeader;
this.parsedBody = parsedBody;
this.innerError = innerError;
}
util.inherits(JwtParseError, JwtError);

function UnsupportedSigningAlgorithmJwtError() {
UnsupportedSigningAlgorithmJwtError.super_.call(this, 'Unsupported signing algorithm');
this.name = 'UnsupportedSigningAlgorithmJwtError';
}
util.inherits(UnsupportedSigningAlgorithmJwtError, JwtError);

function SigningKeyRequiredJwtError() {
SigningKeyRequiredJwtError.super_.call(this, 'Signing key is required');
this.name = 'SigningKeyRequiredJwtError';
}
util.inherits(SigningKeyRequiredJwtError, JwtError);

function NotActiveJwtParseError(jwtString, parsedHeader, parsedBody) {
NotActiveJwtParseError.super_.call(this, jwtString, parsedHeader, parsedBody);
this.name = 'NotActiveJwtParseError';
this.message = this.userMessage = 'Jwt not active';
}
util.inherits(NotActiveJwtParseError, JwtParseError);

function ExpiredJwtParseError(jwtString, parsedHeader, parsedBody) {
ExpiredJwtParseError.super_.call(this, jwtString, parsedHeader, parsedBody);
this.name = 'ExpiredJwtParseError';
this.message = this.userMessage = 'Jwt is expired';
}
util.inherits(ExpiredJwtParseError, JwtParseError);

function SignatureAlgorithmMismatchJwtParseError(jwtString, parsedHeader, parsedBody) {
SignatureAlgorithmMismatchJwtParseError.super_.call(this, jwtString, parsedHeader, parsedBody);
this.name = 'SignatureAlgorithmMismatchJwtParseError';
this.message = this.userMessage = 'Unexpected signature algorithm';
}
util.inherits(SignatureAlgorithmMismatchJwtParseError, JwtParseError);

function SignatureMismatchJwtParseError(jwtString, parsedHeader, parsedBody, innerError) {
SignatureMismatchJwtParseError.super_.call(this, jwtString, parsedHeader, parsedBody, innerError);
this.name = 'SignatureMismatchJwtParseError';
this.message = this.userMessage = 'Signature verification failed';
}
util.inherits(SignatureMismatchJwtParseError, JwtParseError);

module.exports = {
JwtError: JwtError,
JwtParseError: JwtParseError,
UnsupportedSigningAlgorithmJwtError: UnsupportedSigningAlgorithmJwtError,
SigningKeyRequiredJwtError: SigningKeyRequiredJwtError,
NotActiveJwtParseError: NotActiveJwtParseError,
ExpiredJwtParseError: ExpiredJwtParseError,
SignatureAlgorithmMismatchJwtParseError: SignatureAlgorithmMismatchJwtParseError,
SignatureMismatchJwtParseError: SignatureMismatchJwtParseError
};
69 changes: 30 additions & 39 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
'use strict';

var util = require('util');
var uuid = require('uuid');
var crypto = require('crypto');
var ecdsaSigFormatter = require('ecdsa-sig-formatter');
var properties = require('./properties.json');
var errors = require('./errors');

var algCryptoMap = {
HS256: 'SHA256',
Expand Down Expand Up @@ -68,22 +67,6 @@ function handleError(cb,err,value){
}
}

function JwtError(message) {
this.name = 'JwtError';
this.message = this.userMessage = message;
}
util.inherits(JwtError, Error);

function JwtParseError(message,jwtString,parsedHeader,parsedBody,innerError) {
this.name = 'JwtParseError';
this.message = this.userMessage = message;
this.jwtString = jwtString;
this.parsedHeader = parsedHeader;
this.parsedBody = parsedBody;
this.innerError = innerError;
}
util.inherits(JwtParseError, Error);

function JwtBody(claims){
if(!(this instanceof JwtBody)){
return new JwtBody(claims);
Expand Down Expand Up @@ -194,7 +177,7 @@ Jwt.prototype.setSigningKey = function setSigningKey(key) {
};
Jwt.prototype.setSigningAlgorithm = function setSigningAlgorithm(alg) {
if(!this.isSupportedAlg(alg)){
throw new JwtError(properties.errors.UNSUPPORTED_SIGNING_ALG);
throw new errors.UnsupportedSigningAlgorithmJwtError();
}
this.header.alg = alg;
return this;
Expand All @@ -207,7 +190,7 @@ Jwt.prototype.sign = function sign(payload, algorithm, cryptoInput) {
var signingType = algTypeMap[algorithm];

if (!cryptoAlgName) {
throw new JwtError(properties.errors.UNSUPPORTED_SIGNING_ALG);
throw new errors.UnsupportedSigningAlgorithmJwtError();
}

if (signingType === 'hmac') {
Expand All @@ -234,12 +217,13 @@ Jwt.prototype.compact = function compact() {
segments.push(this.body.compact());

if(this.header.alg !== 'none'){
if (this.signingKey) {
this.signature = this.sign(segments.join('.'), this.header.alg, this.signingKey);
segments.push(this.signature);
}else{
throw new Error(properties.errors.SIGNING_KEY_REQUIRED);
if (!this.signingKey) {
throw new errors.SigningKeyRequiredJwtError();
}

this.signature = this.sign(segments.join('.'), this.header.alg, this.signingKey);

segments.push(this.signature);
}

return segments.join('.');
Expand Down Expand Up @@ -278,7 +262,7 @@ Parser.prototype.parse = function parse(jwtString,cb){
var signature;

if(segments.length<2 || segments.length>3){
return done(new JwtParseError(properties.errors.PARSE_ERROR,jwtString,null,null));
return done(new errors.JwtParseError(jwtString));
}

var header = this.safeJsonParse(segments[0]);
Expand All @@ -290,10 +274,10 @@ Parser.prototype.parse = function parse(jwtString,cb){
}

if(header instanceof Error){
return done(new JwtParseError(properties.errors.PARSE_ERROR,jwtString,null,null,header));
return done(new errors.JwtParseError(jwtString, null, null, header));
}
if(body instanceof Error){
return done(new JwtParseError(properties.errors.PARSE_ERROR,jwtString,header,null,body));
return done(new errors.JwtParseError(jwtString, header, null, body));
}
var jwt = new Jwt(body, false);
jwt.setSigningAlgorithm(header.alg);
Expand All @@ -312,7 +296,7 @@ function Verifier(){
}
Verifier.prototype.setSigningAlgorithm = function setSigningAlgorithm(alg) {
if(!this.isSupportedAlg(alg)){
throw new JwtError(properties.errors.UNSUPPORTED_SIGNING_ALG);
throw new errors.UnsupportedSigningAlgorithmJwtError();
}
this.signingAlgorithm = alg;
return this;
Expand Down Expand Up @@ -342,15 +326,15 @@ Verifier.prototype.verify = function verify(jwtString,cb){
var signingType = algTypeMap[header.alg];

if (header.alg !== this.signingAlgorithm) {
return done(new JwtParseError(properties.errors.SIGNATURE_ALGORITHM_MISMTACH,jwtString,header,body));
return done(new errors.SignatureAlgorithmMismatchJwtParseError(jwtString, header, body));
}

if (jwt.isExpired()) {
return done(new JwtParseError(properties.errors.EXPIRED,jwtString,header,body));
return done(new errors.ExpiredJwtParseError(jwtString, header, body));
}

if (jwt.isNotBefore()) {
return done(new JwtParseError(properties.errors.NOT_ACTIVE,jwtString,header,body));
return done(new errors.NotActiveJwtParseError(jwtString, header, body));
}

var digstInput = jwt.verificationInput;
Expand All @@ -371,7 +355,7 @@ Verifier.prototype.verify = function verify(jwtString,cb){
try {
unescapedSignature = ecdsaSigFormatter.joseToDer(signature, header.alg);
} catch (err) {
return done(new JwtParseError(properties.errors.SIGNATURE_MISMTACH,jwtString,header,body,err));
return done(new errors.SignatureMismatchJwtParseError(jwtString, header, body, err));
}
} else {
signatureType = 'base64';
Expand All @@ -392,7 +376,7 @@ Verifier.prototype.verify = function verify(jwtString,cb){
newJwt.header = new JwtHeader(header);

if (!verified) {
return done(new JwtParseError(properties.errors.SIGNATURE_MISMTACH,jwtString,header,body));
return done(new errors.SignatureMismatchJwtParseError(jwtString, header, body));
}

return done(null, newJwt);
Expand Down Expand Up @@ -441,15 +425,22 @@ var jwtLib = {
}else{
jwt = new Jwt(claims);
}
if(alg!=='none' && !secret){
throw new Error(properties.errors.SIGNING_KEY_REQUIRED);
}else{
jwt.setSigningAlgorithm(args.length===3 ? alg : 'HS256');
jwt.setSigningKey(secret);

if(alg !== 'none' && !secret){
throw new errors.SigningKeyRequiredJwtError();
}

jwt.setSigningAlgorithm(args.length===3 ? alg : 'HS256');
jwt.setSigningKey(secret);
jwt.setExpiration((nowEpochSeconds() + (60*60))*1000); // one hour

return jwt;
}
};

// Copy errors onto export object.
for (var key in errors) {
jwtLib[key] = errors[key];
}

module.exports = jwtLib;
11 changes: 0 additions & 11 deletions properties.json

This file was deleted.

11 changes: 5 additions & 6 deletions test/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ var assert = require('chai').assert;
var nJwt = require('../');
var uuid = require('uuid');

var properties = require('../properties.json');
var errors = require('../errors');

describe('Jwt()',function(){
describe('signWith()',function(){
describe('if called with an unsupported algorithm',function(){
it('should throw',function(){
assert.throws(function(){
new nJwt.Jwt().setSigningAlgorithm('unsupported');
},properties.errors.UNSUPPORTED_SIGNING_ALG);
},errors.UnsupportedSigningAlgorithmJwtError);
});
});
});
Expand All @@ -20,10 +20,10 @@ describe('Jwt()',function(){

describe('create()',function(){

it('should throw SIGNING_KEY_REQUIRED if passed no options',function(){
it('should throw UnsupportedSigningAlgorithmJwtError if passed no options',function(){
assert.throws(function(){
nJwt.create();
},properties.errors.SIGNING_KEY_REQUIRED);
},errors.SigningKeyRequiredJwtError);
});

it('should create a default token if the scret is the only value',function(){
Expand All @@ -33,7 +33,7 @@ describe('create()',function(){
it('should throw if using defaults without a secret key',function(){
assert.throws(function(){
nJwt.create({});
},properties.errors.SIGNING_KEY_REQUIRED);
},errors.SigningKeyRequiredJwtError);
});

it('should not throw if none is specified when omitting the key',function(){
Expand All @@ -43,7 +43,6 @@ describe('create()',function(){
});

describe('with a signing key',function(){

it('should return a JWT',function(){
assert(nJwt.create({},uuid()) instanceof nJwt.Jwt);
});
Expand Down
Loading