mirror of https://github.com/xwiki-labs/cryptpad
Merge branch 'authsso' into staging
This commit is contained in:
commit
df6b0a0e98
|
@ -13,6 +13,7 @@ customization
|
|||
.*.swp
|
||||
*.db
|
||||
/customize/
|
||||
/lib/plugins
|
||||
customize
|
||||
messages.log
|
||||
.DS_Store
|
||||
|
@ -27,3 +28,4 @@ block/
|
|||
logs/
|
||||
privileged.conf
|
||||
config/config.js
|
||||
config/sso.js
|
||||
|
|
|
@ -117,6 +117,20 @@ module.exports = {
|
|||
*/
|
||||
// maxWorkers: 4,
|
||||
|
||||
/* =====================
|
||||
* Sessions
|
||||
* ===================== */
|
||||
|
||||
/* Accounts can be protected with an OTP (One Time Password) system
|
||||
* to add a second authentication layer. Such accounts use a session
|
||||
* with a given lifetime after which they are logged out and need
|
||||
* to be re-authenticated. You can configure the lifetime of these
|
||||
* sessions here.
|
||||
*
|
||||
* defaults to 7 days
|
||||
*/
|
||||
//otpSessionExpiration: 7*24, // hours
|
||||
|
||||
/* =====================
|
||||
* Admin
|
||||
* ===================== */
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
module.exports = {
|
||||
// Enable SSO login on this instance
|
||||
enabled: false,
|
||||
// Block registration for non-SSO users on this instance
|
||||
enforced: false,
|
||||
// Allow users to add an additional CryptPad password to their SSO account
|
||||
cpPassword: false,
|
||||
// List of SSO providers
|
||||
list: [
|
||||
/*
|
||||
{
|
||||
name: 'google',
|
||||
type: 'oidc',
|
||||
url: 'https://accounts.google.com',
|
||||
client_id: "{your_client_id}",
|
||||
client_secret: "{your_client_secret}",
|
||||
jwt_alg: 'RS256' (optional)
|
||||
}
|
||||
*/
|
||||
]
|
||||
};
|
||||
|
|
@ -8,6 +8,7 @@ define([
|
|||
'/components/chainpad-crypto/crypto.js',
|
||||
'/common/common-util.js',
|
||||
'/common/outer/network-config.js',
|
||||
'/common/common-login.js',
|
||||
'/common/common-credential.js',
|
||||
'/components/chainpad/chainpad.dist.js',
|
||||
'/common/common-realtime.js',
|
||||
|
@ -24,14 +25,14 @@ define([
|
|||
|
||||
'/components/tweetnacl/nacl-fast.min.js',
|
||||
'/components/scrypt-async/scrypt-async.min.js', // better load speed
|
||||
], function ($, Listmap, Crypto, Util, NetConfig, Cred, ChainPad, Realtime, Constants, UI,
|
||||
], function ($, Listmap, Crypto, Util, NetConfig, Login, Cred, ChainPad, Realtime, Constants, UI,
|
||||
Feedback, h, LocalStore, Messages, nThen, Block, Hash, ServerCommand) {
|
||||
var Exports = {
|
||||
Cred: Cred,
|
||||
Block: Block,
|
||||
// this is depended on by non-customizable files
|
||||
// be careful when modifying login.js
|
||||
requiredBytes: 192,
|
||||
requiredBytes: Login.requiredBytes,
|
||||
};
|
||||
|
||||
var Nacl = window.nacl;
|
||||
|
@ -44,511 +45,31 @@ define([
|
|||
redirectTo = newPad.href;
|
||||
}
|
||||
};
|
||||
if (window.location.hash) {
|
||||
setRedirectTo();
|
||||
}
|
||||
if (window.location.hash) { setRedirectTo(); }
|
||||
|
||||
var allocateBytes = Exports.allocateBytes = function (bytes) {
|
||||
var dispense = Cred.dispenser(bytes);
|
||||
|
||||
var opt = {};
|
||||
|
||||
// dispense 18 bytes of entropy for your encryption key
|
||||
var encryptionSeed = dispense(18);
|
||||
// 16 bytes for a deterministic channel key
|
||||
var channelSeed = dispense(16);
|
||||
// 32 bytes for a curve key
|
||||
var curveSeed = dispense(32);
|
||||
|
||||
var curvePair = Nacl.box.keyPair.fromSecretKey(new Uint8Array(curveSeed));
|
||||
opt.curvePrivate = Nacl.util.encodeBase64(curvePair.secretKey);
|
||||
opt.curvePublic = Nacl.util.encodeBase64(curvePair.publicKey);
|
||||
|
||||
// 32 more for a signing key
|
||||
var edSeed = opt.edSeed = dispense(32);
|
||||
|
||||
// 64 more bytes to seed an additional signing key
|
||||
var blockKeys = opt.blockKeys = Block.genkeys(new Uint8Array(dispense(64)));
|
||||
opt.blockHash = Block.getBlockHash(blockKeys);
|
||||
|
||||
// derive a private key from the ed seed
|
||||
var signingKeypair = Nacl.sign.keyPair.fromSeed(new Uint8Array(edSeed));
|
||||
|
||||
opt.edPrivate = Nacl.util.encodeBase64(signingKeypair.secretKey);
|
||||
opt.edPublic = Nacl.util.encodeBase64(signingKeypair.publicKey);
|
||||
|
||||
var keys = opt.keys = Crypto.createEditCryptor(null, encryptionSeed);
|
||||
|
||||
// 24 bytes of base64
|
||||
keys.editKeyStr = keys.editKeyStr.replace(/\//g, '-');
|
||||
|
||||
// 32 bytes of hex
|
||||
var channelHex = opt.channelHex = Util.uint8ArrayToHex(channelSeed);
|
||||
|
||||
// should never happen
|
||||
if (channelHex.length !== 32) { throw new Error('invalid channel id'); }
|
||||
|
||||
var channel64 = Util.hexToBase64(channelHex);
|
||||
|
||||
// we still generate a v1 hash because this function needs to deterministically
|
||||
// derive the same values as it always has. New accounts will generate their own
|
||||
// userHash values
|
||||
opt.userHash = '/1/edit/' + [channel64, opt.keys.editKeyStr].join('/') + '/';
|
||||
|
||||
return opt;
|
||||
};
|
||||
|
||||
|
||||
var loginOptionsFromBlock = Exports.loginOptionsFromBlock = function (blockInfo) {
|
||||
var opt = {};
|
||||
var parsed = Hash.getSecrets('pad', blockInfo.User_hash);
|
||||
opt.channelHex = parsed.channel;
|
||||
opt.keys = parsed.keys;
|
||||
opt.edPublic = blockInfo.edPublic;
|
||||
return opt;
|
||||
};
|
||||
|
||||
var loadUserObject = Exports.loadUserObject = function (opt, _cb) {
|
||||
var cb = Util.once(Util.mkAsync(_cb));
|
||||
var config = {
|
||||
websocketURL: NetConfig.getWebsocketURL(),
|
||||
channel: opt.channelHex,
|
||||
data: {},
|
||||
validateKey: opt.keys.validateKey, // derived validation key
|
||||
crypto: Crypto.createEncryptor(opt.keys),
|
||||
logLevel: 1,
|
||||
classic: true,
|
||||
ChainPad: ChainPad,
|
||||
owners: [opt.edPublic]
|
||||
};
|
||||
|
||||
var rt = opt.rt = Listmap.create(config);
|
||||
rt.proxy
|
||||
.on('ready', function () {
|
||||
setTimeout(function () { cb(void 0, rt); });
|
||||
})
|
||||
.on('error', function (info) {
|
||||
cb(info.type, {reason: info.message});
|
||||
})
|
||||
.on('disconnect', function (info) {
|
||||
cb('E_DISCONNECT', info);
|
||||
Exports.ssoAuth = function (provider, cb) {
|
||||
var keys = Nacl.sign.keyPair();
|
||||
localStorage.CP_sso_auth = JSON.stringify({
|
||||
s: Nacl.util.encodeBase64(keys.secretKey),
|
||||
p: Nacl.util.encodeBase64(keys.publicKey)
|
||||
});
|
||||
ServerCommand(keys, {
|
||||
command: 'SSO_AUTH',
|
||||
provider: provider,
|
||||
register: true
|
||||
}, cb);
|
||||
};
|
||||
Exports.ssoLogin = function () {
|
||||
|
||||
};
|
||||
|
||||
var isProxyEmpty = function (proxy) {
|
||||
var l = Object.keys(proxy).length;
|
||||
return l === 0 || (l === 2 && proxy._events && proxy.on);
|
||||
Exports.allocateBytes = Login.allocateBytes;
|
||||
Exports.loadUserObject = Login.loadUserObject;
|
||||
|
||||
var setMergeAnonDrive = function (value) {
|
||||
Exports.mergeAnonDrive = Boolean(value);
|
||||
};
|
||||
|
||||
var setMergeAnonDrive = function () {
|
||||
Exports.mergeAnonDrive = 1;
|
||||
};
|
||||
|
||||
Exports.loginOrRegister = function (uname, passwd, isRegister, shouldImport, onOTP, cb) {
|
||||
if (typeof(cb) !== 'function') { return; }
|
||||
|
||||
// Usernames are all lowercase. No going back on this one
|
||||
uname = uname.toLowerCase();
|
||||
|
||||
// validate inputs
|
||||
if (!Cred.isValidUsername(uname)) { return void cb('INVAL_USER'); }
|
||||
if (!Cred.isValidPassword(passwd)) { return void cb('INVAL_PASS'); }
|
||||
if (isRegister && !Cred.isLongEnoughPassword(passwd)) {
|
||||
return void cb('PASS_TOO_SHORT');
|
||||
}
|
||||
|
||||
// results...
|
||||
var res = {
|
||||
register: isRegister,
|
||||
};
|
||||
|
||||
var RT, blockKeys, blockHash, blockUrl, Pinpad, rpc, userHash;
|
||||
|
||||
nThen(function (waitFor) {
|
||||
// derive a predefined number of bytes from the user's inputs,
|
||||
// and allocate them in a deterministic fashion
|
||||
Cred.deriveFromPassphrase(uname, passwd, Exports.requiredBytes, waitFor(function (bytes) {
|
||||
res.opt = allocateBytes(bytes);
|
||||
blockHash = res.opt.blockHash;
|
||||
blockKeys = res.opt.blockKeys;
|
||||
}));
|
||||
}).nThen(function (waitFor) {
|
||||
// the allocated bytes can be used either in a legacy fashion,
|
||||
// or in such a way that a previously unused byte range determines
|
||||
// the location of a layer of indirection which points users to
|
||||
// an encrypted block, from which they can recover the location of
|
||||
// the rest of their data
|
||||
|
||||
// determine where a block for your set of keys would be stored
|
||||
blockUrl = Block.getBlockUrl(res.opt.blockKeys);
|
||||
|
||||
var TOTP_prompt = function (err, cb) {
|
||||
onOTP(function (code) {
|
||||
ServerCommand(res.opt.blockKeys.sign, {
|
||||
command: 'TOTP_VALIDATE',
|
||||
code: code,
|
||||
// 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:
|
||||
// ie. just a simple "remember me" checkbox?
|
||||
// allow them to specify a lifetime for the session?
|
||||
// "log me out after one day"?
|
||||
}, cb);
|
||||
}, false, err);
|
||||
};
|
||||
|
||||
var done = waitFor();
|
||||
var responseToDecryptedBlock = function (response, cb) {
|
||||
response.arrayBuffer().then(arraybuffer => {
|
||||
arraybuffer = new Uint8Array(arraybuffer);
|
||||
var decryptedBlock = Block.decrypt(arraybuffer, blockKeys);
|
||||
if (!decryptedBlock) {
|
||||
console.error("BLOCK DECRYPTION ERROR");
|
||||
return void cb("BLOCK_DECRYPTION_ERROR");
|
||||
}
|
||||
cb(void 0, decryptedBlock);
|
||||
});
|
||||
};
|
||||
|
||||
var TOTP_response;
|
||||
nThen(function (w) {
|
||||
Util.getBlock(blockUrl, {
|
||||
// request the block without credentials
|
||||
}, w(function (err, response) {
|
||||
if (err === 401) {
|
||||
return void console.log("Block requires 2FA");
|
||||
}
|
||||
|
||||
if (err === 404 && response && response.reason) {
|
||||
waitFor.abort();
|
||||
w.abort();
|
||||
/*
|
||||
// the following block prevent users from re-using an old password
|
||||
if (isRegister) { return void cb('HAS_PLACEHOLDER'); }
|
||||
*/
|
||||
return void cb('DELETED_USER', response);
|
||||
}
|
||||
|
||||
// Some other error?
|
||||
if (err) {
|
||||
console.error(err);
|
||||
w.abort();
|
||||
return void done();
|
||||
}
|
||||
|
||||
// If the block was returned without requiring authentication
|
||||
// then we can abort the subsequent steps of this nested nThen
|
||||
w.abort();
|
||||
|
||||
// decrypt the response and continue the normal procedure with its payload
|
||||
responseToDecryptedBlock(response, function (err, decryptedBlock) {
|
||||
if (err) {
|
||||
// if a block was present but you were not able to decrypt it...
|
||||
console.error(err);
|
||||
waitFor.abort();
|
||||
return void cb(err);
|
||||
}
|
||||
res.blockInfo = decryptedBlock;
|
||||
done();
|
||||
});
|
||||
}));
|
||||
}).nThen(function (w) {
|
||||
// if you're here then you need to request a JWT
|
||||
var done = w();
|
||||
var tries = 3;
|
||||
var ask = function () {
|
||||
if (!tries) {
|
||||
w.abort();
|
||||
waitFor.abort();
|
||||
return void cb('TOTP_ATTEMPTS_EXHAUSTED');
|
||||
}
|
||||
tries--;
|
||||
TOTP_prompt(tries !== 2, function (err, response) {
|
||||
// ask again until your number of tries are exhausted
|
||||
if (err) {
|
||||
console.error(err);
|
||||
console.log("Normal failure. Asking again...");
|
||||
return void ask();
|
||||
}
|
||||
if (!response || !response.bearer) {
|
||||
console.log(response);
|
||||
console.log("Unexpected failure. No bearer token. Asking again");
|
||||
return void ask();
|
||||
}
|
||||
console.log("Successfully retrieved a bearer token");
|
||||
res.TOTP_token = TOTP_response = response;
|
||||
done();
|
||||
});
|
||||
};
|
||||
ask();
|
||||
}).nThen(function (w) {
|
||||
Util.getBlock(blockUrl, TOTP_response, function (err, response) {
|
||||
if (err) {
|
||||
w.abort();
|
||||
console.error(err);
|
||||
return void cb('BLOCK_ERROR_3');
|
||||
}
|
||||
|
||||
responseToDecryptedBlock(response, function (err, decryptedBlock) {
|
||||
if (err) {
|
||||
waitFor.abort();
|
||||
return void cb(err);
|
||||
}
|
||||
res.blockInfo = decryptedBlock;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
}).nThen(function (waitFor) {
|
||||
// we assume that if there is a block, it was created in a valid manner
|
||||
// so, just proceed to the next block which handles that stuff
|
||||
if (res.blockInfo) { return; }
|
||||
|
||||
var opt = res.opt;
|
||||
|
||||
// load the user's object using the legacy credentials
|
||||
loadUserObject(opt, waitFor(function (err, rt) {
|
||||
if (err) {
|
||||
waitFor.abort();
|
||||
if (err === 'EDELETED') { return void cb('DELETED_USER', rt); }
|
||||
return void cb(err);
|
||||
}
|
||||
|
||||
// if a proxy is marked as deprecated, it is because someone had a non-owned drive
|
||||
// but changed their password, and couldn't delete their old data.
|
||||
// if they are here, they have entered their old credentials, so we should not
|
||||
// allow them to proceed. In time, their old drive should get deleted, since
|
||||
// it will should be pinned by anyone's drive.
|
||||
if (rt.proxy[Constants.deprecatedKey]) {
|
||||
waitFor.abort();
|
||||
return void cb('NO_SUCH_USER', res);
|
||||
}
|
||||
|
||||
if (isRegister && isProxyEmpty(rt.proxy)) {
|
||||
// If they are trying to register,
|
||||
// and the proxy is empty, then there is no 'legacy user' either
|
||||
// so we should just shut down this session and disconnect.
|
||||
//rt.network.disconnect();
|
||||
return; // proceed to the next async block
|
||||
}
|
||||
|
||||
// they tried to just log in but there's no such user
|
||||
// and since we're here at all there is no modern-block
|
||||
if (!isRegister && isProxyEmpty(rt.proxy)) {
|
||||
//rt.network.disconnect(); // clean up after yourself
|
||||
waitFor.abort();
|
||||
return void cb('NO_SUCH_USER', res);
|
||||
}
|
||||
|
||||
// they tried to register, but those exact credentials exist
|
||||
if (isRegister && !isProxyEmpty(rt.proxy)) {
|
||||
//rt.network.disconnect();
|
||||
waitFor.abort();
|
||||
Feedback.send('LOGIN', true);
|
||||
return void cb('ALREADY_REGISTERED', res);
|
||||
}
|
||||
|
||||
// if you are here, then there is no block, the user is trying
|
||||
// to log in. The proxy is **not** empty. All values assigned here
|
||||
// should have been deterministically created using their credentials
|
||||
// so setting them is just a precaution to keep things in good shape
|
||||
res.proxy = rt.proxy;
|
||||
res.realtime = rt.realtime;
|
||||
res.network = rt.network;
|
||||
|
||||
// they're registering...
|
||||
res.userHash = opt.userHash;
|
||||
res.userName = uname;
|
||||
|
||||
// export their signing key
|
||||
res.edPrivate = opt.edPrivate;
|
||||
res.edPublic = opt.edPublic;
|
||||
|
||||
// export their encryption key
|
||||
res.curvePrivate = opt.curvePrivate;
|
||||
res.curvePublic = opt.curvePublic;
|
||||
|
||||
if (shouldImport) { setMergeAnonDrive(); }
|
||||
|
||||
// don't proceed past this async block.
|
||||
waitFor.abort();
|
||||
|
||||
// We have to call whenRealtimeSyncs asynchronously here because in the current
|
||||
// version of listmap, onLocal calls `chainpad.contentUpdate(newValue)`
|
||||
// asynchronously.
|
||||
// The following setTimeout is here to make sure whenRealtimeSyncs is called after
|
||||
// `contentUpdate` so that we have an update userDoc in chainpad.
|
||||
setTimeout(function () {
|
||||
Realtime.whenRealtimeSyncs(rt.realtime, function () {
|
||||
// the following stages are there to initialize a new drive
|
||||
// if you are registering
|
||||
LocalStore.login(res.userHash, undefined, res.userName, function () {
|
||||
setTimeout(function () { cb(void 0, res); });
|
||||
});
|
||||
});
|
||||
});
|
||||
}));
|
||||
}).nThen(function (waitFor) { // MODERN REGISTRATION / LOGIN
|
||||
var opt;
|
||||
if (res.blockInfo) {
|
||||
opt = loginOptionsFromBlock(res.blockInfo);
|
||||
userHash = res.blockInfo.User_hash;
|
||||
//console.error(opt, userHash);
|
||||
} else {
|
||||
console.log("allocating random bytes for a new user object");
|
||||
opt = allocateBytes(Nacl.randomBytes(Exports.requiredBytes));
|
||||
// create a random v2 hash, since we don't need backwards compatibility
|
||||
userHash = opt.userHash = Hash.createRandomHash('drive');
|
||||
var secret = Hash.getSecrets('drive', userHash);
|
||||
opt.keys = secret.keys;
|
||||
opt.channelHex = secret.channel;
|
||||
}
|
||||
|
||||
// according to the location derived from the credentials which you entered
|
||||
loadUserObject(opt, waitFor(function (err, rt) {
|
||||
if (err) {
|
||||
waitFor.abort();
|
||||
if (err === 'EDELETED') { return void cb('DELETED_USER', rt); }
|
||||
return void cb('MODERN_REGISTRATION_INIT');
|
||||
}
|
||||
|
||||
//console.error(JSON.stringify(rt.proxy));
|
||||
|
||||
// export the realtime object you checked
|
||||
RT = rt;
|
||||
|
||||
var proxy = rt.proxy;
|
||||
if (isRegister && !isProxyEmpty(proxy) && (!proxy.edPublic || !proxy.edPrivate)) {
|
||||
console.error("INVALID KEYS");
|
||||
console.log(JSON.stringify(proxy));
|
||||
return;
|
||||
}
|
||||
|
||||
res.proxy = rt.proxy;
|
||||
res.realtime = rt.realtime;
|
||||
res.network = rt.network;
|
||||
|
||||
// they're registering...
|
||||
res.userHash = userHash;
|
||||
res.userName = uname;
|
||||
|
||||
// somehow they have a block present, but nothing in the user object it specifies
|
||||
// this shouldn't happen, but let's send feedback if it does
|
||||
if (!isRegister && isProxyEmpty(rt.proxy)) {
|
||||
// this really shouldn't happen, but let's handle it anyway
|
||||
Feedback.send('EMPTY_LOGIN_WITH_BLOCK');
|
||||
|
||||
//rt.network.disconnect(); // clean up after yourself
|
||||
waitFor.abort();
|
||||
return void cb('NO_SUCH_USER', res);
|
||||
}
|
||||
|
||||
// they tried to register, but those exact credentials exist
|
||||
if (isRegister && !isProxyEmpty(rt.proxy)) {
|
||||
//rt.network.disconnect();
|
||||
waitFor.abort();
|
||||
res.blockHash = blockHash;
|
||||
if (shouldImport) {
|
||||
setMergeAnonDrive();
|
||||
}
|
||||
|
||||
return void cb('ALREADY_REGISTERED', res);
|
||||
}
|
||||
|
||||
if (!isRegister && !isProxyEmpty(rt.proxy)) {
|
||||
waitFor.abort();
|
||||
if (shouldImport) {
|
||||
setMergeAnonDrive();
|
||||
}
|
||||
var l = Util.find(rt.proxy, ['settings', 'general', 'language']);
|
||||
var LS_LANG = "CRYPTPAD_LANG";
|
||||
if (l) {
|
||||
localStorage.setItem(LS_LANG, l);
|
||||
}
|
||||
|
||||
if (res.TOTP_token && res.TOTP_token.bearer) {
|
||||
LocalStore.setSessionToken(res.TOTP_token.bearer);
|
||||
}
|
||||
return void LocalStore.login(undefined, blockHash, uname, function () {
|
||||
cb(void 0, res);
|
||||
});
|
||||
}
|
||||
|
||||
if (isRegister && isProxyEmpty(rt.proxy)) {
|
||||
proxy.edPublic = opt.edPublic;
|
||||
proxy.edPrivate = opt.edPrivate;
|
||||
proxy.curvePublic = opt.curvePublic;
|
||||
proxy.curvePrivate = opt.curvePrivate;
|
||||
proxy.login_name = uname;
|
||||
proxy[Constants.displayNameKey] = uname;
|
||||
if (shouldImport) {
|
||||
setMergeAnonDrive();
|
||||
} else {
|
||||
proxy.version = 11;
|
||||
}
|
||||
|
||||
Feedback.send('REGISTRATION', true);
|
||||
} else {
|
||||
Feedback.send('LOGIN', true);
|
||||
}
|
||||
|
||||
setTimeout(waitFor(function () {
|
||||
Realtime.whenRealtimeSyncs(rt.realtime, waitFor());
|
||||
}));
|
||||
}));
|
||||
}).nThen(function (waitFor) {
|
||||
require(['/common/pinpad.js'], waitFor(function (_Pinpad) {
|
||||
console.log("loaded rpc module");
|
||||
Pinpad = _Pinpad;
|
||||
}));
|
||||
}).nThen(function (waitFor) {
|
||||
// send an RPC to store the block which you created.
|
||||
console.log("initializing rpc interface");
|
||||
|
||||
Pinpad.create(RT.network, Block.keysToRPCFormat(res.opt.blockKeys), waitFor(function (e, _rpc) {
|
||||
if (e) {
|
||||
waitFor.abort();
|
||||
console.error(e); // INVALID_KEYS
|
||||
return void cb('RPC_CREATION_ERROR');
|
||||
}
|
||||
rpc = _rpc;
|
||||
console.log("rpc initialized");
|
||||
}));
|
||||
}).nThen(function (waitFor) {
|
||||
console.log("creating request to publish a login block");
|
||||
|
||||
// Finally, create the login block for the object you just created.
|
||||
var toPublish = {};
|
||||
|
||||
toPublish[Constants.userHashKey] = userHash;
|
||||
toPublish.edPublic = RT.proxy.edPublic;
|
||||
|
||||
Block.writeLoginBlock({
|
||||
blockKeys: blockKeys,
|
||||
content: toPublish
|
||||
}, waitFor(function (e) {
|
||||
if (e) {
|
||||
console.error(e);
|
||||
waitFor.abort();
|
||||
return void cb(e);
|
||||
}
|
||||
}));
|
||||
}).nThen(function (waitFor) {
|
||||
// confirm that the block was actually written before considering registration successful
|
||||
Util.fetch(blockUrl, waitFor(function (err /*, block */) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
waitFor.abort();
|
||||
return void cb(err);
|
||||
}
|
||||
|
||||
console.log("blockInfo available at:", blockHash);
|
||||
LocalStore.login(undefined, blockHash, uname, function () {
|
||||
cb(void 0, res);
|
||||
});
|
||||
}));
|
||||
});
|
||||
};
|
||||
Exports.redirect = function () {
|
||||
if (redirectTo) {
|
||||
var h = redirectTo;
|
||||
|
@ -573,6 +94,8 @@ define([
|
|||
if (hashing) { return void console.log("hashing is already in progress"); }
|
||||
hashing = true;
|
||||
|
||||
setMergeAnonDrive(shouldImport);
|
||||
|
||||
var proceed = function (result) {
|
||||
hashing = false;
|
||||
// NOTE: test is also use as a cb for the install page
|
||||
|
@ -594,7 +117,12 @@ define([
|
|||
// We need a setTimeout(cb, 0) otherwise the loading screen is only displayed
|
||||
// after hashing the password
|
||||
window.setTimeout(function () {
|
||||
Exports.loginOrRegister(uname, passwd, isRegister, shouldImport, onOTP, function (err, result) {
|
||||
Login.loginOrRegister({
|
||||
uname,
|
||||
passwd,
|
||||
isRegister,
|
||||
onOTP
|
||||
}, function (err, result) {
|
||||
var proxy;
|
||||
if (result) { proxy = result.proxy; }
|
||||
|
||||
|
|
|
@ -11,13 +11,18 @@ define([
|
|||
], function (h, UI, Msg, Pages, Config) {
|
||||
return function () {
|
||||
document.title = Msg.login_login;
|
||||
|
||||
var ssoEnabled = (Config.sso && Config.sso.list && Config.sso.list.length) ?'': '.cp-hidden';
|
||||
var ssoEnforced = (Config.sso && Config.sso.force) ? '.cp-hidden' : '';
|
||||
Msg.sso_login_description = "Login from SSO...."; // XXX
|
||||
|
||||
return [h('div#cp-main', [
|
||||
Pages.infopageTopbar(),
|
||||
h('div.container.cp-container', [
|
||||
h('div.row.cp-page-title', h('h1', Msg.login_login)),
|
||||
h('div.row', [
|
||||
h('div.col-md-3'),
|
||||
h('div#userForm.form-group.hidden.col-md-6', [
|
||||
h('div.col-md-3'+ssoEnforced),
|
||||
h('div#userForm.form-group.col-md-6'+ssoEnforced, [
|
||||
h('div.cp-login-instance', Msg._getKey('login_instance', [ Pages.Instance.name ])),
|
||||
h('div.big-container', [
|
||||
h('div.input-container', [
|
||||
|
@ -48,17 +53,22 @@ define([
|
|||
]),
|
||||
h('div.extra', [
|
||||
(Config.restrictRegistration?
|
||||
undefined:
|
||||
h('div'):
|
||||
h('a#register', {
|
||||
href: "/register/",
|
||||
}, Msg.login_register)
|
||||
),
|
||||
h('button.login', Msg.login_login)
|
||||
])
|
||||
h('button.login', Msg.login_login),
|
||||
]),
|
||||
]),
|
||||
h('div.col-md-3')
|
||||
h('div.col-md-3'),
|
||||
h('div.col-md-3'+ssoEnabled),
|
||||
h('div#ssoForm.form-group.col-md-6'+ssoEnabled, [
|
||||
h('div.cp-login-sso', Msg.sso_login_description)
|
||||
]),
|
||||
h('div.col-md-3'+ssoEnabled),
|
||||
]),
|
||||
h('div.row', [
|
||||
h('div.row.cp-login-encryption', [
|
||||
h('div.col-md-3'),
|
||||
h('div.col-md-6', Msg.register_warning_note),
|
||||
h('div.col-md-3'),
|
||||
|
|
|
@ -14,6 +14,10 @@ define([
|
|||
document.title = Msg.register_header;
|
||||
var tos = $(UI.createCheckbox('accept-terms')).find('.cp-checkmark-label').append(Msg.register_acceptTerms).parent()[0];
|
||||
|
||||
var ssoEnabled = (Config.sso && Config.sso.list && Config.sso.list.length) ?'': '.cp-hidden';
|
||||
var ssoEnforced = (Config.sso && Config.sso.force) ? '.cp-hidden' : '';
|
||||
Msg.sso_register_description = "Register from SSO...."; // XXX
|
||||
|
||||
var termsLink = Pages.customURLs.terms;
|
||||
$(tos).find('a').attr({
|
||||
href: termsLink,
|
||||
|
@ -53,7 +57,7 @@ define([
|
|||
Pages.setHTML(h('div.cp-register-notes'), Msg.register_notes)
|
||||
]),
|
||||
h('div.cp-reg-form.col-md-6', [
|
||||
h('div#userForm.form-group.hidden', [
|
||||
h('div#userForm.form-group'+ssoEnforced, [
|
||||
h('div.cp-register-instance', [
|
||||
Msg._getKey('register_instance', [Pages.Instance.name]),
|
||||
h('br'),
|
||||
|
@ -95,8 +99,11 @@ define([
|
|||
UI.createCheckbox('import-recent', Msg.register_importRecent, true)
|
||||
]),
|
||||
termsCheck,
|
||||
h('button#register', Msg.login_register)
|
||||
])
|
||||
h('button#register', Msg.login_register),
|
||||
]),
|
||||
h('div#ssoForm.form-group.col-md-6'+ssoEnabled, [
|
||||
h('div.cp-register-sso', Msg.sso_register_description)
|
||||
]),
|
||||
]),
|
||||
])
|
||||
]);
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
define([
|
||||
'/api/config',
|
||||
'jquery',
|
||||
'/common/hyperscript.js',
|
||||
'/common/common-interface.js',
|
||||
'/customize/messages.js',
|
||||
'/customize/pages.js'
|
||||
], function (Config, $, h, UI, Msg, Pages) {
|
||||
|
||||
Msg.ssoauth_header = "SSO authentication"; // XXX
|
||||
Msg.ssoauth_form_hint_register = "Add a CryptPad password for extra security or leave empty and continue";
|
||||
Msg.ssoauth_form_hint_login = "Please enter your CryptPad password";
|
||||
Msg.continue = "Continue";
|
||||
|
||||
return function () {
|
||||
document.title = Msg.ssoauth_header;
|
||||
|
||||
var frame = function (content) {
|
||||
return [
|
||||
h('div#cp-main', [
|
||||
Pages.infopageTopbar(),
|
||||
h('div.container.cp-container', [
|
||||
h('div.row.cp-page-title', h('h1', Msg.ssoauth_header)),
|
||||
].concat(content)),
|
||||
Pages.infopageFooter(),
|
||||
]),
|
||||
];
|
||||
};
|
||||
|
||||
return frame([
|
||||
h('div.row', [
|
||||
h('div.hidden.col-md-3'),
|
||||
h('div#userForm.form-group.col-md-6.cp-ssoauth-pw', [
|
||||
h('p.cp-isregister.cp-login-instance', Msg.ssoauth_form_hint_register),
|
||||
h('p.cp-islogin.cp-login-instance', Msg.ssoauth_form_hint_login),
|
||||
h('input.form-control#password', {
|
||||
type: 'password',
|
||||
placeholder: Msg.login_password,
|
||||
}),
|
||||
h('input.form-control.cp-isregister#passwordconfirm', {
|
||||
type: 'password',
|
||||
placeholder: Msg.login_confirm,
|
||||
}),
|
||||
h('div.cp-ssoauth-button.extra',
|
||||
h('div'),
|
||||
h('button.login#cp-ssoauth-button', Msg.continue)
|
||||
)
|
||||
]),
|
||||
h('div.hidden.col-md-3'),
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
@import (reference) "../include/infopages.less";
|
||||
@import (reference) "../include/colortheme-all.less";
|
||||
@import (reference) "../include/alertify.less";
|
||||
@import (reference) "../include/forms.less";
|
||||
|
||||
.login_main() {
|
||||
#userForm, #ssoForm {
|
||||
.cp-shadow();
|
||||
background-color: @cp_static-card-bg;
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
border-radius: @infopages-radius-L;
|
||||
.cp-login-instance, .cp-login-sso-description {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.form-control {
|
||||
border-radius: @infopages-radius;
|
||||
color: @cryptpad_text_col;
|
||||
background-color: @cp_forms-bg;
|
||||
margin-bottom: 10px;
|
||||
&:focus {
|
||||
border-color: @cryptpad_color_brand;
|
||||
}
|
||||
.tools_placeholder-color();
|
||||
}
|
||||
.checkbox-container {
|
||||
color: @cryptpad_text_col;
|
||||
}
|
||||
}
|
||||
#ssoForm {
|
||||
margin-top: 10px;
|
||||
button {
|
||||
margin: 0 5px;
|
||||
}
|
||||
.cp-login-sso {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.cp-default-label {
|
||||
display: none;
|
||||
}
|
||||
.extra {
|
||||
margin-top: 1em;
|
||||
button.login {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
@import (reference) "../include/alertify.less";
|
||||
@import (reference) "../include/checkmark.less";
|
||||
@import (reference) "../include/forms.less";
|
||||
@import (reference) "../include/login.less";
|
||||
|
||||
&.cp-page-login {
|
||||
.infopages_main();
|
||||
|
@ -25,42 +26,18 @@
|
|||
}
|
||||
}
|
||||
.cp-container {
|
||||
#userForm {
|
||||
.cp-shadow();
|
||||
background-color: @cp_static-card-bg;
|
||||
padding: 10px;
|
||||
margin: 0 10px 10px 10px;
|
||||
border-radius: @infopages-radius-L;
|
||||
.cp-login-instance {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.form-control {
|
||||
border-radius: @infopages-radius;
|
||||
color: @cryptpad_text_col;
|
||||
background-color: @cp_forms-bg;
|
||||
margin-bottom: 10px;
|
||||
&:focus {
|
||||
border-color: @cryptpad_color_brand;
|
||||
}
|
||||
.tools_placeholder-color();
|
||||
}
|
||||
.checkbox-container {
|
||||
color: @cryptpad_text_col;
|
||||
}
|
||||
.cp-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
.login_main();
|
||||
.align-items-center {
|
||||
box-shadow: 0 5px 15px @cp_shadow-color;
|
||||
background: @cryptpad_color_white;
|
||||
}
|
||||
.extra {
|
||||
margin-top: 1em;
|
||||
button.login {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
.cp-default-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.cp-login-encryption {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.cp-password-form {
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
@import (reference) "../include/alertify.less";
|
||||
@import (reference) "../include/checkmark.less";
|
||||
@import (reference) "../include/forms.less";
|
||||
@import (reference) "../include/login.less";
|
||||
|
||||
&.cp-page-register {
|
||||
.infopages_main();
|
||||
|
@ -91,32 +92,25 @@
|
|||
z-index: 0;
|
||||
}
|
||||
}
|
||||
#userForm {
|
||||
padding: 15px;
|
||||
background-color: @cp_static-card-bg;
|
||||
.cp-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
.login_main();
|
||||
#userForm, #ssoForm {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-bottom: 100px;
|
||||
border-radius: @infopages-radius-L;
|
||||
.cp-shadow();
|
||||
.form-control {
|
||||
border-radius: @infopages-radius;
|
||||
color: @cryptpad_text_col;
|
||||
background-color: @cp_forms-bg;
|
||||
margin-bottom: 10px;
|
||||
&:focus {
|
||||
border-color: @cryptpad_color_brand;
|
||||
}
|
||||
.tools_placeholder-color();
|
||||
}
|
||||
max-width: 100%;
|
||||
padding: 15px;
|
||||
.checkbox-container {
|
||||
margin-top: 0.5rem;
|
||||
color: @cryptpad_text_col;
|
||||
}
|
||||
button#register {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
#ssoForm {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.cp-register-notes {
|
||||
ul.cp-notes-list {
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
@import (reference) "../include/infopages.less";
|
||||
@import (reference) "../include/colortheme-all.less";
|
||||
@import (reference) "../include/alertify.less";
|
||||
@import (reference) "../include/forms.less";
|
||||
@import (reference) "../include/login.less";
|
||||
|
||||
&.cp-page-ssoauth {
|
||||
.infopages_main();
|
||||
.forms_main();
|
||||
.alertify_main();
|
||||
|
||||
.form-group {
|
||||
.extra {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
div.cp-ssoauth-pw {
|
||||
display: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cp-container {
|
||||
.login_main();
|
||||
}
|
||||
|
||||
&.cp-register {
|
||||
.cp-islogin {
|
||||
display: none;
|
||||
}
|
||||
#passwordconfirm {
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
}
|
||||
&.cp-login {
|
||||
.cp-isregister {
|
||||
display: none;
|
||||
}
|
||||
#password {
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -69,6 +69,8 @@ $(function () {
|
|||
require([ '/install/main.js' ], function () {});
|
||||
} else if (/^\/recovery\//.test(pathname)) {
|
||||
require([ '/recovery/main.js' ], function () {});
|
||||
} else if (/^\/ssoauth\//.test(pathname)) {
|
||||
require([ '/ssoauth/main.js' ], function () {});
|
||||
} else if (/^\/login\//.test(pathname)) {
|
||||
require([ '/login/main.js' ], function () {});
|
||||
} else if (/^\/($|^\/index\.html$)/.test(pathname)) {
|
||||
|
|
|
@ -13,6 +13,9 @@ const Core = require("./commands/core");
|
|||
const Metadata = require("./commands/metadata");
|
||||
const Meta = require("./metadata");
|
||||
const Logger = require("./log");
|
||||
const plugins = require("./plugin-manager");
|
||||
|
||||
let SSOUtils = plugins.SSO && plugins.SSO.utils;
|
||||
|
||||
const Path = require("path");
|
||||
const Fse = require("fs-extra");
|
||||
|
@ -200,6 +203,12 @@ COMMANDS.start = (edPublic, blockId, reason) => {
|
|||
}
|
||||
Log.info('MODERATION_ACCOUNT_BLOCK', safeKey, waitFor());
|
||||
}));
|
||||
if (!SSOUtils) { return; }
|
||||
SSOUtils.deleteAccount(Env, blockId, waitFor((err) => {
|
||||
if (err) {
|
||||
return Log.error('MODERATION_ACCOUNT_BLOCK_SSO', err, waitFor());
|
||||
}
|
||||
}));
|
||||
}));
|
||||
}).nThen((waitFor) => {
|
||||
var report = {
|
||||
|
@ -289,6 +298,12 @@ COMMANDS.restore = (edPublic) => {
|
|||
}
|
||||
Log.info('MODERATION_ACCOUNT_BLOCK_RESTORE', safeKey, waitFor());
|
||||
}));
|
||||
if (!SSOUtils) { return; }
|
||||
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) => {
|
||||
|
|
|
@ -5,6 +5,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;
|
||||
|
||||
|
@ -55,8 +56,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
|
||||
|
|
|
@ -12,6 +12,7 @@ const MFA = require("../storage/mfa");
|
|||
const Sessions = require("../storage/sessions");
|
||||
const BlockStore = require("../storage/block");
|
||||
const Block = require("../commands/block");
|
||||
const config = require("../load-config");
|
||||
|
||||
const Commands = module.exports;
|
||||
|
||||
|
@ -61,21 +62,40 @@ var decode32 = S => {
|
|||
};
|
||||
|
||||
|
||||
// Decide expire time
|
||||
// Allow user settings?
|
||||
var EXPIRATION = 7 * 24 * 3600 * 1000; // Sessions are valid 7 days
|
||||
var EXPIRATION = (config.otpSessionExpiration || 7 * 24) * 3600 * 1000;
|
||||
|
||||
// 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();
|
||||
let SSOUtils = Env.plugins && Env.plugins.SSO && Env.plugins.SSO.utils;
|
||||
|
||||
// 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 || !SSOUtils) { 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),
|
||||
|
@ -86,7 +106,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,
|
||||
|
@ -207,7 +232,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
|
||||
|
@ -257,7 +282,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);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -305,12 +330,12 @@ 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
|
||||
const check = Commands.TOTP_CHECK = function (Env, body, cb) {
|
||||
const check = Commands.TOTP_MFA_CHECK = function (Env, body, cb) {
|
||||
var { publicKey, auth } = body;
|
||||
const code = auth;
|
||||
if (!isValidOTP(code)) { return void cb('E_INVALID'); }
|
||||
|
@ -436,7 +461,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) => {
|
||||
|
@ -448,7 +474,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');
|
||||
|
@ -456,7 +482,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);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -726,6 +726,8 @@ var archiveBlock = function (Env, Server, cb, data) {
|
|||
});
|
||||
cb(err);
|
||||
});
|
||||
let SSOUtils = Env.plugins && Env.plugins.SSO && Env.plugins.SSO.utils;
|
||||
if (SSOUtils) { SSOUtils.deleteAccount(Env, key, () => {}); }
|
||||
};
|
||||
|
||||
var restoreArchivedBlock = function (Env, Server, cb, data) {
|
||||
|
@ -740,6 +742,11 @@ var restoreArchivedBlock = function (Env, Server, cb, data) {
|
|||
key: key,
|
||||
reason: reason || '',
|
||||
});
|
||||
|
||||
// Also restore SSO data
|
||||
let SSOUtils = Env.plugins && Env.plugins.SSO && Env.plugins.SSO.utils;
|
||||
if (SSOUtils) { SSOUtils.restoreAccount(Env, key, () => {}); }
|
||||
|
||||
cb(err);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -186,5 +186,20 @@ 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.
|
||||
let SSOUtils = Env.plugins && Env.plugins.SSO && Env.plugins.SSO.utils;
|
||||
|
||||
if (!SSOUtils) { return; }
|
||||
if (reason !== 'PASSWORD_CHANGE') {
|
||||
SSOUtils.deleteAccount(Env, publicKey, () => {});
|
||||
} else {
|
||||
SSOUtils.deleteBlock(Env, publicKey, () => {});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ const Package = require("../package.json");
|
|||
const Default = require("./defaults");
|
||||
const Path = require("path");
|
||||
|
||||
const plugins = require('./plugin-manager');
|
||||
|
||||
const Nacl = require("tweetnacl/nacl-fast");
|
||||
|
||||
var canonicalizeOrigin = function (s) {
|
||||
|
@ -83,6 +85,7 @@ module.exports.create = function (config) {
|
|||
const curve = Nacl.box.keyPair();
|
||||
|
||||
const Env = {
|
||||
plugins: plugins,
|
||||
logFeedback: Boolean(config.logFeedback),
|
||||
mainPages: config.mainPages || Default.mainPages(),
|
||||
|
||||
|
@ -227,6 +230,8 @@ module.exports.create = function (config) {
|
|||
evictionReport: {},
|
||||
commandTimers: {},
|
||||
|
||||
sso: config.sso,
|
||||
|
||||
// initialized as undefined
|
||||
bearerSecret: void 0,
|
||||
curvePrivate: curve.secretKey,
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
var Nacl = require("tweetnacl/nacl-fast");
|
||||
var Util = require('./common-util.js');
|
||||
const plugins = require("./plugin-manager");
|
||||
|
||||
var Challenge = require("./storage/challenge.js");
|
||||
// C.read(Env, id, cb)
|
||||
|
@ -66,17 +67,26 @@ 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");
|
||||
COMMANDS.TOTP_SETUP = TOTP.TOTP_SETUP;
|
||||
COMMANDS.TOTP_VALIDATE = TOTP.TOTP_VALIDATE;
|
||||
COMMANDS.TOTP_CHECK = TOTP.TOTP_CHECK;
|
||||
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;
|
||||
|
||||
try {
|
||||
// SSO plugin may not be installed
|
||||
const SSO = plugins.SSO && plugins.SSO.challenge;
|
||||
COMMANDS.SSO_AUTH = SSO.SSO_AUTH;
|
||||
COMMANDS.SSO_AUTH_CB = SSO.SSO_AUTH_CB;
|
||||
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;
|
||||
} catch (e) {}
|
||||
|
||||
var randomToken = () => Nacl.util.encodeBase64(Nacl.randomBytes(24)).replace(/\//g, '-');
|
||||
|
||||
|
@ -145,7 +155,7 @@ var handleCommand = function (Env, req, res) {
|
|||
date: date,
|
||||
});
|
||||
});
|
||||
});
|
||||
}, req);
|
||||
} catch (err) {
|
||||
Env.Log.error("CHALLENGE_COMMAND_THROWN_ERROR", {
|
||||
error: Util.serializeError(err),
|
||||
|
@ -295,7 +305,7 @@ var handleResponse = function (Env, req, res) {
|
|||
});
|
||||
}
|
||||
res.status(200).json(content);
|
||||
});
|
||||
}, req, res);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -13,13 +13,19 @@ const Logger = require("./log");
|
|||
const AuthCommands = require("./http-commands");
|
||||
const MFA = require("./storage/mfa");
|
||||
const Sessions = require("./storage/sessions");
|
||||
const cookieParser = require("cookie-parser");
|
||||
const bodyParser = require('body-parser');
|
||||
const BlobStore = require("./storage/blob");
|
||||
const BlockStore = require("./storage/block");
|
||||
const plugins = require("./plugin-manager");
|
||||
|
||||
const DEFAULT_QUERY_TIMEOUT = 5000;
|
||||
const PID = process.pid;
|
||||
|
||||
let SSOUtils = plugins.SSO && plugins.SSO.utils;
|
||||
|
||||
var Env = JSON.parse(process.env.Env);
|
||||
Env.plugins = plugins;
|
||||
const response = Util.response(function (errLabel, info) {
|
||||
if (!Env.Log) { return; }
|
||||
Env.Log.error(errLabel, info);
|
||||
|
@ -64,6 +70,7 @@ EVENTS.ENV_UPDATE = function (data /*, cb */) {
|
|||
try {
|
||||
Env = JSON.parse(data);
|
||||
Env.Log = Log;
|
||||
Env.plugins = plugins;
|
||||
Env.incrementBytesWritten = function () {};
|
||||
} catch (err) {
|
||||
Log.error('HTTP_WORKER_ENV_UPDATE', Util.serializeError(err));
|
||||
|
@ -165,6 +172,11 @@ const Express = require("express");
|
|||
Express.static.mime.define({'application/wasm': ['wasm']});
|
||||
var app = Express();
|
||||
|
||||
app.use(bodyParser.urlencoded({
|
||||
extended: true
|
||||
}));
|
||||
app.use(cookieParser());
|
||||
|
||||
(function () {
|
||||
if (!Env.logFeedback) { return; }
|
||||
|
||||
|
@ -200,6 +212,30 @@ const wsProxy = createProxyMiddleware({
|
|||
|
||||
app.use('/cryptpad_websocket', wsProxy);
|
||||
|
||||
app.use('/ssoauth', (req, res, next) => {
|
||||
if (SSOUtils && req && req.body && req.body.SAMLResponse) {
|
||||
req.method = 'GET';
|
||||
|
||||
let token = Util.uid();
|
||||
let smres = req.body.SAMLResponse;
|
||||
return SSOUtils.writeRequest(Env, {
|
||||
id: token,
|
||||
type: 'saml',
|
||||
content: smres
|
||||
}, (err) => {
|
||||
if (err) {
|
||||
Log.error('E_SSO_WRITE_REQ', err);
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
let value = `samltoken="${token}"; SameSite=Strict; HttpOnly`;
|
||||
res.setHeader('Set-Cookie', value);
|
||||
next();
|
||||
});
|
||||
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
|
||||
app.use('/blob', function (req, res, next) {
|
||||
/* Head requests are used to check the size of a blob.
|
||||
|
@ -265,6 +301,7 @@ app.use(function (req, res, next) {
|
|||
next();
|
||||
});
|
||||
|
||||
|
||||
// serve custom app content from the customize directory
|
||||
// useful for testing pages customized with opengraph data
|
||||
app.use(Express.static(Path.resolve('./customize/www')));
|
||||
|
@ -303,7 +340,7 @@ app.use('/block/', function (req, res, next) {
|
|||
|
||||
var authorization = req.headers.authorization;
|
||||
|
||||
var mfa_params;
|
||||
var mfa_params, sso_params;
|
||||
nThen(function (w) {
|
||||
// First, check whether the block id in question has any MFA settings stored
|
||||
MFA.read(Env, name, w(function (err, content) {
|
||||
|
@ -312,8 +349,7 @@ app.use('/block/', function (req, res, next) {
|
|||
// in either case you can abort and fall through
|
||||
// allowing the static webserver to handle either case
|
||||
if (err && err.code === 'ENOENT') {
|
||||
w.abort();
|
||||
return void next();
|
||||
return;
|
||||
}
|
||||
|
||||
// we're not expecting other errors. the sensible thing is to fail
|
||||
|
@ -344,10 +380,31 @@ app.use('/block/', function (req, res, next) {
|
|||
});
|
||||
}
|
||||
}));
|
||||
|
||||
// Same for SSO settings
|
||||
if (!SSOUtils) { return; }
|
||||
SSOUtils.readBlock(Env, name, w(function (err, content) {
|
||||
if (err && (err.code === 'ENOENT' || err === 'ENOENT')) {
|
||||
return;
|
||||
}
|
||||
if (err) {
|
||||
Log.error('GET_BLOCK_METADATA', err);
|
||||
return void res.status(500).json({
|
||||
code: 500,
|
||||
error: "UNEXPECTED_ERROR",
|
||||
});
|
||||
}
|
||||
sso_params = content;
|
||||
}));
|
||||
}).nThen(function (w) {
|
||||
if (!mfa_params && !sso_params) {
|
||||
w.abort();
|
||||
next();
|
||||
}
|
||||
}).nThen(function (w) {
|
||||
// We should only be able to reach this logic
|
||||
// if we successfully loaded and parsed some JSON
|
||||
// representing the user's MFA settings.
|
||||
// representing the user's MFA and/or SSO settings.
|
||||
|
||||
// Failures at this point relate to insufficient or incorrect authorization.
|
||||
// This function standardizes how we reject such requests.
|
||||
|
@ -359,18 +416,19 @@ app.use('/block/', function (req, res, next) {
|
|||
var no = function () {
|
||||
w.abort();
|
||||
res.status(401).json({
|
||||
method: mfa_params.method,
|
||||
sso: Boolean(sso_params),
|
||||
method: mfa_params && mfa_params.method,
|
||||
code: 401
|
||||
});
|
||||
};
|
||||
|
||||
// if you are here it is because this block is protected by MFA.
|
||||
// if you are here it is because this block is protected by MFA or SSO.
|
||||
// they will need to provide a JSON Web Token, so we can reject them outright
|
||||
// if one is not present in their authorization header
|
||||
if (!authorization) { return void no(); }
|
||||
|
||||
// The authorization header should be of the form
|
||||
// "Authorization: Bearer <JWT>"
|
||||
// "Authorization: Bearer <SessionId>"
|
||||
// We can reject the request if it is malformed.
|
||||
let token = authorization.replace(/^Bearer\s+/, '').trim();
|
||||
if (!token) { return void no(); }
|
||||
|
@ -379,14 +437,18 @@ 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,
|
||||
});
|
||||
}
|
||||
|
||||
let content = Util.tryParse(contentStr);
|
||||
|
||||
if (content.mfa && content.mfa.exp && ((+new Date()) > content.mfa.exp)) {
|
||||
if (mfa_params && !content.mfa) { return void no(); }
|
||||
if (sso_params && !content.sso) { return void no(); }
|
||||
|
||||
if (content.mfa && content.mfa.exp && (+new Date()) > content.mfa.exp) {
|
||||
Log.error("OTP_SESSION_EXPIRED", content.mfa);
|
||||
Sessions.delete(Env, name, token, function (err) {
|
||||
if (err) {
|
||||
|
@ -398,12 +460,20 @@ app.use('/block/', function (req, res, next) {
|
|||
return void no();
|
||||
}
|
||||
|
||||
// we could also check whether the content of the file matches the token,
|
||||
// but clients don't have any influence over the reference and can only
|
||||
// request to create tokens that are scoped to a public key they control.
|
||||
// I don' think there's any practical benefit to such a check.
|
||||
|
||||
// So, interpret the existence of a file in that location as the continued
|
||||
if (content.sso && content.sso.exp && (+new Date()) > content.sso.exp) {
|
||||
Log.error("SSO_SESSION_EXPIRED", content.sso);
|
||||
Sessions.delete(Env, name, token, function (err) {
|
||||
if (err) {
|
||||
Log.error('SSO_SESSION_DELETE_EXPIRED_ERROR', err);
|
||||
return;
|
||||
}
|
||||
Log.info('SSO_SESSION_DELETE_EXPIRED', err);
|
||||
});
|
||||
return void no();
|
||||
}
|
||||
|
||||
// Interpret the existence of a file in that location as the continued
|
||||
// validity of the session. Fall through and let the built-in webserver
|
||||
// handle the 404 or serving the file.
|
||||
next();
|
||||
|
@ -481,6 +551,13 @@ var makeRouteCache = function (template, cacheName) {
|
|||
};
|
||||
};
|
||||
|
||||
const ssoList = Env.sso && Env.sso.enabled && Array.isArray(Env.sso.list) &&
|
||||
Env.sso.list.map(function (obj) { return obj.name; }) || [];
|
||||
const ssoCfg = (SSOUtils && ssoList.length) ? {
|
||||
force: (Env.sso && Env.sso.enforced && 1) || 0,
|
||||
password: (Env.sso && Env.sso.cpPassword && (Env.sso.forceCpPassword ? 2 : 1)) || 0,
|
||||
list: ssoList
|
||||
} : false;
|
||||
var serveConfig = makeRouteCache(function () {
|
||||
return [
|
||||
'define(function(){',
|
||||
|
@ -507,6 +584,7 @@ var serveConfig = makeRouteCache(function () {
|
|||
shouldUpdateNode: Env.shouldUpdateNode || undefined,
|
||||
listMyInstance: Env.listMyInstance,
|
||||
accounts_api: Env.accounts_api,
|
||||
sso: ssoCfg
|
||||
}, null, '\t'),
|
||||
'});'
|
||||
].join(';\n');
|
||||
|
|
|
@ -46,5 +46,12 @@ if (!isPositiveNumber(config.premiumUploadSize) || config.premiumUploadSize < co
|
|||
delete config.premiumUploadSize;
|
||||
}
|
||||
|
||||
config.sso = {};
|
||||
try {
|
||||
config.sso = require("../config/sso");
|
||||
} catch (e) {
|
||||
console.log("SSO config not found");
|
||||
}
|
||||
|
||||
module.exports = config;
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
const fs = require('node:fs');
|
||||
const plugins = {};
|
||||
|
||||
try {
|
||||
let pluginsDir = fs.readdirSync(__dirname + '/plugins');
|
||||
pluginsDir.forEach((name) => {
|
||||
try {
|
||||
let plugin = require(`./plugins/${name}/index`);
|
||||
plugins[plugin.name] = plugin.modules;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') { console.error(err); }
|
||||
}
|
||||
|
||||
module.exports = plugins;
|
|
@ -25,6 +25,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) => {
|
||||
|
@ -70,4 +71,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);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -46,6 +46,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);
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
const Basic = require("./basic");
|
||||
const Path = require("node:path");
|
||||
const Util = require("../common-util");
|
||||
|
||||
const SSO = module.exports;
|
||||
/* This module manages storage related to Single Sign-On (SSO) settings.
|
||||
|
||||
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 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.
|
||||
*/
|
||||
|
||||
var pathFromId = function (Env, id, subPath) {
|
||||
if (!id || typeof(id) !== 'string') { return; }
|
||||
id = Util.escapeKeyCharacters(id);
|
||||
return Path.join(Env.paths.base, subPath, id.slice(0, 2), `${id}.json`);
|
||||
};
|
||||
var reqPathFromId = function (Env, id) {
|
||||
return pathFromId(Env, id, 'sso_request');
|
||||
};
|
||||
var blockPathFromId = function (Env, id) {
|
||||
return pathFromId(Env, id, 'sso_block');
|
||||
};
|
||||
|
||||
var userPathFromId = function (Env, id, provider) {
|
||||
if (!id || typeof(id) !== 'string') { return; }
|
||||
if (!provider || typeof(provider) !== 'string') { return; }
|
||||
id = Util.escapeKeyCharacters(id);
|
||||
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) {
|
||||
var path = reqPathFromId(Env, id);
|
||||
Basic.read(Env, path, cb);
|
||||
};
|
||||
Req.write = function (Env, id, data, cb) {
|
||||
var path = reqPathFromId(Env, id);
|
||||
Basic.write(Env, path, data, cb);
|
||||
};
|
||||
Req.delete = function (Env, id, cb) {
|
||||
var path = reqPathFromId(Env, id);
|
||||
Basic.delete(Env, path, cb);
|
||||
};
|
||||
|
||||
|
||||
const User = SSO.user = {};
|
||||
|
||||
User.read = function (Env, provider, id, cb) {
|
||||
var path = userPathFromId(Env, id, provider);
|
||||
Basic.read(Env, path, cb);
|
||||
};
|
||||
User.write = function (Env, provider, id, data, cb) {
|
||||
var path = userPathFromId(Env, id, provider);
|
||||
Basic.write(Env, path, data, cb);
|
||||
};
|
||||
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 = {};
|
||||
|
||||
Block.read = function (Env, id, cb) {
|
||||
var path = blockPathFromId(Env, id);
|
||||
Basic.read(Env, path, cb);
|
||||
};
|
||||
Block.write = function (Env, id, data, cb) {
|
||||
var path = blockPathFromId(Env, id);
|
||||
Basic.write(Env, path, data, cb);
|
||||
};
|
||||
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);
|
||||
};
|
|
@ -10,7 +10,9 @@
|
|||
"license": "AGPL-3.0+",
|
||||
"dependencies": {
|
||||
"@mcrowe/minibloom": "^0.2.0",
|
||||
"@node-saml/node-saml": "^4.0.5",
|
||||
"alertify.js": "1.0.11",
|
||||
"body-parser": "^1.20.2",
|
||||
"bootstrap": "^4.0.0",
|
||||
"bootstrap-tokenfield": "^0.12.0",
|
||||
"chainpad": "^5.2.6",
|
||||
|
@ -21,6 +23,7 @@
|
|||
"ckeditor": "npm:ckeditor4@~4.22.1",
|
||||
"codemirror": "^5.19.0",
|
||||
"components-font-awesome": "^4.6.3",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"croppie": "^2.5.0",
|
||||
"dragula": "3.7.2",
|
||||
"drawio": "cryptpad/drawio-npm#npm",
|
||||
|
@ -42,6 +45,7 @@
|
|||
"notp": "^2.0.3",
|
||||
"nthen": "0.1.8",
|
||||
"open-sans-fontface": "^1.4.0",
|
||||
"openid-client": "^5.4.2",
|
||||
"pako": "^2.1.0",
|
||||
"prompt-confirm": "^2.0.4",
|
||||
"pull-stream": "^3.6.1",
|
||||
|
@ -86,6 +90,48 @@
|
|||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-saml/node-saml": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@node-saml/node-saml/-/node-saml-4.0.5.tgz",
|
||||
"integrity": "sha512-J5DglElbY1tjOuaR1NPtjOXkXY5bpUhDoKVoeucYN98A3w4fwgjIOPqIGcb6cQsqFq2zZ6vTCeKn5C/hvefSaw==",
|
||||
"dependencies": {
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/passport": "^1.0.11",
|
||||
"@types/xml-crypto": "^1.4.2",
|
||||
"@types/xml-encryption": "^1.2.1",
|
||||
"@types/xml2js": "^0.4.11",
|
||||
"@xmldom/xmldom": "^0.8.6",
|
||||
"debug": "^4.3.4",
|
||||
"xml-crypto": "^3.0.1",
|
||||
"xml-encryption": "^3.0.2",
|
||||
"xml2js": "^0.5.0",
|
||||
"xmlbuilder": "^15.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-saml/node-saml/node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@node-saml/node-saml/node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/@nodelib/fs.stat": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
|
||||
|
@ -95,6 +141,53 @@
|
|||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.4.tgz",
|
||||
"integrity": "sha512-N7UDG0/xiPQa2D/XrVJXjkWbpqHCd2sBaB32ggRF2l83RhPfamgKGF8gwwqyksS95qUS5ZYF9aF+lLPRlwI2UA==",
|
||||
"dependencies": {
|
||||
"@types/connect": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/connect": {
|
||||
"version": "3.4.37",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.37.tgz",
|
||||
"integrity": "sha512-zBUSRqkfZ59OcwXon4HVxhx5oWCJmc0OtBTK05M+p0dYjgN6iTwIL2T/WbsQZrEsdnwaF9cWQ+azOnpPvIqY3Q==",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/debug": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.10.tgz",
|
||||
"integrity": "sha512-tOSCru6s732pofZ+sMv9o4o3Zc+Sa8l3bxd/tweTQudFn06vAzb13ZX46Zi6m6EJ+RUbRTHvgQJ1gBtSgkaUYA==",
|
||||
"dependencies": {
|
||||
"@types/ms": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/express": {
|
||||
"version": "4.17.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.20.tgz",
|
||||
"integrity": "sha512-rOaqlkgEvOW495xErXMsmyX3WKBInbhG5eqojXYi3cGUaLoRDlXa5d52fkfWZT963AZ3v2eZ4MbKE6WpDAGVsw==",
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "^4.17.33",
|
||||
"@types/qs": "*",
|
||||
"@types/serve-static": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/express-serve-static-core": {
|
||||
"version": "4.17.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.38.tgz",
|
||||
"integrity": "sha512-hXOtc0tuDHZPFwwhuBJXPbjemWtXnJjbvuuyNH2Y5Z6in+iXc63c4eXYDc7GGGqHy+iwYqAJMdaItqdnbcBKmg==",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/qs": "*",
|
||||
"@types/range-parser": "*",
|
||||
"@types/send": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/glob": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
|
||||
|
@ -105,6 +198,11 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/http-errors": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.3.tgz",
|
||||
"integrity": "sha512-pP0P/9BnCj1OVvQR2lF41EkDG/lWWnDyA203b/4Fmi2eTyORnBtcDoKDwjWQthELrBvWkMOrvSOnZ8OVlW6tXA=="
|
||||
},
|
||||
"node_modules/@types/http-proxy": {
|
||||
"version": "1.17.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.12.tgz",
|
||||
|
@ -113,17 +211,89 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz",
|
||||
"integrity": "sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg=="
|
||||
},
|
||||
"node_modules/@types/minimatch": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
|
||||
"integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/ms": {
|
||||
"version": "0.7.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.33.tgz",
|
||||
"integrity": "sha512-AuHIyzR5Hea7ij0P9q7vx7xu4z0C28ucwjAZC0ja7JhINyCnOw8/DnvAPQQ9TfOlCtZAmCERKQX9+o1mgQhuOQ=="
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.3.tgz",
|
||||
"integrity": "sha512-HksnYH4Ljr4VQgEy2lTStbCKv/P590tmPe5HqOnv9Gprffgv5WXAY+Y5Gqniu0GGqeTCUdBnzC3QSrzPkBkAMA=="
|
||||
},
|
||||
"node_modules/@types/passport": {
|
||||
"version": "1.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.14.tgz",
|
||||
"integrity": "sha512-D6p2ygR2S7Cq5PO7iUaEIQu/5WrM0tONu6Lxgk0C9r3lafQIlVpWCo3V/KI9To3OqHBxcfQaOeK+8AvwW5RYmw==",
|
||||
"dependencies": {
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.9.tgz",
|
||||
"integrity": "sha512-wYLxw35euwqGvTDx6zfY1vokBFnsK0HNrzc6xNHchxfO2hpuRg74GbkEW7e3sSmPvj0TjCDT1VCa6OtHXnubsg=="
|
||||
},
|
||||
"node_modules/@types/range-parser": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.6.tgz",
|
||||
"integrity": "sha512-+0autS93xyXizIYiyL02FCY8N+KkKPhILhcUSA276HxzreZ16kl+cmwvV2qAM/PuCCwPXzOXOWhiPcw20uSFcA=="
|
||||
},
|
||||
"node_modules/@types/send": {
|
||||
"version": "0.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.2.tgz",
|
||||
"integrity": "sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw==",
|
||||
"dependencies": {
|
||||
"@types/mime": "^1",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/serve-static": {
|
||||
"version": "1.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.3.tgz",
|
||||
"integrity": "sha512-yVRvFsEMrv7s0lGhzrggJjNOSmZCdgCjw9xWrPr/kNNLp6FaDfMC1KaYl3TSJ0c58bECwNBMoQrZJ8hA8E1eFg==",
|
||||
"dependencies": {
|
||||
"@types/http-errors": "*",
|
||||
"@types/mime": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/xml-crypto": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/xml-crypto/-/xml-crypto-1.4.3.tgz",
|
||||
"integrity": "sha512-pnvKYb7vUsUIMc+C6JM/j779YWQgOMcwjnqHJ9cdaWXwWEBE1hAqthzeszRx62V5RWMvS+XS9w9tXMOYyUc8zg==",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"xpath": "0.0.27"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/xml-encryption": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/xml-encryption/-/xml-encryption-1.2.2.tgz",
|
||||
"integrity": "sha512-UeuYOqW3ZzUQfwb/mb3GNZ2/DlVdh5mjJNmB/yFXgQr8/pwlVJ9I2w+AHPfRDzLshe7YpgUB4T1//qgbk6U87Q==",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/xml2js": {
|
||||
"version": "0.4.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.12.tgz",
|
||||
"integrity": "sha512-CZPpQKBZ8db66EP5hCjwvYrLThgZvnyZrPXK2W+UI1oOaWezGt34iOaUCX4Jah2X8+rQqjvl9VKEIT8TR1I0rA==",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@xmldom/xmldom": {
|
||||
"version": "0.8.10",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
|
||||
|
@ -681,12 +851,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
|
||||
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
||||
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"content-type": "~1.0.4",
|
||||
"content-type": "~1.0.5",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
|
@ -694,7 +864,7 @@
|
|||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.11.0",
|
||||
"raw-body": "2.5.1",
|
||||
"raw-body": "2.5.2",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
|
@ -1117,6 +1287,26 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-parser": {
|
||||
"version": "1.4.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
|
||||
"integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
|
||||
"dependencies": {
|
||||
"cookie": "0.4.1",
|
||||
"cookie-signature": "1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-parser/node_modules/cookie": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
|
||||
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
|
@ -1564,6 +1754,43 @@
|
|||
"node": ">= 0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/body-parser": {
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
|
||||
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"content-type": "~1.0.4",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.11.0",
|
||||
"raw-body": "2.5.1",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/raw-body": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
|
||||
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/extend-shallow": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
||||
|
@ -2436,6 +2663,14 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "4.15.3",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.3.tgz",
|
||||
"integrity": "sha512-RZJdL9Qjd1sqNdyiVteRGV/bnWtik/+PJh1JP4kT6+x1QQMn+7ryueRys5BEueuayvSVY8CWGCisCDazeRLTuw==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/jquery": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
|
||||
|
@ -3182,6 +3417,14 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-hash": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
|
||||
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.12.3",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
|
||||
|
@ -3213,6 +3456,14 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/oidc-token-hash": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz",
|
||||
"integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==",
|
||||
"engines": {
|
||||
"node": "^10.13.0 || >=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
|
@ -3238,6 +3489,20 @@
|
|||
"resolved": "https://registry.npmjs.org/open-sans-fontface/-/open-sans-fontface-1.4.0.tgz",
|
||||
"integrity": "sha512-d1VXrt1qPScsZnDHbZTOf1SmUnanr3KQgQM6+ye6KoFgrLo8a8mkX/J/ZJ2+w7vf0sCC02lRia5SAiaz0JPEog=="
|
||||
},
|
||||
"node_modules/openid-client": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.1.tgz",
|
||||
"integrity": "sha512-PtrWsY+dXg6y8mtMPyL/namZSYVz8pjXz3yJiBNZsEdCnu9miHLB4ELVC85WvneMKo2Rg62Ay7NkuCpM0bgiLQ==",
|
||||
"dependencies": {
|
||||
"jose": "^4.15.1",
|
||||
"lru-cache": "^6.0.0",
|
||||
"object-hash": "^2.2.0",
|
||||
"oidc-token-hash": "^5.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/optimist": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
|
||||
|
@ -3649,9 +3914,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
|
||||
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
|
||||
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"http-errors": "2.0.0",
|
||||
|
@ -3879,6 +4144,11 @@
|
|||
"resolved": "https://registry.npmjs.org/saferphore/-/saferphore-0.0.1.tgz",
|
||||
"integrity": "sha512-/KaXQyumYbALQwhl4/Qov6voayHwtBD12AWRI3zxYBW7cY3Mjtkk7PLttZPq+25nyfu/k3APHUptPLZI9A7yHA=="
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz",
|
||||
"integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA=="
|
||||
},
|
||||
"node_modules/scrypt-async": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/scrypt-async/-/scrypt-async-1.2.0.tgz",
|
||||
|
@ -4809,6 +5079,83 @@
|
|||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/xml-crypto": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-3.2.0.tgz",
|
||||
"integrity": "sha512-qVurBUOQrmvlgmZqIVBqmb06TD2a/PpEUfFPgD7BuBfjmoH4zgkqaWSIJrnymlCvM2GGt9x+XtJFA+ttoAufqg==",
|
||||
"dependencies": {
|
||||
"@xmldom/xmldom": "^0.8.8",
|
||||
"xpath": "0.0.32"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xml-crypto/node_modules/xpath": {
|
||||
"version": "0.0.32",
|
||||
"resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.32.tgz",
|
||||
"integrity": "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==",
|
||||
"engines": {
|
||||
"node": ">=0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xml-encryption": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xml-encryption/-/xml-encryption-3.0.2.tgz",
|
||||
"integrity": "sha512-VxYXPvsWB01/aqVLd6ZMPWZ+qaj0aIdF+cStrVJMcFj3iymwZeI0ABzB3VqMYv48DkSpRhnrXqTUkR34j+UDyg==",
|
||||
"dependencies": {
|
||||
"@xmldom/xmldom": "^0.8.5",
|
||||
"escape-html": "^1.0.3",
|
||||
"xpath": "0.0.32"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/xml-encryption/node_modules/xpath": {
|
||||
"version": "0.0.32",
|
||||
"resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.32.tgz",
|
||||
"integrity": "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==",
|
||||
"engines": {
|
||||
"node": ">=0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xml2js": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
|
||||
"integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
|
||||
"dependencies": {
|
||||
"sax": ">=0.6.0",
|
||||
"xmlbuilder": "~11.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xml2js/node_modules/xmlbuilder": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlbuilder": {
|
||||
"version": "15.1.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
|
||||
"integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==",
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xpath": {
|
||||
"version": "0.0.27",
|
||||
"resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz",
|
||||
"integrity": "sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ==",
|
||||
"engines": {
|
||||
"node": ">=0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
"@mcrowe/minibloom": "^0.2.0",
|
||||
"chainpad-crypto": "^0.2.5",
|
||||
"chainpad-server": "^5.1.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"body-parser": "^1.20.2",
|
||||
"express": "~4.18.2",
|
||||
"fs-extra": "^7.0.0",
|
||||
"get-folder-size": "^2.0.1",
|
||||
|
@ -23,6 +25,8 @@
|
|||
"jsonwebtoken": "^9.0.0",
|
||||
"notp": "^2.0.3",
|
||||
"nthen": "0.1.8",
|
||||
"openid-client": "^5.4.2",
|
||||
"@node-saml/node-saml": "^4.0.5",
|
||||
"prompt-confirm": "^2.0.4",
|
||||
"pull-stream": "^3.6.1",
|
||||
"saferphore": "0.0.1",
|
||||
|
|
|
@ -10,6 +10,7 @@ define(['/customize/application_config.js'], function (AppConfig) {
|
|||
blockHashKey: 'Block_hash',
|
||||
fileHashKey: 'FS_hash',
|
||||
sessionJWT: 'Session_JWT',
|
||||
ssoSeed: 'SSO_seed',
|
||||
|
||||
// Store
|
||||
displayNameKey: 'cryptpad.username',
|
||||
|
|
|
@ -0,0 +1,549 @@
|
|||
define([
|
||||
'chainpad-listmap',
|
||||
'/components/chainpad-crypto/crypto.js',
|
||||
'/common/common-util.js',
|
||||
'/common/outer/network-config.js',
|
||||
'/common/common-credential.js',
|
||||
'/components/chainpad/chainpad.dist.js',
|
||||
'/common/common-realtime.js',
|
||||
'/common/common-constants.js',
|
||||
'/common/common-interface.js',
|
||||
'/common/common-feedback.js',
|
||||
'/common/outer/local-store.js',
|
||||
'/customize/messages.js',
|
||||
'/components/nthen/index.js',
|
||||
'/common/outer/login-block.js',
|
||||
'/common/common-hash.js',
|
||||
'/common/outer/http-command.js',
|
||||
|
||||
'/components/tweetnacl/nacl-fast.min.js',
|
||||
'/components/scrypt-async/scrypt-async.min.js', // better load speed
|
||||
], function (Listmap, Crypto, Util, NetConfig, Cred, ChainPad, Realtime, Constants, UI,
|
||||
Feedback, LocalStore, Messages, nThen, Block, Hash, ServerCommand) {
|
||||
var Nacl = window.nacl;
|
||||
|
||||
var Exports = {
|
||||
requiredBytes: 192,
|
||||
};
|
||||
|
||||
var allocateBytes = Exports.allocateBytes = function (bytes) {
|
||||
var dispense = Cred.dispenser(bytes);
|
||||
|
||||
var opt = {};
|
||||
|
||||
// dispense 18 bytes of entropy for your encryption key
|
||||
var encryptionSeed = dispense(18);
|
||||
// 16 bytes for a deterministic channel key
|
||||
var channelSeed = dispense(16);
|
||||
// 32 bytes for a curve key
|
||||
var curveSeed = dispense(32);
|
||||
|
||||
var curvePair = Nacl.box.keyPair.fromSecretKey(new Uint8Array(curveSeed));
|
||||
opt.curvePrivate = Nacl.util.encodeBase64(curvePair.secretKey);
|
||||
opt.curvePublic = Nacl.util.encodeBase64(curvePair.publicKey);
|
||||
|
||||
// 32 more for a signing key
|
||||
var edSeed = opt.edSeed = dispense(32);
|
||||
|
||||
// 64 more bytes to seed an additional signing key
|
||||
var blockKeys = opt.blockKeys = Block.genkeys(new Uint8Array(dispense(64)));
|
||||
opt.blockHash = Block.getBlockHash(blockKeys);
|
||||
|
||||
// derive a private key from the ed seed
|
||||
var signingKeypair = Nacl.sign.keyPair.fromSeed(new Uint8Array(edSeed));
|
||||
|
||||
opt.edPrivate = Nacl.util.encodeBase64(signingKeypair.secretKey);
|
||||
opt.edPublic = Nacl.util.encodeBase64(signingKeypair.publicKey);
|
||||
|
||||
var keys = opt.keys = Crypto.createEditCryptor(null, encryptionSeed);
|
||||
|
||||
// 24 bytes of base64
|
||||
keys.editKeyStr = keys.editKeyStr.replace(/\//g, '-');
|
||||
|
||||
// 32 bytes of hex
|
||||
var channelHex = opt.channelHex = Util.uint8ArrayToHex(channelSeed);
|
||||
|
||||
// should never happen
|
||||
if (channelHex.length !== 32) { throw new Error('invalid channel id'); }
|
||||
|
||||
var channel64 = Util.hexToBase64(channelHex);
|
||||
|
||||
// we still generate a v1 hash because this function needs to deterministically
|
||||
// derive the same values as it always has. New accounts will generate their own
|
||||
// userHash values
|
||||
opt.userHash = '/1/edit/' + [channel64, opt.keys.editKeyStr].join('/') + '/';
|
||||
|
||||
return opt;
|
||||
};
|
||||
|
||||
var loginOptionsFromBlock = Exports.loginOptionsFromBlock = function (blockInfo) {
|
||||
var opt = {};
|
||||
var parsed = Hash.getSecrets('pad', blockInfo.User_hash);
|
||||
opt.channelHex = parsed.channel;
|
||||
opt.keys = parsed.keys;
|
||||
opt.edPublic = blockInfo.edPublic;
|
||||
return opt;
|
||||
};
|
||||
|
||||
var loadUserObject = Exports.loadUserObject = function (opt, _cb) {
|
||||
var cb = Util.once(Util.mkAsync(_cb));
|
||||
var config = {
|
||||
websocketURL: NetConfig.getWebsocketURL(),
|
||||
channel: opt.channelHex,
|
||||
data: {},
|
||||
validateKey: opt.keys.validateKey, // derived validation key
|
||||
crypto: Crypto.createEncryptor(opt.keys),
|
||||
logLevel: 1,
|
||||
classic: true,
|
||||
ChainPad: ChainPad,
|
||||
owners: [opt.edPublic]
|
||||
};
|
||||
|
||||
var rt = opt.rt = Listmap.create(config);
|
||||
rt.proxy
|
||||
.on('ready', function () {
|
||||
setTimeout(function () { cb(void 0, rt); });
|
||||
})
|
||||
.on('error', function (info) {
|
||||
cb(info.type, {reason: info.message});
|
||||
})
|
||||
.on('disconnect', function (info) {
|
||||
cb('E_DISCONNECT', info);
|
||||
});
|
||||
};
|
||||
|
||||
var isProxyEmpty = Exports.isProxyEmpty = function (proxy) {
|
||||
var l = Object.keys(proxy).length;
|
||||
return l === 0 || (l === 2 && proxy._events && proxy.on);
|
||||
};
|
||||
|
||||
var legacyLogin = function (opt, isRegister, cb, res) {
|
||||
res = res || {};
|
||||
loadUserObject(opt, function (err, rt) {
|
||||
if (err) { return void cb(err); }
|
||||
|
||||
// if a proxy is marked as deprecated, it is because someone had a non-owned drive
|
||||
// but changed their password, and couldn't delete their old data.
|
||||
// if they are here, they have entered their old credentials, so we should not
|
||||
// allow them to proceed. In time, their old drive should get deleted, since
|
||||
// it will should be pinned by anyone's drive.
|
||||
if (rt.proxy[Constants.deprecatedKey]) {
|
||||
return void cb('NO_SUCH_USER');
|
||||
}
|
||||
|
||||
if (isRegister && isProxyEmpty(rt.proxy)) {
|
||||
// If they are trying to register,
|
||||
// and the proxy is empty, then there is no 'legacy user' either
|
||||
// so we should just shut down this session and disconnect.
|
||||
//rt.network.disconnect();
|
||||
return void cb(); // proceed to the next async block
|
||||
}
|
||||
|
||||
// they tried to just log in but there's no such user
|
||||
// and since we're here at all there is no modern-block
|
||||
if (!isRegister && isProxyEmpty(rt.proxy)) {
|
||||
return void cb('NO_SUCH_USER');
|
||||
}
|
||||
|
||||
// they tried to register, but those exact credentials exist
|
||||
if (isRegister && !isProxyEmpty(rt.proxy)) {
|
||||
Feedback.send('LOGIN', true);
|
||||
return void cb('ALREADY_REGISTERED');
|
||||
}
|
||||
|
||||
// if you are here, then there is no block, the user is trying
|
||||
// to log in. The proxy is **not** empty. All values assigned here
|
||||
// should have been deterministically created using their credentials
|
||||
// so setting them is just a precaution to keep things in good shape
|
||||
res.proxy = rt.proxy;
|
||||
res.realtime = rt.realtime;
|
||||
res.network = rt.network;
|
||||
|
||||
// they're registering...
|
||||
res.userHash = opt.userHash;
|
||||
res.userName = res.uname;
|
||||
|
||||
// export their signing key
|
||||
res.edPrivate = opt.edPrivate;
|
||||
res.edPublic = opt.edPublic;
|
||||
|
||||
// export their encryption key
|
||||
res.curvePrivate = opt.curvePrivate;
|
||||
res.curvePublic = opt.curvePublic;
|
||||
|
||||
// don't proceed past this async block.
|
||||
|
||||
// We have to call whenRealtimeSyncs asynchronously here because in the current
|
||||
// version of listmap, onLocal calls `chainpad.contentUpdate(newValue)`
|
||||
// asynchronously.
|
||||
// The following setTimeout is here to make sure whenRealtimeSyncs is called after
|
||||
// `contentUpdate` so that we have an update userDoc in chainpad.
|
||||
setTimeout(function () {
|
||||
Realtime.whenRealtimeSyncs(rt.realtime, function () {
|
||||
// the following stages are there to initialize a new drive
|
||||
// if you are registering
|
||||
LocalStore.login(res.userHash, undefined, res.userName, function () {
|
||||
setTimeout(function () { cb(void 0, res); });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var getProxyOpt = function (blockInfo) {
|
||||
var opt;
|
||||
if (blockInfo) {
|
||||
opt = loginOptionsFromBlock(blockInfo);
|
||||
opt.userHash = blockInfo.User_hash;
|
||||
} else {
|
||||
console.log("allocating random bytes for a new user object");
|
||||
opt = allocateBytes(Nacl.randomBytes(Exports.requiredBytes));
|
||||
// create a random v2 hash, since we don't need backwards compatibility
|
||||
opt.userHash = Hash.createRandomHash('drive');
|
||||
var secret = Hash.getSecrets('drive', opt.userHash);
|
||||
opt.keys = secret.keys;
|
||||
opt.channelHex = secret.channel;
|
||||
}
|
||||
|
||||
console.warn(opt);
|
||||
return opt;
|
||||
};
|
||||
|
||||
var modernLoginRegister = function (opt, isRegister, cb, res) {
|
||||
res = res || {};
|
||||
// according to the location derived from the credentials which you entered
|
||||
loadUserObject(opt, function (err, rt) {
|
||||
if (err) { return void cb('MODERN_REGISTRATION_INIT'); }
|
||||
|
||||
// export the realtime object you checked
|
||||
var RT = rt;
|
||||
|
||||
var proxy = rt.proxy;
|
||||
if (isRegister && !isProxyEmpty(proxy) && (!proxy.edPublic || !proxy.edPrivate)) {
|
||||
console.error("INVALID KEYS");
|
||||
console.log(JSON.stringify(proxy));
|
||||
return void cb(void 0, void 0, RT);
|
||||
}
|
||||
|
||||
res.proxy = rt.proxy;
|
||||
res.realtime = rt.realtime;
|
||||
res.network = rt.network;
|
||||
|
||||
// they're registering...
|
||||
res.userHash = opt.userHash;
|
||||
res.userName = res.uname;
|
||||
|
||||
// somehow they have a block present, but nothing in the user object it specifies
|
||||
// this shouldn't happen, but let's send feedback if it does
|
||||
if (!isRegister && isProxyEmpty(rt.proxy)) {
|
||||
// this really shouldn't happen, but let's handle it anyway
|
||||
Feedback.send('EMPTY_LOGIN_WITH_BLOCK');
|
||||
return void cb('NO_SUCH_USER');
|
||||
}
|
||||
|
||||
// they tried to register, but those exact credentials exist
|
||||
if (isRegister && !isProxyEmpty(rt.proxy)) {
|
||||
//rt.network.disconnect();
|
||||
return void cb('ALREADY_REGISTERED');
|
||||
}
|
||||
|
||||
if (!isRegister && !isProxyEmpty(rt.proxy)) {
|
||||
var l = Util.find(rt.proxy, ['settings', 'general', 'language']);
|
||||
var LS_LANG = "CRYPTPAD_LANG";
|
||||
if (l) { localStorage.setItem(LS_LANG, l); }
|
||||
|
||||
if (res.auth_token && res.auth_token.bearer) {
|
||||
LocalStore.setSessionToken(res.auth_token.bearer);
|
||||
}
|
||||
return void LocalStore.login(undefined, res.blockHash, res.uname, function () {
|
||||
cb(void 0, res, RT);
|
||||
});
|
||||
}
|
||||
|
||||
if (isRegister && isProxyEmpty(rt.proxy)) {
|
||||
proxy.edPublic = opt.edPublic;
|
||||
proxy.edPrivate = opt.edPrivate;
|
||||
proxy.curvePublic = opt.curvePublic;
|
||||
proxy.curvePrivate = opt.curvePrivate;
|
||||
proxy.login_name = res.uname;
|
||||
proxy[Constants.displayNameKey] = res.uname;
|
||||
proxy.version = 11;
|
||||
|
||||
Feedback.send('REGISTRATION', true);
|
||||
} else {
|
||||
Feedback.send('LOGIN', true);
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
Realtime.whenRealtimeSyncs(rt.realtime, function () {
|
||||
cb(void 0, void 0, RT);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Exports.loginOrRegister = function (config, cb) {
|
||||
let { uname, passwd, isRegister, onOTP, ssoAuth } = config;
|
||||
if (typeof(cb) !== 'function') { return; }
|
||||
|
||||
// Usernames are all lowercase. No going back on this one
|
||||
uname = uname.toLowerCase();
|
||||
|
||||
// validate inputs
|
||||
if (!Cred.isValidUsername(uname)) { return void cb('INVAL_USER'); }
|
||||
if (!Cred.isValidPassword(passwd) && !ssoAuth) { return void cb('INVAL_PASS'); }
|
||||
if (isRegister && !ssoAuth && !Cred.isLongEnoughPassword(passwd)) {
|
||||
return void cb('PASS_TOO_SHORT');
|
||||
}
|
||||
|
||||
// results...
|
||||
var res = {
|
||||
register: isRegister,
|
||||
uname: uname,
|
||||
auth_token: {}
|
||||
};
|
||||
|
||||
var RT, blockKeys, blockUrl;
|
||||
|
||||
nThen(function (waitFor) {
|
||||
// derive a predefined number of bytes from the user's inputs,
|
||||
// and allocate them in a deterministic fashion
|
||||
Cred.deriveFromPassphrase(uname, passwd, Exports.requiredBytes, waitFor(function (bytes) {
|
||||
res.opt = allocateBytes(bytes);
|
||||
res.blockHash = res.opt.blockHash;
|
||||
blockKeys = res.opt.blockKeys;
|
||||
if (ssoAuth && ssoAuth.name) { uname = res.uname = ssoAuth.name; }
|
||||
}));
|
||||
}).nThen(function (waitFor) {
|
||||
// the allocated bytes can be used either in a legacy fashion,
|
||||
// or in such a way that a previously unused byte range determines
|
||||
// the location of a layer of indirection which points users to
|
||||
// an encrypted block, from which they can recover the location of
|
||||
// the rest of their data
|
||||
|
||||
// determine where a block for your set of keys would be stored
|
||||
blockUrl = Block.getBlockUrl(res.opt.blockKeys);
|
||||
|
||||
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:
|
||||
// ie. just a simple "remember me" checkbox?
|
||||
// allow them to specify a lifetime for the session?
|
||||
// "log me out after one day"?
|
||||
}, cb);
|
||||
}, false, err);
|
||||
};
|
||||
|
||||
var done = waitFor();
|
||||
var responseToDecryptedBlock = function (response, cb) {
|
||||
response.arrayBuffer().then(arraybuffer => {
|
||||
arraybuffer = new Uint8Array(arraybuffer);
|
||||
var decryptedBlock = Block.decrypt(arraybuffer, blockKeys);
|
||||
if (!decryptedBlock) {
|
||||
console.error("BLOCK DECRYPTION ERROR");
|
||||
return void cb("BLOCK_DECRYPTION_ERROR");
|
||||
}
|
||||
cb(void 0, decryptedBlock);
|
||||
});
|
||||
};
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
if (err === 404 && response && response.reason) {
|
||||
waitFor.abort();
|
||||
w.abort();
|
||||
/*
|
||||
// the following block prevent users from re-using an old password
|
||||
if (isRegister) { return void cb('HAS_PLACEHOLDER'); }
|
||||
*/
|
||||
return void cb('DELETED_USER', response);
|
||||
}
|
||||
|
||||
// Some other error?
|
||||
if (err) {
|
||||
console.error(err);
|
||||
w.abort();
|
||||
return void done();
|
||||
}
|
||||
|
||||
// If the block was returned without requiring authentication
|
||||
// then we can abort the subsequent steps of this nested nThen
|
||||
w.abort();
|
||||
|
||||
// decrypt the response and continue the normal procedure with its payload
|
||||
responseToDecryptedBlock(response, function (err, decryptedBlock) {
|
||||
if (err) {
|
||||
// if a block was present but you were not able to decrypt it...
|
||||
console.error(err);
|
||||
waitFor.abort();
|
||||
return void cb(err);
|
||||
}
|
||||
res.blockInfo = decryptedBlock;
|
||||
done();
|
||||
});
|
||||
}));
|
||||
}).nThen(function (w) {
|
||||
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) {
|
||||
console.error(err);
|
||||
w.abort();
|
||||
waitFor.abort();
|
||||
return void cb(err);
|
||||
}
|
||||
res.auth_token = response;
|
||||
}));
|
||||
}).nThen(function (w) {
|
||||
if (missingAuth !== 'TOTP') { return; }
|
||||
// if you're here then you need to request a JWT
|
||||
var done = w();
|
||||
var tries = 3;
|
||||
var ask = function () {
|
||||
if (!tries) {
|
||||
w.abort();
|
||||
waitFor.abort();
|
||||
return void cb('TOTP_ATTEMPTS_EXHAUSTED');
|
||||
}
|
||||
tries--;
|
||||
// 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);
|
||||
console.log("Normal failure. Asking again...");
|
||||
return void ask();
|
||||
}
|
||||
if (!response || !response.bearer) {
|
||||
console.log(response);
|
||||
console.log("Unexpected failure. No bearer token. Asking again");
|
||||
return void ask();
|
||||
}
|
||||
console.log("Successfully retrieved a bearer token");
|
||||
res.auth_token = response;
|
||||
done();
|
||||
});
|
||||
};
|
||||
ask();
|
||||
}).nThen(function (w) {
|
||||
Util.getBlock(blockUrl, res.auth_token, function (err, response) {
|
||||
if (err) {
|
||||
w.abort();
|
||||
console.error(err);
|
||||
return void cb('BLOCK_ERROR_3');
|
||||
}
|
||||
|
||||
responseToDecryptedBlock(response, function (err, decryptedBlock) {
|
||||
if (err) {
|
||||
waitFor.abort();
|
||||
return void cb(err);
|
||||
}
|
||||
res.blockInfo = decryptedBlock;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
}).nThen(function (waitFor) {
|
||||
|
||||
// we assume that if there is a block, it was created in a valid manner
|
||||
// so, just proceed to the next block which handles that stuff
|
||||
if (res.blockInfo) { return; }
|
||||
|
||||
var opt = res.opt;
|
||||
|
||||
// load the user's object using the legacy credentials
|
||||
legacyLogin(opt, isRegister, waitFor(function (err, data) {
|
||||
if (err) {
|
||||
waitFor.abort();
|
||||
return void cb(err);
|
||||
}
|
||||
if (!data) { return; } // Go to next block (modern registration)
|
||||
|
||||
// No error and data: success legacy login
|
||||
waitFor.abort();
|
||||
cb(void 0, data);
|
||||
}), res);
|
||||
}).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();
|
||||
return void cb(err);
|
||||
}
|
||||
RT = _RT;
|
||||
if (!data) { return; } // Go to next block (modern registration)
|
||||
|
||||
// No error and data: success modern login
|
||||
waitFor.abort();
|
||||
cb(void 0, data);
|
||||
}), res);
|
||||
}).nThen(function (waitFor) {
|
||||
console.log("creating request to publish a login block");
|
||||
|
||||
// Finally, create the login block for the object you just created.
|
||||
var toPublish = {};
|
||||
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,
|
||||
}, waitFor(function (e, res) {
|
||||
if (e === 'SSO_NO_SESSION') { return; } // account created, need re-login
|
||||
if (e) {
|
||||
console.error(e);
|
||||
waitFor.abort();
|
||||
return void cb(e);
|
||||
}
|
||||
if (res && res.bearer) {
|
||||
LocalStore.setSessionToken(res.bearer);
|
||||
}
|
||||
}));
|
||||
}).nThen(function (waitFor) {
|
||||
// confirm that the block was actually written before considering registration successful
|
||||
Util.getBlock(blockUrl, {}, waitFor(function (err /*, block */) {
|
||||
if (err && err !== 401) { // 401 is fine
|
||||
console.error(err);
|
||||
waitFor.abort();
|
||||
return void cb(err);
|
||||
}
|
||||
|
||||
console.log("blockInfo available at:", res.blockHash);
|
||||
LocalStore.login(undefined, res.blockHash, uname, function () {
|
||||
cb(void 0, res);
|
||||
});
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
return Exports;
|
||||
});
|
|
@ -2005,6 +2005,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);
|
||||
|
@ -2053,21 +2058,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;
|
||||
|
|
|
@ -886,6 +886,7 @@ define([
|
|||
}));
|
||||
}).nThen(function (waitFor) {
|
||||
// Delete Drive
|
||||
store.ownDeletion = true;
|
||||
Store.removeOwnedChannel(clientId, {
|
||||
channel: store.driveChannel,
|
||||
force: true
|
||||
|
@ -3059,6 +3060,7 @@ define([
|
|||
})
|
||||
.on('error', function (info) {
|
||||
if (info.error && info.error === 'EDELETED') {
|
||||
if (store.ownDeletion) { return; }
|
||||
broadcast([], "LOGOUT", {
|
||||
reason: info.message
|
||||
});
|
||||
|
|
|
@ -88,6 +88,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];
|
||||
};
|
||||
|
@ -134,6 +141,7 @@ define([
|
|||
Constants.userHashKey,
|
||||
Constants.blockHashKey,
|
||||
Constants.sessionJWT,
|
||||
Constants.ssoSeed,
|
||||
'loginToken',
|
||||
'plan',
|
||||
].forEach(function (k) {
|
||||
|
|
|
@ -169,9 +169,7 @@ define([
|
|||
const { blockKeys, auth } = data;
|
||||
|
||||
var command = 'MFA_CHECK';
|
||||
if (auth && auth.type === 'TOTP') {
|
||||
command = 'TOTP_CHECK';
|
||||
}
|
||||
if (auth && auth.type) { command = `${auth.type.toUpperCase()}_` + command; }
|
||||
|
||||
ServerCommand(blockKeys.sign, {
|
||||
command: command,
|
||||
|
@ -179,29 +177,27 @@ define([
|
|||
}, cb);
|
||||
};
|
||||
Block.writeLoginBlock = function (data, cb) {
|
||||
const { content, blockKeys, oldBlockKeys, auth } = data;
|
||||
const { content, blockKeys, oldBlockKeys, auth, pw, session } = data;
|
||||
|
||||
var command = 'WRITE_BLOCK';
|
||||
if (auth && auth.type === 'TOTP') {
|
||||
command = 'TOTP_WRITE_BLOCK';
|
||||
}
|
||||
if (auth && auth.type) { command = `${auth.type.toUpperCase()}_` + command; }
|
||||
|
||||
var block = Block.serialize(JSON.stringify(content), blockKeys);
|
||||
block.auth = auth && auth.data;
|
||||
block.hasPassword = pw;
|
||||
block.registrationProof = oldBlockKeys && Block.proveAncestor(oldBlockKeys);
|
||||
|
||||
ServerCommand(blockKeys.sign, {
|
||||
command: command,
|
||||
content: block
|
||||
content: block,
|
||||
session: session // sso session
|
||||
}, cb);
|
||||
};
|
||||
Block.removeLoginBlock = function (data, cb) {
|
||||
const { reason, blockKeys, auth } = data;
|
||||
|
||||
var command = 'REMOVE_BLOCK';
|
||||
if (auth && auth.type === 'TOTP') {
|
||||
command = 'TOTP_REMOVE_BLOCK';
|
||||
}
|
||||
if (auth && auth.type) { command = `${auth.type.toUpperCase()}_` + command; }
|
||||
|
||||
ServerCommand(blockKeys.sign, {
|
||||
command: command,
|
||||
|
@ -210,5 +206,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;
|
||||
});
|
||||
|
|
|
@ -130,7 +130,8 @@ define([
|
|||
function (yes) {
|
||||
if (!yes) { return; }
|
||||
|
||||
Login.loginOrRegisterUI(uname, passwd, true, shouldImport, false, function (data) {
|
||||
Login.loginOrRegisterUI(uname, passwd, true, shouldImport,
|
||||
UI.getOTPScreen, false, function (data) {
|
||||
var proxy = data.proxy;
|
||||
if (!proxy || !proxy.edPublic) { UI.alert(Messages.error); return true; }
|
||||
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
define([
|
||||
'/api/config',
|
||||
'jquery',
|
||||
'/common/hyperscript.js',
|
||||
'/common/cryptpad-common.js',
|
||||
'/customize/login.js',
|
||||
'/common/common-interface.js',
|
||||
|
@ -13,7 +15,7 @@ define([
|
|||
//'/common/test.js',
|
||||
|
||||
'css!/components/components-font-awesome/css/font-awesome.min.css',
|
||||
], function ($, Cryptpad, Login, UI, Realtime, Feedback, LocalStore /*, Test */) {
|
||||
], function (Config, $, h, Cryptpad, Login, UI, Realtime, Feedback, LocalStore /*, Test */) {
|
||||
if (window.top !== window) { return; }
|
||||
$(function () {
|
||||
var $checkImport = $('#import-recent');
|
||||
|
@ -23,6 +25,31 @@ define([
|
|||
return;
|
||||
}
|
||||
|
||||
if (Config.sso) {
|
||||
// TODO
|
||||
// Config.sso.force => no legacy login allowed
|
||||
// Config.sso.password => cp password required or forbidden
|
||||
// Config.sso.list => list of configured identity providers
|
||||
var $sso = $('div.cp-login-sso');
|
||||
var list = Config.sso.list.map(function (name) {
|
||||
var b = h('button.btn.btn-secondary', name);
|
||||
var $b = $(b).click(function () {
|
||||
$b.prop('disabled', 'disabled');
|
||||
Login.ssoAuth(name, function (err, data) {
|
||||
if (data.url) {
|
||||
window.location.href = data.url;
|
||||
}
|
||||
});
|
||||
});
|
||||
return b;
|
||||
});
|
||||
$sso.append(list);
|
||||
|
||||
// Disable bfcache (back/forward cache) to prevent SSO button
|
||||
// being disabled when using the browser "back" feature on the SSO page
|
||||
$(window).on('unload', () => {});
|
||||
}
|
||||
|
||||
/* Log in UI */
|
||||
// deferred execution to avoid unnecessary asset loading
|
||||
var loginReady = function (cb) {
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
define([
|
||||
'/api/config',
|
||||
'jquery',
|
||||
'/customize/login.js',
|
||||
'/common/cryptpad-common.js',
|
||||
|
@ -18,7 +19,7 @@ define([
|
|||
'/customize/pages.js',
|
||||
|
||||
'css!/components/components-font-awesome/css/font-awesome.min.css',
|
||||
], function ($, Login, Cryptpad, /*Test,*/ Cred, UI, Util, Realtime, Constants, Feedback, LocalStore, h, Pages) {
|
||||
], function (Config, $, Login, Cryptpad, /*Test,*/ Cred, UI, Util, Realtime, Constants, Feedback, LocalStore, h, Pages) {
|
||||
if (window.top !== window) { return; }
|
||||
var Messages = Cryptpad.Messages;
|
||||
$(function () {
|
||||
|
@ -53,6 +54,32 @@ define([
|
|||
var I_REALLY_WANT_TO_USE_MY_EMAIL_FOR_MY_USERNAME = false;
|
||||
var br = function () { return h('br'); };
|
||||
|
||||
if (Config.sso) {
|
||||
// TODO
|
||||
// Config.sso.force => no legacy login allowed
|
||||
// Config.sso.password => cp password required or forbidden
|
||||
// Config.sso.list => list of configured identity providers
|
||||
var $sso = $('div.cp-register-sso');
|
||||
var list = Config.sso.list.map(function (name) {
|
||||
var b = h('button.btn.btn-secondary', name);
|
||||
var $b = $(b).click(function () {
|
||||
console.log('sso register click:', name);
|
||||
$b.prop('disabled', 'disabled');
|
||||
Login.ssoAuth(name, function (err, data) {
|
||||
if (data.url) {
|
||||
window.location.href = data.url;
|
||||
}
|
||||
});
|
||||
});
|
||||
return b;
|
||||
});
|
||||
$sso.append(list);
|
||||
|
||||
// Disable bfcache (back/forward cache) to prevent SSO button
|
||||
// being disabled when using the browser "back" feature on the SSO page
|
||||
$(window).on('unload', () => {});
|
||||
}
|
||||
|
||||
var registerClick = function () {
|
||||
var uname = $uname.val().trim();
|
||||
// trim whitespace surrounding the username since it is otherwise included in key derivation
|
||||
|
@ -130,7 +157,8 @@ define([
|
|||
function (yes) {
|
||||
if (!yes) { return; }
|
||||
|
||||
Login.loginOrRegisterUI(uname, passwd, true, shouldImport, false /*Test.testing*/, function () {
|
||||
Login.loginOrRegisterUI(uname, passwd, true, shouldImport,
|
||||
UI.getOTPScreen, false /*Test.testing*/, function () {
|
||||
if (test) {
|
||||
localStorage.clear();
|
||||
test.pass();
|
||||
|
|
|
@ -582,10 +582,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;
|
||||
}));
|
||||
|
@ -660,6 +667,7 @@ define([
|
|||
|
||||
create['change-password'] = function() {
|
||||
if (!common.isLoggedIn()) { return; }
|
||||
if (privateData.isSSO && ApiConfig.sso && ApiConfig.sso.password === 0) { return; }
|
||||
|
||||
var $div = $('<div>', { 'class': 'cp-settings-change-password cp-sidebarlayout-element' });
|
||||
|
||||
|
@ -730,8 +738,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;
|
||||
}));
|
||||
|
@ -1061,6 +1076,7 @@ define([
|
|||
var Base32, QRCode, Nacl;
|
||||
var blockKeys;
|
||||
var recoverySecret;
|
||||
var ssoSeed;
|
||||
nThen(function (waitFor) {
|
||||
require([
|
||||
'/auth/base32.js',
|
||||
|
@ -1071,12 +1087,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,
|
||||
|
@ -1168,7 +1191,6 @@ define([
|
|||
lock = true;
|
||||
|
||||
var data = {
|
||||
command: 'TOTP_SETUP',
|
||||
secret: secret,
|
||||
contact: "secret:" + recoverySecret, // TODO other recovery options
|
||||
code: code,
|
||||
|
|
|
@ -85,7 +85,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);
|
||||
|
@ -115,11 +118,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);
|
||||
});
|
||||
|
@ -144,8 +154,9 @@ define([
|
|||
category = window.location.hash.slice(1);
|
||||
window.location.hash = '';
|
||||
}
|
||||
var addData = function (obj) {
|
||||
var addData = function (obj, Cryptpad, user, Utils) {
|
||||
if (category) { obj.category = category; }
|
||||
obj.isSSO = Boolean(Utils.LocalStore.getSSOSeed());
|
||||
};
|
||||
SFCommonO.start({
|
||||
noRealtime: true,
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!-- If this file is not called customize.dist/src/template.html, it is generated -->
|
||||
<head>
|
||||
<title data-localization="main_title">CryptPad: Collaboration suite, encrypted and open-source</title>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<link rel="icon" type="image/png" href="/customize/favicon/main-favicon.png" id="favicon"/>
|
||||
<script src="/customize/pre-loading.js?ver=1.1"></script>
|
||||
<link href="/customize/src/pre-loading.css?ver=1.0" rel="stylesheet" type="text/css">
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.5"></script>
|
||||
</head>
|
||||
<body class="html">
|
||||
<noscript></noscript>
|
||||
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
define([
|
||||
'/api/config',
|
||||
'jquery',
|
||||
'/common/hyperscript.js',
|
||||
'/common/common-util.js',
|
||||
'/common/common-credential.js',
|
||||
'/common/common-interface.js',
|
||||
'/common/common-login.js',
|
||||
'/common/common-constants.js',
|
||||
'/common/outer/http-command.js',
|
||||
'/common/outer/local-store.js',
|
||||
'/common/outer/login-block.js',
|
||||
'/customize/messages.js',
|
||||
|
||||
'/components/tweetnacl/nacl-fast.min.js',
|
||||
], function (ApiConfig, $, h, Util, Cred, UI, Login, Constants,
|
||||
ServerCommand, LocalStore, Block, Messages) {
|
||||
if (window.top !== window) { return; }
|
||||
|
||||
let Nacl = window.nacl;
|
||||
|
||||
let ssoAuthCb = function (cb) {
|
||||
var b64Keys = Util.tryParse(localStorage.CP_sso_auth);
|
||||
if (!b64Keys) {
|
||||
UI.errorLoadingScreen("MISSING_SIGNATURE_KEYS");
|
||||
return;
|
||||
}
|
||||
var keys = {
|
||||
secretKey: Nacl.util.decodeBase64(b64Keys.s),
|
||||
publicKey: Nacl.util.decodeBase64(b64Keys.p)
|
||||
};
|
||||
ServerCommand(keys, {
|
||||
command: 'SSO_AUTH_CB',
|
||||
url: window.location.href
|
||||
}, function (err, data) {
|
||||
delete localStorage.CP_sso_auth;
|
||||
document.cookie = 'ssotoken=; Max-Age=-99999999;';
|
||||
cb(err, data);
|
||||
});
|
||||
};
|
||||
let ssoLoginRegister = function (seed, pw, jwt, name, isRegister) {
|
||||
Login.loginOrRegister({
|
||||
uname: seed,
|
||||
passwd: pw,
|
||||
isRegister: isRegister,
|
||||
onOTP: UI.getOTPScreen,
|
||||
ssoAuth: {
|
||||
name: name,
|
||||
type: 'SSO',
|
||||
data: jwt
|
||||
}
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
UI.removeLoadingScreen();
|
||||
var msg = Messages.error;
|
||||
if (err === 'NO_SUCH_USER') { msg = Messages.drive_sfPasswordError; }
|
||||
let $button = $('button#cp-ssoauth-button');
|
||||
$button.prop('disabled', '');
|
||||
return void UI.warn(msg);
|
||||
}
|
||||
LocalStore.setSSOSeed(seed.toLowerCase());
|
||||
window.location.href = '/drive/';
|
||||
});
|
||||
};
|
||||
|
||||
$(function () {
|
||||
if (!ApiConfig.sso) { return void UI.errorLoadingScreen(Messages.error); }
|
||||
|
||||
UI.addLoadingScreen();
|
||||
ssoAuthCb(function (err, data) {
|
||||
if (err || !data || !data.jwt) {
|
||||
console.error(err || 'NO_DATA');
|
||||
return void UI.warn(Messages.error);
|
||||
}
|
||||
let jwt = data.jwt;
|
||||
let seed = data.seed;
|
||||
let name = data.name;
|
||||
$('body').addClass(data.register ? 'cp-register' : 'cp-login');
|
||||
|
||||
UI.removeLoadingScreen();
|
||||
|
||||
|
||||
// Login with a password OR register and password allowed
|
||||
let $button = $('button#cp-ssoauth-button');
|
||||
let $pw = $('#password');
|
||||
let $pw2 = $('#passwordconfirm');
|
||||
let next = (pw) => {
|
||||
// TODO login err ==> re-enable button
|
||||
|
||||
setTimeout(() => { // First setTimeout to remove mobile devices' keyboard
|
||||
UI.addLoadingScreen({
|
||||
loadingText: Messages.login_hashing,
|
||||
hideTips: true,
|
||||
});
|
||||
setTimeout(function () { // Second timeout for the loading screen befofe Scrypt
|
||||
ssoLoginRegister(seed, pw, jwt, name, data.register);
|
||||
}, 100);
|
||||
}, 100);
|
||||
|
||||
};
|
||||
|
||||
// Existing account, no CP password, continue
|
||||
if (!data.register && !data.password) {
|
||||
return void next('');
|
||||
}
|
||||
|
||||
// Registration and CP password disabled, continue
|
||||
if (data.register && !ApiConfig.sso.password) {
|
||||
return void next('');
|
||||
}
|
||||
|
||||
$('div.cp-ssoauth-pw').show();
|
||||
$pw.focus();
|
||||
|
||||
$pw.on('keypress', (ev) => {
|
||||
if (ev.which !== 13) { return; }
|
||||
if (!$pw2.val() && data.register) {
|
||||
return void $pw2.focus();
|
||||
}
|
||||
$button.click();
|
||||
});
|
||||
$pw2.on('keypress', (ev) => {
|
||||
if (ev.which !== 13) { return; }
|
||||
$button.click();
|
||||
});
|
||||
|
||||
$button.click(() => {
|
||||
let pw = $pw.val();
|
||||
if (data.register && pw !== $pw2.val()) {
|
||||
return void UI.warn(Messages.register_passwordsDontMatch);
|
||||
}
|
||||
if (data.register && pw && !Cred.isLongEnoughPassword(pw)) {
|
||||
return void UI.warn(Messages.register_passwordTooShort);
|
||||
}
|
||||
if (data.register && !pw && ApiConfig.sso.password === 2) {
|
||||
return void UI.warn(Messages.register_passwordTooShort);
|
||||
}
|
||||
$button.prop('disabled', 'disabled');
|
||||
|
||||
if (data.register) {
|
||||
var span = h('span', [
|
||||
h('h2', [
|
||||
h('i.fa.fa-warning'),
|
||||
' ',
|
||||
Messages.register_warning,
|
||||
]),
|
||||
Messages.register_warning_note
|
||||
]);
|
||||
UI.confirm(span, function (yes) {
|
||||
if (!yes) {
|
||||
$button.removeAttr('disabled');
|
||||
return;
|
||||
}
|
||||
next(pw);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
next(pw);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue