UNPKG

simple-node-framework

Version:

Simple nodeJs framework that provides easy ways to use log, cache, database, session, redis, share request scope and more.

281 lines (237 loc) 8.22 kB
const jwksClient = require("jwks-rsa"); const jwt = require("jsonwebtoken"); const Loggable = require("./base/loggable"); const config = require("./config"); const errorHandler = require("./error").instance; const database = require("./database").instance; const Security = require("./security"); // this class abstracts the authorization concept class Authorization extends Loggable { constructor() { super({ module: "SNF Authorization", }); this.errorHandler = errorHandler; this.jwt = jwt; this.config = config; this.jwksClient = jwksClient; this.security = new Security(); } // create JWT token // Ex.: You can call this method after your login process and than send the generated token in the 'Authorization' header for the protected routes createJWT(data, expire = true) { const options = expire ? { expiresIn: this.config.authorization.jwt.expiresIn } : {}; return this.jwt.sign(data, this.config.authorization.jwt.secret, options); } static parse(req, res, next) { if (!req) return; req.authorization = {} if (req.headers.authorization) { const [scheme, credentials] = req.headers.authorization.trim().split(' ', 2) const parsed = { scheme, credentials } if (scheme.toLowerCase() === 'basic') { const decoded = Buffer.from(credentials, 'base64').toString('utf8'); if (!decoded) throw new InvalidHeaderError('Authorization header invalid'); const [username, password] = decoded.split(':', 2) parsed.basic = { username, password } } req.authorization = parsed } return next() } // to protect a route you have to call this middleware at the route definition. after this, // it will be prepared to receive the 'Authorization' header // Ex.: server.get('/my-application/api/sample-module', authorization.protect.bind(authorization), (req, res, next) => { ... }); // curl -H 'Authorization: Bearer some_jwt_token' -X 'GET' localhost:8080/my-application/api/sample-module protect(req, res, next) { const isEnabled = this.config.authorization && this.config.authorization.enabled; if (isEnabled) { const hasSentAuthorization = Object.keys(req.authorization).length; // the req.authorization property is injected by authorizationParser plugin. if you dont want to use this plugin // you have to get the header this way: req.header('Authorization') if (hasSentAuthorization) { switch (req.authorization.scheme) { case "Basic": return this.basicValidate(req, res, next); case "Bearer": return this.bearerValidate(req, res, next); case "Auth0": return this.auth0Validate(req, res, next); default: return next( this.errorHandler.throw( "Authorization scheme defined in configuration file is not valid", "UnauthorizedError" ) ); // 401 } } else { res.header( "WWW-Authenticate", `${req.authorization.scheme} realm='Get API'` ); return next( this.errorHandler.throw( "You did not sent the authorization header", "UnauthorizedError" ) ); // 401 } } else { return next(); } } // do the basic validation of credentials basicValidate(req, res, next) { if (this.config.authorization.basicDatabase) return this.basicDatabase(req, res, next); const { username, password } = req.authorization.basic; const isValidUser = this.config.authorization.basic.users.find( (x) => String(x.username) === String(username) && String(x.password) === String(password) ); if (isValidUser) { req.user = username; next(); } else { next( this.errorHandler.throw( "Invalid username or password", "ForbiddenError" ) ); } } // do the basic validation of credentials on database async basicDatabase(req, res, next) { const { lastLogin } = this.config.authorization.basicDatabase const { username, password } = req.authorization.basic; const module = process.env.SNF_CRYPTO_MODULE; const model = await this.getSnfAuthorizationModel() const user = await model.findOne({ user: username, module }); if ( user && (await this.security.decrypt(user.encryptedPassword)) === password ) { req.user = username; if (lastLogin !== false) { user.lastLogin = new Date(); user.save(); } next(); } else { next( this.errorHandler.throw( "Invalid username or password", "ForbiddenError" ) ); } } async getSnfAuthorizationModel(autoConnect = false) { const connectionName = this.config.authorization.basicDatabase.connectionName || 'application' if (autoConnect && !database.connections.mongodb) { await database.connectOnMongo(this.config.db['mongodb'][connectionName], connectionName) } const connection = database.connections.mongodb[connectionName]; const model = connection.models.SNFAuthorization || connection.model( "SNFAuthorization", new database.mongoose.Schema( { user: { type: String, index: true, unique: true }, encryptedPassword: Object, module: { type: String, index: true }, lastLogin: Date, }, { autoCreate: true, collection: "snf-authorization", } ) ); return model } async createUser(user, password, module) { const sec = new Security(); if (!process.env.SNF_CRYPTO_KEY) { console.log('You need set SNF_CRYPTO_KEY environment variable.'); return; } if (!password) { console.log('You must set a password'); return; } console.log(`Creating user [${user}]...`); const newPassword = await sec.encrypt(password); const model = await this.getSnfAuthorizationModel(true) await model.create({ user, encryptedPassword: newPassword, module, }) const moduleDetail = module ? ` and module [${module}] ` : ' '; console.log(`Your user [${user}] with password [${password}]${moduleDetail}was created at snf-authorization collection`); await database.close() return newPassword; } // do the bearer validation of credentials // sample jwt: eyJhbGciOiJIUzI1NiJ9.bm90aGluZw.pncy0bQ9c9J4AAuQQfnJNtH8LBV1FiuPJBxfJbzlIFA bearerValidate(req, res, next) { const key = req.authorization.credentials; this.jwt.verify( key, this.config.authorization.jwt.secret, (err, decoded) => { if (err) { if (err.name === "TokenExpiredError") { next(this.errorHandler.throw("Expired JWT.", "ForbiddenError")); // 403 } else { next(this.errorHandler.throw("Invalid JWT", "UnauthorizedError")); // 401 } } else { req.user = decoded; next(); } } ); } auth0Validate(req, res, next) { const key = req.authorization.credentials; const { domain } = this.config.authorization.auth0; const getJwtPublicKey = (header, callback) => { const client = this.jwksClient({ strictSsl: false, jwksUri: `https://${domain}/.well-known/jwks.json`, }); client.getSigningKey(header.kid, (err, credentialKey) => { const signingKey = credentialKey.publicKey || credentialKey.rsaPublicKey; callback(null, signingKey); }); }; this.jwt.verify(key, getJwtPublicKey, {}, (err, decoded) => { if (err) { if (err.name === "TokenExpiredError") { next(this.errorHandler.throw("Expired JWT.", "ForbiddenError")); // 403 } else { next(this.errorHandler.throw("Invalid JWT", "UnauthorizedError")); // 401 } } req.user = decoded; next(); }); } } module.exports = { class: Authorization, instance: new Authorization(), };