Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 30 additions & 14 deletions lib/api/2fa/totp.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,15 @@ module.exports = (db, server, userHandler) => {
},
queryParams: {},
pathParams: { user: userId },
response: { 200: { description: 'Success', model: Joi.object({ success: successRes }).$_setFlag('objectName', 'SuccessResponse') } }
response: {
200: {
description: 'Success',
model: Joi.object({
success: successRes,
token: Joi.string().description('User auth token returned when this check completes a pending 2FA login')
}).$_setFlag('objectName', 'ValidateTOTPTokenResponse')
}
}
}
},
tools.responseWrapper(async (req, res) => {
Expand Down Expand Up @@ -259,21 +267,12 @@ module.exports = (db, server, userHandler) => {

// permissions check
if (req.user && req.user === result.value.user) {
req.validate(roles.can(req.role).readOwn('users'));
req.validate(roles.can(req.role).createOwn('authentication'));
} else {
req.validate(roles.can(req.role).readAny('users'));
}

if (req.user === 'root') {
res.status(403);
return res.json({
error: 'TOTP validation is not allowed with master token',
code: 'InvalidToken'
});
req.validate(roles.can(req.role).createAny('authentication'));
}

let user = new ObjectId(result.value.user);
result.value.accessTokenHash = req.accessToken?.hash;
let totp = await userHandler.checkTotp(user, result.value);

if (!totp) {
Expand All @@ -284,9 +283,26 @@ module.exports = (db, server, userHandler) => {
});
}

return res.json({
let response = {
success: true
});
};

if (totp.pending2fa && totp.pending2fa.tokenRequested) {
try {
response.token = await userHandler.generateAuthToken(user);
} catch (err) {
await userHandler.restorePending2faAuth(totp.pending2fa);

res.status(403);
return res.json({
error: err.message,
code: err.code || 'AuthFailed',
id: user.toString()
});
}
}

return res.json(response);
})
);

Expand Down
15 changes: 14 additions & 1 deletion lib/api/2fa/webauthn.js
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@ module.exports = (db, server, userHandler) => {
),

rpId: Joi.string().hostname().empty('').description('Relaying party ID. Domain'),
twoFactorNonce: Joi.string().hex().length(40).description('Short-lived nonce returned by /authenticate'),

sess: sessSchema,
ip: sessIPSchema
Expand Down Expand Up @@ -515,6 +516,7 @@ module.exports = (db, server, userHandler) => {
.description('Private key encrypted signature to verify with public key on the server. Hex string'),

rpId: Joi.string().hostname().empty('').description('Relaying party ID. Domain'),
twoFactorNonce: Joi.string().hex().length(40).description('Short-lived nonce returned by /authenticate'),

token: booleanSchema.default(false).description('If true response will contain the user auth token'),

Expand Down Expand Up @@ -582,16 +584,27 @@ module.exports = (db, server, userHandler) => {
let user = new ObjectId(result.value.user);

let authData = await userHandler.webauthnAssertAuthentication(user, result.value);
let pending2fa = authData.pending2fa;
delete authData.pending2fa;

let authResponse = {
success: true,
response: authData
};

if (result.value.token) {
let tokenRequested = result.value.token;
if (pending2fa) {
tokenRequested = pending2fa.tokenRequested;
}

if (tokenRequested) {
try {
authResponse.token = await userHandler.generateAuthToken(user);
} catch (err) {
if (pending2fa) {
await userHandler.restorePending2faAuth(pending2fa);
}

let response = {
error: err.message,
code: err.code || 'AuthFailed',
Expand Down
25 changes: 21 additions & 4 deletions lib/api/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,9 @@ module.exports = (db, server, userHandler) => {
.description('If true then the account is flagged as requiring 2FA to be enabled'),
requirePasswordChange: booleanSchema.required().description('Indicates if account password has been reset and should be replaced'),
token: Joi.string().description(
'If access token was requested then this is the value to use as access token when making API requests on behalf of logged in user.'
'If access token was requested and no WildDuck-verifiable 2FA challenge is required then this is the value to use as access token when making API requests on behalf of logged in user.'
),
twoFactorNonce: Joi.string().hex().length(40).description('Short-lived nonce for completing 2FA authentication'),
totpNonce: Joi.string().hex().length(40).description('Short-lived nonce for completing TOTP authentication'),
passwordPwned: booleanSchema.description(
'Indicates whether account password has been found in the list of Pwned passwords and should be replaced'
Expand Down Expand Up @@ -277,9 +278,25 @@ module.exports = (db, server, userHandler) => {

if (result.value.token) {
try {
authResponse.token = await userHandler.generateAuthToken(authData.user);
if (Array.isArray(authData.require2fa) && authData.require2fa.includes('totp')) {
authResponse.totpNonce = await userHandler.generateTotpNonce(authData.user, authResponse.token);
const pending2faMethods = Array.isArray(authData.require2fa) ? authData.require2fa.filter(method => method !== 'custom') : [];

if (pending2faMethods.length) {
authResponse.twoFactorNonce = await userHandler.generatePending2faNonce(authData.user, {
methods: pending2faMethods,
tokenRequested: true
});

if (!authResponse.twoFactorNonce) {
let err = new Error('Failed to create 2FA authentication nonce');
err.code = 'AuthFailed';
throw err;
}

if (pending2faMethods.includes('totp')) {
authResponse.totpNonce = authResponse.twoFactorNonce;
}
} else {
authResponse.token = await userHandler.generateAuthToken(authData.user);
}
} catch (err) {
let response = {
Expand Down
Loading