added oauth refresh token support

Co-authored-by: timvisee <tim@visee.me>
This commit is contained in:
Danny Coates 2020-07-24 18:11:50 -07:00 committed by timvisee
parent b15c017dcd
commit 4f273eca03
No known key found for this signature in database
GPG Key ID: B8DB720BC383E172
8 changed files with 118 additions and 27 deletions

View File

@ -49,8 +49,8 @@ export default function(state, emitter) {
state.user.login(email); state.user.login(email);
}); });
emitter.on('logout', () => { emitter.on('logout', async () => {
state.user.logout(); await state.user.logout();
metrics.loggedOut({ trigger: 'button' }); metrics.loggedOut({ trigger: 'button' });
emitter.emit('pushState', '/'); emitter.emit('pushState', '/');
}); });
@ -178,6 +178,12 @@ export default function(state, emitter) {
//cancelled. do nothing //cancelled. do nothing
metrics.cancelledUpload(archive, err.duration); metrics.cancelledUpload(archive, err.duration);
render(); render();
} else if (err.message === '401') {
const refreshed = await state.user.refresh();
if (refreshed) {
return emitter.emit('upload');
}
emitter.emit('pushState', '/error');
} else { } else {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(err); console.error(err);

View File

@ -54,12 +54,17 @@ class Account extends Component {
createElement() { createElement() {
if (!this.enabled) { if (!this.enabled) {
return html` return html`
<div></div> <send-account></send-account>
`; `;
} }
const user = this.state.user; const user = this.state.user;
const translate = this.state.translate; const translate = this.state.translate;
this.setLocal(); this.setLocal();
if (user.loginRequired && !this.local.loggedIn) {
return html`
<send-account></send-account>
`;
}
if (!this.local.loggedIn) { if (!this.local.loggedIn) {
return html` return html`
<send-account> <send-account>

View File

@ -53,6 +53,9 @@ module.exports = function(trigger) {
type="submit" type="submit"
/> />
</form> </form>
${state.user.loginRequired
? ''
: html`
<button <button
class="my-3 link-blue font-medium" class="my-3 link-blue font-medium"
title="${state.translate('deletePopupCancel')}" title="${state.translate('deletePopupCancel')}"
@ -60,6 +63,7 @@ module.exports = function(trigger) {
> >
${state.translate('deletePopupCancel')} ${state.translate('deletePopupCancel')}
</button> </button>
`}
</section> </section>
</send-signup-dialog> </send-signup-dialog>
`; `;

View File

@ -76,6 +76,10 @@ export default class User {
return this.info.access_token; return this.info.access_token;
} }
get refreshToken() {
return this.info.refresh_token;
}
get maxSize() { get maxSize() {
return this.loggedIn return this.loggedIn
? this.limits.MAX_FILE_SIZE ? this.limits.MAX_FILE_SIZE
@ -135,6 +139,7 @@ export default class User {
const code_challenge = await preparePkce(this.storage); const code_challenge = await preparePkce(this.storage);
const options = { const options = {
action: 'email', action: 'email',
access_type: 'offline',
client_id: this.authConfig.client_id, client_id: this.authConfig.client_id,
code_challenge, code_challenge,
code_challenge_method: 'S256', code_challenge_method: 'S256',
@ -192,12 +197,64 @@ export default class User {
}); });
const userInfo = await infoResponse.json(); const userInfo = await infoResponse.json();
userInfo.access_token = auth.access_token; userInfo.access_token = auth.access_token;
userInfo.refresh_token = auth.refresh_token;
userInfo.fileListKey = await getFileListKey(this.storage, auth.keys_jwe); userInfo.fileListKey = await getFileListKey(this.storage, auth.keys_jwe);
this.info = userInfo; this.info = userInfo;
this.storage.remove('pkceVerifier'); 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.storage.clearLocalFiles();
this.info = {}; this.info = {};
} }
@ -211,17 +268,29 @@ export default class User {
const key = b64ToArray(this.info.fileListKey); const key = b64ToArray(this.info.fileListKey);
const sha = await crypto.subtle.digest('SHA-256', key); const sha = await crypto.subtle.digest('SHA-256', key);
const kid = arrayToB64(new Uint8Array(sha)).substring(0, 16); 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 { try {
const encrypted = await getFileList(this.bearerToken, kid); const encrypted = await getFileList(
this.bearerToken,
this.refreshToken,
kid
);
const decrypted = await streamToArrayBuffer( const decrypted = await streamToArrayBuffer(
decryptStream(blobStream(encrypted), key) decryptStream(blobStream(encrypted), key)
); );
list = JSON.parse(textDecoder.decode(decrypted)); list = JSON.parse(textDecoder.decode(decrypted));
} catch (e) { } catch (e) {
if (e.message === '401') { return retry(e);
this.logout();
return { incoming: true };
}
} }
changes = await this.storage.merge(list); changes = await this.storage.merge(list);
if (!changes.outgoing) { if (!changes.outgoing) {
@ -234,9 +303,9 @@ export default class User {
const encrypted = await streamToArrayBuffer( const encrypted = await streamToArrayBuffer(
encryptStream(blobStream(blob), key) encryptStream(blobStream(blob), key)
); );
await setFileList(this.bearerToken, kid, encrypted); await setFileList(this.bearerToken, this.refreshToken, kid, encrypted);
} catch (e) { } catch (e) {
// return retry(e);
} }
return changes; return changes;
} }

View File

@ -70,6 +70,10 @@ module.exports = {
const token = authHeader.split(' ')[1]; const token = authHeader.split(' ')[1];
req.user = await fxa.verify(token); req.user = await fxa.verify(token);
} }
return next(); if (req.user) {
next();
} else {
res.sendStatus(401);
}
} }
}; };

View File

@ -13,9 +13,6 @@ function id(user, kid) {
module.exports = { module.exports = {
async get(req, res) { async get(req, res) {
if (!req.user) {
return res.sendStatus(401);
}
const kid = req.params.id; const kid = req.params.id;
try { try {
const fileId = id(req.user, kid); const fileId = id(req.user, kid);
@ -32,9 +29,6 @@ module.exports = {
}, },
async post(req, res) { async post(req, res) {
if (!req.user) {
return res.sendStatus(401);
}
const kid = req.params.id; const kid = req.params.id;
try { try {
const limiter = new Limiter(1024 * 1024 * 10); const limiter = new Limiter(1024 * 1024 * 10);

View File

@ -41,6 +41,14 @@ module.exports = function(ws, req) {
? config.max_downloads ? config.max_downloads
: config.anon_max_downloads; : config.anon_max_downloads;
if (config.fxa_required && !user) {
ws.send(
JSON.stringify({
error: 401
})
);
return ws.close();
}
if ( if (
!metadata || !metadata ||
!auth || !auth ||

View File

@ -181,14 +181,15 @@ describe('Upload / Download flow', function() {
it('can allow multiple downloads', async function() { it('can allow multiple downloads', async function() {
const fs = new FileSender(); 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({ const fr = new FileReceiver({
secretKey: file.toJSON().secretKey, secretKey: file.toJSON().secretKey,
id: file.id, id: file.id,
nonce: file.keychain.nonce, nonce: file.keychain.nonce,
requiresPassword: false requiresPassword: false
}); });
await file.changeLimit(2);
await fr.getMetadata(); await fr.getMetadata();
await fr.download(options); await fr.download(options);
await file.updateDownloadCount(); await file.updateDownloadCount();