Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 7 additions & 11 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, session, abort
from flask_socketio import SocketIO
from flask_login import logout_user, login_required, current_user
from flask_login import login_required, current_user
from flask_wtf.csrf import CSRFProtect
from werkzeug.middleware.proxy_fix import ProxyFix
import config
Expand All @@ -18,6 +18,7 @@
from .tailscale_ssh import user_can_use_tailscale_ssh
from .runtime_lifecycle import RuntimeLifecycle
from .browser_identity import connection_history_scope
from .auth_assurance import clear_browser_authentication

socketio = SocketIO(
async_mode=config.SOCKETIO_ASYNC_MODE,
Expand Down Expand Up @@ -173,8 +174,7 @@ def enforce_restore_maintenance_and_session_epoch():

user_id = current_user.id
user_lifecycle.revoke_user_access(user_id, socketio)
logout_user()
session.clear()
clear_browser_authentication()
return redirect(url_for('login'))
if (
current_user.is_authenticated
Expand All @@ -197,8 +197,7 @@ def enforce_restore_maintenance_and_session_epoch():
error=type(exc).__name__,
)
user_lifecycle.revoke_user_access(user_id, socketio)
logout_user()
session.clear()
clear_browser_authentication()
return redirect(url_for('login'))
session['_ldap_verified_at'] = int(time.time())
if initialize_storage and current_user.is_authenticated:
Expand All @@ -208,17 +207,15 @@ def enforce_restore_maintenance_and_session_epoch():
if stored_epoch is None:
session['_auth_epoch'] = epoch
elif stored_epoch != epoch:
logout_user()
session.clear()
clear_browser_authentication()
return redirect(url_for('login', next=request.path))
if initialize_storage and current_user.is_authenticated:
from .auth_assurance import current_authentication_session

auth_session = current_authentication_session()
if auth_session is None:
username = current_user.username
logout_user()
session.clear()
clear_browser_authentication()
log_warning(
'Authentication session rejected',
user=username,
Expand Down Expand Up @@ -618,8 +615,7 @@ def logout():
error=type(exc).__name__,
)
user_lifecycle.revoke_user_access(user_id, socketio)
logout_user()
session.clear()
clear_browser_authentication()
return redirect(url_for('login'))

@app.route('/change-password', methods=['GET', 'POST'])
Expand Down
12 changes: 10 additions & 2 deletions app/auth_assurance.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,15 @@ def _session_lifetime(remember):
return timedelta(seconds=max(1, int(value)))


def clear_browser_authentication():
"""Clear browser auth state while preserving remember-cookie removal."""
logout_user()
clear_remember_cookie = session.get('_remember') == 'clear'
session.clear()
if clear_remember_cookie:
session['_remember'] = 'clear'


def finalize_login(pending, *, methods, strong_authenticated_at=None):
"""Create the sole authenticated browser-session record."""
if not isinstance(pending, PendingAuthentication):
Expand Down Expand Up @@ -313,8 +322,7 @@ def finalize_login(pending, *, methods, strong_authenticated_at=None):
db.session.commit()
except Exception as exc:
db.session.rollback()
logout_user()
session.clear()
clear_browser_authentication()
raise AuthenticationFinalizationError(
'authentication session could not be stored'
) from exc
Expand Down
25 changes: 25 additions & 0 deletions tests/test_auth_assurance.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,28 @@ def test_remember_cookie_restores_the_bound_authentication_session(app, client):
assert browser_session.get('_auth_session')


def test_legacy_remember_cookie_is_cleared_before_login_redirect(app, client):
from flask_login.utils import encode_cookie
from app.models import User, db

user_id = _create_user(app, 'legacyremembered')
with app.app_context():
legacy_identifier = db.session.get(User, user_id).get_id()
legacy_cookie = encode_cookie(legacy_identifier)

client.set_cookie('remember_token', legacy_cookie)

rejected = client.get('/')

assert rejected.status_code == 302
assert '/login?next=' in rejected.headers['Location']
assert client.get_cookie('remember_token') is None

login = client.get(rejected.headers['Location'])

assert login.status_code == 200


def test_request_guard_rejects_invalid_server_side_sessions(
app,
client,
Expand Down Expand Up @@ -430,12 +452,15 @@ def test_logout_deletes_current_authentication_session(app, client):
assert client.post('/login', data={
'username': 'logoutassurance',
'password': 'password123',
'remember': 'on',
}).status_code == 302
assert client.get_cookie('remember_token') is not None
with app.app_context():
assert AuthenticationSession.query.count() == 1

response = client.post('/logout')

assert response.status_code == 302
assert client.get_cookie('remember_token') is None
with app.app_context():
assert AuthenticationSession.query.count() == 0
Loading