key-sentry
Version:
A secure JWT token generation and management library
150 lines (133 loc) • 5.12 kB
JavaScript
const jwt = require("jsonwebtoken");
const crypto = require("crypto");
class KeySentry {
#secretKey;
#expiresIn;
#algorithm;
#refreshSecretKey;
#refreshExpiresIn;
/**
* Creates a JWT service instance
* @param {Object} options - JWT configuration options
* @param {string} options.secretKey - Secret key for signing tokens
* @param {string} options.expiresIn - Token expiration time (e.g. "1h", "7d")
* @param {string} options.algorithm - Algorithm used for signing (default: "HS256")
* @param {string} options.refreshSecretKey - Secret key for refresh tokens (defaults to different key)
* @param {string} options.refreshExpiresIn - Refresh token expiration (e.g. "7d", "30d")
*/
constructor(
secretKey,
expiresIn,
algorithm,
refreshSecretKey,
refreshExpiresIn
) {
this.#secretKey = secretKey || this.#generateSecretKey();
this.#expiresIn = expiresIn || "1h";
this.#algorithm = algorithm || "HS256";
this.#refreshSecretKey = refreshSecretKey || this.#generateSecretKey();
this.#refreshExpiresIn = refreshExpiresIn || "7d";
}
#generateSecretKey() {
return crypto.randomBytes(32).toString("hex");
}
#decodeToken(token) {
return jwt.decode(token);
}
/**
* Generates access token and optionally a refresh token
* @param {Object} payload - Data to be included in the token
* @param {boolean} [withRefreshToken=true] - Whether to generate a refresh token alongside the access token
* @returns {Object} - Object containing generated tokens and metadata
* @returns {string} - return.accessToken - The generated JWT access token
* @returns {number} - return.accessTokenExpiresAt - Access token expiration timestamp in milliseconds
* @returns {number} - return.issuedAt - Token issuance timestamp in milliseconds
* @returns {Object} - return.payload - The payload data included in the token
* @returns {string} - [return.refreshToken] - The refresh token (if withRefreshToken is true)
* @returns {number} - [return.refreshTokenExpiresAt] - Refresh token expiration timestamp (if withRefreshToken is true)
* @returns {number} - [return.refreshIssuedAt] - Refresh token issuance timestamp (if withRefreshToken is true)
* @throws {Error} - If payload is invalid or token generation fails
*/
generateToken(payload, withRefreshToken = true) {
if (!payload) {
throw new Error("Payload is required to generate a token.");
}
try {
const accessToken = jwt.sign(payload, this.#secretKey, {
expiresIn: this.#expiresIn,
algorithm: this.#algorithm,
});
const decodedAccess = this.#decodeToken(accessToken);
const result = {
accessToken,
accessTokenExpiresAt: decodedAccess.exp * 1000,
issuedAt: decodedAccess.iat * 1000,
payload: { ...payload },
};
if (!withRefreshToken) {
return result;
}
const refreshPayload = {
...payload,
jti: crypto.randomBytes(16).toString("hex"),
};
const refreshToken = jwt.sign(refreshPayload, this.#refreshSecretKey, {
expiresIn: this.#refreshExpiresIn,
algorithm: this.#algorithm,
});
const decodedRefresh = this.#decodeToken(refreshToken);
result.refreshToken = refreshToken;
result.refreshTokenExpiresAt = decodedRefresh.exp * 1000;
result.refreshIssuedAt = decodedRefresh.iat * 1000;
return result;
} catch (error) {
throw new Error(`Failed to generate token: ${error.message}`);
}
}
/**
* Verifies a token's authenticity and expiration status
* @param {string} token - The token to verify
* @param {string} tokenType - Type of token ('access' or 'refresh')
* @returns {Object} - Verification result with validity, expiration status and payload
* @throws {Error} - If token is invalid or verification fails
*/
verifyToken(token, tokenType = "access") {
const validTokenTypes = ["access", "refresh"];
if (!validTokenTypes.includes(tokenType)) {
throw new Error(
`Invalid token type. Expected one of ${validTokenTypes.join(", ")}`
);
}
if (!token || typeof token !== "string") {
throw new Error("Token must be a non-empty string");
}
const secretKey =
tokenType === "refresh" ? this.#refreshSecretKey : this.#secretKey;
try {
const payload = jwt.verify(token, secretKey, {
algorithms: [this.#algorithm],
});
return {
valid: true,
expired: false,
payload,
};
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
const payload = this.#decodeToken(token);
return {
valid: false,
expired: true,
payload,
};
}
return {
valid: false,
expired: false,
payload: null,
error: error.message,
};
}
}
}
module.exports = KeySentry;