1
0
mirror of https://github.com/Foltik/Shimapan synced 2025-02-09 03:33:39 -05:00

Clean up auth code and util

This commit is contained in:
Jack Foltz 2019-01-02 15:20:53 -05:00
parent 03b8c53885
commit d04f681986
Signed by: foltik
GPG Key ID: D1F0331758D1F29A
4 changed files with 81 additions and 97 deletions

View File

@ -2,17 +2,18 @@ const express = require('express');
const router = express.Router();
const config = require('config');
const fs = require('fs').promises;
const passport = require('passport');
const canonicalize = require('../../util/auth/canonicalize');
const ModelPath = '../../models/';
const User = require(ModelPath + 'User.js');
const Invite = require(ModelPath + 'Invite.js');
const passport = require('passport');
const canonicalizeRequest = require('../../util/canonicalize').canonicalizeRequest;
const requireAuth = require('../../util/auth').requireAuth;
const verifyBody = require('../../util/verifyBody');
const rateLimit = require('express-rate-limit');
const rateLimit = require('../../util/rateLimit');
// Wraps passport.authenticate to return a promise
const authenticate = (req, res, next) => {
@ -23,113 +24,87 @@ const authenticate = (req, res, next) => {
});
};
// Wraps passport session creation for async usage
// Wraps passport session creation to return a promise
const login = (user, req) => {
return new Promise((resolve) => {
req.login(user, resolve);
});
};
// Query the database for a valid invite code. An error message property is set if invalid.
const validateInvite = async (req, res, next) => {
const invite = await Invite.findOne({code: req.body.invite}).catch(next);
if (!invite) {
// Log failure
await fs.appendFile('auth.log', `${new Date().toISOString()} register ${req.ip}\n`);
return res.status(422).json({message: 'Invalid invite code.'});
}
if (invite.used)
return res.status(422).json({message: 'Invite already used.'});
if (invite.expires != null && invite.expires < Date.now())
return res.status(422).json({message: 'Invite expired.'});
req.invite = invite;
next();
};
// Check if the requested username is valid
const validateUsername = async (req, res, next) => {
const username = req.body.username;
const count = await User.countDocuments({username: username}).catch(next);
if (count !== 0)
return res.status(422).json({message: 'Username in use.'});
next();
};
const registerLimiter = config.get('RateLimit.enable')
? rateLimit({
windowMs: config.get('RateLimit.register.window') * 1000,
max: config.get('RateLimit.register.max'),
skipSuccessfulRequests: true
})
: (req, res, next) => { next(); };
const registerProps = [
{
name: 'displayname',
type: 'string',
maxLength: config.get('User.Username.maxLength'),
sanitize: true,
restrict: new RegExp(config.get('User.Username.restrictedChars')),
},
const registerParams = [
{name: 'displayname', type: 'string', maxLength: config.get('User.Username.maxLength'), sanitize: true, restrict: new RegExp(config.get('User.Username.restrictedChars'))},
{name: 'password', type: 'string'},
{name: 'invite', type: 'string'}];
router.post('/register',
registerLimiter,
verifyBody(registerProps), canonicalizeRequest,
validateInvite, validateUsername,
async (req, res, next) => {
// Update the database
await Promise.all([
User.register({
username: req.body.username,
rateLimit(config.get('RateLimit.register.window'), config.get('RateLimit.register.max'), true),
verifyBody(registerParams),
async (req, res) => {
const username = canonicalize(req.body.displayname);
// Retrieve invite and username status
const [invite, usernameCount] = await Promise.all([
Invite.findOne({code: req.body.invite}),
User.countDocuments({username: username})
]);
// Validate the invite
if (!invite)
return res.status(422).json({message: 'Invalid invite code.'});
if (invite.used)
return res.status(422).json({message: 'Invite already used.'});
if (invite.expires != null && invite.expires < Date.now())
return res.status(422).json({message: 'Invite expired.'});
// Validate the username
if (usernameCount !== 0)
return res.status(422).json({message: 'Username in use.'});
// Create the user object
await User.register({
username: username,
displayname: req.body.displayname,
scope: req.invite.scope,
scope: invite.scope,
date: Date.now()
}, req.body.password).catch(next),
Invite.updateOne({code: req.invite.code}, {recipient: req.body.username, used: Date.now()}).catch(next)
]);
}, req.body.password);
res.status(200).json({'message': 'Registration successful.'});
});
// Update the invite as used
await Invite.updateOne({code: invite.code}, {recipient: username, used: Date.now()});
const loginLimiter = config.get('RateLimit.enable')
? rateLimit({
windowMs: config.get('RateLimit.login.window') * 1000,
max: config.get('RateLimit.login.max'),
skipSuccessfulRequests: true
})
: (req, res, next) => { next(); };
const loginProps = [
{name: 'username', type: 'string', optional: true},
{name: 'displayname', type: 'string', optional: true},
res.status(200).json({'message': 'Registration successful.'});
});
const loginParams = [
{name: 'displayname', type: 'string'},
{name: 'password', type: 'string'}];
router.post('/login',
loginLimiter,
verifyBody(loginProps),
canonicalizeRequest,
rateLimit(config.get('RateLimit.login.window'), config.get('RateLimit.login.max'), true),
verifyBody(loginParams),
async (req, res, next) => {
// Authenticate
const user = await authenticate(req, res, next);
if (!user) {
// Log failure
await fs.appendFile('auth.log', `${new Date().toISOString()} login ${req.ip}\n`);
return res.status(401).json({'message': 'Unauthorized.'});
}
req.body.username = canonicalize(req.body.displayname);
// Create session
await login(user, req);
// Authenticate
const user = await authenticate(req, res, next);
if (!user) {
// Log failure
await fs.appendFile('auth.log', `${new Date().toISOString()} login ${req.ip}\n`);
return res.status(401).json({'message': 'Unauthorized.'});
}
// Create session
await login(user, req);
// Set session vars
req.session.passport.displayname = user.displayname;
req.session.passport.scope = user.scope;
res.status(200).json({'message': 'Logged in.'});
});
// Set session vars
req.session.passport.displayname = user.displayname;
req.session.passport.scope = user.scope;
res.status(200).json({'message': 'Logged in.'});
});
router.post('/logout', (req, res) => {
if (!req.isAuthenticated())
@ -139,6 +114,8 @@ router.post('/logout', (req, res) => {
res.status(200).json({'message': 'Logged out.'});
});
router.get('/whoami', requireAuth(), (req, res) => {
res.status(200).json({
username: req.username,

View File

@ -0,0 +1,3 @@
const canonicalize = str => str.normalize('NFKD').toLowerCase();
module.exports = canonicalize;

9
app/util/rateLimit.js Normal file
View File

@ -0,0 +1,9 @@
const config = require('config');
const rateLimit = require('express-rate-limit');
const rateLimitRequest = (window, max, skipSuccessful) =>
config.get('RateLimit.enable')
? rateLimit({windowMs: window * 1000, max: max, skipSuccessfulRequests: skipSuccessful})
: (req, res, next) => { next(); };
module.exports = rateLimitRequest;

View File

@ -13,7 +13,7 @@ const Invite = require(ModelPath + 'Invite.js');
const View = require(ModelPath + 'View.js');
const util = require('./testUtil.js');
const canonicalize = require('../app/util/canonicalize').canonicalize;
const canonicalize = require('../app/util/auth/canonicalize');
const config = require('config');
let app;
@ -171,11 +171,6 @@ describe('Authentication', () => {
return verifySuccessfulLogin({displayname: 'user', password: 'pass'});
});
it('SHOULD accept a username instead of a displayname', async () => {
await util.createTestUser(agent);
return verifySuccessfulLogin({username: 'user', password: 'pass'});
});
it('SHOULD accept any non-normalized variant of a username with a valid password', async () => {
await util.createTestInvite();
await util.registerUser({displayname: 'ᴮᴵᴳᴮᴵᴿᴰ', password: 'pass', invite: 'code'}, agent);