OWASP/SSO_Project

View on GitHub
js-backend/utils/middleware.js

Summary

Maintainability
A
1 hr
Test Coverage
const crypto = require("crypto");
const rateLimit = require("express-rate-limit");
const { User, JWT, Audit } = require("../utils");

// This class is prone to circular dependancies (if part of utils) and missing context (https://stackoverflow.com/q/45643005/1424378) - beware
class MiddlewareHelper {
    constructor(db) {
        this.ownJwtToken = process.env.UNIQUEJWTTOKEN;
        this.hostname = process.env.DOMAIN || "localhost";
        this.frontendPort = process.env.FRONTENDPORT || 8080;
    }
    
    // Protection against timing attacks
    antiTiming(req, res, next) {
        crypto.randomBytes(4, function(ex, buf) {
            var hex = buf.toString("hex");
            var randInt = parseInt(hex, 16);

            setTimeout(() => {
                next();
            }, randInt % 1500);
        });
    }

    // Middleware for Authorization header
    parseAuthHeader(req, res, next) {
        if(!req || !req.headers || !req.headers.authorization) return next();
        const authHeader = req.headers.authorization;
        if(authHeader.indexOf(" ") == -1) return next();

        const headerParts = authHeader.split(" ");
        if(headerParts[0] != "Bearer") return next();

        this.checkAuthToken(headerParts[1]).then(authentication => {
            if(Number.isInteger(authentication.sub)) {
                req.user = authentication;
            }

            next();
        }).catch(() => {
            next();
        });
    }

    checkAuthToken(token) {
        return JWT.verify(token, this.ownJwtToken, {
            maxAge: JWT.age().LONG,
        });
    }

    // Is the user just logged in (first factor)?
    isLoggedIn(req, res, next) {
        if(req.user && req.user.id) {
            next();
        } else {
            res.status(403).send("You need to be signed in");
        }
    }

    // Is the user device authenticated (via 2FA)?
    isAuthenticated(req, res, next) {
        if(!req.user) return res.status(403).send("User not logged in");
        if(!req.user.token) return res.status(403).send("Authorization token missing");
        
        User.validateSession(req.user.token).then(session => {
            if(req.user.id == session.userId) {
                next();
            } else {
                User.deleteSession(req.user.token).then(() => {
                    res.status(400).send("Token mismatch");
                });
            }
        }).catch(err => {
            res.status(400).send(err);
        });
    }

    showSuccess(req, res) {
        return res.status(200).send("success");
    }

    createLoginToken(req, res, next) {
        const email = req.loginEmail;
        
        User.findUserByName(email).then(user => {
            return JWT.sign({
                sub: user.id,
                id: user.id,
                username: email,
            }, this.ownJwtToken, JWT.age().MEDIUM);
        }).then(jwtData => {
            let returnObj = {
                "token": jwtData,
                "username": email,
                "factor": 1,
            };
            if(req.returnExtra) {
                returnObj = Object.assign(returnObj, req.returnExtra);
            }
            
            return res.status(200).json(returnObj);
        }).catch(err => {
            console.error(err);
            res.status(400).send(err.message);
        });
    }

    createAuthToken(req, res, next) {
        if(!req.user.id) {
            return res.status(403).send("User needs to be logged in to finish authentication");
        }
        
        let publicAttributes;
        Promise.all([
            User.createSession(req.user.id),
            User.findUserById(req.user.id),
        ]).then(values => {
            const {password, last_login, created, ...publicAttributesTmp} = values[1];
            publicAttributes = publicAttributesTmp;
            publicAttributes.sub = req.user.id;
            publicAttributes.token = values[0];
            publicAttributes.authenticators.forEach(v => {
                delete v.userCounter;
                delete v.userKey;
            });
            
            return JWT.sign(publicAttributes, this.ownJwtToken, JWT.age().LONG);
        }).then(jwtData => {
            let returnObj = {
                "token": jwtData,
                "username": publicAttributes.username,
                "factor": 2,
            };
            if(req.returnExtra) {
                returnObj = Object.assign(returnObj, req.returnExtra);
            }
            
            if(req.body.authorizationToken) {
                res.status(200).send("<script>const authData = JSON.parse('" +JSON.stringify(returnObj)+ "'); window.parent.postMessage(authData, 'https://" + this.hostname + ":" + this.frontendPort + "');</script>");
            } else {
                res.status(200).json(returnObj);
            }
        }).catch(err => {
            console.error(err);
            return res.status(400).send(err.message);
        });
    }
    
    rateLimit(windowM, max, message) {
        return rateLimit({
            windowMs: windowM * 60 * 1000,
            max,
            message,
            headers: false,
            keyGenerator: req => {
                return Audit.getIP(req);
            },
            skip: req => {
                // Whitelist docker networks, where we don't get a real IP and every user has the same IP
                const userIP = Audit.getIP(req);
                const skipIp = (userIP.startsWith("172.") || userIP.startsWith("192."));
                //console.log("skip rate", skipIp);
                return skipIp;
            },
        });
    }
}

exports.MiddlewareHelper = MiddlewareHelper;