diff --git a/src/directoryserver.js b/src/directoryserver.js index 7e15a8ae6..3f20eca8d 100644 --- a/src/directoryserver.js +++ b/src/directoryserver.js @@ -300,8 +300,12 @@ async function userAuth(req, res, next) { const commonName = req.dn.rdns[0].attrs[cnAttributeName].value; if (!commonName) return next(new ldap.NoSuchObjectError('Missing CN')); + // totptoken is passed as the "attribute" using the '+' separator in the first RDNS of the request DN + // when totptoken attribute is present, it signals that we must enforce totp check + // totp check is currently requested by the client. this is the only way to auth against external cloudron dashboard, external cloudron app and external apps const TOTPTOKEN_ATTRIBUTE_NAME = 'totptoken'; // This has to be in-sync with externalldap.js - const totpToken = req.dn.rdns[0].attrs[TOTPTOKEN_ATTRIBUTE_NAME] ? req.dn.rdns[0].attrs[TOTPTOKEN_ATTRIBUTE_NAME].value : null; + const totpToken = TOTPTOKEN_ATTRIBUTE_NAME in req.dn.rdns[0].attrs ? req.dn.rdns[0].attrs[TOTPTOKEN_ATTRIBUTE_NAME].value : null; + const relaxedTotpCheck = !(TOTPTOKEN_ATTRIBUTE_NAME in req.dn.rdns[0].attrs); let verifyFunc; if (cnAttributeName === 'mail') { @@ -314,7 +318,7 @@ async function userAuth(req, res, next) { verifyFunc = users.verifyWithUsername; } - const [error, user] = await safe(verifyFunc(commonName, req.credentials || '', '', { totpToken })); + const [error, user] = await safe(verifyFunc(commonName, req.credentials || '', '', { totpToken, relaxedTotpCheck })); if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(error.message)); if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(error.message)); if (error) return next(new ldap.OperationsError(error.message)); diff --git a/src/test/directoryserver-test.js b/src/test/directoryserver-test.js index 571c5b111..dab6b691e 100644 --- a/src/test/directoryserver-test.js +++ b/src/test/directoryserver-test.js @@ -141,8 +141,8 @@ describe('Directory Server (LDAP)', function () { await users.enableTwoFactorAuthentication(admin.id, totpToken, auditSource); }); - it('fails without 2fa', async function () { - const [error] = await safe(ldapBind(`cn=${admin.id},ou=users,dc=cloudron`, admin.password)); + it('fails with empty 2fa', async function () { + const [error] = await safe(ldapBind(`cn=${admin.id}+totptoken=,ou=users,dc=cloudron`, admin.password)); expect(error).to.be.a(ldap.InvalidCredentialsError); expect(error.lde_message).to.be('A totpToken must be provided'); }); @@ -153,6 +153,10 @@ describe('Directory Server (LDAP)', function () { expect(error.lde_message).to.be('Invalid totpToken'); }); + it('fails with no 2fa', async function () { + await ldapBind(`cn=${admin.id},ou=users,dc=cloudron`, admin.password); + }); + it('succeeds with valid 2fa', async function () { const totpToken = speakeasy.totp({ secret: twofa.secret, encoding: 'base32' }); await ldapBind(`cn=${admin.email}+totpToken=${totpToken},ou=users,dc=cloudron`, admin.password);