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