diff --git a/app.py b/app.py index 7bac0e1..8671eff 100644 --- a/app.py +++ b/app.py @@ -150,14 +150,22 @@ def oidc_callback(): if not OAUTH: abort(404) if request.args.get("error"): - return redirect(url_for("index", error=request.args["error"])) + return render_template("index.html", email_address=EMAIL_USERNAME, oauth=True, connected=False, + error=request.args["error"]), 400 try: OAUTH.finish(request.args.get("state", ""), request.args.get("code", "")) except OAuthError as e: logger.warning("Mailbox connect failed: %s", e) - return redirect(url_for("index", error=str(e))) + return render_template("index.html", email_address=EMAIL_USERNAME, oauth=True, connected=False, + error=str(e)), 400 logger.info("Collection mailbox %s connected via OAuth", EMAIL_USERNAME) - return redirect(url_for("index")) + # the browser session now belongs to the technical account, which forward_auth does not let in: + # confirm here instead of redirecting to the start page + return ( + "Connected" + "

Collection mailbox connected. Close this private window and open the Unsubscriber again " + "in your normal browser.

" + ) @app.route("/disconnect", methods=["POST"]) diff --git a/index.html b/index.html index d8ba050..3dd8da4 100644 --- a/index.html +++ b/index.html @@ -19,8 +19,8 @@ {% endif %} {% else %} -

The collection mailbox is not connected yet. Open this page in a private window and sign in - as {{ email_address }} (technical account) – not with your own account.

+

The collection mailbox is not connected yet. Open /connect of this site in a private + window and sign in as {{ email_address }} (technical account).

diff --git a/oauth.py b/oauth.py index 670a192..6f5f4a8 100644 --- a/oauth.py +++ b/oauth.py @@ -21,6 +21,7 @@ from cryptography.fernet import Fernet, InvalidToken SCOPES = "openid profile email offline_access" REFRESH_MARGIN_SEC = 60 PENDING_TTL_SEC = 600 +MAX_PENDING = 20 # /connect is reachable without login: bound the open requests class OAuthError(Exception): @@ -108,6 +109,8 @@ class MailboxOAuth: now = self.clock() with self._lock: self._pending = {k: v for k, v in self._pending.items() if now - v[1] < PENDING_TTL_SEC} + while len(self._pending) >= MAX_PENDING: + del self._pending[min(self._pending, key=lambda k: self._pending[k][1])] self._pending[state] = (verifier, now) query = { "response_type": "code", diff --git a/tests/test_oauth.py b/tests/test_oauth.py index d9c4298..ff8c0a9 100644 --- a/tests/test_oauth.py +++ b/tests/test_oauth.py @@ -139,3 +139,37 @@ def test_index_shows_connect_when_not_connected(monkeypatch): monkeypatch.setattr(unsub, "OAUTH", FakeOAuth()) html = unsub.app.test_client().get("/").get_data(as_text=True) assert 'action="/connect"' in html and 'action="/result"' not in html + + +def test_callback_confirms_without_redirect(monkeypatch): + import app as unsub + + class FakeOAuth: + def finish(self, state, code): + assert (state, code) == ("s", "c") + + monkeypatch.setattr(unsub, "OAUTH", FakeOAuth()) + r = unsub.app.test_client().get("/oidc/callback?state=s&code=c") + assert r.status_code == 200 and "connected" in r.get_data(as_text=True) + + +def test_callback_error_is_shown(monkeypatch): + import app as unsub + + class FakeOAuth: + def finish(self, state, code): + raise oauth.OAuthError("Signed in as philipp@example.org, expected unsubscribe-bot@example.org") + + def connected(self): + return False + + monkeypatch.setattr(unsub, "OAUTH", FakeOAuth()) + r = unsub.app.test_client().get("/oidc/callback?state=s&code=c") + assert r.status_code == 400 and "expected unsubscribe-bot" in r.get_data(as_text=True) + + +def test_pending_requests_are_bounded(tmp_path): + mb = _mb(tmp_path, []) + for _ in range(oauth.MAX_PENDING + 15): + mb.start() + assert len(mb._pending) == oauth.MAX_PENDING