SSO + OTP account deletion and password change

This commit is contained in:
yflory 2023-10-26 17:55:54 +02:00
parent 49f6b69db5
commit 7d3f67cd86
21 changed files with 389 additions and 53 deletions

View File

@ -9,6 +9,7 @@ const Core = require("./commands/core");
const Metadata = require("./commands/metadata");
const Meta = require("./metadata");
const Logger = require("./log");
const SSOUtils = require('./sso-utils');
const Path = require("path");
const Fse = require("fs-extra");
@ -196,6 +197,11 @@ COMMANDS.start = (edPublic, blockId, reason) => {
}
Log.info('MODERATION_ACCOUNT_BLOCK', safeKey, waitFor());
}));
SSOUtils.deleteAccount(Env, blockId, waitFor((err) => {
if (err) {
return Log.error('MODERATION_ACCOUNT_BLOCK_SSO', err, waitFor());
}
}));
}));
}).nThen((waitFor) => {
var report = {
@ -285,6 +291,11 @@ COMMANDS.restore = (edPublic) => {
}
Log.info('MODERATION_ACCOUNT_BLOCK_RESTORE', safeKey, waitFor());
}));
SSOUtils.restoreAccount(Env, blockId, waitFor(function (err) {
if (err) {
return Log.error('MODERATION_ACCOUNT_BLOCK_RESTORE_SSO', err, waitFor());
}
}));
}));
}).nThen((waitFor) => {
deleteReport(Env, safeKey, waitFor((err) => {

View File

@ -1,6 +1,7 @@
const Block = require("../commands/block");
const MFA = require("../storage/mfa");
const Util = require("../common-util");
const Sessions = require("../storage/sessions");
const Commands = module.exports;
@ -51,8 +52,16 @@ const writeBlock = Commands.WRITE_BLOCK = function (Env, body, cb) {
};
writeBlock.complete = function (Env, body, cb) {
const { content } = body;
Block.writeLoginBlock(Env, content, cb);
const { publicKey, content, session } = body;
Block.writeLoginBlock(Env, content, (err) => {
if (err) { return void cb(err); }
if (!session) { return void cb(); }
const proof = Util.tryParse(content.registrationProof);
const oldKey = proof && proof[0];
Sessions.update(Env, publicKey, oldKey, session, "", cb);
});
};
// Remove a login block IFF

View File

@ -164,8 +164,8 @@ register.complete = function (Env, body, cb) {
ssoUser = user;
}));
}).nThen((w) => {
const { sub } = payload;
SSOUtils.writeBlock(Env, publicKey, sub, w((err) => {
const { sub, provider } = payload;
SSOUtils.writeBlock(Env, publicKey, provider, sub, w((err) => {
if (err) {
w.abort();
return void cb('SSO_BLOCK_WRITE');
@ -181,8 +181,10 @@ register.complete = function (Env, body, cb) {
ssoUser.complete = true;
SSOUtils.updateUser(Env, provider, sub, ssoUser, w());
}).nThen(() => {
const { data, provider } = payload;
SSOUtils.makeSession(Env, publicKey, provider, data, cb);
const { data, sub, provider } = payload;
let session = Util.clone(data);
session.id = sub;
SSOUtils.makeSession(Env, publicKey, provider, session, cb);
});
};
@ -228,8 +230,74 @@ login.complete = function (Env, body, cb) {
}
}));
}).nThen(() => {
const { data, provider } = payload;
SSOUtils.makeSession(Env, publicKey, provider, data, cb);
const { data, sub, provider } = payload;
let session = Util.clone(data);
session.id = sub;
SSOUtils.makeSession(Env, publicKey, provider, session, cb);
});
};
const update = Commands.SSO_UPDATE_BLOCK = function (Env, body, cb) {
const { publicKey, ancestorProof } = body;
// they must provide a valid block public key
if (!Block.isValidBlockId(publicKey)) { return void cb("INVALID_KEY"); }
let oldKey;
nThen((w) => {
BlockStore.isAvailable(Env, publicKey, w((err, result) => {
if (err && !result) {
w.abort();
return void cb(err);
}
// Block found: next
}));
}).nThen((w) => {
Block.validateAncestorProof(Env, ancestorProof, w((err, provenKey) => {
if (err) {
w.abort();
return void cb(err);
}
// Ancestor found and proven
oldKey = provenKey;
}));
}).nThen(() => {
SSOUtils.readBlock(Env, oldKey, (err) => {
if (err) {
return void cb('INVALID_OLD_BLOCK');
}
cb();
});
});
};
update.complete = function (Env, body, cb) {
const { publicKey, ancestorProof } = body;
// We've already proven that the "proof" is valid so we can extract its key
const proof = Util.tryParse(ancestorProof);
const oldKey = proof && proof[0];
let oldBlock;
nThen((w) => {
SSOUtils.readBlock(Env, oldKey, w((err, data) => {
if (err || !data) {
w.abort();
return void cb('INVALID_OLD_BLOCK');
}
if (!data.id) {
w.abort();
return void cb('INVALID_SSO_BLOCK_CONTENT');
}
oldBlock = data;
}));
}).nThen((w) => {
SSOUtils.writeBlock(Env, publicKey, oldBlock.provider, oldBlock.id, w((err) => {
if (err) {
w.abort();
return void cb('SSO_UPDATE_BLOCK_WRITE');
}
}));
}).nThen(() => {
cb();
});
};

View File

@ -8,6 +8,7 @@ const MFA = require("../storage/mfa");
const Sessions = require("../storage/sessions");
const BlockStore = require("../storage/block");
const Block = require("../commands/block");
const SSOUtils = require("../sso-utils");
const Commands = module.exports;
@ -62,16 +63,35 @@ var decode32 = S => {
var EXPIRATION = 7 * 24 * 3600 * 1000; // Sessions are valid 7 days
// Create a session with a token for the given public key
const makeSession = (Env, publicKey, cb) => {
const sessionId = Sessions.randomId();
const makeSession = (Env, publicKey, oldKey, ssoSession, cb) => {
const sessionId = ssoSession || Sessions.randomId();
// For password change, we need to get the sso session associated to the old block key
// In other cases (login and totp_setup), the sso session is associated to the current block
oldKey = oldKey || publicKey; // use the current block if no old key
let isUpdate = false;
nThen(function (w) {
if (!ssoSession) { return; }
// If we have an session token, confirm this is an sso account
SSOUtils.readBlock(Env, oldKey, w((err) => {
if (err === 'ENOENT') { return; } // No sso block, no need to update the session
if (err) {
w.abort();
return void cb('TOTP_VALIDATE_READ_SSO');
}
// We have an existing session for an SSO account: update the existing session
isUpdate = true;
}));
}).nThen(function (w) {
// store the token
Sessions.write(Env, publicKey, sessionId, JSON.stringify({
let sessionData = {
mfa: {
type: 'otp',
exp: (+new Date()) + EXPIRATION
}
}), w(function (err) {
};
var then = w(function (err) {
if (err) {
Env.Log.error("TOTP_VALIDATE_SESSION_WRITE", {
error: Util.serializeError(err),
@ -82,7 +102,12 @@ const makeSession = (Env, publicKey, cb) => {
return void cb("SESSION_WRITE_ERROR");
}
// else continue
}));
});
if (isUpdate) {
Sessions.update(Env, publicKey, oldKey, sessionId, JSON.stringify(sessionData), then);
} else {
Sessions.write(Env, publicKey, sessionId, JSON.stringify(sessionData), then);
}
}).nThen(function () {
cb(void 0, {
bearer: sessionId,
@ -203,7 +228,7 @@ const TOTP_SETUP = Commands.TOTP_SETUP = function (Env, body, cb) {
// There's still a little bit more to do and it could still fail.
TOTP_SETUP.complete = function (Env, body, cb) {
// the OTP code should have already been validated
var { publicKey, secret, contact } = body;
var { publicKey, secret, contact, session } = body;
// the device from which they configure MFA settings
// is assumed to be safe, so we'll respond with a JWT token
@ -253,7 +278,7 @@ TOTP_SETUP.complete = function (Env, body, cb) {
// we have already stored the MFA data, which will cause access to the resource to be restricted to the provided TOTP secret.
// we attempt to create a session as a matter of convenience - so if it fails
// that just means they'll be forced to authenticate
makeSession(Env, publicKey, cb);
makeSession(Env, publicKey, null, session, cb);
});
};
@ -301,8 +326,8 @@ So, we should:
2. send them the token
*/
var { publicKey } = body;
makeSession(Env, publicKey, cb);
var { publicKey, session } = body;
makeSession(Env, publicKey, null, session, cb);
};
// Same as TOTP_VALIDATE but without making a session at the end
@ -432,7 +457,8 @@ const writeBlock = Commands.TOTP_WRITE_BLOCK = function (Env, body, cb) {
writeBlock.complete = function (Env, body, cb) {
const { publicKey, content } = body;
const { publicKey, content, session } = body;
let oldKey;
nThen(function (w) {
// Write new block
Block.writeLoginBlock(Env, content, w((err) => {
@ -444,7 +470,7 @@ writeBlock.complete = function (Env, body, cb) {
}).nThen(function (w) {
// Copy MFA settings
const proof = Util.tryParse(content.registrationProof);
const oldKey = proof && proof[0];
oldKey = proof && proof[0];
if (!oldKey) {
w.abort();
return void cb('INVALID_ANCESTOR');
@ -452,7 +478,7 @@ writeBlock.complete = function (Env, body, cb) {
MFA.copy(Env, oldKey, publicKey, w());
}).nThen(function () {
// Create a session for the current user
makeSession(Env, publicKey, cb);
makeSession(Env, publicKey, oldKey, session, cb);
});
};

View File

@ -11,6 +11,7 @@ const Channel = require("./channel");
const BlockStore = require("../storage/block");
const MFA = require("../storage/mfa");
const ArchiveAccount = require('../archive-account');
const SSOUtils = require("../sso-utils");
/* jshint ignore:start */
const { Worker } = require('node:worker_threads');
/* jshint ignore:end */
@ -722,6 +723,7 @@ var archiveBlock = function (Env, Server, cb, data) {
});
cb(err);
});
SSOUtils.deleteAccount(Env, key, () => {});
};
var restoreArchivedBlock = function (Env, Server, cb, data) {
@ -736,6 +738,10 @@ var restoreArchivedBlock = function (Env, Server, cb, data) {
key: key,
reason: reason || '',
});
// Also restore SSO data
SSOUtils.restoreAccount(Env, key, () => {});
cb(err);
});
};

View File

@ -5,6 +5,7 @@ const Nacl = require("tweetnacl/nacl-fast");
const nThen = require("nthen");
const Util = require("../common-util");
const BlockStore = require("../storage/block");
const SSOUtils = require("../sso-utils");
var isString = s => typeof(s) === 'string';
Block.isValidBlockId = id => {
@ -182,5 +183,17 @@ Block.removeLoginBlock = function (Env, publicKey, reason, _cb) {
});
cb(err);
});
// We should also try to remove the SSO data. Errors will be logged
// but they don't have to be shown to the user. The account data
// is already deleted anyway.
// If this is NOT a password change, also delete sso user.
if (reason !== 'PASSWORD_CHANGE') {
SSOUtils.deleteAccount(Env, publicKey, () => {});
} else {
SSOUtils.deleteBlock(Env, publicKey, () => {});
}
};

View File

@ -62,7 +62,7 @@ var COMMANDS = {};
// and to authenticate new sessions once a TOTP secret has been associated with their account,
const NOAUTH = require("./challenge-commands/base.js");
COMMANDS.MFA_CHECK = NOAUTH.MFA_CHECK;
COMMANDS.WRITE_BLOCK = NOAUTH.WRITE_BLOCK;
COMMANDS.WRITE_BLOCK = NOAUTH.WRITE_BLOCK; // Account creation + password change
COMMANDS.REMOVE_BLOCK = NOAUTH.REMOVE_BLOCK;
const TOTP = require("./challenge-commands/totp.js");
@ -70,13 +70,14 @@ COMMANDS.TOTP_SETUP = TOTP.TOTP_SETUP;
COMMANDS.TOTP_VALIDATE = TOTP.TOTP_VALIDATE;
COMMANDS.TOTP_MFA_CHECK = TOTP.TOTP_MFA_CHECK;
COMMANDS.TOTP_REVOKE = TOTP.TOTP_REVOKE;
COMMANDS.TOTP_WRITE_BLOCK = TOTP.TOTP_WRITE_BLOCK;
COMMANDS.TOTP_WRITE_BLOCK = TOTP.TOTP_WRITE_BLOCK; // Password change only for now (v5.5.0)
COMMANDS.TOTP_REMOVE_BLOCK = TOTP.TOTP_REMOVE_BLOCK;
const SSO = require("./challenge-commands/sso.js");
COMMANDS.SSO_AUTH = SSO.SSO_AUTH;
COMMANDS.SSO_AUTH_CB = SSO.SSO_AUTH_CB;
COMMANDS.SSO_WRITE_BLOCK = SSO.SSO_WRITE_BLOCK;
COMMANDS.SSO_WRITE_BLOCK = SSO.SSO_WRITE_BLOCK; // Account creation only
COMMANDS.SSO_UPDATE_BLOCK = SSO.SSO_UPDATE_BLOCK; // Password change
COMMANDS.SSO_VALIDATE = SSO.SSO_VALIDATE;
var randomToken = () => Nacl.util.encodeBase64(Nacl.randomBytes(24)).replace(/\//g, '-');

View File

@ -204,7 +204,6 @@ const wsProxy = createProxyMiddleware({
app.use('/cryptpad_websocket', wsProxy);
app.use('/ssoauth', (req, res, next) => {
console.log(req.body.SAMLResponse);
if (req && req.body && req.body.SAMLResponse) {
req.method = 'GET';
@ -407,7 +406,8 @@ app.use('/block/', function (req, res, next) {
var no = function () {
w.abort();
res.status(401).json({
method: (mfa_params && mfa_params.method) || (sso_params && 'SSO'),
sso: Boolean(sso_params),
method: mfa_params && mfa_params.method,
code: 401
});
};
@ -427,7 +427,8 @@ app.use('/block/', function (req, res, next) {
if (err) {
Log.error('SESSION_READ_ERROR', err);
return res.status(401).json({
method: mfa_params.method,
sso: Boolean(sso_params),
method: mfa_params && mfa_params.method,
code: 401,
});
}
@ -439,7 +440,6 @@ app.use('/block/', function (req, res, next) {
if (content.mfa && content.mfa.exp && (+new Date()) > content.mfa.exp) {
Log.error("OTP_SESSION_EXPIRED", content.mfa);
// XXX Only delete the mfa part
Sessions.delete(Env, name, token, function (err) {
if (err) {
Log.error('SESSION_DELETE_EXPIRED_ERROR', err);

View File

@ -4,6 +4,7 @@ const Nacl = require("tweetnacl/nacl-fast");
const JWT = require("jsonwebtoken");
const Util = require("./common-util");
const config = require("./load-config");
const nThen = require("nthen");
const SSOUtils = module.exports;
@ -17,7 +18,6 @@ SSOUtils.getOptions = () => {
};
};
// XXX const SAML = require('node-saml'); // https://www.npmjs.com/package/node-saml
const TYPES = SSOUtils.TYPES = {
oidc: require('./plugins/sso/oidc'),
saml: require('./plugins/sso/saml')
@ -82,8 +82,11 @@ SSOUtils.readUser = (Env, provider, id, cb) => {
cb(void 0, Util.tryParse(user));
});
};
SSOUtils.deleteUser = (Env, provider, id, cb) => {
SSO.user.archive(Env, provider, id, cb);
};
SSOUtils.updateUser = (Env, provider, id, data, cb) => {
SSO.user.delete(Env, provider, id, () => {
SSO.user.archive(Env, provider, id, () => {
SSO.user.write(Env, provider, id, JSON.stringify(data), (err) => {
if (err) { return void cb(err); }
cb();
@ -91,9 +94,10 @@ SSOUtils.updateUser = (Env, provider, id, data, cb) => {
});
};
SSOUtils.writeBlock = (Env, id, ssoID, cb) => {
SSOUtils.writeBlock = (Env, id, provider, ssoID, cb) => {
SSO.block.write(Env, id, JSON.stringify({
id: ssoID
id: ssoID,
provider: provider
}), (err) => {
if (err) { return void cb(err); }
cb();
@ -101,10 +105,58 @@ SSOUtils.writeBlock = (Env, id, ssoID, cb) => {
};
SSOUtils.readBlock = (Env, id, cb) => {
SSO.block.read(Env, id, (err, blockData) => {
if (err) { return void cb(err); }
if (err && err !== 'ENOENT' && err.code !== 'ENOENT') {
Env.Log.error("SSO_READ_BLOCK", {
error: Util.serializeError(err),
publicKey: id
});
}
if (err) { return void cb(err.code || err); }
cb(void 0, Util.tryParse(blockData));
});
};
SSOUtils.deleteBlock = (Env, id, cb) => {
SSO.block.archive(Env, id, (err) => {
if (err) { return void cb(err); }
cb();
});
};
// Archive SSO account data
SSOUtils.deleteAccount = (Env, publicKey, cb) => {
SSOUtils.readBlock(Env, publicKey, (err, data) => {
if (err && err !== 'ENOENT') { return void cb(err); }
if (!data) { return void cb(); }
let provider = data.provider;
let userId = data.id;
nThen((w) => {
SSOUtils.deleteUser(Env, provider, userId, w((err) => {
Env.Log.error("SSO_DELETE_USER", {
error: Util.serializeError(err),
provider: provider,
id: userId
});
}));
SSOUtils.deleteBlock(Env, publicKey, w());
}).nThen(() => {
cb();
});
});
};
SSOUtils.restoreAccount = (Env, publicKey, cb) => {
SSO.block.restore(Env, publicKey, (err) => {
if (err && err.code === 'ENOENT') { return void cb(); }
if (err) { return void cb(err); }
SSOUtils.readBlock(Env, publicKey, (err, data) => {
let provider = data.provider;
let userId = data.id;
SSO.user.restore(Env, provider, userId, (err) => {
cb(err);
});
});
});
};
// Store the SSO data (tokens, etc.) in a JWT while waiting for the user's CryptPad password
SSOUtils.createJWT = (Env, ssoId, provider, data, cb) => {
@ -149,7 +201,6 @@ SSOUtils.checkJWT = (Env, token, cb) => {
SSOUtils.makeSession = (Env, publicKey, provider, ssoData, cb) => {
const sessionId = Sessions.randomId();
// XXX If we already have an OTP session, recover it
Sessions.write(Env, publicKey, sessionId, JSON.stringify({
sso: {
exp: +new Date() + SESSION_EXPIRATIION,
@ -171,3 +222,4 @@ SSOUtils.makeSession = (Env, publicKey, provider, ssoData, cb) => {
});
};

View File

@ -21,6 +21,7 @@ Feel free to migrate all of these to a relational DB at some point in the future
const Basic = module.exports;
const Fs = require("node:fs");
const Fse = require("fs-extra");
const Path = require("node:path");
var pathError = (cb) => {
@ -66,4 +67,17 @@ Basic.deleteDir = function (Env, path, cb) {
Fs.rm(path, { recursive: true, force: true }, cb);
};
Basic.archive = function (Env, path, archivePath, cb) {
Fse.move(path, archivePath, {
overwrite: true,
}, (err) => {
cb(err);
});
};
Basic.restore = function (Env, archivePath, path, cb) {
Fse.move(archivePath, path, {
//overwrite: true,
}, (err) => {
cb(err);
});
};

View File

@ -42,6 +42,19 @@ Sessions.delete = function (Env, id, ref, cb) {
Basic.delete(Env, path, cb);
};
Sessions.update = function (Env, id, oldId, ref, dataStr, cb) {
var data = Util.tryParse(dataStr);
Sessions.read(Env, oldId, ref, (err, oldData) => {
let content = Util.tryParse(oldData) || {};
Object.keys(data || {}).forEach((type) => {
content[type] = data[type];
});
Sessions.delete(Env, oldId, ref, () => {
Sessions.write(Env, id, ref, JSON.stringify(content), cb);
});
});
};
Sessions.deleteUser = function (Env, id, cb) {
if (!id || typeof(id) !== 'string') { return; }
id = Util.escapeKeyCharacters(id);

View File

@ -7,7 +7,7 @@ const SSO = module.exports;
A first part (sso-requests) contains temporary files for sso authentication with a remote service
A second part (sso-users) is a database of accounts registered via SSO (SSO id ==> block seed)
A third part (sso-blocks) is a databse of blocks that are sso-protected (block id ==> SSO id...)
A third part (sso-blocks) is a database of blocks that are sso-protected (block id ==> SSO id...)
The path for requests is based on the "authentication request" token depending on the type of SSO.
The path for the user database is based on their persistent identifier (id) from the SSO.
@ -32,6 +32,13 @@ var userPathFromId = function (Env, id, provider) {
return Path.join(Env.paths.base, 'sso_user', provider, id.slice(0, 2), `${id}.json`);
};
var blockArchivePath = function (Env, id) {
return Path.join(Env.paths.archive, 'sso_block', id.slice(0, 2), `${id}.json`);
};
var userArchivePath = function (Env, id, provider) {
return Path.join(Env.paths.archive, 'sso_user', provider, id.slice(0, 2), `${id}.json`);
};
const Req = SSO.request = {};
Req.read = function (Env, id, cb) {
@ -62,6 +69,16 @@ User.delete = function (Env, provider, id, cb) {
var path = userPathFromId(Env, id, provider);
Basic.delete(Env, path, cb);
};
User.archive = function (Env, provider, id, cb) {
var path = userPathFromId(Env, id, provider);
var archivePath = userArchivePath(Env, id, provider);
Basic.archive(Env, path, archivePath, cb);
};
User.restore = function (Env, provider, id, cb) {
var path = userPathFromId(Env, id, provider);
var archivePath = userArchivePath(Env, id, provider);
Basic.restore(Env, archivePath, path, cb);
};
const Block = SSO.block = {};
@ -77,3 +94,13 @@ Block.delete = function (Env, id, cb) {
var path = blockPathFromId(Env, id);
Basic.delete(Env, path, cb);
};
Block.archive = function (Env, id, cb) {
var path = blockPathFromId(Env, id);
var archivePath = blockArchivePath(Env, id);
Basic.archive(Env, path, archivePath, cb);
};
Block.restore = function (Env, id, cb) {
var path = blockPathFromId(Env, id);
var archivePath = blockArchivePath(Env, id);
Basic.restore(Env, archivePath, path, cb);
};

View File

@ -6,6 +6,7 @@ define(['/customize/application_config.js'], function (AppConfig) {
blockHashKey: 'Block_hash',
fileHashKey: 'FS_hash',
sessionJWT: 'Session_JWT',
ssoSeed: 'SSO_seed',
// Store
displayNameKey: 'cryptpad.username',

View File

@ -299,7 +299,8 @@ define([
// results...
var res = {
register: isRegister,
uname: uname
uname: uname,
auth_token: {}
};
var RT, blockKeys, blockUrl;
@ -323,11 +324,12 @@ define([
// determine where a block for your set of keys would be stored
blockUrl = Block.getBlockUrl(res.opt.blockKeys);
var TOTP_prompt = function (err, cb) {
var TOTP_prompt = function (err, ssoSession, cb) {
onOTP(function (code) {
ServerCommand(res.opt.blockKeys.sign, {
command: 'TOTP_VALIDATE',
code: code,
session: ssoSession
// TODO optionally allow the user to specify a lifetime for this session?
// this will require a little bit of server work
// and more UI/UX:
@ -352,11 +354,13 @@ define([
};
var missingAuth;
var missingSSO;
nThen(function (w) {
Util.getBlock(blockUrl, {
// request the block without credentials
}, w(function (err, response) {
if (err === 401) {
missingSSO = response && response.sso;
missingAuth = response && response.method;
return void console.log("Block requires 2FA");
}
@ -395,19 +399,24 @@ define([
});
}));
}).nThen(function (w) {
if (missingAuth !== 'SSO') { return; } // XXX multiple auth
if (!missingSSO) { return; }
// SSO session should always be applied before the OTP one
// because we can't transform an account into an SSO account later
// so we probably don't need to recover the OTP session here
ServerCommand(res.opt.blockKeys.sign, {
command: 'SSO_VALIDATE',
jwt: ssoAuth.data,
}, w(function (err, response) {
if (err) {
// XXX
return;
console.error(err);
w.abort();
waitFor.abort();
return void cb(err);
}
res.auth_token = response;
}));
}).nThen(function (w) {
if (missingAuth !== 'TOTP') { return; } // XXX multiple auth
if (missingAuth !== 'TOTP') { return; }
// if you're here then you need to request a JWT
var done = w();
var tries = 3;
@ -418,7 +427,9 @@ define([
return void cb('TOTP_ATTEMPTS_EXHAUSTED');
}
tries--;
TOTP_prompt(tries !== 2, function (err, response) {
// If we have an SSO account, provide the SSO session to update it with OTP
var ssoSession = (res.auth_token && res.auth_token.bearer) || '';
TOTP_prompt(tries !== 2, ssoSession, function (err, response) {
// ask again until your number of tries are exhausted
if (err) {
console.error(err);
@ -477,6 +488,7 @@ define([
}).nThen(function (waitFor) { // MODERN REGISTRATION / LOGIN
var opt = getProxyOpt(res.blockInfo);
LocalStore.setSessionToken('');
modernLoginRegister(opt, isRegister, waitFor(function (err, data, _RT) {
if (err) {
waitFor.abort();
@ -497,11 +509,14 @@ define([
toPublish[Constants.userHashKey] = res.userHash;
toPublish.edPublic = RT.proxy.edPublic;
// FIXME We currently can't create an account with OTP by default
// NOTE If we ever want to do that for SSO accounts it will require major changes
// because writeLoginBlock only supports one type of authentication at a time
Block.writeLoginBlock({
pw: Boolean(passwd),
auth: ssoAuth,
blockKeys: blockKeys,
content: toPublish
content: toPublish,
}, waitFor(function (e, res) {
if (e === 'SSO_NO_SESSION') { return; } // account created, need re-login
if (e) {

View File

@ -1994,6 +1994,11 @@ define([
console.log("no block found");
return;
}
if (err && err === 401) {
// there is a protected block at the next location, abort FIXME check
waitFor.abort();
return void cb({ error: 'EEXISTS' });
}
response.arrayBuffer().then(waitFor(arraybuffer => {
var block = new Uint8Array(arraybuffer);
@ -2042,21 +2047,42 @@ define([
User_hash: newHash,
edPublic: edPublic,
};
var sessionToken = LocalStore.getSessionToken() || undefined;
Block.writeLoginBlock({
auth: auth,
blockKeys: blockKeys,
oldBlockKeys: oldBlockKeys,
content: content
content: content,
session: sessionToken // Recover existing SSO session
}, waitFor(function (err, data) {
if (err) {
waitFor.abort();
return void cb({error: err});
}
// Update the session if OTP is enabled
// If OTP is disabled, keep the existing SSO session
if (data && data.bearer) {
LocalStore.setSessionToken(data.bearer);
}
}));
}).nThen(function (waitFor) {
var isSSO = Boolean(LocalStore.getSSOSeed());
if (!isSSO) { return; }
// Update "sso_block" data for SSO accounts
Block.updateSSOBlock({
blockKeys: blockKeys,
oldBlockKeys: oldBlockKeys
}, waitFor(function (err) {
if (err) {
// If we can't move the sso_block data, we won't be able to log in later
// so we must abort the password change.
console.error(err);
waitFor.abort();
return void cb({error: err});
}
}));
}).nThen(function (waitFor) {
var blockUrl = Block.getBlockUrl(blockKeys);
var sessionToken = LocalStore.getSessionToken() || undefined;

View File

@ -881,6 +881,7 @@ define([
}));
}).nThen(function (waitFor) {
// Delete Drive
store.ownDeletion = true;
Store.removeOwnedChannel(clientId, {
channel: store.driveChannel,
force: true
@ -3055,6 +3056,7 @@ define([
})
.on('error', function (info) {
if (info.error && info.error === 'EDELETED') {
if (store.ownDeletion) { return; }
broadcast([], "LOGOUT", {
reason: info.message
});

View File

@ -47,7 +47,7 @@ define([
return hash;
};
var getUserHash = LocalStore.getUserHash = function () {
LocalStore.getUserHash = function () {
var hash = localStorage[Constants.userHashKey];
if (['undefined', 'undefined/'].indexOf(hash) !== -1) {
@ -84,6 +84,13 @@ define([
safeSet(Constants.sessionJWT, token);
};
LocalStore.getSSOSeed = function () {
return localStorage[Constants.ssoSeed];
};
LocalStore.setSSOSeed = function (seed) {
safeSet(Constants.ssoSeed, seed);
};
LocalStore.getAccountName = function () {
return localStorage[Constants.userNameKey];
};
@ -130,6 +137,7 @@ define([
Constants.userHashKey,
Constants.blockHashKey,
Constants.sessionJWT,
Constants.ssoSeed,
'loginToken',
'plan',
].forEach(function (k) {

View File

@ -173,7 +173,7 @@ define([
}, cb);
};
Block.writeLoginBlock = function (data, cb) {
const { content, blockKeys, oldBlockKeys, auth, pw } = data;
const { content, blockKeys, oldBlockKeys, auth, pw, session } = data;
var command = 'WRITE_BLOCK';
if (auth && auth.type) { command = `${auth.type.toUpperCase()}_` + command; }
@ -185,7 +185,8 @@ define([
ServerCommand(blockKeys.sign, {
command: command,
content: block
content: block,
session: session // sso session
}, cb);
};
Block.removeLoginBlock = function (data, cb) {
@ -201,5 +202,16 @@ define([
}, cb);
};
Block.updateSSOBlock = function (data, cb) {
const { blockKeys, oldBlockKeys } = data;
var oldProof = oldBlockKeys && Block.proveAncestor(oldBlockKeys);
ServerCommand(blockKeys.sign, {
command: 'SSO_UPDATE_BLOCK',
ancestorProof: oldProof
}, cb);
};
return Block;
});

View File

@ -577,10 +577,17 @@ define([
loadingText: Messages.settings_deleteTitle
});
setTimeout(function () {
var name = privateData.accountName;
var bytes;
var auth = {};
var ssoSeed;
nThen(function (w) {
sframeChan.query("Q_SETTINGS_GET_SSO_SEED", {
}, w(function (err, obj) {
if (!obj || !obj.seed) { return; } // Not an sso account?
ssoSeed = obj.seed;
}));
}).nThen(function (w) {
var name = ssoSeed || privateData.accountName;
deriveBytes(name, password, w(function (_bytes) {
bytes = _bytes;
}));
@ -723,8 +730,15 @@ define([
setTimeout(function () {
var oldBytes, newBytes;
var auth = {};
var ssoSeed;
nThen(function (w) {
var name = privateData.accountName;
sframeChan.query("Q_SETTINGS_GET_SSO_SEED", {
}, w(function (err, obj) {
if (!obj || !obj.seed) { return; } // Not an sso account?
ssoSeed = obj.seed;
}));
}).nThen(function (w) {
var name = ssoSeed || privateData.accountName;
deriveBytes(name, oldPassword, w(function (bytes) {
oldBytes = bytes;
}));
@ -1053,6 +1067,7 @@ define([
var Base32, QRCode, Nacl;
var blockKeys;
var recoverySecret;
var ssoSeed;
nThen(function (waitFor) {
require([
'/auth/base32.js',
@ -1063,12 +1078,19 @@ define([
QRCode = window.QRCode;
Nacl = window.nacl;
}));
}).nThen(function (waitFor) {
sframeChan.query("Q_SETTINGS_GET_SSO_SEED", {
}, waitFor(function (err, obj) {
if (!obj || !obj.seed) { return; } // Not an sso account?
ssoSeed = obj.seed;
}));
}).nThen(function (waitFor) {
var next = waitFor();
// scrypt locks up the UI before the DOM has a chance
// to update (displaying logs, etc.), so do a set timeout
setTimeout(function () {
Login.Cred.deriveFromPassphrase(name, password, Login.requiredBytes, function (bytes) {
var salt = ssoSeed || name;
Login.Cred.deriveFromPassphrase(salt, password, Login.requiredBytes, function (bytes) {
var result = Login.allocateBytes(bytes);
sframeChan.query("Q_SETTINGS_CHECK_PASSWORD", {
blockHash: result.blockHash,
@ -1160,7 +1182,6 @@ define([
lock = true;
var data = {
command: 'TOTP_SETUP',
secret: secret,
contact: "secret:" + recoverySecret, // TODO other recovery options
code: code,

View File

@ -81,7 +81,10 @@ define([
require([
'/common/outer/http-command.js',
], function (ServerCommand) {
ServerCommand(obj.key, obj.data, function (err, response) {
var data = obj.data;
data.command = 'TOTP_SETUP';
data.session = Utils.LocalStore.getSessionToken();
ServerCommand(obj.key, data, function (err, response) {
cb({ success: Boolean(!err && response && response.bearer) });
if (response && response.bearer) {
Utils.LocalStore.setSessionToken(response.bearer);
@ -111,11 +114,18 @@ define([
Utils.Util.getBlock(parsed.href, {}, function (err, data) {
cb({
mfa: err === 401,
sso: data && data.sso,
type: data && data.method
});
});
});
});
sframeChan.on('Q_SETTINGS_GET_SSO_SEED', function (obj, _cb) {
var cb = Utils.Util.mkAsync(_cb);
cb({
seed: Utils.LocalStore.getSSOSeed()
});
});
sframeChan.on('Q_SETTINGS_REMOVE_OWNED_PADS', function (data, cb) {
Cryptpad.removeOwnedPads(data, cb);
});

View File

@ -58,6 +58,7 @@ define([
$button.prop('disabled', '');
return void UI.warn(msg);
}
LocalStore.setSSOSeed(seed.toLowerCase());
window.location.href = '/drive/';
});
};