diff --git a/app/controller.js b/app/controller.js
index 3943721d..1056f8ba 100644
--- a/app/controller.js
+++ b/app/controller.js
@@ -49,8 +49,8 @@ export default function(state, emitter) {
state.user.login(email);
});
- emitter.on('logout', () => {
- state.user.logout();
+ emitter.on('logout', async () => {
+ await state.user.logout();
metrics.loggedOut({ trigger: 'button' });
emitter.emit('pushState', '/');
});
@@ -178,6 +178,12 @@ export default function(state, emitter) {
//cancelled. do nothing
metrics.cancelledUpload(archive, err.duration);
render();
+ } else if (err.message === '401') {
+ const refreshed = await state.user.refresh();
+ if (refreshed) {
+ return emitter.emit('upload');
+ }
+ emitter.emit('pushState', '/error');
} else {
// eslint-disable-next-line no-console
console.error(err);
diff --git a/app/ui/account.js b/app/ui/account.js
index a81117e7..7f6430ec 100644
--- a/app/ui/account.js
+++ b/app/ui/account.js
@@ -54,12 +54,17 @@ class Account extends Component {
createElement() {
if (!this.enabled) {
return html`
-
+
`;
}
const user = this.state.user;
const translate = this.state.translate;
this.setLocal();
+ if (user.loginRequired && !this.local.loggedIn) {
+ return html`
+
+ `;
+ }
if (!this.local.loggedIn) {
return html`
diff --git a/app/ui/signupDialog.js b/app/ui/signupDialog.js
index 5d4c85d6..23fe9a66 100644
--- a/app/ui/signupDialog.js
+++ b/app/ui/signupDialog.js
@@ -53,13 +53,17 @@ module.exports = function(trigger) {
type="submit"
/>
-
+ ${state.user.loginRequired
+ ? ''
+ : html`
+
+ `}
`;
diff --git a/app/user.js b/app/user.js
index c4303941..293f90c5 100644
--- a/app/user.js
+++ b/app/user.js
@@ -76,6 +76,10 @@ export default class User {
return this.info.access_token;
}
+ get refreshToken() {
+ return this.info.refresh_token;
+ }
+
get maxSize() {
return this.loggedIn
? this.limits.MAX_FILE_SIZE
@@ -135,6 +139,7 @@ export default class User {
const code_challenge = await preparePkce(this.storage);
const options = {
action: 'email',
+ access_type: 'offline',
client_id: this.authConfig.client_id,
code_challenge,
code_challenge_method: 'S256',
@@ -192,12 +197,64 @@ export default class User {
});
const userInfo = await infoResponse.json();
userInfo.access_token = auth.access_token;
+ userInfo.refresh_token = auth.refresh_token;
userInfo.fileListKey = await getFileListKey(this.storage, auth.keys_jwe);
this.info = userInfo;
this.storage.remove('pkceVerifier');
}
- logout() {
+ async refresh() {
+ if (!this.refreshToken) {
+ return false;
+ }
+ try {
+ const tokenResponse = await fetch(this.authConfig.token_endpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ client_id: this.authConfig.client_id,
+ grant_type: 'refresh_token',
+ refresh_token: this.refreshToken
+ })
+ });
+ const auth = await tokenResponse.json();
+ this.info.access_token = auth.access_token;
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ async logout() {
+ try {
+ if (this.refreshToken) {
+ await fetch(this.authConfig.revocation_endpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ refresh_token: this.refreshToken
+ })
+ });
+ }
+ if (this.bearerToken) {
+ await fetch(this.authConfig.revocation_endpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ token: this.bearerToken
+ })
+ });
+ }
+ } catch (e) {
+ console.error(e);
+ // oh well, we tried
+ }
this.storage.clearLocalFiles();
this.info = {};
}
@@ -211,17 +268,29 @@ export default class User {
const key = b64ToArray(this.info.fileListKey);
const sha = await crypto.subtle.digest('SHA-256', key);
const kid = arrayToB64(new Uint8Array(sha)).substring(0, 16);
+ async function retry(e) {
+ if (e.message === '401') {
+ const refreshed = await this.refresh();
+ if (refreshed) {
+ return await this.syncFileList();
+ } else {
+ await this.logout();
+ return { incoming: true };
+ }
+ }
+ }
try {
- const encrypted = await getFileList(this.bearerToken, kid);
+ const encrypted = await getFileList(
+ this.bearerToken,
+ this.refreshToken,
+ kid
+ );
const decrypted = await streamToArrayBuffer(
decryptStream(blobStream(encrypted), key)
);
list = JSON.parse(textDecoder.decode(decrypted));
} catch (e) {
- if (e.message === '401') {
- this.logout();
- return { incoming: true };
- }
+ return retry(e);
}
changes = await this.storage.merge(list);
if (!changes.outgoing) {
@@ -234,9 +303,9 @@ export default class User {
const encrypted = await streamToArrayBuffer(
encryptStream(blobStream(blob), key)
);
- await setFileList(this.bearerToken, kid, encrypted);
+ await setFileList(this.bearerToken, this.refreshToken, kid, encrypted);
} catch (e) {
- //
+ return retry(e);
}
return changes;
}
diff --git a/server/middleware/auth.js b/server/middleware/auth.js
index 133b0992..c98d095b 100644
--- a/server/middleware/auth.js
+++ b/server/middleware/auth.js
@@ -70,6 +70,10 @@ module.exports = {
const token = authHeader.split(' ')[1];
req.user = await fxa.verify(token);
}
- return next();
+ if (req.user) {
+ next();
+ } else {
+ res.sendStatus(401);
+ }
}
};
diff --git a/server/routes/filelist.js b/server/routes/filelist.js
index 700fe745..043c8714 100644
--- a/server/routes/filelist.js
+++ b/server/routes/filelist.js
@@ -13,9 +13,6 @@ function id(user, kid) {
module.exports = {
async get(req, res) {
- if (!req.user) {
- return res.sendStatus(401);
- }
const kid = req.params.id;
try {
const fileId = id(req.user, kid);
@@ -32,9 +29,6 @@ module.exports = {
},
async post(req, res) {
- if (!req.user) {
- return res.sendStatus(401);
- }
const kid = req.params.id;
try {
const limiter = new Limiter(1024 * 1024 * 10);
diff --git a/server/routes/ws.js b/server/routes/ws.js
index 32ea7905..f56fad1d 100644
--- a/server/routes/ws.js
+++ b/server/routes/ws.js
@@ -41,6 +41,14 @@ module.exports = function(ws, req) {
? config.max_downloads
: config.anon_max_downloads;
+ if (config.fxa_required && !user) {
+ ws.send(
+ JSON.stringify({
+ error: 401
+ })
+ );
+ return ws.close();
+ }
if (
!metadata ||
!auth ||
diff --git a/test/frontend/tests/workflow-tests.js b/test/frontend/tests/workflow-tests.js
index d96e4ba0..3a9709c4 100644
--- a/test/frontend/tests/workflow-tests.js
+++ b/test/frontend/tests/workflow-tests.js
@@ -181,14 +181,15 @@ describe('Upload / Download flow', function() {
it('can allow multiple downloads', async function() {
const fs = new FileSender();
- const file = await fs.upload(archive);
+ const a = new Archive([blob]);
+ a.dlimit = 2;
+ const file = await fs.upload(a);
const fr = new FileReceiver({
secretKey: file.toJSON().secretKey,
id: file.id,
nonce: file.keychain.nonce,
requiresPassword: false
});
- await file.changeLimit(2);
await fr.getMetadata();
await fr.download(options);
await file.updateDownloadCount();