Overhaul sessions, tokens, and authentication

- Use cookies + CSRF token for API authentication in the default client. This mitigates potential XSS attacks by making the token unavailable to JavaScript. The Authorization header is still supported, but not used by default.
- Make sensitive/destructive actions (editing a user, permanently deleting anything, visiting the admin CP) require the user to re-enter their password if they haven't entered it in the last 30 minutes.
- Refactor and clean up the authentication middleware.
- Add an `onhide` hook to the Modal component. (+1 squashed commit)
This commit is contained in:
Toby Zerner
2015-11-05 16:17:00 +10:30
parent a1e1635019
commit 9896378b59
69 changed files with 1076 additions and 509 deletions

View File

@ -2,6 +2,7 @@ import ItemList from 'flarum/utils/ItemList';
import Alert from 'flarum/components/Alert';
import Button from 'flarum/components/Button';
import RequestErrorModal from 'flarum/components/RequestErrorModal';
import ConfirmPasswordModal from 'flarum/components/ConfirmPasswordModal';
import Translator from 'flarum/Translator';
import extract from 'flarum/utils/extract';
import patchMithril from 'flarum/utils/patchMithril';
@ -182,14 +183,17 @@ export default class App {
* @return {Promise}
* @public
*/
request(options) {
request(originalOptions) {
const options = Object.assign({}, originalOptions);
// Set some default options if they haven't been overridden. We want to
// authenticate all requests with the session token. We also want all
// requests to run asynchronously in the background, so that they don't
// prevent redraws from occurring.
options.config = options.config || this.session.authorize.bind(this.session);
options.background = options.background || true;
extend(options, 'config', (result, xhr) => xhr.setRequestHeader('X-CSRF-Token', this.session.csrfToken));
// If the method is something like PATCH or DELETE, which not all servers
// support, then we'll send it as a POST request with a the intended method
// specified in the X-Fake-Http-Method header.
@ -218,7 +222,7 @@ export default class App {
if (original) {
responseText = original(xhr.responseText);
} else {
responseText = xhr.responseText.length > 0 ? xhr.responseText : null;
responseText = xhr.responseText || null;
}
const status = xhr.status;
@ -227,6 +231,11 @@ export default class App {
throw new RequestError(status, responseText, options, xhr);
}
if (xhr.getResponseHeader) {
const csrfToken = xhr.getResponseHeader('X-CSRF-Token');
if (csrfToken) app.session.csrfToken = csrfToken;
}
try {
return JSON.parse(responseText);
} catch (e) {
@ -238,9 +247,20 @@ export default class App {
// Now make the request. If it's a failure, inspect the error that was
// returned and show an alert containing its contents.
return m.request(options).then(null, error => {
const deferred = m.deferred();
m.request(options).then(response => deferred.resolve(response), error => {
this.requestError = error;
if (error.response && error.response.errors && error.response.errors[0] && error.response.errors[0].code === 'invalid_access_token') {
this.modal.show(new ConfirmPasswordModal({
deferredRequest: originalOptions,
deferred,
error
}));
return;
}
let children;
switch (error.status) {
@ -283,8 +303,10 @@ export default class App {
this.alerts.show(error.alert);
}
throw error;
deferred.reject(error);
});
return deferred.promise;
}
/**