@hutiwephy/ape
Version:
Authenticate and Encrypt HTTP with User Credentials (Not TLS-SRP).
296 lines (267 loc) • 10.2 kB
JavaScript
const CryptoJS = require("crypto-js");
const jwt = (function(){
// Cryptography Compatibility Layer (CryptoJS required!!!)
const c = (function(){
function sign(algorithm, data, key){
if(algorithm.startsWith("H")){ // HS256, HS384, HS512 => HMAC-SHA*
var alg_hash = `HmacSHA${algorithm.split("S")[1]}`;
return CryptoJS[alg_hash](data, key);
}else{
throw "Unsupported algorithm sanity is to low";
}
}
function verify(algorithm, data, key, signature){
if(algorithm.startsWith("H")){ // HMAC-SHA*
var tmp = sign(algorithm, data, key);
return (CryptoJS.enc.Base64.stringify(tmp) === CryptoJS.enc.Base64.stringify(signature))? true : false;
}else{
throw "Unsupported algorithm sanity is to low";
}
}
return {
sign,
verify,
};
})();
const RegisteredClaims = {
ISSUER: "iss",
SUBJECT: "sub",
AUDIENCE: "aud",
EXPIRATION: "exp",
NOTBEFORE: "nbf",
ISSUEDAT: "iat",
JWTID: "jti"
};
/**
* Decode a JWT, but do not verify it's validity
*
* @param {string} token
* @returns {({
* header: {
* alg: string,
* typ: string,
* cty?: string,
* },
* payload: Uint8Array|Buffer|{
* iss?: string,
* sub?: string,
* aud?: string,
* exp?: string,
* nbf?: string,
* iat?: string,
* jti?: string,
* },
* content: string,
* signature?: Uint8Array|Buffer,
* })}
*/
function decode(token){
if(typeof token != "string"){
throw new TypeError("Invalid Data Type");
}
var parts = token.split(".");
if(parts.length < 2 || parts.length > 3){
throw new Error("Not a JWT");
}
var tmp = {
header: null,
payload: null,
content: CryptoJS.enc.Utf8.parse(`${parts[0]}.${parts[1]}`),
};
function Base64Url2object(str){
return JSON.parse(CryptoJS.enc.Utf8.stringify(CryptoJS.enc.Base64url.parse(str)));
}
try{
tmp.header = Base64Url2object(parts[0]);
}catch(exp){
throw new Error("Invalid Header format");
}
try{
tmp.payload = Base64Url2object(parts[1]);
}catch(exp){
throw new Error("Invalid Payload format");
}
if(parts.length == 3){
if(tmp.header.alg == "none"){
throw new Error("Algorithm not provided but signature section present");
}
tmp.signature = CryptoJS.enc.Base64url.parse(parts[2]);
}
return tmp;
}
function validatorTester(title, validator, data){
if(typeof validator == "function"){
if(!validator(data)){
throw new Error(`Invalid ${title}`);
}
}else{
if(typeof validator.includes == "function"){
if(!validator.includes(data)){
throw new Error(`Invalid ${title}`);
}else{
throw new TypeError(`Invalid ${title} Validator Type`);
}
}
}
}
function validateFields(result, options={}){
var payload = result.payload;
var entries = Object.keys(payload);
var keys = Object.keys(options);
var validators = (options.validators == undefined)? [] : Object.keys(options.validators);
if(keys.includes("critical")){
if(!Array.isArray(options.critical)){
throw new TypeError(`Invalid "critical" Type expected Array.<string> got ${typeof options.critical}`);
}
// Negative check if all the values in options.critical are in payload
if(!options.critical.every(function(value){ return payload.includes(value); })){
throw new Error("Invalid Token not all required paramters are present");
}
}
if(keys.includes("validators")){
if(entries.includes(RegisteredClaims.ISSUER) && validators.includes("issuer")){
validatorTester("Issuer", options.validators.issuer, payload[RegisteredClaims.ISSUER]);
}
if(entries.includes(RegisteredClaims.SUBJECT) && validators.includes("subject")){
validatorTester("Subject", options.validators.subject, payload[RegisteredClaims.SUBJECT]);
}
if(entries.includes(RegisteredClaims.AUDIENCE) && validators.includes("audience")){
validatorTester("Audience", options.validators.audience, payload[RegisteredClaims.AUDIENCE]);
}
if(entries.includes(RegisteredClaims.JWTID) && validators.includes("jwtid")){
validatorTester("JWT ID", options.validators.jwtid, payload[RegisteredClaims.JWTID]);
}
}
var date = Date.now();
var tolerance = null;
if(keys.includes("date")){
if(typeof options.date != "number"){
throw new TypeError(`Invalid "date" Type expected number got ${typeof options.date}`);
}
date = options.date;
}
if(keys.includes("tolerance")){
if(typeof options.tolerance != "number"){
throw new TypeError(`Invalid "tolerance" Type expected number got ${typeof options.tolerance}`);
}
tolerance = options.tolerance;
if(keys.includes("maxlifetime")){
if(typeof options.maxlifetime != "number"){
throw new TypeError(`Invalid "maxlifetime" Type expected number got ${typeof options.maxlifetime}`);
}
var maxlifetime = options.maxlifetime;
// Verify exp-iat
if(entries.includes(RegisteredClaims.EXPIRATION) && entries.includes(RegisteredClaims.ISSUEDAT)){
if(payload[RegisteredClaims.EXPIRATION] - payload[RegisteredClaims.ISSUEDAT] >= maxlifetime+tolerance){
throw new Error(`Invalid Token violates maximum lifetime constrain`);
}
}
}
// Verify exp
if(entries.includes(RegisteredClaims.EXPIRATION)){
if(payload[RegisteredClaims.EXPIRATION] >= date+tolerance){
throw new Error(`Invalid Token violates expiration constrain`);
}
}
// Verify nbf
if(entries.includes(RegisteredClaims.NOTBEFORE)){
if(payload[RegisteredClaims.NOTBEFORE] <= date-tolerance){
throw new Error(`Invalid Token violates not before constrain`);
}
}
// Verify iat
if(entries.includes(RegisteredClaims.ISSUEDAT)){
if(payload[RegisteredClaims.ISSUEDAT] <= date-tolerance){
throw new Error(`Invalid Token violates issue date constrain`);
}
}
}
return true;
}
function validate(token, options){
var tmp = decode(token);
validateFields(tmp, options);
return tmp;
}
function encode(header, payload){
if(typeof header != "object"){
throw new TypeError("Invalid header type expected object");
}
if(typeof payload != "object"){
throw new TypeError("Invalid payload type expected object");
}
if(typeof header.alg == "undefined"){
throw new Error("Algorithm not provided");
}
function object2Base64Url(obj){
return CryptoJS.enc.Base64url.stringify(CryptoJS.enc.Utf8.parse(JSON.stringify(obj)));
}
return `${object2Base64Url(header)}.${object2Base64Url(payload)}`;
}
return {
RegisteredClaims,
/**
* Decode a JWT, but do not verify it's validity
*
* @param {string} token
* @returns {({
* header: {
* alg: string,
* typ: string,
* cty?: string,
* },
* payload: Uint8Array|Buffer|{
* iss?: string,
* sub?: string,
* aud?: string,
* exp?: string,
* nbf?: string,
* iat?: string,
* jti?: string,
* },
* content: string,
* signature?: Uint8Array|Buffer,
* })}
*/
decode,
/**
* Creates a JWT from an header and payload object and signs it with a key
*
* @param {object} header
* @param {object} payload
* @param {object|string} key
* @returns {string} JWT string
*/
sign: function(header, payload, key){
var tmp = encode(header, payload);
if(header.alg == "none"){ return tmp; }
tmp += `.${CryptoJS.enc.Base64url.stringify(c.sign(header.alg, CryptoJS.enc.Utf8.parse(tmp), key))}`;
return tmp;
},
/**
* Validates a JWT
*
* @param {string} token
* @param {Buffer} key
* @param {({
* critical?: Array.<string>,
* date?: number,
* maxlifetime?: number,
* tolerance?: number,
* validators?: {
* issuer?: Array.<string>|function(string):boolean,
* subject?: Array.<string>|function(string):boolean,
* audience?: Array.<string>|function(string):boolean,
* jwtid?: Array.<string>|function(string):boolean,
* },
* })} options
* @returns {boolean}
*/
verify: function(token, key, options={}){
var tmp = validate(token, options);
if(tmp.header.alg == "none"){ return true; }
return c.verify(tmp.header.alg, tmp.content, key, tmp.signature);
},
};
})();
module.exports = jwt;