jsonwebtoken 是 JWT 令牌生成与验证的核心库,提供基础的签名和验证功能。express-jwt 是 Express 中间件,用于验证请求中的 JWT 令牌。express-jwt-authz 和 express-jwt-permissions 是基于 express-jwt 的授权扩展,用于处理权限检查。passport-jwt 是 Passport.js 的 JWT 策略,适合需要多种认证方式的复杂场景。这些库共同构成了 Node.js 后端 JWT 认证的完整生态,但各自定位不同,需要根据项目需求合理选择。
在 Node.js 后端开发中,JWT(JSON Web Token)是最常用的无状态认证方案。但围绕 JWT 的 npm 包众多,各自定位不同。本文将从实际工程角度,对比 express-jwt、express-jwt-authz、express-jwt-permissions、jsonwebtoken 和 passport-jwt 这五个核心包,帮助你做出正确的技术选型。
express-jwt、express-jwt-authz、express-jwt-permissions 已标记为 deprecated。Auth0 官方建议迁移到 express-oauth2-jwt-bearer 或其他替代方案。新项目应谨慎使用这些包,已有项目需规划迁移路径。
jsonwebtoken 和 passport-jwt 仍在积极维护,可安全用于新项目。
这五个包分为三个层次:
| 层次 | 包名 | 职责 |
|---|---|---|
| 底层核心 | jsonwebtoken | JWT 令牌生成、签名、验证 |
| Express 中间件 | express-jwt | Express 请求中的 JWT 验证 |
| 授权扩展 | express-jwt-authz、express-jwt-permissions | 基于 JWT 的权限检查 |
| 认证框架 | passport-jwt | Passport 生态的 JWT 策略 |
jsonwebtoken 是唯一提供令牌生成能力的包。其他包都只负责验证。
// jsonwebtoken: 生成令牌
const jwt = require('jsonwebtoken');
const token = jwt.sign(
{ userId: 123, role: 'admin' },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
express-jwt 不生成令牌,只验证:
// express-jwt: 验证中间件
const expressJwt = require('express-jwt');
app.use(expressJwt({
secret: process.env.JWT_SECRET,
algorithms: ['HS256']
}));
passport-jwt 通过 Strategy 配置验证:
// passport-jwt: 策略配置
const JwtStrategy = require('passport-jwt').Strategy;
passport.use(new JwtStrategy({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET
}, (payload, done) => {
// 验证用户是否存在
User.findById(payload.userId, (err, user) => {
if (err) return done(err, false);
return done(null, user);
});
}));
express-jwt-authz 和 express-jwt-permissions 不负责验证,只做授权检查:
// express-jwt-authz: 基于 scopes 授权
const authz = require('express-jwt-authz');
app.get('/admin', authz(['read:users']), (req, res) => {
res.json({ data: 'admin content' });
});
// express-jwt-permissions: 基于角色 + 权限
const permissions = require('express-jwt-permissions');
const guard = permissions();
app.get('/dashboard', guard(['role:admin', 'permission:read']), (req, res) => {
res.json({ data: 'dashboard' });
});
不同包从请求中提取 JWT 的方式不同:
express-jwt 默认从 Authorization: Bearer <token> 头提取:
// express-jwt: 默认提取方式
app.use(expressJwt({
secret: process.env.JWT_SECRET,
getToken: function fromHeaderOrQuerystring(req) {
if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
return req.headers.authorization.split(' ')[1];
} else if (req.query && req.query.token) {
return req.query.token;
}
return null;
}
}));
passport-jwt 需要显式配置提取器:
// passport-jwt: 多种提取方式
const { ExtractJwt } = require('passport-jwt');
const options = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
// 或:ExtractJwt.fromUrlQueryParameter('token')
// 或:ExtractJwt.fromBodyField('token')
secretOrKey: process.env.JWT_SECRET
};
jsonwebtoken 不处理提取,只验证原始字符串:
// jsonwebtoken: 手动提取后验证
const token = req.headers.authorization?.split(' ')[1];
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
req.user = payload;
} catch (err) {
res.status(401).json({ error: 'Invalid token' });
}
express-jwt-authz 只支持简单的 scopes 检查:
// express-jwt-authz: scopes 必须存在于 token 的 scope 字段
app.get('/api/users', authz(['read:users']), (req, res) => {
// token 必须包含 scope: "read:users"
});
express-jwt-permissions 支持更复杂的权限规则:
// express-jwt-permissions: 支持角色和权限组合
const guard = permissions({
rolesProperty: 'role',
permissionsProperty: 'permissions'
});
app.get('/api/admin', guard(['role:admin', 'permission:delete:users']), (req, res) => {
// 可检查角色和权限的组合
});
passport-jwt 需要在 Strategy 回调中手动实现授权逻辑:
// passport-jwt: 手动授权检查
passport.use(new JwtStrategy(options, (payload, done) => {
if (payload.role !== 'admin') {
return done(null, false, { message: 'Insufficient permissions' });
}
User.findById(payload.userId, (err, user) => {
return done(err, user);
});
}));
jsonwebtoken 需要完全手动实现:
// jsonwebtoken: 完全手动授权
function checkPermission(requiredPermission) {
return (req, res, next) => {
const userPermissions = req.user.permissions || [];
if (!userPermissions.includes(requiredPermission)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
app.get('/api/secure', checkPermission('read:secret'), (req, res) => {
res.json({ data: 'secure content' });
});
express-jwt 自动返回 401,可配置自定义错误处理:
// express-jwt: 错误处理中间件
app.use(expressJwt({ secret: process.env.JWT_SECRET }));
app.use((err, req, res, next) => {
if (err.name === 'UnauthorizedError') {
res.status(401).json({ error: 'Invalid token' });
} else {
next(err);
}
});
passport-jwt 通过 failWithError 选项控制:
// passport-jwt: 错误传递
app.post('/login', passport.authenticate('jwt', { session: false, failWithError: true }), (req, res) => {
res.json({ user: req.user });
}, (err, req, res, next) => {
res.status(401).json({ error: err.message });
});
jsonwebtoken 需要手动 try-catch:
// jsonwebtoken: 手动错误处理
try {
const payload = jwt.verify(token, secret);
} catch (err) {
if (err.name === 'TokenExpiredError') {
res.status(401).json({ error: 'Token expired' });
} else if (err.name === 'JsonWebTokenError') {
res.status(401).json({ error: 'Invalid token' });
}
}
express-jwt 作为全局或路由级中间件:
// express-jwt: 全局保护
app.use(expressJwt({ secret: process.env.JWT_SECRET }).unless({ path: ['/public'] }));
// 或路由级保护
app.get('/protected', expressJwt({ secret: process.env.JWT_SECRET }), handler);
passport-jwt 需要初始化 Passport 并配置策略:
// passport-jwt: 完整初始化流程
const passport = require('passport');
passport.use(new JwtStrategy(options, verifyCallback));
app.post('/profile', passport.authenticate('jwt', { session: false }), (req, res) => {
res.json({ user: req.user });
});
jsonwebtoken 完全手动集成:
// jsonwebtoken: 手动中间件
function authMiddleware(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'No token' });
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch (err) {
res.status(401).json({ error: 'Invalid token' });
}
}
app.use('/api', authMiddleware);
| 功能 | express-jwt | express-jwt-authz | express-jwt-permissions | jsonwebtoken | passport-jwt |
|---|---|---|---|---|---|
| 令牌生成 | ❌ | ❌ | ❌ | ✅ | ❌ |
| 令牌验证 | ✅ | ❌ | ❌ | ✅ | ✅ |
| 授权检查 | ❌ | ✅ (scopes) | ✅ (角色 + 权限) | ❌ | 手动实现 |
| Express 集成 | ✅ | ✅ | ✅ | 手动 | ✅ |
| 多策略支持 | ❌ | ❌ | ❌ | ❌ | ✅ |
| 维护状态 | ⚠️ deprecated | ⚠️ deprecated | ⚠️ deprecated | ✅ 活跃 | ✅ 活跃 |
只需要验证用户是否登录,不需要复杂权限。
// 推荐:jsonwebtoken + 自定义中间件(express-jwt 已 deprecated)
const jwt = require('jsonwebtoken');
function authMiddleware(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'No token' });
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
}
需要同时支持 JWT、OAuth、本地登录等。
// 推荐:passport-jwt + 其他 Passport 策略
passport.use(new JwtStrategy(jwtOptions, jwtVerify));
passport.use(new LocalStrategy(localVerify));
passport.use(new GoogleStrategy(googleOptions, googleVerify));
需要基于角色和权限的复杂授权。
// 推荐:jsonwebtoken + 自定义权限中间件(express-jwt-permissions 已 deprecated)
function permissionMiddleware(requiredPermissions) {
return (req, res, next) => {
const userPermissions = req.user.permissions || [];
const hasPermission = requiredPermissions.every(p => userPermissions.includes(p));
if (!hasPermission) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
现有项目已深度集成 express-jwt 生态。
// 短期:继续使用,但规划迁移
// 长期:迁移到 express-oauth2-jwt-bearer 或自定义方案
const { auth } = require('express-oauth2-jwt-bearer');
app.use(auth({
issuerBaseURL: 'https://your-auth0-domain.auth0.com/',
audience: 'https://your-api-identifier'
}));
express-jwt、express-jwt-authz、express-jwt-permissions 已不再推荐用于新项目。Auth0 官方建议:
express-oauth2-jwt-bearer 或基于 jsonwebtoken 的自定义方案// 迁移示例:从 express-jwt 到 express-oauth2-jwt-bearer
// 旧代码
const expressJwt = require('express-jwt');
app.use(expressJwt({ secret: process.env.JWT_SECRET }));
// 新代码
const { auth } = require('express-oauth2-jwt-bearer');
app.use(auth({
issuerBaseURL: process.env.AUTH0_ISSUER,
audience: process.env.AUTH0_AUDIENCE
}));
| 项目类型 | 推荐方案 | 理由 |
|---|---|---|
| 新项目 | jsonwebtoken + 自定义中间件 | 完全控制,无 deprecated 风险 |
| 复杂认证 | passport-jwt + 其他策略 | Passport 生态成熟,扩展性强 |
| Auth0 用户 | express-oauth2-jwt-bearer | 官方推荐,持续维护 |
| 已有 express-jwt 项目 | 评估后逐步迁移 | 避免突然中断,降低风险 |
核心原则:
jsonwebtokenJWT 认证看似简单,但选型不当会带来长期维护负担。理解每个包的定位和限制,才能做出适合项目的正确决策。
选择 jsonwebtoken 如果你需要直接生成和验证 JWT 令牌,而不依赖特定框架。它是底层核心库,几乎所有 JWT 相关方案都基于它。适合需要完全控制令牌生命周期的场景。
选择 passport-jwt 如果你需要灵活的认证策略(如同时支持 JWT、OAuth、本地登录等)。Passport 生态成熟,适合复杂认证需求,但配置相对繁琐,学习曲线较陡。
选择 express-jwt 如果你只需要在 Express 应用中验证 JWT 令牌,且不需要复杂的认证流程。它轻量、直接,适合简单的 API 保护场景。但注意该库已标记为 deprecated,新项目建议考虑替代方案。
选择 express-jwt-authz 如果你已经使用 express-jwt 且需要基于 scopes 的简单授权检查。它与 express-jwt 紧密集成,但同样受限于 express-jwt 的 deprecated 状态,新项目需谨慎评估。
选择 express-jwt-permissions 如果你需要更细粒度的权限控制(如角色 + 权限组合)。它扩展了 express-jwt-authz 的功能,但依赖链较长,维护风险需纳入考量。
| Build | Dependency |
|---|---|
An implementation of JSON Web Tokens.
This was developed against draft-ietf-oauth-json-web-token-08. It makes use of node-jws
$ npm install jsonwebtoken
(Asynchronous) If a callback is supplied, the callback is called with the err or the JWT.
(Synchronous) Returns the JsonWebToken as string
payload could be an object literal, buffer or string representing valid JSON.
Please note that
expor any other claim is only set if the payload is an object literal. Buffer or string payloads are not checked for JSON validity.
If
payloadis not a buffer or a string, it will be coerced into a string usingJSON.stringify.
secretOrPrivateKey is a string (utf-8 encoded), buffer, object, or KeyObject containing either the secret for HMAC algorithms or the PEM
encoded private key for RSA and ECDSA. In case of a private key with passphrase an object { key, passphrase } can be used (based on crypto documentation), in this case be sure you pass the algorithm option.
When signing with RSA algorithms the minimum modulus length is 2048 except when the allowInsecureKeySizes option is set to true. Private keys below this size will be rejected with an error.
options:
algorithm (default: HS256)expiresIn: expressed in seconds or a string describing a time span vercel/ms.
Eg:
60,"2 days","10h","7d". A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default ("120"is equal to"120ms").
notBefore: expressed in seconds or a string describing a time span vercel/ms.
Eg:
60,"2 days","10h","7d". A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default ("120"is equal to"120ms").
audienceissuerjwtidsubjectnoTimestampheaderkeyidmutatePayload: if true, the sign function will modify the payload object directly. This is useful if you need a raw reference to the payload after claims have been applied to it but before it has been encoded into a token.allowInsecureKeySizes: if true allows private keys with a modulus below 2048 to be used for RSAallowInvalidAsymmetricKeyTypes: if true, allows asymmetric keys which do not match the specified algorithm. This option is intended only for backwards compatability and should be avoided.There are no default values for
expiresIn,notBefore,audience,subject,issuer. These claims can also be provided in the payload directly withexp,nbf,aud,subandissrespectively, but you can't include in both places.
Remember that exp, nbf and iat are NumericDate, see related Token Expiration (exp claim)
The header can be customized via the options.header object.
Generated jwts will include an iat (issued at) claim by default unless noTimestamp is specified. If iat is inserted in the payload, it will be used instead of the real timestamp for calculating other things like exp given a timespan in options.expiresIn.
Synchronous Sign with default (HMAC SHA256)
var jwt = require('jsonwebtoken');
var token = jwt.sign({ foo: 'bar' }, 'shhhhh');
Synchronous Sign with RSA SHA256
// sign with RSA SHA256
var privateKey = fs.readFileSync('private.key');
var token = jwt.sign({ foo: 'bar' }, privateKey, { algorithm: 'RS256' });
Sign asynchronously
jwt.sign({ foo: 'bar' }, privateKey, { algorithm: 'RS256' }, function(err, token) {
console.log(token);
});
Backdate a jwt 30 seconds
var older_token = jwt.sign({ foo: 'bar', iat: Math.floor(Date.now() / 1000) - 30 }, 'shhhhh');
The standard for JWT defines an exp claim for expiration. The expiration is represented as a NumericDate:
A JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds. This is equivalent to the IEEE Std 1003.1, 2013 Edition [POSIX.1] definition "Seconds Since the Epoch", in which each day is accounted for by exactly 86400 seconds, other than that non-integer values can be represented. See RFC 3339 [RFC3339] for details regarding date/times in general and UTC in particular.
This means that the exp field should contain the number of seconds since the epoch.
Signing a token with 1 hour of expiration:
jwt.sign({
exp: Math.floor(Date.now() / 1000) + (60 * 60),
data: 'foobar'
}, 'secret');
Another way to generate a token like this with this library is:
jwt.sign({
data: 'foobar'
}, 'secret', { expiresIn: 60 * 60 });
//or even better:
jwt.sign({
data: 'foobar'
}, 'secret', { expiresIn: '1h' });
(Asynchronous) If a callback is supplied, function acts asynchronously. The callback is called with the decoded payload if the signature is valid and optional expiration, audience, or issuer are valid. If not, it will be called with the error.
(Synchronous) If a callback is not supplied, function acts synchronously. Returns the payload decoded if the signature is valid and optional expiration, audience, or issuer are valid. If not, it will throw the error.
Warning: When the token comes from an untrusted source (e.g. user input or external requests), the returned decoded payload should be treated like any other user input; please make sure to sanitize and only work with properties that are expected
token is the JsonWebToken string
secretOrPublicKey is a string (utf-8 encoded), buffer, or KeyObject containing either the secret for HMAC algorithms, or the PEM
encoded public key for RSA and ECDSA.
If jwt.verify is called asynchronous, secretOrPublicKey can be a function that should fetch the secret or public key. See below for a detailed example
As mentioned in this comment, there are other libraries that expect base64 encoded secrets (random bytes encoded using base64), if that is your case you can pass Buffer.from(secret, 'base64'), by doing this the secret will be decoded using base64 and the token verification will use the original random bytes.
options
algorithms: List of strings with the names of the allowed algorithms. For instance, ["HS256", "HS384"].
If not specified a defaults will be used based on the type of key provided
- secret - ['HS256', 'HS384', 'HS512']
- rsa - ['RS256', 'RS384', 'RS512']
- ec - ['ES256', 'ES384', 'ES512']
- default - ['RS256', 'RS384', 'RS512']
audience: if you want to check audience (aud), provide a value here. The audience can be checked against a string, a regular expression or a list of strings and/or regular expressions.
Eg:
"urn:foo",/urn:f[o]{2}/,[/urn:f[o]{2}/, "urn:bar"]
complete: return an object with the decoded { payload, header, signature } instead of only the usual content of the payload.issuer (optional): string or array of strings of valid values for the iss field.jwtid (optional): if you want to check JWT ID (jti), provide a string value here.ignoreExpiration: if true do not validate the expiration of the token.ignoreNotBefore...subject: if you want to check subject (sub), provide a value hereclockTolerance: number of seconds to tolerate when checking the nbf and exp claims, to deal with small clock differences among different serversmaxAge: the maximum allowed age for tokens to still be valid. It is expressed in seconds or a string describing a time span vercel/ms.
Eg:
1000,"2 days","10h","7d". A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default ("120"is equal to"120ms").
clockTimestamp: the time in seconds that should be used as the current time for all necessary comparisons.nonce: if you want to check nonce claim, provide a string value here. It is used on Open ID for the ID Tokens. (Open ID implementation notes)allowInvalidAsymmetricKeyTypes: if true, allows asymmetric keys which do not match the specified algorithm. This option is intended only for backwards compatability and should be avoided.// verify a token symmetric - synchronous
var decoded = jwt.verify(token, 'shhhhh');
console.log(decoded.foo) // bar
// verify a token symmetric
jwt.verify(token, 'shhhhh', function(err, decoded) {
console.log(decoded.foo) // bar
});
// invalid token - synchronous
try {
var decoded = jwt.verify(token, 'wrong-secret');
} catch(err) {
// err
}
// invalid token
jwt.verify(token, 'wrong-secret', function(err, decoded) {
// err
// decoded undefined
});
// verify a token asymmetric
var cert = fs.readFileSync('public.pem'); // get public key
jwt.verify(token, cert, function(err, decoded) {
console.log(decoded.foo) // bar
});
// verify audience
var cert = fs.readFileSync('public.pem'); // get public key
jwt.verify(token, cert, { audience: 'urn:foo' }, function(err, decoded) {
// if audience mismatch, err == invalid audience
});
// verify issuer
var cert = fs.readFileSync('public.pem'); // get public key
jwt.verify(token, cert, { audience: 'urn:foo', issuer: 'urn:issuer' }, function(err, decoded) {
// if issuer mismatch, err == invalid issuer
});
// verify jwt id
var cert = fs.readFileSync('public.pem'); // get public key
jwt.verify(token, cert, { audience: 'urn:foo', issuer: 'urn:issuer', jwtid: 'jwtid' }, function(err, decoded) {
// if jwt id mismatch, err == invalid jwt id
});
// verify subject
var cert = fs.readFileSync('public.pem'); // get public key
jwt.verify(token, cert, { audience: 'urn:foo', issuer: 'urn:issuer', jwtid: 'jwtid', subject: 'subject' }, function(err, decoded) {
// if subject mismatch, err == invalid subject
});
// alg mismatch
var cert = fs.readFileSync('public.pem'); // get public key
jwt.verify(token, cert, { algorithms: ['RS256'] }, function (err, payload) {
// if token alg != RS256, err == invalid signature
});
// Verify using getKey callback
// Example uses https://github.com/auth0/node-jwks-rsa as a way to fetch the keys.
var jwksClient = require('jwks-rsa');
var client = jwksClient({
jwksUri: 'https://sandrino.auth0.com/.well-known/jwks.json'
});
function getKey(header, callback){
client.getSigningKey(header.kid, function(err, key) {
var signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
});
}
jwt.verify(token, getKey, options, function(err, decoded) {
console.log(decoded.foo) // bar
});
(Synchronous) Returns the decoded payload without verifying if the signature is valid.
Warning: This will not verify whether the signature is valid. You should not use this for untrusted messages. You most likely want to use
jwt.verifyinstead.
Warning: When the token comes from an untrusted source (e.g. user input or external request), the returned decoded payload should be treated like any other user input; please make sure to sanitize and only work with properties that are expected
token is the JsonWebToken string
options:
json: force JSON.parse on the payload even if the header doesn't contain "typ":"JWT".complete: return an object with the decoded payload and header.Example
// get the decoded payload ignoring signature, no secretOrPrivateKey needed
var decoded = jwt.decode(token);
// get the decoded payload and header
var decoded = jwt.decode(token, {complete: true});
console.log(decoded.header);
console.log(decoded.payload)
Possible thrown errors during verification. Error is the first argument of the verification callback.
Thrown error if the token is expired.
Error object:
jwt.verify(token, 'shhhhh', function(err, decoded) {
if (err) {
/*
err = {
name: 'TokenExpiredError',
message: 'jwt expired',
expiredAt: 1408621000
}
*/
}
});
Error object:
.)jwt.verify(token, 'shhhhh', function(err, decoded) {
if (err) {
/*
err = {
name: 'JsonWebTokenError',
message: 'jwt malformed'
}
*/
}
});
Thrown if current time is before the nbf claim.
Error object:
jwt.verify(token, 'shhhhh', function(err, decoded) {
if (err) {
/*
err = {
name: 'NotBeforeError',
message: 'jwt not active',
date: 2018-10-04T16:10:44.000Z
}
*/
}
});
Array of supported algorithms. The following algorithms are currently supported.
| alg Parameter Value | Digital Signature or MAC Algorithm |
|---|---|
| HS256 | HMAC using SHA-256 hash algorithm |
| HS384 | HMAC using SHA-384 hash algorithm |
| HS512 | HMAC using SHA-512 hash algorithm |
| RS256 | RSASSA-PKCS1-v1_5 using SHA-256 hash algorithm |
| RS384 | RSASSA-PKCS1-v1_5 using SHA-384 hash algorithm |
| RS512 | RSASSA-PKCS1-v1_5 using SHA-512 hash algorithm |
| PS256 | RSASSA-PSS using SHA-256 hash algorithm (only node ^6.12.0 OR >=8.0.0) |
| PS384 | RSASSA-PSS using SHA-384 hash algorithm (only node ^6.12.0 OR >=8.0.0) |
| PS512 | RSASSA-PSS using SHA-512 hash algorithm (only node ^6.12.0 OR >=8.0.0) |
| ES256 | ECDSA using P-256 curve and SHA-256 hash algorithm |
| ES384 | ECDSA using P-384 curve and SHA-384 hash algorithm |
| ES512 | ECDSA using P-521 curve and SHA-512 hash algorithm |
| none | No digital signature or MAC value included |
First of all, we recommend you to think carefully if auto-refreshing a JWT will not introduce any vulnerability in your system.
We are not comfortable including this as part of the library, however, you can take a look at this example to show how this could be accomplished. Apart from that example there are an issue and a pull request to get more knowledge about this topic.
If you have found a bug or if you have a feature request, please report them at this repository issues section. Please do not report security vulnerabilities on the public GitHub issue tracker. The Responsible Disclosure Program details the procedure for disclosing security issues.
This project is licensed under the MIT license. See the LICENSE file for more info.