From 04d4b1b3e7a64038274558e31848ece4358a5a45 Mon Sep 17 00:00:00 2001
From: Cole Stowell
Date: Fri, 19 Dec 2025 12:28:25 -0600
Subject: [PATCH 01/13] bump: clean up, bump deps
---
config.env.py | 12 +-
docker-compose.yaml | 6 +-
requirements.dev | 22 --
requirements.in | 19 ++
requirements.txt | 330 ++++++++++++++++++++--------
selfservice/__init__.py | 10 +-
selfservice/blueprints/recovery.py | 4 +-
selfservice/models.py | 6 +-
selfservice/templates/recovery.html | 2 +-
9 files changed, 276 insertions(+), 135 deletions(-)
delete mode 100644 requirements.dev
create mode 100644 requirements.in
diff --git a/config.env.py b/config.env.py
index e510b81..cfb8823 100644
--- a/config.env.py
+++ b/config.env.py
@@ -35,12 +35,12 @@
)
SQLALCHEMY_TRACK_MODIFICATIONS = False
-RECAPTCHA_ENABLED = True
-RECAPTCHA_SITE_KEY = os.environ.get("RECAPTCHA_SITE_KEY", "")
-RECAPTCHA_SECRET_KEY = os.environ.get("RECAPTCHA_SECRET_KEY", "")
-RECAPTCHA_THEME = "light"
-RECAPTCHA_TYPE = "image"
-RECAPTCHA_SIZE = "normal"
+XCAPTCHA_ENABLED = True
+XCAPTCHA_SITE_KEY = os.environ.get("XCAPTCHA_SITE_KEY", "")
+XCAPTCHA_SECRET_KEY = os.environ.get("XCAPTCHA_SECRET_KEY", "")
+XCAPTCHA_THEME = "light"
+XCAPTCHA_TYPE = "image"
+XCAPTCHA_SIZE = "normal"
TWILIO_SID = os.environ.get("TWILIO_SID", "")
TWILIO_TOKEN = os.environ.get("TWILIO_TOKEN", "")
diff --git a/docker-compose.yaml b/docker-compose.yaml
index e088131..fbc8ff6 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -1,7 +1,7 @@
version: '2'
services:
postgres:
- image: postgres:9.6
+ image: docker.io/postgres:9.6
container_name: selfservice-postgres
restart: always
volumes:
@@ -12,7 +12,7 @@ services:
ports:
- 127.0.0.1:5433:5432
phppgadmin:
- image: bitnami/phppgadmin:latest
+ image: docker.io/dockage/phppgadmin:latest
container_name: selfservice-pgadmin
links:
- postgres
@@ -22,4 +22,4 @@ services:
restart: always
ports:
- 127.0.0.1:8081:8080
- - 127.0.0.1:8444:8443
\ No newline at end of file
+ - 127.0.0.1:8444:8443
diff --git a/requirements.dev b/requirements.dev
deleted file mode 100644
index 8394dae..0000000
--- a/requirements.dev
+++ /dev/null
@@ -1,22 +0,0 @@
-beautifulsoup4
-black
-bs4
-csh-ldap>=2.2.0
-dill
-dnspython<2.0.0
-Flask
-Flask-Limiter
-Flask-Migrate
-Flask-pyoidc
-Flask-QRcode
-Flask-ReCaptcha
-Flask-SQLAlchemy
-psycopg2
-pylint
-python-freeipa
-python-keycloak
-qrcode
-requests
-sentry-sdk[flask]~=0.13.1
-srvlookup
-twillio
\ No newline at end of file
diff --git a/requirements.in b/requirements.in
new file mode 100644
index 0000000..22a14fc
--- /dev/null
+++ b/requirements.in
@@ -0,0 +1,19 @@
+Flask~=3.1.2
+Flask-pyoidc~=3.14.3
+Flask-Migrate~=4.1.0
+Flask-SQLAlchemy~=3.1.1
+Flask-Limiter~=4.1.1
+Flask-QRcode~=3.2.0
+Flask-xCaptcha~=0.5.5
+srvlookup~=3.0.0
+csh-ldap @ git+https://github.com/costowell/csh_ldap@cole-dev
+python-freeipa~=1.0.10
+python-keycloak~=5.8.1
+sentry-sdk[Flask]
+psycopg2-binary~=2.9.11
+twilio~=9.9.0
+pyotp~=2.9.0
+dill~=0.4.0
+beautifulsoup4~=4.14.3
+passlib~=1.7.4
+xkcdpass~=1.20.0
diff --git a/requirements.txt b/requirements.txt
index ee9be7c..ebe8767 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,93 +1,237 @@
-alabaster==0.7.10
-alembic==0.9.9
-appdirs==1.4.3
-asn1crypto==0.24.0
-aspy.yaml==1.1.1
-astroid==2.4.2
-attrs==18.1.0
-Beaker==1.9.1
-beautifulsoup4==4.9.1
-black==20.8b1
-blinker==1.4
-bs4==0.0.1
-cached-property==1.4.2
-certifi==2018.4.16
-cffi==1.13.2
-cfgv==1.0.0
-chardet==3.0.4
-click==7.1.2
-cryptography==3.2
-csh-ldap==2.3.1
-dataclasses==0.7
-defusedxml==0.6.0
-dill==0.3.2
-distlib==0.3.1
-dnspython==1.16.0
-ecdsa==0.13.3
-filelock==3.0.12
-Flask==1.1.2
-Flask-Limiter==1.4
-Flask-Migrate==2.5.3
-Flask-pyoidc==3.5.1
-Flask-QRcode==3.0.0
-Flask-ReCaptcha==0.4.2
-Flask-SQLAlchemy==2.4.4
-future==0.16.0
-gunicorn==19.8.1
-httmock==1.2.5
-identify==1.0.18
-idna==2.6
-importlib-metadata==1.7.0
-importlib-resources==3.3.0
-isort==4.3.4
-itsdangerous==0.24
-Jinja2==2.10.1
-lazy-object-proxy==1.4.3
-limits==1.3
-Mako==1.0.7
-MarkupSafe==1.0
-mccabe==0.6.1
-mypy-extensions==0.4.3
-nodeenv==1.3.0
-oic==1.1.2
-passlib==1.7.1
-pathspec==0.8.0
-Pillow==6.2.2
-pre-commit==1.10.1
-psycopg2==2.8.5
-pyasn1==0.4.2
-pyasn1-modules==0.2.1
-pycparser==2.18
-pycrypto==2.6.1
-pycryptodomex==3.6.1
-pyjwkest==1.4.0
-PyJWT==1.7.1
-pylint==2.6.0
-pyOpenSSL==18.0.0
-pyotp==2.2.6
-python-dateutil==2.7.3
-python-editor==1.0.3
-python-freeipa==1.0.6
-python-jose==1.4.0
-python-keycloak==0.22.0
-python-ldap~=3.0.0
-pytz==2020.1
-PyYAML==5.4.1
-qrcode==6.1
-regex==2020.7.14
-requests==2.24.0
-sentry-sdk==0.13.5
-six==1.12.0
-soupsieve==2.0.1
-SQLAlchemy==1.3.0
-srvlookup==2.0.0
-toml==0.10.1
-twilio==6.45.1
-typed-ast==1.4.1
-typing-extensions==3.7.4.3
-urllib3==1.24.2
-virtualenv==16.0.0
-Werkzeug==0.15.3
-wrapt==1.11.2
-xkcdpass==1.16.5
-zipp==3.1.0
+# This file was autogenerated by uv via the following command:
+# uv pip compile requirements.in
+aiofiles==25.1.0
+ # via python-keycloak
+aiohappyeyeballs==2.6.1
+ # via aiohttp
+aiohttp==3.13.2
+ # via
+ # aiohttp-retry
+ # twilio
+aiohttp-retry==2.9.1
+ # via twilio
+aiosignal==1.4.0
+ # via aiohttp
+alembic==1.17.2
+ # via flask-migrate
+annotated-types==0.7.0
+ # via pydantic
+anyio==4.12.0
+ # via httpx
+async-property==0.2.2
+ # via python-keycloak
+attrs==25.4.0
+ # via aiohttp
+beautifulsoup4==4.14.3
+ # via -r requirements.in
+blinker==1.9.0
+ # via
+ # flask
+ # sentry-sdk
+certifi==2025.11.12
+ # via
+ # httpcore
+ # httpx
+ # requests
+ # sentry-sdk
+cffi==2.0.0
+ # via cryptography
+charset-normalizer==3.4.4
+ # via requests
+click==8.3.1
+ # via flask
+cryptography==46.0.3
+ # via
+ # jwcrypto
+ # oic
+csh-ldap @ git+https://github.com/costowell/csh_ldap@dbc3193482615b4cd52c5d1fb260662ec8157ace
+ # via -r requirements.in
+defusedxml==0.7.1
+ # via oic
+deprecated==1.3.1
+ # via limits
+deprecation==2.1.0
+ # via python-keycloak
+dill==0.4.0
+ # via -r requirements.in
+dnspython==2.8.0
+ # via srvlookup
+flask==3.1.2
+ # via
+ # -r requirements.in
+ # flask-limiter
+ # flask-migrate
+ # flask-pyoidc
+ # flask-qrcode
+ # flask-sqlalchemy
+ # flask-xcaptcha
+ # sentry-sdk
+flask-limiter==4.1.1
+ # via -r requirements.in
+flask-migrate==4.1.0
+ # via -r requirements.in
+flask-pyoidc==3.14.3
+ # via -r requirements.in
+flask-qrcode==3.2.0
+ # via -r requirements.in
+flask-sqlalchemy==3.1.1
+ # via
+ # -r requirements.in
+ # flask-migrate
+flask-xcaptcha==0.5.5
+ # via -r requirements.in
+frozenlist==1.8.0
+ # via
+ # aiohttp
+ # aiosignal
+future==1.0.0
+ # via pyjwkest
+greenlet==3.3.0
+ # via sqlalchemy
+h11==0.16.0
+ # via httpcore
+httpcore==1.0.9
+ # via httpx
+httpx==0.28.1
+ # via python-keycloak
+idna==3.11
+ # via
+ # anyio
+ # httpx
+ # requests
+ # yarl
+importlib-resources==6.5.2
+ # via flask-pyoidc
+itsdangerous==2.2.0
+ # via flask
+jinja2==3.1.6
+ # via flask
+jwcrypto==1.5.6
+ # via python-keycloak
+limits==5.6.0
+ # via flask-limiter
+mako==1.3.10
+ # via
+ # alembic
+ # oic
+markupsafe==3.0.3
+ # via
+ # flask
+ # flask-xcaptcha
+ # jinja2
+ # mako
+ # sentry-sdk
+ # werkzeug
+multidict==6.7.0
+ # via
+ # aiohttp
+ # yarl
+oic==1.6.1
+ # via flask-pyoidc
+ordered-set==4.1.0
+ # via flask-limiter
+packaging==25.0
+ # via
+ # deprecation
+ # limits
+passlib==1.7.4
+ # via -r requirements.in
+pillow==12.0.0
+ # via flask-qrcode
+propcache==0.4.1
+ # via
+ # aiohttp
+ # yarl
+psycopg2-binary==2.9.11
+ # via -r requirements.in
+pyasn1==0.6.1
+ # via
+ # pyasn1-modules
+ # python-ldap
+pyasn1-modules==0.4.2
+ # via python-ldap
+pycparser==2.23
+ # via cffi
+pycryptodomex==3.23.0
+ # via
+ # oic
+ # pyjwkest
+pydantic==2.12.5
+ # via pydantic-settings
+pydantic-core==2.41.5
+ # via pydantic
+pydantic-settings==2.12.0
+ # via oic
+pyjwkest==1.4.4
+ # via oic
+pyjwt==2.10.1
+ # via twilio
+pyotp==2.9.0
+ # via -r requirements.in
+python-dotenv==1.2.1
+ # via pydantic-settings
+python-freeipa==1.0.10
+ # via -r requirements.in
+python-keycloak==5.8.1
+ # via -r requirements.in
+python-ldap==3.4.5
+ # via csh-ldap
+qrcode==8.2
+ # via flask-qrcode
+requests==2.32.5
+ # via
+ # flask-pyoidc
+ # flask-xcaptcha
+ # oic
+ # pyjwkest
+ # python-freeipa
+ # python-keycloak
+ # requests-toolbelt
+ # twilio
+requests-toolbelt==1.0.0
+ # via python-keycloak
+sentry-sdk==2.48.0
+ # via -r requirements.in
+six==1.17.0
+ # via pyjwkest
+soupsieve==2.8.1
+ # via beautifulsoup4
+sqlalchemy==2.0.45
+ # via
+ # alembic
+ # flask-sqlalchemy
+srvlookup==3.0.0
+ # via
+ # -r requirements.in
+ # csh-ldap
+twilio==9.9.0
+ # via -r requirements.in
+typing-extensions==4.15.0
+ # via
+ # aiosignal
+ # alembic
+ # anyio
+ # beautifulsoup4
+ # flask-limiter
+ # jwcrypto
+ # limits
+ # pydantic
+ # pydantic-core
+ # sqlalchemy
+ # typing-inspection
+typing-inspection==0.4.2
+ # via
+ # pydantic
+ # pydantic-settings
+urllib3==2.6.2
+ # via
+ # requests
+ # sentry-sdk
+werkzeug==3.1.4
+ # via flask
+wrapt==2.0.1
+ # via deprecated
+xkcdpass==1.20.0
+ # via -r requirements.in
+yarl==1.22.0
+ # via aiohttp
diff --git a/selfservice/__init__.py b/selfservice/__init__.py
index f8e2889..ee47b63 100644
--- a/selfservice/__init__.py
+++ b/selfservice/__init__.py
@@ -13,7 +13,7 @@
from flask import Flask, render_template, request, redirect, url_for, flash
from flask_pyoidc.flask_pyoidc import OIDCAuthentication
from flask_pyoidc.provider_configuration import ProviderConfiguration, ClientMetadata
-from flask_recaptcha import ReCaptcha
+from flask_xcaptcha import XCaptcha
from flask_sqlalchemy import SQLAlchemy
from python_freeipa import Client
from flask_limiter import Limiter
@@ -52,9 +52,9 @@
migrate = Migrate(app, db)
-# Create recaptcha object
-recaptcha = ReCaptcha()
-recaptcha.init_app(app)
+# Create xcaptcha object
+if app.config["XCAPTCHA_ENABLED"]:
+ xcaptcha = XCaptcha(app=app)
# OIDC Initialization
OIDC_PROVIDER = "csh"
@@ -77,7 +77,7 @@
# Configure rate limiting
if not app.config["DEBUG"]:
limiter = Limiter(
- app, key_func=get_remote_address, default_limits=["50 per day", "10 per hour"]
+ get_remote_address, app=app, default_limits=["50 per day", "10 per hour"]
)
# Initialize QR Code Generator
diff --git a/selfservice/blueprints/recovery.py b/selfservice/blueprints/recovery.py
index 1efbb1b..addc6ba 100644
--- a/selfservice/blueprints/recovery.py
+++ b/selfservice/blueprints/recovery.py
@@ -18,7 +18,7 @@
from selfservice.utilities.ldap import verif_methods, get_members
from selfservice.models import RecoverySession, PhoneVerification, ResetToken
-from selfservice import db, auth, recaptcha, ldap, version, OIDC_PROVIDER
+from selfservice import db, auth, xcaptcha, ldap, version, OIDC_PROVIDER
LOG = logging.getLogger(__name__)
@@ -35,7 +35,7 @@ def create_session():
if request.method == "GET":
return render_template("recovery.html", version=version)
- if recaptcha.verify():
+ if xcaptcha.verify():
# If we can't find an account, flash error.
try:
diff --git a/selfservice/models.py b/selfservice/models.py
index 7e73bc5..e7f9916 100644
--- a/selfservice/models.py
+++ b/selfservice/models.py
@@ -9,7 +9,7 @@
ForeignKey,
DateTime,
Boolean,
- Binary,
+ LargeBinary,
func,
)
from selfservice import db
@@ -62,8 +62,8 @@ class OTPSession(db.Model):
__tablename__ = "otp_session"
secret = Column(String(100), primary_key=True)
- form = Column(Binary)
- session = Column(Binary)
+ form = Column(LargeBinary)
+ session = Column(LargeBinary)
class AppSpecificPassword(db.Model):
diff --git a/selfservice/templates/recovery.html b/selfservice/templates/recovery.html
index 6d186b1..743eb2f 100644
--- a/selfservice/templates/recovery.html
+++ b/selfservice/templates/recovery.html
@@ -17,7 +17,7 @@ Account Recovery