FEATURE: detect when client thinks user is logged on but is not

This cleans up an error condition where UI thinks a user is logged on
but the user is not. If this happens user will be prompted to refresh.
This commit is contained in:
Sam 2018-03-06 16:49:31 +11:00
parent f0d5f83424
commit 0134e41286
9 changed files with 82 additions and 11 deletions

View File

@ -11,6 +11,7 @@
// Stuff we need to load first // Stuff we need to load first
//= require ./discourse/lib/utilities //= require ./discourse/lib/utilities
//= require ./discourse/lib/page-visible //= require ./discourse/lib/page-visible
//= require ./discourse/lib/logout
//= require ./discourse/lib/ajax //= require ./discourse/lib/ajax
//= require ./discourse/lib/text //= require ./discourse/lib/text
//= require ./discourse/lib/hash //= require ./discourse/lib/hash

View File

@ -1,5 +1,7 @@
import logout from 'discourse/lib/logout'; import logout from 'discourse/lib/logout';
let _showingLogout = false;
// Subscribe to "logout" change events via the Message Bus // Subscribe to "logout" change events via the Message Bus
export default { export default {
name: "logout", name: "logout",
@ -7,14 +9,22 @@ export default {
initialize: function (container) { initialize: function (container) {
const messageBus = container.lookup('message-bus:main'); const messageBus = container.lookup('message-bus:main');
const siteSettings = container.lookup('site-settings:main');
const keyValueStore = container.lookup('key-value-store:main');
if (!messageBus) { return; } if (!messageBus) { return; }
const callback = () => logout(siteSettings, keyValueStore);
messageBus.subscribe("/logout", function () { messageBus.subscribe("/logout", function () {
bootbox.dialog(I18n.t("logout"), {label: I18n.t("refresh"), callback}, {onEscape: callback, backdrop: 'static'}); if (!_showingLogout) {
_showingLogout = true;
bootbox.dialog(I18n.t("logout"), {
label: I18n.t("refresh"),
callback: logout
}, {
onEscape: logout,
backdrop: 'static'
});
}
}); });
} }
}; };

View File

@ -1,7 +1,9 @@
import pageVisible from 'discourse/lib/page-visible'; import pageVisible from 'discourse/lib/page-visible';
import logout from 'discourse/lib/logout';
let _trackView = false; let _trackView = false;
let _transientHeader = null; let _transientHeader = null;
let _showingLogout = false;
export function setTransientHeader(key, value) { export function setTransientHeader(key, value) {
_transientHeader = {key, value}; _transientHeader = {key, value};
@ -39,6 +41,10 @@ export function ajax() {
args.headers = args.headers || {}; args.headers = args.headers || {};
if (Discourse.__container__.lookup('current-user:main')) {
args.headers['Discourse-Logged-In'] = "true";
}
if (_transientHeader) { if (_transientHeader) {
args.headers[_transientHeader.key] = _transientHeader.value; args.headers[_transientHeader.key] = _transientHeader.value;
_transientHeader = null; _transientHeader = null;
@ -54,7 +60,22 @@ export function ajax() {
args.headers['Discourse-Visible'] = "true"; args.headers['Discourse-Visible'] = "true";
} }
let handleLogoff = function(xhr) {
if (xhr.getResponseHeader('Discourse-Logged-Out') && !_showingLogout) {
_showingLogout = true;
bootbox.dialog(
I18n.t("logout"), {label: I18n.t("refresh"), callback: logout},
{
onEscape: () => logout(),
backdrop: 'static'
}
);
}
};
args.success = (data, textStatus, xhr) => { args.success = (data, textStatus, xhr) => {
handleLogoff(xhr);
if (xhr.getResponseHeader('Discourse-Readonly')) { if (xhr.getResponseHeader('Discourse-Readonly')) {
Ember.run(() => Discourse.Site.currentProp('isReadOnly', true)); Ember.run(() => Discourse.Site.currentProp('isReadOnly', true));
} }
@ -67,6 +88,8 @@ export function ajax() {
}; };
args.error = (xhr, textStatus, errorThrown) => { args.error = (xhr, textStatus, errorThrown) => {
handleLogoff(xhr);
// note: for bad CSRF we don't loop an extra request right away. // note: for bad CSRF we don't loop an extra request right away.
// this allows us to eliminate the possibility of having a loop. // this allows us to eliminate the possibility of having a loop.
if (xhr.status === 403 && xhr.responseText === "[\"BAD CSRF\"]") { if (xhr.status === 403 && xhr.responseText === "[\"BAD CSRF\"]") {

View File

@ -1,4 +1,10 @@
export default function logout(siteSettings, keyValueStore) { export default function logout(siteSettings, keyValueStore) {
if (!siteSettings || !keyValueStore) {
const container = Discourse.__container__;
siteSettings = siteSettings || container.lookup('site-settings:main');
keyValueStore = keyValueStore || container.lookup('key-value-store:main');
}
keyValueStore.abandonLocal(); keyValueStore.abandonLocal();
const redirect = siteSettings.logout_redirect; const redirect = siteSettings.logout_redirect;

View File

@ -22,13 +22,18 @@ def setup_message_bus_env(env)
user.groups.pluck('groups.id') user.groups.pluck('groups.id')
end end
hash = { extra_headers = {
extra_headers:
{
"Access-Control-Allow-Origin" => Discourse.base_url_no_prefix, "Access-Control-Allow-Origin" => Discourse.base_url_no_prefix,
"Access-Control-Allow-Methods" => "GET, POST", "Access-Control-Allow-Methods" => "GET, POST",
"Access-Control-Allow-Headers" => "X-SILENCE-LOGGER, X-Shared-Session-Key, Dont-Chunk, Discourse-Visible" "Access-Control-Allow-Headers" => "X-SILENCE-LOGGER, X-Shared-Session-Key, Dont-Chunk, Discourse-Visible"
}, }
if env[Auth::DefaultCurrentUserProvider::BAD_TOKEN]
extra_headers['Discourse-Logged-Out'] = '1'
end
hash = {
extra_headers: extra_headers,
user_id: user_id, user_id: user_id,
group_ids: group_ids, group_ids: group_ids,
is_admin: is_admin, is_admin: is_admin,

View File

@ -14,6 +14,7 @@ class Auth::DefaultCurrentUserProvider
TOKEN_COOKIE ||= "_t" TOKEN_COOKIE ||= "_t"
PATH_INFO ||= "PATH_INFO" PATH_INFO ||= "PATH_INFO"
COOKIE_ATTEMPTS_PER_MIN ||= 10 COOKIE_ATTEMPTS_PER_MIN ||= 10
BAD_TOKEN ||= "_DISCOURSE_BAD_TOKEN"
# do all current user initialization here # do all current user initialization here
def initialize(env) def initialize(env)
@ -58,7 +59,8 @@ class Auth::DefaultCurrentUserProvider
current_user = @user_token.try(:user) current_user = @user_token.try(:user)
end end
unless current_user if !current_user
@env[BAD_TOKEN] = true
begin begin
limiter.performed! limiter.performed!
rescue RateLimiter::LimitExceeded rescue RateLimiter::LimitExceeded
@ -69,6 +71,8 @@ class Auth::DefaultCurrentUserProvider
) )
end end
end end
elsif @env['HTTP_DISCOURSE_LOGGED_IN']
@env[BAD_TOKEN] = true
end end
if current_user && should_update_last_seen? if current_user && should_update_last_seen?

View File

@ -78,6 +78,10 @@ module Hijack
headers['Content-Length'] = body.bytesize headers['Content-Length'] = body.bytesize
headers['Connection'] = "close" headers['Connection'] = "close"
if env[Auth::DefaultCurrentUserProvider::BAD_TOKEN]
headers['Discourse-Logged-Out'] = '1'
end
status_string = Rack::Utils::HTTP_STATUS_CODES[response.status.to_i] || "Unknown" status_string = Rack::Utils::HTTP_STATUS_CODES[response.status.to_i] || "Unknown"
io.write "#{response.status} #{status_string}\r\n" io.write "#{response.status} #{status_string}\r\n"

View File

@ -169,6 +169,11 @@ class Middleware::RequestTracker
if info && (headers = result[1]) if info && (headers = result[1])
headers["X-Runtime"] = "%0.6f" % info[:total_duration] headers["X-Runtime"] = "%0.6f" % info[:total_duration]
end end
if env[Auth::DefaultCurrentUserProvider::BAD_TOKEN] && (headers = result[1])
headers['Discourse-Logged-Out'] = '1'
end
result result
ensure ensure
if (limiters = env['DISCOURSE_RATE_LIMITERS']) && env['DISCOURSE_IS_ASSET_PATH'] if (limiters = env['DISCOURSE_RATE_LIMITERS']) && env['DISCOURSE_IS_ASSET_PATH']

View File

@ -182,4 +182,17 @@ RSpec.describe SessionController do
end end
end end
end end
context 'logoff support' do
it 'can log off users cleanly' do
user = Fabricate(:user)
sign_in(user)
UserAuthToken.destroy_all
# we need a route that will call current user
post '/draft.json', params: {}
expect(response.headers['Discourse-Logged-Out']).to eq("1")
end
end
end end