Skip to content

Commit

Permalink
add typed error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
typerandom committed Oct 1, 2016
1 parent 9e177c3 commit 5904561
Show file tree
Hide file tree
Showing 9 changed files with 547 additions and 80 deletions.
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 scenario, 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
68 changes: 68 additions & 0 deletions errors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
var util = require('util');

function JwtError(message) {
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

0 comments on commit 5904561

Please sign in to comment.