Skip to content

Commit 8aa173a

Browse files
committed
Finish MFA implementation
1 parent 35938db commit 8aa173a

File tree

20 files changed

+7843
-1507
lines changed

20 files changed

+7843
-1507
lines changed

backend/internal/mfa.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
const authModel = require('../models/auth');
2+
const error = require('../lib/error');
3+
const speakeasy = require('speakeasy');
4+
5+
module.exports = {
6+
validateMfaTokenForUser: (userId, token) => {
7+
return authModel
8+
.query()
9+
.where('user_id', userId)
10+
.first()
11+
.then((auth) => {
12+
if (!auth || !auth.mfa_enabled) {
13+
throw new error.AuthError('MFA is not enabled for this user.');
14+
}
15+
const verified = speakeasy.totp.verify({
16+
secret: auth.mfa_secret,
17+
encoding: 'base32',
18+
token: token,
19+
window: 2
20+
});
21+
if (!verified) {
22+
throw new error.AuthError('Invalid MFA token.');
23+
}
24+
return true;
25+
});
26+
},
27+
isMfaEnabledForUser: (userId) => {
28+
return authModel
29+
.query()
30+
.where('user_id', userId)
31+
.first()
32+
.then((auth) => {
33+
if (!auth) {
34+
throw new error.AuthError('User not found.');
35+
}
36+
return auth.mfa_enabled === 1;
37+
});
38+
},
39+
createMfaSecretForUser: (userId) => {
40+
const secret = speakeasy.generateSecret({ length: 20 });
41+
console.log(secret);
42+
return authModel
43+
.query()
44+
.where('user_id', userId)
45+
.update({
46+
mfa_secret: secret.base32
47+
})
48+
.then(() => secret);
49+
},
50+
enableMfaForUser: (userId, token) => {
51+
return authModel
52+
.query()
53+
.where('user_id', userId)
54+
.first()
55+
.then((auth) => {
56+
if (!auth || !auth.mfa_secret) {
57+
throw new error.AuthError('MFA is not set up for this user.');
58+
}
59+
const verified = speakeasy.totp.verify({
60+
secret: auth.mfa_secret,
61+
encoding: 'base32',
62+
token: token,
63+
window: 2
64+
});
65+
if (!verified) {
66+
throw new error.AuthError('Invalid MFA token.');
67+
}
68+
return authModel
69+
.query()
70+
.where('user_id', userId)
71+
.update({ mfa_enabled: 1 })
72+
.then(() => true);
73+
});
74+
},
75+
};

backend/internal/token.js

Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const userModel = require('../models/user');
44
const authModel = require('../models/auth');
55
const helpers = require('../lib/helpers');
66
const TokenModel = require('../models/token');
7+
const mfa = require('../internal/mfa'); // <-- added MFA import
78

89
const ERROR_MESSAGE_INVALID_AUTH = 'Invalid email or password';
910

@@ -21,6 +22,8 @@ module.exports = {
2122
getTokenFromEmail: (data, issuer) => {
2223
let Token = new TokenModel();
2324

25+
console.log(data);
26+
2427
data.scope = data.scope || 'user';
2528
data.expiry = data.expiry || '1d';
2629

@@ -41,34 +44,66 @@ module.exports = {
4144
.then((auth) => {
4245
if (auth) {
4346
return auth.verifyPassword(data.secret)
44-
.then((valid) => {
47+
.then(async (valid) => {
4548
if (valid) {
46-
4749
if (data.scope !== 'user' && _.indexOf(user.roles, data.scope) === -1) {
48-
// The scope requested doesn't exist as a role against the user,
49-
// you shall not pass.
5050
throw new error.AuthError('Invalid scope: ' + data.scope);
5151
}
52-
53-
// Create a moment of the expiry expression
54-
let expiry = helpers.parseDatePeriod(data.expiry);
55-
if (expiry === null) {
56-
throw new error.AuthError('Invalid expiry time: ' + data.expiry);
57-
}
58-
59-
return Token.create({
60-
iss: issuer || 'api',
61-
attrs: {
62-
id: user.id
63-
},
64-
scope: [data.scope],
65-
expiresIn: data.expiry
66-
})
67-
.then((signed) => {
68-
return {
69-
token: signed.token,
70-
expires: expiry.toISOString()
71-
};
52+
return await mfa.isMfaEnabledForUser(user.id)
53+
.then((mfaEnabled) => {
54+
if (mfaEnabled) {
55+
if (!data.mfa_token) {
56+
throw new error.AuthError('MFA token required');
57+
}
58+
console.log(data.mfa_token);
59+
return mfa.validateMfaTokenForUser(user.id, data.mfa_token)
60+
.then((mfaValid) => {
61+
if (!mfaValid) {
62+
throw new error.AuthError('Invalid MFA token');
63+
}
64+
// Create a moment of the expiry expression
65+
let expiry = helpers.parseDatePeriod(data.expiry);
66+
if (expiry === null) {
67+
throw new error.AuthError('Invalid expiry time: ' + data.expiry);
68+
}
69+
70+
return Token.create({
71+
iss: issuer || 'api',
72+
attrs: {
73+
id: user.id
74+
},
75+
scope: [data.scope],
76+
expiresIn: data.expiry
77+
})
78+
.then((signed) => {
79+
return {
80+
token: signed.token,
81+
expires: expiry.toISOString()
82+
};
83+
});
84+
});
85+
} else {
86+
// Create a moment of the expiry expression
87+
let expiry = helpers.parseDatePeriod(data.expiry);
88+
if (expiry === null) {
89+
throw new error.AuthError('Invalid expiry time: ' + data.expiry);
90+
}
91+
92+
return Token.create({
93+
iss: issuer || 'api',
94+
attrs: {
95+
id: user.id
96+
},
97+
scope: [data.scope],
98+
expiresIn: data.expiry
99+
})
100+
.then((signed) => {
101+
return {
102+
token: signed.token,
103+
expires: expiry.toISOString()
104+
};
105+
});
106+
}
72107
});
73108
} else {
74109
throw new error.AuthError(ERROR_MESSAGE_INVALID_AUTH);

backend/internal/user.js

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ const authModel = require('../models/auth');
77
const gravatar = require('gravatar');
88
const internalToken = require('./token');
99
const internalAuditLog = require('./audit-log');
10-
const authenticator = require('authenticator');
11-
const qrcode = require('qrcode');
1210

1311
function omissions () {
1412
return ['is_deleted'];
@@ -511,35 +509,6 @@ const internalUser = {
511509
});
512510
},
513511

514-
createMFAKey: (access, data) => {
515-
return access.can('users:activate_mfa', data.id)
516-
.then(() => {
517-
return internalUser.get(access, {id: data.id});
518-
})
519-
.then((user) => {
520-
let secret = authenticator.generateKey();
521-
return userModel
522-
.query()
523-
.patchAndFetchById(user.id, { mfa_key: secret })
524-
.then(() => {
525-
let uri = authenticator.generateTotpUri(secret, user.email, 'NginxProxyManager');
526-
return qrcode.toDataURL(uri);
527-
})
528-
.then((qrCode) => {
529-
return { user, qrCode };
530-
});
531-
})
532-
.then(({ user, qrCode }) => {
533-
return internalAuditLog.add(access, {
534-
action: 'updated',
535-
object_type: 'user',
536-
object_id: user.id,
537-
meta: data
538-
539-
})
540-
.then(() => ({ user, qrCode }));
541-
});
542-
}
543512
};
544513

545514
module.exports = internalUser;

backend/migrations/20250115041439_mfa_integeration.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ exports.up = function (knex/*, Promise*/) {
1414

1515
logger.info('[' + migrate_name + '] Migrating Up...');
1616

17-
return knex.schema.alterTable('user', (table) => {
17+
return knex.schema.alterTable('auth', (table) => {
1818
table.string('mfa_secret');
19+
table.boolean('mfa_enabled').defaultTo(false);
1920
})
2021
.then(() => {
2122
logger.info('[' + migrate_name + '] User Table altered');
@@ -33,8 +34,9 @@ exports.up = function (knex/*, Promise*/) {
3334
exports.down = function (knex/*, Promise*/) {
3435
logger.info('[' + migrate_name + '] Migrating Down...');
3536

36-
return knex.schema.alterTable('user', (table) => {
37+
return knex.schema.alterTable('auth', (table) => {
3738
table.dropColumn('mfa_key');
39+
table.dropColumn('mfa_enabled');
3840
})
3941
.then(() => {
4042
logger.info('[' + migrate_name + '] User Table altered');

backend/models/certificate.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ class Certificate extends Model {
6767
}
6868

6969
static get relationMappings () {
70-
const ProxyHost = require('./proxy_host');
71-
const DeadHost = require('./dead_host');
72-
const User = require('./user');
70+
const ProxyHost = require('./proxy_host');
71+
const DeadHost = require('./dead_host');
72+
const User = require('./user');
7373
const RedirectionHost = require('./redirection_host');
7474

7575
return {

0 commit comments

Comments
 (0)