added oauth refresh token support
Co-authored-by: timvisee <tim@visee.me>
This commit is contained in:
parent
b15c017dcd
commit
4f273eca03
|
@ -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);
|
||||
|
|
|
@ -54,12 +54,17 @@ class Account extends Component {
|
|||
createElement() {
|
||||
if (!this.enabled) {
|
||||
return html`
|
||||
<div></div>
|
||||
<send-account></send-account>
|
||||
`;
|
||||
}
|
||||
const user = this.state.user;
|
||||
const translate = this.state.translate;
|
||||
this.setLocal();
|
||||
if (user.loginRequired && !this.local.loggedIn) {
|
||||
return html`
|
||||
<send-account></send-account>
|
||||
`;
|
||||
}
|
||||
if (!this.local.loggedIn) {
|
||||
return html`
|
||||
<send-account>
|
||||
|
|
|
@ -53,13 +53,17 @@ module.exports = function(trigger) {
|
|||
type="submit"
|
||||
/>
|
||||
</form>
|
||||
<button
|
||||
class="my-3 link-blue font-medium"
|
||||
title="${state.translate('deletePopupCancel')}"
|
||||
onclick=${cancel}
|
||||
>
|
||||
${state.translate('deletePopupCancel')}
|
||||
</button>
|
||||
${state.user.loginRequired
|
||||
? ''
|
||||
: html`
|
||||
<button
|
||||
class="my-3 link-blue font-medium"
|
||||
title="${state.translate('deletePopupCancel')}"
|
||||
onclick=${cancel}
|
||||
>
|
||||
${state.translate('deletePopupCancel')}
|
||||
</button>
|
||||
`}
|
||||
</section>
|
||||
</send-signup-dialog>
|
||||
`;
|
||||
|
|
85
app/user.js
85
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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 ||
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue