mirror of
https://github.com/cryptpad/cryptpad.git
synced 2026-09-14 11:05:41 +05:00
add client-side support for TOTP-authenticated sessions
This commit is contained in:
parent
6ef1527a29
commit
5cd306a18a
@ -15,11 +15,12 @@ define([
|
||||
'/bower_components/nthen/index.js',
|
||||
'/common/outer/login-block.js',
|
||||
'/common/common-hash.js',
|
||||
'/common/outer/http-command.js',
|
||||
|
||||
'/bower_components/tweetnacl/nacl-fast.min.js',
|
||||
'/bower_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) {
|
||||
Feedback, LocalStore, Messages, nThen, Block, Hash, ServerCommand) {
|
||||
var Exports = {
|
||||
Cred: Cred,
|
||||
Block: Block,
|
||||
@ -172,26 +173,117 @@ define([
|
||||
// determine where a block for your set of keys would be stored
|
||||
blockUrl = Block.getBlockUrl(res.opt.blockKeys);
|
||||
|
||||
// Check whether there is a block at that location
|
||||
Util.fetch(blockUrl, waitFor(function (err, block) {
|
||||
// if users try to log in or register, we must check
|
||||
// whether there is a block.
|
||||
|
||||
// the block is only useful if it can be decrypted, though
|
||||
if (err) {
|
||||
console.log("no block found");
|
||||
return;
|
||||
var TOTP_prompt = function (cb) {
|
||||
// XXX This should use nice UI elements integrated into
|
||||
// the loading screen. window.prompt is here for prototyping only
|
||||
var code = window.prompt('Enter TOTP');
|
||||
if (!code) {
|
||||
return void cb("INVALID_TOTP");
|
||||
}
|
||||
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);
|
||||
};
|
||||
|
||||
var decryptedBlock = Block.decrypt(block, blockKeys);
|
||||
if (!decryptedBlock) {
|
||||
console.error("Found a login block but failed to decrypt");
|
||||
return;
|
||||
}
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
||||
//console.error(decryptedBlock);
|
||||
res.blockInfo = 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");
|
||||
}
|
||||
|
||||
// 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(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
|
||||
@ -357,6 +449,10 @@ define([
|
||||
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(userHash, uname, function () {
|
||||
cb(void 0, res);
|
||||
});
|
||||
|
||||
@ -5,6 +5,8 @@ define(['/customize/application_config.js'], function (AppConfig) {
|
||||
userNameKey: 'User_name',
|
||||
blockHashKey: 'Block_hash',
|
||||
fileHashKey: 'FS_hash',
|
||||
sessionJWT: 'Session_JWT',
|
||||
|
||||
// Store
|
||||
displayNameKey: 'cryptpad.username',
|
||||
oldStorageKey: 'CryptPad_RECENTPADS',
|
||||
|
||||
@ -312,6 +312,35 @@
|
||||
if (!/^[a-f0-9]{48}$/.test(cacheKey)) { cacheKey = undefined; }
|
||||
return cacheKey;
|
||||
};
|
||||
|
||||
|
||||
Util.getBlock = function (src, opt, cb) {
|
||||
var CB = Util.once(Util.mkAsync(cb));
|
||||
|
||||
var headers = {};
|
||||
|
||||
if (typeof(opt.bearer) === 'string' && opt.bearer) {
|
||||
headers.authorization = `Bearer ${opt.bearer}`;
|
||||
}
|
||||
|
||||
|
||||
fetch(src, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: headers,
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
// TODO this should probably be returned as an arraybuffer or something rather than a promise
|
||||
// this is resulting in some code duplication
|
||||
return void CB(void 0, response);
|
||||
}
|
||||
CB(response.status);
|
||||
}).catch(error => {
|
||||
CB(error);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Util.fetch = function (src, cb, progress, cache) {
|
||||
var CB = Util.once(Util.mkAsync(cb));
|
||||
|
||||
|
||||
@ -2483,6 +2483,7 @@ define([
|
||||
}
|
||||
|
||||
}).nThen(function (waitFor) {
|
||||
// if a block URL is present then the user is probably logged in with a modern account
|
||||
var blockHash = LocalStore.getBlockHash();
|
||||
if (blockHash) {
|
||||
console.debug("Block hash is present");
|
||||
@ -2492,28 +2493,64 @@ define([
|
||||
console.error("Failed to parse blockHash");
|
||||
console.log(parsed);
|
||||
return;
|
||||
} else {
|
||||
//console.log(parsed);
|
||||
}
|
||||
Util.fetch(parsed.href, waitFor(function (err, arraybuffer) {
|
||||
if (err) { return void console.log(err); }
|
||||
|
||||
// use the results to load your user hash and
|
||||
// put your userhash into localStorage
|
||||
try {
|
||||
var block_info = Block.decrypt(arraybuffer, parsed.keys);
|
||||
if (!block_info) {
|
||||
console.error("Failed to decrypt !");
|
||||
return;
|
||||
}
|
||||
userHash = block_info[Constants.userHashKey];
|
||||
if (!userHash || userHash !== LocalStore.getUserHash()) {
|
||||
return void requestLogin();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return void console.error("failed to decrypt or decode block content");
|
||||
// they might also have a "session token", which is a JWT.
|
||||
// this indicates that their login block is protected with 2FA
|
||||
var sessionToken = LocalStore.getSessionToken() || undefined;
|
||||
|
||||
var done = waitFor();
|
||||
|
||||
// request the login block, providing credentials if available
|
||||
Util.getBlock(parsed.href, {
|
||||
bearer: sessionToken,
|
||||
}, waitFor((err, response) => {
|
||||
if (err === 401) {
|
||||
// a 401 error indicates insufficient authentication
|
||||
// either their JWT is invalid, or they didn't provide one
|
||||
// when it was expected. Log them out and redirect them to
|
||||
// the login page, where they will be able to authenticate
|
||||
// and request a new JWT
|
||||
waitFor.abort();
|
||||
return void LocalStore.logout(function () {
|
||||
requestLogin();
|
||||
});
|
||||
}
|
||||
|
||||
if (err) {
|
||||
// TODO
|
||||
// it seems wrong that errors here aren't reported or handled
|
||||
// but it's consistent with other failure cases in the rest of this process
|
||||
// that probably justifies some more thorough review.
|
||||
// In particular, it should not be possible to be "half-logged-in"
|
||||
// behaving like a guest after trying to authenticate as a registered user
|
||||
return void console.error(err);
|
||||
}
|
||||
|
||||
// if no errors occurred then we can try to convert the response
|
||||
// to an arraybuffer and decrypt its payload
|
||||
response.arrayBuffer().then(arraybuffer => {
|
||||
arraybuffer = new Uint8Array(arraybuffer);
|
||||
// use the results to load your user hash and
|
||||
// put your userhash into localStorage
|
||||
try {
|
||||
var block_info = Block.decrypt(arraybuffer, parsed.keys);
|
||||
if (!block_info) {
|
||||
console.error("Failed to decrypt !");
|
||||
return;
|
||||
}
|
||||
userHash = block_info[Constants.userHashKey];
|
||||
if (!userHash || userHash !== LocalStore.getUserHash()) {
|
||||
return void LocalStore.logout(function () {
|
||||
requestLogin();
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return void console.error("failed to decrypt or decode block content");
|
||||
}
|
||||
done();
|
||||
});
|
||||
}));
|
||||
}
|
||||
}).nThen(function (waitFor) {
|
||||
|
||||
100
www/common/outer/http-command.js
Normal file
100
www/common/outer/http-command.js
Normal file
@ -0,0 +1,100 @@
|
||||
define([
|
||||
'/bower_components/nthen/index.js',
|
||||
'/common/common-util.js',
|
||||
'/api/config',
|
||||
|
||||
'/bower_components/tweetnacl/nacl-fast.min.js',
|
||||
], function (nThen, Util, ApiConfig) {
|
||||
var Nacl = window.nacl;
|
||||
var clone = o => JSON.parse(JSON.stringify(o));
|
||||
var randomToken = () => Nacl.util.encodeBase64(Nacl.randomBytes(24));
|
||||
var postData = function (url, data, cb) {
|
||||
var CB = Util.once(Util.mkAsync(cb));
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
return void response.json().then(result => { CB(void 0, result); });
|
||||
}
|
||||
|
||||
response.json().then().then(result => {
|
||||
CB(response.status, result);
|
||||
});
|
||||
//CB(response.status, response);
|
||||
}).catch(error => {
|
||||
CB(error);
|
||||
});
|
||||
};
|
||||
|
||||
var API_ORIGIN = (function () {
|
||||
var url;
|
||||
var unsafeOriginURL = new URL(ApiConfig.httpUnsafeOrigin);
|
||||
try {
|
||||
url = new URL(ApiConfig.websocketPath, ApiConfig.httpUnsafeOrigin);
|
||||
url.protocol = unsafeOriginURL.protocol;
|
||||
return url.origin;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return ApiConfig.httpUnsafeOrigin;
|
||||
}
|
||||
}());
|
||||
var serverCommand = function (keypair, my_data, cb) {
|
||||
var obj = clone(my_data);
|
||||
obj.publicKey = Nacl.util.encodeBase64(keypair.publicKey);
|
||||
obj.nonce = randomToken();
|
||||
var href = new URL('/api/auth/', API_ORIGIN);
|
||||
var txid, date;
|
||||
|
||||
var responseBody;
|
||||
|
||||
nThen(function (w) {
|
||||
// Tell the server we want to do some action
|
||||
postData(href, obj, w((err, data) => {
|
||||
if (err) {
|
||||
w.abort();
|
||||
console.error(err);
|
||||
// there might be more info here
|
||||
if (data) { console.error(data); }
|
||||
return void cb(err);
|
||||
}
|
||||
|
||||
// if the requested action is valid, it responds with a txid and a nonce
|
||||
// bundle all that up into an object, stringify it, and sign it.
|
||||
// respond with an object: {sig, txid}
|
||||
if (!data.date || !data.txid) {
|
||||
w.abort();
|
||||
return void cb('REQUEST_REJECTED');
|
||||
}
|
||||
txid = data.txid;
|
||||
date = data.date;
|
||||
}));
|
||||
}).nThen(function (w) {
|
||||
var copy = clone(obj);
|
||||
copy.txid = txid;
|
||||
copy.date = date;
|
||||
var toSign = Nacl.util.decodeUTF8(JSON.stringify(copy));
|
||||
var sig = Nacl.sign.detached(toSign, keypair.secretKey);
|
||||
var encoded = Nacl.util.encodeBase64(sig);
|
||||
var obj2 = {
|
||||
sig: encoded,
|
||||
txid: txid,
|
||||
};
|
||||
postData(href, obj2, w((err, data) => {
|
||||
if (err) {
|
||||
w.abort();
|
||||
console.err(err);
|
||||
// there might be more info here
|
||||
if (data) { console.error(data); }
|
||||
return void cb("RESPONSE_REJECTED");
|
||||
}
|
||||
cb(void 0, data);
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
return serverCommand;
|
||||
});
|
||||
@ -76,6 +76,14 @@ define([
|
||||
safeSet(Constants.blockHashKey, hash);
|
||||
};
|
||||
|
||||
LocalStore.getSessionToken = function () {
|
||||
return localStorage[Constants.sessionJWT];
|
||||
};
|
||||
|
||||
LocalStore.setSessionToken = function (token) {
|
||||
safeSet(Constants.sessionJWT, token);
|
||||
};
|
||||
|
||||
LocalStore.getAccountName = function () {
|
||||
return localStorage[Constants.userNameKey];
|
||||
};
|
||||
@ -121,6 +129,7 @@ define([
|
||||
Constants.userNameKey,
|
||||
Constants.userHashKey,
|
||||
Constants.blockHashKey,
|
||||
Constants.sessionJWT,
|
||||
'loginToken',
|
||||
'plan',
|
||||
].forEach(function (k) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user