added oauth refresh token support
This commit is contained in:
parent
ce507c557f
commit
f3a1fde07f
|
@ -50,8 +50,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', '/');
|
||||||
});
|
});
|
||||||
|
@ -179,6 +179,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);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -45,9 +45,7 @@ function preview(state, emit) {
|
||||||
return noStreams(state, emit);
|
return noStreams(state, emit);
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div class="w-full md:flex md:flex-row items-stretch md:flex-1">
|
||||||
class="w-full overflow-hidden md:flex md:flex-row items-stretch md:flex-1"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="px-2 w-full md:px-0 flex-half md:flex md:flex-col mt-12 md:pr-8 pb-4"
|
class="px-2 w-full md:px-0 flex-half md:flex md:flex-col mt-12 md:pr-8 pb-4"
|
||||||
>
|
>
|
||||||
|
|
|
@ -53,13 +53,17 @@ module.exports = function(trigger) {
|
||||||
type="submit"
|
type="submit"
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
<button
|
${state.user.loginRequired
|
||||||
class="my-3 link-blue font-medium"
|
? ''
|
||||||
title="${state.translate('deletePopupCancel')}"
|
: html`
|
||||||
onclick=${cancel}
|
<button
|
||||||
>
|
class="my-3 link-blue font-medium"
|
||||||
${state.translate('deletePopupCancel')}
|
title="${state.translate('deletePopupCancel')}"
|
||||||
</button>
|
onclick=${cancel}
|
||||||
|
>
|
||||||
|
${state.translate('deletePopupCancel')}
|
||||||
|
</button>
|
||||||
|
`}
|
||||||
</section>
|
</section>
|
||||||
</send-signup-dialog>
|
</send-signup-dialog>
|
||||||
`;
|
`;
|
||||||
|
|
85
app/user.js
85
app/user.js
|
@ -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
|
||||||
|
@ -139,6 +143,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',
|
||||||
|
@ -196,12 +201,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 = {};
|
||||||
}
|
}
|
||||||
|
@ -215,17 +272,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) {
|
||||||
|
@ -238,9 +307,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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -41,13 +41,20 @@ 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 ||
|
||||||
timeLimit <= 0 ||
|
timeLimit <= 0 ||
|
||||||
timeLimit > maxExpireSeconds ||
|
timeLimit > maxExpireSeconds ||
|
||||||
dlimit > maxDownloads ||
|
dlimit > maxDownloads
|
||||||
(config.fxa_required && !user)
|
|
||||||
) {
|
) {
|
||||||
ws.send(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
|
|
@ -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();
|
||||||
|
|
Loading…
Reference in New Issue