mirror of
https://github.com/cryptpad/cryptpad.git
synced 2026-09-14 11:05:41 +05:00
SSO + OTP account deletion and password change
This commit is contained in:
parent
49f6b69db5
commit
7d3f67cd86
@ -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) => {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
@ -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, () => {});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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, '-');
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) => {
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -58,6 +58,7 @@ define([
|
||||
$button.prop('disabled', '');
|
||||
return void UI.warn(msg);
|
||||
}
|
||||
LocalStore.setSSOSeed(seed.toLowerCase());
|
||||
window.location.href = '/drive/';
|
||||
});
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user