regstack 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {regstack-0.3.0 → regstack-0.4.0}/CLAUDE.md +17 -2
- {regstack-0.3.0 → regstack-0.4.0}/PKG-INFO +12 -5
- {regstack-0.3.0 → regstack-0.4.0}/README.md +7 -4
- {regstack-0.3.0 → regstack-0.4.0}/docs/api.md +73 -0
- {regstack-0.3.0 → regstack-0.4.0}/docs/architecture.md +48 -2
- {regstack-0.3.0 → regstack-0.4.0}/docs/changelog.md +18 -0
- {regstack-0.3.0 → regstack-0.4.0}/docs/conf.py +8 -1
- {regstack-0.3.0 → regstack-0.4.0}/docs/configuration.md +34 -1
- {regstack-0.3.0 → regstack-0.4.0}/docs/embedding.md +58 -3
- {regstack-0.3.0 → regstack-0.4.0}/docs/index.md +5 -1
- {regstack-0.3.0 → regstack-0.4.0}/docs/oauth.md +19 -0
- regstack-0.4.0/docs/security-reports/README.md +15 -0
- {regstack-0.3.0 → regstack-0.4.0}/docs/security.md +71 -0
- {regstack-0.3.0 → regstack-0.4.0}/pyproject.toml +15 -1
- regstack-0.4.0/scripts/security-review-prompt.md +583 -0
- regstack-0.4.0/src/regstack/cli/__main__.py +49 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/oauth/base.py +25 -27
- regstack-0.4.0/src/regstack/version.py +1 -0
- regstack-0.4.0/src/regstack/wizard/__init__.py +5 -0
- regstack-0.4.0/src/regstack/wizard/oauth_google/__init__.py +6 -0
- regstack-0.4.0/src/regstack/wizard/oauth_google/cli.py +224 -0
- regstack-0.4.0/src/regstack/wizard/oauth_google/routes.py +269 -0
- regstack-0.4.0/src/regstack/wizard/oauth_google/server.py +121 -0
- regstack-0.4.0/src/regstack/wizard/oauth_google/static/wizard.css +316 -0
- regstack-0.4.0/src/regstack/wizard/oauth_google/static/wizard.js +353 -0
- regstack-0.4.0/src/regstack/wizard/oauth_google/templates/wizard.html +260 -0
- regstack-0.4.0/src/regstack/wizard/oauth_google/validators.py +259 -0
- regstack-0.4.0/src/regstack/wizard/oauth_google/window.py +72 -0
- regstack-0.4.0/src/regstack/wizard/oauth_google/writer.py +248 -0
- {regstack-0.3.0 → regstack-0.4.0}/tasks.py +12 -1
- regstack-0.4.0/tests/e2e/conftest.py +82 -0
- regstack-0.4.0/tests/e2e/test_wizard_oauth_flow.py +144 -0
- regstack-0.4.0/tests/unit/__init__.py +0 -0
- regstack-0.4.0/tests/unit/test_wizard_oauth_cli.py +93 -0
- regstack-0.4.0/tests/unit/test_wizard_oauth_routes.py +232 -0
- regstack-0.4.0/tests/unit/test_wizard_oauth_validators.py +257 -0
- regstack-0.4.0/tests/unit/test_wizard_oauth_writer.py +293 -0
- {regstack-0.3.0 → regstack-0.4.0}/uv.lock +269 -1
- regstack-0.3.0/src/regstack/cli/__main__.py +0 -29
- regstack-0.3.0/src/regstack/version.py +0 -1
- {regstack-0.3.0 → regstack-0.4.0}/.github/workflows/publish.yml +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/.github/workflows/test.yml +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/.gitignore +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/.python-version +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/.readthedocs.yaml +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/CHANGELOG.md +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/LICENSE +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/NOTICE +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/SECURITY.md +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/docs/_static/.gitkeep +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/docs/_templates/.gitkeep +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/docs/cli.md +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/docs/quickstart.md +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/docs/theming.md +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/examples/_common/__init__.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/examples/_common/app.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/examples/mongo/README.md +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/examples/mongo/branding/theme.css +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/examples/mongo/main.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/examples/mongo/regstack.toml +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/examples/postgres/README.md +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/examples/postgres/main.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/examples/postgres/regstack.toml +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/examples/sqlite/README.md +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/examples/sqlite/main.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/examples/sqlite/regstack.toml +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/regstack.toml.example +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/__init__.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/app.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/auth/__init__.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/auth/clock.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/auth/dependencies.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/auth/jwt.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/auth/lockout.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/auth/mfa.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/auth/password.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/auth/tokens.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/__init__.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/base.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/factory.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/mongo/__init__.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/mongo/backend.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/mongo/client.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/mongo/indexes.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/__init__.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/blacklist_repo.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/login_attempt_repo.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/mfa_code_repo.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/oauth_identity_repo.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/oauth_state_repo.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/pending_repo.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/user_repo.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/protocols.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/__init__.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/backend.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/migrations/__init__.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/migrations/env.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/migrations/script.py.mako +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/migrations/versions/0001_initial_schema.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/migrations/versions/0002_oauth.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/repositories/__init__.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/repositories/blacklist_repo.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/repositories/login_attempt_repo.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/repositories/mfa_code_repo.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/repositories/oauth_identity_repo.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/repositories/oauth_state_repo.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/repositories/pending_repo.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/repositories/user_repo.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/schema.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/types.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/cli/__init__.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/cli/_runtime.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/cli/admin.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/cli/doctor.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/cli/init.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/cli/migrate.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/config/__init__.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/config/loader.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/config/schema.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/config/secrets.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/__init__.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/base.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/composer.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/console.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/factory.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/ses.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/smtp.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/templates/email_change.html +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/templates/email_change.subject.txt +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/templates/email_change.txt +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/templates/password_reset.html +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/templates/password_reset.subject.txt +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/templates/password_reset.txt +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/templates/sms_login_mfa.txt +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/templates/sms_phone_setup.txt +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/templates/verification.html +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/templates/verification.subject.txt +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/templates/verification.txt +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/hooks/__init__.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/hooks/events.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/models/__init__.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/models/_objectid.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/models/login_attempt.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/models/mfa_code.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/models/oauth_identity.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/models/oauth_state.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/models/pending_registration.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/models/user.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/oauth/__init__.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/oauth/errors.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/oauth/providers/__init__.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/oauth/providers/google.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/oauth/registry.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/routers/__init__.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/routers/_schemas.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/routers/account.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/routers/admin.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/routers/login.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/routers/logout.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/routers/oauth.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/routers/password.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/routers/phone.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/routers/register.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/routers/verify.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/sms/__init__.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/sms/base.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/sms/factory.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/sms/null.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/sms/sns.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/sms/twilio.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/__init__.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/pages.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/static/css/core.css +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/static/css/theme.css +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/static/js/regstack.js +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/templates/auth/email_change_confirm.html +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/templates/auth/forgot.html +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/templates/auth/login.html +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/templates/auth/me.html +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/templates/auth/mfa_confirm.html +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/templates/auth/oauth_complete.html +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/templates/auth/register.html +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/templates/auth/reset.html +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/templates/auth/verify.html +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/templates/base.html +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tasks/oauth-design.md +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/__init__.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/_fake_google/__init__.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/_fake_google/provider.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/conftest.py +0 -0
- {regstack-0.3.0/tests/integration → regstack-0.4.0/tests/e2e}/__init__.py +0 -0
- {regstack-0.3.0/tests/unit → regstack-0.4.0/tests/integration}/__init__.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/integration/test_account_management.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/integration/test_admin_router.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/integration/test_happy_path.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/integration/test_indexes.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/integration/test_login_lockout.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/integration/test_mfa.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/integration/test_oauth_google_router.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/integration/test_oauth_repos.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/integration/test_oauth_ui.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/integration/test_password_reset.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/integration/test_sql_migrations.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/integration/test_ui_router.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/integration/test_verification.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_base_install_imports.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_cli.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_cli_doctor.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_cli_init.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_config_loader.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_jwt.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_lockout.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_mail_composer.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_mfa_code_repo.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_oauth_google.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_password.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_ses_backend.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_sms.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_smtp_backend.py +0 -0
- {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_ui_env.py +0 -0
|
@@ -60,8 +60,23 @@ The full plan, including milestone scope and deferred items, lives at
|
|
|
60
60
|
Phone setup uses a separate signed `phone_setup` JWT carrying the
|
|
61
61
|
proposed phone as a custom claim — same per-purpose key derivation as
|
|
62
62
|
password-reset and email-change.
|
|
63
|
-
- **OAuth
|
|
64
|
-
|
|
63
|
+
- **OAuth — done (0.3.0).** `OAuthProvider` ABC + `OAuthRegistry`,
|
|
64
|
+
Google provider (Authorization Code with PKCE, ID-token verification
|
|
65
|
+
via `pyjwt[crypto]` + `PyJWKClient`), 5 JSON endpoints,
|
|
66
|
+
`/account/oauth-complete` SSR token-handoff page, "Sign in with
|
|
67
|
+
Google" button on `/account/login`, Connected-accounts panel on
|
|
68
|
+
`/account/me`. Optional extra: `oauth = ["pyjwt[crypto]>=2.8"]`.
|
|
69
|
+
- **OAuth setup wizard — done.** `regstack oauth setup` opens a native
|
|
70
|
+
pywebview window over a 127.0.0.1 FastAPI server (random port +
|
|
71
|
+
one-shot launch token). 12-step SPA, per-step server validation,
|
|
72
|
+
non-clobbering tomlkit merge into `regstack.toml` +
|
|
73
|
+
`regstack.secrets.env`. Lives in `src/regstack/wizard/oauth_google/`;
|
|
74
|
+
Click subcommand registered through a `_LazyOauthGroup` so
|
|
75
|
+
`regstack init` / `doctor` don't pay the pywebview/uvicorn import
|
|
76
|
+
cost. `--print-only` mode runs the same merge headlessly. Tested at
|
|
77
|
+
four layers: validators (unit), writer (golden-file), routes
|
|
78
|
+
(TestClient), full SPA flow (Playwright e2e). Run e2e with
|
|
79
|
+
`inv test-e2e`; `inv test-all` chains it after the backend matrix.
|
|
65
80
|
|
|
66
81
|
## Three kinds of single-use proof
|
|
67
82
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: regstack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Embeddable user registration, login, and account management for FastAPI apps. SQLite / Postgres / MongoDB.
|
|
5
5
|
Project-URL: Homepage, https://github.com/jdrumgoole/regstack
|
|
6
6
|
Project-URL: Repository, https://github.com/jdrumgoole/regstack
|
|
@@ -29,7 +29,10 @@ Requires-Dist: pydantic-settings>=2.2
|
|
|
29
29
|
Requires-Dist: pydantic>=2.6
|
|
30
30
|
Requires-Dist: pyjwt>=2.8
|
|
31
31
|
Requires-Dist: python-multipart>=0.0.9
|
|
32
|
+
Requires-Dist: pywebview>=5.0
|
|
32
33
|
Requires-Dist: sqlalchemy[asyncio]>=2.0
|
|
34
|
+
Requires-Dist: tomlkit>=0.13
|
|
35
|
+
Requires-Dist: uvicorn[standard]>=0.29
|
|
33
36
|
Provides-Extra: dev
|
|
34
37
|
Requires-Dist: anyio>=4.3; extra == 'dev'
|
|
35
38
|
Requires-Dist: asyncpg>=0.29; extra == 'dev'
|
|
@@ -40,6 +43,7 @@ Requires-Dist: pyjwt[crypto]>=2.8; extra == 'dev'
|
|
|
40
43
|
Requires-Dist: pymongo>=4.9; extra == 'dev'
|
|
41
44
|
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
42
45
|
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
46
|
+
Requires-Dist: pytest-playwright>=0.5; extra == 'dev'
|
|
43
47
|
Requires-Dist: pytest-xdist>=3.5; extra == 'dev'
|
|
44
48
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
45
49
|
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
@@ -78,9 +82,9 @@ auth bugs.**
|
|
|
78
82
|
|
|
79
83
|
`pip install regstack`, point it at SQLite (default), [PostgreSQL](https://www.postgresql.org/),
|
|
80
84
|
or [MongoDB](https://www.mongodb.com/), and you have register / login /
|
|
81
|
-
verify-email / reset-password / change-email / delete-account /
|
|
82
|
-
two-factor / admin endpoints / themable
|
|
83
|
-
API and one config file.
|
|
85
|
+
verify-email / reset-password / change-email / delete-account / Sign in
|
|
86
|
+
with Google / optional SMS two-factor / admin endpoints / themable
|
|
87
|
+
HTML pages — all behind a small Python API and one config file.
|
|
84
88
|
|
|
85
89
|
📚 **Docs:** <https://regstack.readthedocs.io>
|
|
86
90
|
·
|
|
@@ -132,6 +136,7 @@ result everywhere is what regstack is for.
|
|
|
132
136
|
✔ Forgot / reset password — anti-enumeration: identical responses
|
|
133
137
|
✔ Change password (revokes old tokens) / change email (re-verify)
|
|
134
138
|
✔ Delete account
|
|
139
|
+
✔ Sign in with Google (PKCE + ID-token verification, opt-in)
|
|
135
140
|
✔ Optional SMS two-factor (TOTP-style 6-digit codes over SMS)
|
|
136
141
|
✔ Server-side login lockout (HTTP 429 + Retry-After)
|
|
137
142
|
✔ Admin endpoints (list / disable / delete users, stats)
|
|
@@ -245,7 +250,9 @@ The same docs are also browsable as Markdown in [`docs/`](https://github.com/jdr
|
|
|
245
250
|
|
|
246
251
|
Alpha. Single-file SQLite is the default and runs with no infrastructure;
|
|
247
252
|
PostgreSQL and MongoDB backends pass the same parametrized integration
|
|
248
|
-
suite.
|
|
253
|
+
suite. OAuth (Google) shipped in `v0.3.0`; the `regstack oauth setup`
|
|
254
|
+
guided wizard shipped in `v0.4.0`. Latest tagged release: `v0.4.0`.
|
|
255
|
+
See the
|
|
249
256
|
[changelog](https://regstack.readthedocs.io/en/latest/changelog.html)
|
|
250
257
|
for the per-release breakdown.
|
|
251
258
|
|
|
@@ -11,9 +11,9 @@ auth bugs.**
|
|
|
11
11
|
|
|
12
12
|
`pip install regstack`, point it at SQLite (default), [PostgreSQL](https://www.postgresql.org/),
|
|
13
13
|
or [MongoDB](https://www.mongodb.com/), and you have register / login /
|
|
14
|
-
verify-email / reset-password / change-email / delete-account /
|
|
15
|
-
two-factor / admin endpoints / themable
|
|
16
|
-
API and one config file.
|
|
14
|
+
verify-email / reset-password / change-email / delete-account / Sign in
|
|
15
|
+
with Google / optional SMS two-factor / admin endpoints / themable
|
|
16
|
+
HTML pages — all behind a small Python API and one config file.
|
|
17
17
|
|
|
18
18
|
📚 **Docs:** <https://regstack.readthedocs.io>
|
|
19
19
|
·
|
|
@@ -65,6 +65,7 @@ result everywhere is what regstack is for.
|
|
|
65
65
|
✔ Forgot / reset password — anti-enumeration: identical responses
|
|
66
66
|
✔ Change password (revokes old tokens) / change email (re-verify)
|
|
67
67
|
✔ Delete account
|
|
68
|
+
✔ Sign in with Google (PKCE + ID-token verification, opt-in)
|
|
68
69
|
✔ Optional SMS two-factor (TOTP-style 6-digit codes over SMS)
|
|
69
70
|
✔ Server-side login lockout (HTTP 429 + Retry-After)
|
|
70
71
|
✔ Admin endpoints (list / disable / delete users, stats)
|
|
@@ -178,7 +179,9 @@ The same docs are also browsable as Markdown in [`docs/`](https://github.com/jdr
|
|
|
178
179
|
|
|
179
180
|
Alpha. Single-file SQLite is the default and runs with no infrastructure;
|
|
180
181
|
PostgreSQL and MongoDB backends pass the same parametrized integration
|
|
181
|
-
suite.
|
|
182
|
+
suite. OAuth (Google) shipped in `v0.3.0`; the `regstack oauth setup`
|
|
183
|
+
guided wizard shipped in `v0.4.0`. Latest tagged release: `v0.4.0`.
|
|
184
|
+
See the
|
|
182
185
|
[changelog](https://regstack.readthedocs.io/en/latest/changelog.html)
|
|
183
186
|
for the per-release breakdown.
|
|
184
187
|
|
|
@@ -18,6 +18,7 @@ The handful of things you import from `regstack` directly:
|
|
|
18
18
|
- [`RegStackConfig`](#regstack.config.schema.RegStackConfig) — top-level config.
|
|
19
19
|
- [`EmailConfig`](#regstack.config.schema.EmailConfig) — email-backend sub-config.
|
|
20
20
|
- [`SmsConfig`](#regstack.config.schema.SmsConfig) — SMS-backend sub-config.
|
|
21
|
+
- [`OAuthConfig`](#regstack.config.schema.OAuthConfig) — OAuth provider sub-config.
|
|
21
22
|
|
|
22
23
|
Most embeddings need only `RegStack` and `RegStackConfig`.
|
|
23
24
|
|
|
@@ -59,6 +60,11 @@ default.
|
|
|
59
60
|
:show-inheritance:
|
|
60
61
|
:exclude-members: model_config, model_fields, model_computed_fields
|
|
61
62
|
|
|
63
|
+
.. autoclass:: regstack.config.schema.OAuthConfig
|
|
64
|
+
:members:
|
|
65
|
+
:show-inheritance:
|
|
66
|
+
:exclude-members: model_config, model_fields, model_computed_fields
|
|
67
|
+
|
|
62
68
|
.. autofunction:: regstack.config.loader.load_config
|
|
63
69
|
```
|
|
64
70
|
|
|
@@ -247,6 +253,73 @@ MessageBird, …) and pass the instance to `regstack.set_email_backend`
|
|
|
247
253
|
.. autofunction:: regstack.sms.factory.build_sms_service
|
|
248
254
|
```
|
|
249
255
|
|
|
256
|
+
## OAuth
|
|
257
|
+
|
|
258
|
+
Opt-in subsystem behind `enable_oauth` and the `oauth` extra. v1
|
|
259
|
+
ships Google; the abstraction is shaped so adding GitHub /
|
|
260
|
+
Microsoft / Apple later is a new module under
|
|
261
|
+
`regstack.oauth.providers` plus a registry entry. The full host
|
|
262
|
+
guide is in [OAuth](oauth.md); the threat model is in
|
|
263
|
+
[Security model](security.md#oauth-sign-in-with-google).
|
|
264
|
+
|
|
265
|
+
### Provider abstraction
|
|
266
|
+
|
|
267
|
+
```{eval-rst}
|
|
268
|
+
.. autoclass:: regstack.oauth.base.OAuthProvider
|
|
269
|
+
:members:
|
|
270
|
+
|
|
271
|
+
.. autoclass:: regstack.oauth.base.OAuthTokens
|
|
272
|
+
:members:
|
|
273
|
+
|
|
274
|
+
.. autoclass:: regstack.oauth.base.OAuthUserInfo
|
|
275
|
+
:members:
|
|
276
|
+
|
|
277
|
+
.. autoclass:: regstack.oauth.registry.OAuthRegistry
|
|
278
|
+
:members:
|
|
279
|
+
|
|
280
|
+
.. autoexception:: regstack.oauth.errors.OAuthError
|
|
281
|
+
|
|
282
|
+
.. autoexception:: regstack.oauth.errors.OAuthConfigError
|
|
283
|
+
|
|
284
|
+
.. autoexception:: regstack.oauth.errors.OAuthTokenExchangeError
|
|
285
|
+
|
|
286
|
+
.. autoexception:: regstack.oauth.errors.OAuthIdTokenError
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Google provider
|
|
290
|
+
|
|
291
|
+
```{eval-rst}
|
|
292
|
+
.. autoclass:: regstack.oauth.providers.google.GoogleProvider
|
|
293
|
+
:members:
|
|
294
|
+
:show-inheritance:
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Identity + state storage
|
|
298
|
+
|
|
299
|
+
```{eval-rst}
|
|
300
|
+
.. autoclass:: regstack.models.oauth_identity.OAuthIdentity
|
|
301
|
+
:members:
|
|
302
|
+
:exclude-members: model_config, model_fields, model_computed_fields
|
|
303
|
+
|
|
304
|
+
.. autoclass:: regstack.models.oauth_state.OAuthState
|
|
305
|
+
:members:
|
|
306
|
+
:exclude-members: model_config, model_fields, model_computed_fields
|
|
307
|
+
|
|
308
|
+
.. autoclass:: regstack.backends.protocols.OAuthIdentityRepoProtocol
|
|
309
|
+
:members:
|
|
310
|
+
|
|
311
|
+
.. autoclass:: regstack.backends.protocols.OAuthStateRepoProtocol
|
|
312
|
+
:members:
|
|
313
|
+
|
|
314
|
+
.. autoexception:: regstack.backends.protocols.OAuthIdentityAlreadyLinkedError
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Router
|
|
318
|
+
|
|
319
|
+
```{eval-rst}
|
|
320
|
+
.. autofunction:: regstack.routers.oauth.build_oauth_router
|
|
321
|
+
```
|
|
322
|
+
|
|
250
323
|
## Hooks
|
|
251
324
|
|
|
252
325
|
The event bus regstack uses to fire side-effect notifications
|
|
@@ -139,8 +139,52 @@ The composite `router` conditionally includes:
|
|
|
139
139
|
- `password` (forgot/reset) — when `enable_password_reset`.
|
|
140
140
|
- `phone` and the `mfa-confirm` route — when `enable_sms_2fa`.
|
|
141
141
|
- `admin` — when `enable_admin_router`.
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
- `oauth` — when `enable_oauth` AND at least one provider is
|
|
143
|
+
registered on `rs.oauth`.
|
|
144
|
+
|
|
145
|
+
`ui_router` mounts the same conditional pages, plus
|
|
146
|
+
`/account/oauth-complete` when `enable_oauth` is on.
|
|
147
|
+
|
|
148
|
+
## OAuth subsystem
|
|
149
|
+
|
|
150
|
+
Opt-in. Lives in `regstack.oauth/`; hosts pull it in via the
|
|
151
|
+
`oauth` extra (`pyjwt[crypto]>=2.8`). Imports are lazy — the
|
|
152
|
+
package keeps importing on a base install with no `cryptography`
|
|
153
|
+
installed, and the OAuth-specific modules only get loaded when
|
|
154
|
+
`enable_oauth` is on.
|
|
155
|
+
|
|
156
|
+
The shape is:
|
|
157
|
+
|
|
158
|
+
- `OAuthProvider` ABC — three methods: `authorization_url`,
|
|
159
|
+
`exchange_code`, `verify_id_token`.
|
|
160
|
+
- `OAuthRegistry` — name-keyed map of providers, scoped to one
|
|
161
|
+
`RegStack` instance. The `RegStack` constructor reads
|
|
162
|
+
`config.oauth` and registers `GoogleProvider` automatically when
|
|
163
|
+
`enable_oauth` and the credentials are set; hosts can also
|
|
164
|
+
register custom providers post-construction.
|
|
165
|
+
- `GoogleProvider` — Authorization Code with PKCE, ID-token
|
|
166
|
+
verification via `pyjwt[crypto]` + `PyJWKClient` against Google's
|
|
167
|
+
JWKS. ~150 lines hand-rolled rather than pulling `authlib`.
|
|
168
|
+
- Two new repos via the protocol pattern:
|
|
169
|
+
`OAuthIdentityRepoProtocol` (links between regstack users and
|
|
170
|
+
external accounts; double-unique on `(provider, subject_id)` and
|
|
171
|
+
`(user_id, provider)`) and `OAuthStateRepoProtocol` (in-flight
|
|
172
|
+
state rows carrying the PKCE `code_verifier`, redirect target,
|
|
173
|
+
mode, and the `result_token` slot the SPA exchanges).
|
|
174
|
+
- `build_oauth_router(rs)` — the router with the five endpoints
|
|
175
|
+
(`/start`, `/callback`, `/exchange`, `/link/start`, `/link`) plus
|
|
176
|
+
`/oauth/providers` for the SSR connected-accounts panel.
|
|
177
|
+
|
|
178
|
+
The token-handoff round-trip avoids putting access tokens in URLs:
|
|
179
|
+
the callback stashes the freshly-minted session JWT on the state
|
|
180
|
+
row's `result_token`, redirects to `/account/oauth-complete?id=…`,
|
|
181
|
+
and the SPA POSTs that id back to `/oauth/exchange` to retrieve the
|
|
182
|
+
token. The exchange consumes the row atomically — the same id can't
|
|
183
|
+
be exchanged twice.
|
|
184
|
+
|
|
185
|
+
The full design (including the four-milestone build sequence and
|
|
186
|
+
the threat model) is in
|
|
187
|
+
[`tasks/oauth-design.md`](https://github.com/jdrumgoole/regstack/blob/main/tasks/oauth-design.md).
|
|
144
188
|
|
|
145
189
|
## Hooks
|
|
146
190
|
|
|
@@ -157,6 +201,8 @@ primary auth flow. Known events:
|
|
|
157
201
|
- `phone_setup_started` / `mfa_login_started`
|
|
158
202
|
- `mfa_enabled` / `mfa_disabled`
|
|
159
203
|
- `user_deleted`
|
|
204
|
+
- `oauth_signin_started` / `oauth_signin_completed`
|
|
205
|
+
- `oauth_account_linked` / `oauth_account_unlinked`
|
|
160
206
|
|
|
161
207
|
Hosts are free to subscribe to custom event names too — the registry
|
|
162
208
|
is just a `defaultdict(list)`. Use this surface to push events into
|
|
@@ -3,6 +3,24 @@
|
|
|
3
3
|
All notable changes to this project are documented here. Versions follow
|
|
4
4
|
[Semantic Versioning](https://semver.org/) once `1.0.0` ships.
|
|
5
5
|
|
|
6
|
+
## 0.4.0 — 2026-05-02
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **OAuth setup wizard.** `regstack oauth setup` opens a native
|
|
11
|
+
webview window that walks an operator through registering a Google
|
|
12
|
+
OAuth 2.0 client and merges the credentials into `regstack.toml` +
|
|
13
|
+
`regstack.secrets.env` non-destructively (preserves comments, other
|
|
14
|
+
tables, unrelated keys). 12-step SPA inside a local-only
|
|
15
|
+
127.0.0.1 FastAPI server, gated by a per-launch random token. Each
|
|
16
|
+
Next click hits a server-side validator so the Write step can never
|
|
17
|
+
be reached with bad data. `--print-only` mode skips the GUI for
|
|
18
|
+
headless / CI use.
|
|
19
|
+
- Three new base dependencies: `pywebview>=5.0`, `tomlkit>=0.13`,
|
|
20
|
+
`uvicorn[standard]>=0.29` (the wizard's local server).
|
|
21
|
+
- `pytest-playwright` added to the `dev` extra; new `inv test-e2e`
|
|
22
|
+
task chained into `inv test-all`.
|
|
23
|
+
|
|
6
24
|
## 0.3.0 — 2026-04-30
|
|
7
25
|
|
|
8
26
|
**OAuth — Sign in with Google.** Built across four PRs (M1–M4 of
|
|
@@ -38,7 +38,14 @@ extensions = [
|
|
|
38
38
|
source_suffix = {".md": "markdown", ".rst": "restructuredtext"}
|
|
39
39
|
master_doc = "index"
|
|
40
40
|
templates_path = ["_templates"]
|
|
41
|
-
exclude_patterns = [
|
|
41
|
+
exclude_patterns = [
|
|
42
|
+
"_build",
|
|
43
|
+
"Thumbs.db",
|
|
44
|
+
".DS_Store",
|
|
45
|
+
# Daily security review reports — these are GitHub-rendered
|
|
46
|
+
# markdown, not part of the published Sphinx site.
|
|
47
|
+
"security-reports/**",
|
|
48
|
+
]
|
|
42
49
|
|
|
43
50
|
# Suppress noisy autodoc warnings on dynamically-typed pydantic helpers.
|
|
44
51
|
suppress_warnings = ["autodoc.import_object"]
|
|
@@ -63,6 +63,12 @@ addressed in env using a `__` separator: `REGSTACK_EMAIL__FROM_ADDRESS`.
|
|
|
63
63
|
* - `mfa_code_collection`
|
|
64
64
|
- `"mfa_codes"`
|
|
65
65
|
-
|
|
66
|
+
* - `oauth_identity_collection`
|
|
67
|
+
- `"oauth_identities"`
|
|
68
|
+
-
|
|
69
|
+
* - `oauth_state_collection`
|
|
70
|
+
- `"oauth_states"`
|
|
71
|
+
-
|
|
66
72
|
```
|
|
67
73
|
|
|
68
74
|
## Backends
|
|
@@ -165,7 +171,9 @@ The active backend exposes the same five repository protocols on
|
|
|
165
171
|
- Mounts `/phone/*` routes and gates the MFA second step in `/login`.
|
|
166
172
|
* - `enable_oauth`
|
|
167
173
|
- `false`
|
|
168
|
-
-
|
|
174
|
+
- Mounts `/oauth/*` routes when at least one provider is registered
|
|
175
|
+
(currently Google). Requires the ``oauth`` extra
|
|
176
|
+
(``pip install 'regstack[oauth]'``).
|
|
169
177
|
```
|
|
170
178
|
|
|
171
179
|
## Lockout (login)
|
|
@@ -256,6 +264,31 @@ twilio_account_sid = "AC…"
|
|
|
256
264
|
# twilio_auth_token via REGSTACK_SMS__TWILIO_AUTH_TOKEN
|
|
257
265
|
```
|
|
258
266
|
|
|
267
|
+
`[oauth]` (`OAuthConfig`):
|
|
268
|
+
|
|
269
|
+
```toml
|
|
270
|
+
[oauth]
|
|
271
|
+
google_client_id = "12345.apps.googleusercontent.com"
|
|
272
|
+
# google_client_secret via REGSTACK_OAUTH__GOOGLE_CLIENT_SECRET
|
|
273
|
+
# google_redirect_uri = "https://your.app/api/auth/oauth/google/callback"
|
|
274
|
+
# (default: f"{base_url}{api_prefix}/oauth/google/callback")
|
|
275
|
+
|
|
276
|
+
# Account-linking policy. Off by default — see docs/oauth.md and
|
|
277
|
+
# docs/security.md for the threat model. On = a Google sign-in for an
|
|
278
|
+
# existing email-registered user is auto-linked when Google's
|
|
279
|
+
# email_verified=true. Hosts choosing on are accepting the email-
|
|
280
|
+
# recycling-at-the-provider risk in exchange for less friction.
|
|
281
|
+
auto_link_verified_emails = false
|
|
282
|
+
|
|
283
|
+
# When true, an OAuth sign-in for a user with SMS MFA enabled still
|
|
284
|
+
# goes through the second-factor step. Off by default — the OAuth
|
|
285
|
+
# provider already authenticated the human.
|
|
286
|
+
enforce_mfa_on_oauth_signin = false
|
|
287
|
+
|
|
288
|
+
state_ttl_seconds = 300 # in-flight state row lifetime
|
|
289
|
+
completion_ttl_seconds = 30 # /oauth/exchange window after callback
|
|
290
|
+
```
|
|
291
|
+
|
|
259
292
|
## SSR / theming
|
|
260
293
|
|
|
261
294
|
```{list-table}
|
|
@@ -54,11 +54,22 @@ async def _send_to_crm(user) -> None:
|
|
|
54
54
|
@regstack.on("user_deleted")
|
|
55
55
|
async def _purge_host_data(user) -> None:
|
|
56
56
|
await my_app.delete_all_data_for(user.id)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@regstack.on("oauth_signin_completed")
|
|
60
|
+
async def _track_signin(*, user, provider, mode, was_new) -> None:
|
|
61
|
+
if was_new:
|
|
62
|
+
await analytics.track("signup", {"user": user.id, "provider": provider})
|
|
63
|
+
else:
|
|
64
|
+
await analytics.track("login", {"user": user.id, "provider": provider})
|
|
57
65
|
```
|
|
58
66
|
|
|
59
67
|
Handlers can be sync or async. Exceptions are logged but never break
|
|
60
68
|
the primary auth flow — see [`HookRegistry`](architecture.md#hooks).
|
|
61
|
-
The full event list
|
|
69
|
+
The full event list (including the four OAuth events:
|
|
70
|
+
``oauth_signin_started``, ``oauth_signin_completed``,
|
|
71
|
+
``oauth_account_linked``, ``oauth_account_unlinked``) is in the
|
|
72
|
+
architecture guide.
|
|
62
73
|
|
|
63
74
|
## Custom email or SMS backends
|
|
64
75
|
|
|
@@ -101,6 +112,48 @@ regstack.set_email_backend(PostmarkEmailService(server_token=os.environ["POSTMAR
|
|
|
101
112
|
The same pattern applies for SMS via `SmsService` and
|
|
102
113
|
`set_sms_backend(...)`.
|
|
103
114
|
|
|
115
|
+
## Enabling OAuth
|
|
116
|
+
|
|
117
|
+
Install the extra and configure a provider:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
uv add 'regstack[oauth]'
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```toml
|
|
124
|
+
# regstack.toml
|
|
125
|
+
enable_oauth = true
|
|
126
|
+
|
|
127
|
+
[oauth]
|
|
128
|
+
google_client_id = "12345.apps.googleusercontent.com"
|
|
129
|
+
# google_client_secret in regstack.secrets.env
|
|
130
|
+
auto_link_verified_emails = false # security default — see oauth.md
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The router mounts five JSON endpoints under `/oauth/` (start /
|
|
134
|
+
callback / exchange / link-start / unlink) plus a `/oauth/providers`
|
|
135
|
+
list. The bundled SSR pages pick up the rest automatically: a
|
|
136
|
+
"Sign in with Google" button on `/account/login` and a Connected-
|
|
137
|
+
accounts panel on `/account/me`.
|
|
138
|
+
|
|
139
|
+
Hosts that need a custom provider (Apple, Microsoft, an internal
|
|
140
|
+
OIDC) can register one programmatically on the registry:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
rs.oauth.register(MyCustomProvider(...))
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Anything implementing :class:`~regstack.oauth.base.OAuthProvider`
|
|
147
|
+
works — three abstract methods (`authorization_url`,
|
|
148
|
+
`exchange_code`, `verify_id_token`). The router parametrizes its
|
|
149
|
+
URL paths on the provider name, so a registered provider named
|
|
150
|
+
``"github"`` is reachable at `/oauth/github/start` without any
|
|
151
|
+
router changes.
|
|
152
|
+
|
|
153
|
+
The full host guide — Google client setup, the linking-policy
|
|
154
|
+
decision, OAuth-only-user knock-on effects — is in
|
|
155
|
+
[OAuth](oauth.md).
|
|
156
|
+
|
|
104
157
|
## Overriding email and HTML templates
|
|
105
158
|
|
|
106
159
|
Both surfaces share `RegStack.add_template_dir(path)`:
|
|
@@ -193,5 +246,7 @@ for production probes that need more than a TCP hit.
|
|
|
193
246
|
- It does not enforce HTTPS. Run behind a TLS terminator.
|
|
194
247
|
- It does not provision SES identities, Route 53 records, IAM users,
|
|
195
248
|
or anything else outside the database.
|
|
196
|
-
- It does not ship OAuth providers
|
|
197
|
-
abstraction
|
|
249
|
+
- It does not ship OAuth providers other than Google. The
|
|
250
|
+
abstraction is shaped to take GitHub / Microsoft / Apple later;
|
|
251
|
+
hosts that want a different provider today can implement
|
|
252
|
+
:class:`~regstack.oauth.base.OAuthProvider` and register it.
|
|
@@ -78,6 +78,10 @@ each time".
|
|
|
78
78
|
Full template overrides are still possible per host.
|
|
79
79
|
- **CLIs.** `regstack init` (interactive setup wizard),
|
|
80
80
|
`regstack create-admin`, `regstack doctor`.
|
|
81
|
+
- **OAuth — Sign in with Google** (opt-in, since 0.3.0). Authorization
|
|
82
|
+
Code with PKCE, ID-token verification, identity-linking with a
|
|
83
|
+
default-refuse policy hosts can opt out of. Connected-accounts
|
|
84
|
+
panel on the SSR `/account/me` page. See [the OAuth guide](oauth.md).
|
|
81
85
|
- **Pluggable email and SMS.** Email backends: `console` (dev), SMTP,
|
|
82
86
|
[Amazon SES](https://aws.amazon.com/ses/). SMS backends:
|
|
83
87
|
[Amazon SNS](https://aws.amazon.com/sns/),
|
|
@@ -135,7 +139,7 @@ changelog
|
|
|
135
139
|
|
|
136
140
|
## Project status
|
|
137
141
|
|
|
138
|
-
Alpha. Latest tagged release: `v0.
|
|
142
|
+
Alpha. Latest tagged release: `v0.4.0`. SQLite is the default and
|
|
139
143
|
runs with no infrastructure; PostgreSQL and MongoDB pass the same
|
|
140
144
|
parametrized integration suite. The full backend matrix runs in
|
|
141
145
|
parallel against every test, so a green CI on `main` is a strong
|
|
@@ -53,6 +53,25 @@ In the [Google Cloud Console](https://console.cloud.google.com/apis/credentials)
|
|
|
53
53
|
|
|
54
54
|
## Configure regstack
|
|
55
55
|
|
|
56
|
+
The fastest path is the **OAuth setup wizard**, which opens a native
|
|
57
|
+
window and walks you through every step (registering the GCP client,
|
|
58
|
+
pasting the redirect URI, picking your linking policy) and finally
|
|
59
|
+
merges the credentials into your existing `regstack.toml` and
|
|
60
|
+
`regstack.secrets.env` without disturbing other settings:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
uv run regstack oauth setup
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The wizard is **non-clobbering** — it preserves comments, unrelated
|
|
67
|
+
top-level keys, and unrelated tables (`[email]`, `[sms]`, etc.). Re-run
|
|
68
|
+
it any time you need to rotate credentials or change the linking
|
|
69
|
+
policy. On a headless host (CI, server) use
|
|
70
|
+
`regstack oauth setup --print-only --client-id=… --client-secret=…`
|
|
71
|
+
to get the same merge with no GUI.
|
|
72
|
+
|
|
73
|
+
If you'd rather edit by hand, the resulting files look like:
|
|
74
|
+
|
|
56
75
|
```toml
|
|
57
76
|
# regstack.toml
|
|
58
77
|
enable_oauth = true
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Security reports
|
|
2
|
+
|
|
3
|
+
Daily output from the scheduled security-review agent (see
|
|
4
|
+
[`scripts/security-review-prompt.md`](../../scripts/security-review-prompt.md)).
|
|
5
|
+
|
|
6
|
+
Each file is named `YYYY-MM-DD.md` and follows the structure declared
|
|
7
|
+
in the prompt: 🔴 CRITICAL / 🟠 WARNING / 🟡 INFO / 🟢 CLEAN findings,
|
|
8
|
+
plus a summary block.
|
|
9
|
+
|
|
10
|
+
The agent files a PR per report so each review is reviewable as a
|
|
11
|
+
diff against `main`. PR title prefix:
|
|
12
|
+
|
|
13
|
+
- `[security-critical]` — at least one CRITICAL finding.
|
|
14
|
+
- `[security-warning]` — at least one WARNING finding (no CRITICALs).
|
|
15
|
+
- `[security-clean]` — clean bill of health.
|
|
@@ -161,6 +161,77 @@ visible to logged-out users only.
|
|
|
161
161
|
instead of a session token, sends an SMS, and requires
|
|
162
162
|
`POST /login/mfa-confirm` to complete.
|
|
163
163
|
|
|
164
|
+
## OAuth (Sign in with Google)
|
|
165
|
+
|
|
166
|
+
Opt-in subsystem behind `enable_oauth` and the `oauth` extra. Five
|
|
167
|
+
JSON endpoints plus an SSR token-handoff page. The full host-facing
|
|
168
|
+
guide is in [OAuth](oauth.md); this section is the threat model.
|
|
169
|
+
|
|
170
|
+
- **Server-side PKCE.** The `code_verifier` is generated server-side
|
|
171
|
+
and persisted on a `oauth_states` row; only its SHA-256
|
|
172
|
+
`code_challenge` ever travels through the browser. The token
|
|
173
|
+
exchange POSTs the verifier directly from the regstack server to
|
|
174
|
+
Google's token endpoint, so a leaked browser-side state value
|
|
175
|
+
alone can't drive a token exchange.
|
|
176
|
+
- **State row is the OAuth `state` parameter.** Random 32-byte
|
|
177
|
+
url-safe id; carries `code_verifier`, `nonce`, `redirect_to`,
|
|
178
|
+
`mode` (`signin` or `link`), optional `linking_user_id`. The
|
|
179
|
+
callback looks the row up by id, rejects missing / expired rows
|
|
180
|
+
with `?error=bad_state` or `?error=state_expired`. Mongo gets free
|
|
181
|
+
TTL via `expireAfterSeconds`; SQL backends rely on read-side
|
|
182
|
+
`expires_at > now()` plus `purge_expired()`.
|
|
183
|
+
- **ID token verification.** Signature against Google's JWKS
|
|
184
|
+
(`PyJWKClient` cached), `iss` matches Google, `aud` matches the
|
|
185
|
+
configured `client_id`, `exp > now`, `nonce` matches the value
|
|
186
|
+
stashed on the state row. Any failure raises
|
|
187
|
+
`OAuthIdTokenError` and the callback redirects to the login page
|
|
188
|
+
with `?error=id_token_failed` — the specific check that failed is
|
|
189
|
+
logged but not echoed.
|
|
190
|
+
- **Account-linking policy.** Defaults to **refuse**. If a Google
|
|
191
|
+
sign-in carries an email already owned by a regstack user, the
|
|
192
|
+
callback returns `?error=email_in_use` and the user has to sign
|
|
193
|
+
in with their existing password before linking from
|
|
194
|
+
`/account/me`. Auto-linking is available behind
|
|
195
|
+
`oauth.auto_link_verified_emails = true`; even then, regstack
|
|
196
|
+
requires `email_verified=true` on the ID token. The threat
|
|
197
|
+
auto-link accepts is *email recycling at the provider* — if
|
|
198
|
+
someone later acquires the original Gmail address, they could
|
|
199
|
+
sign in as the original regstack user. Hosts choosing auto-link
|
|
200
|
+
do so eyes-open. Full writeup in
|
|
201
|
+
`tasks/oauth-design.md` § 1.
|
|
202
|
+
- **One-time token-handoff.** After a successful callback, the
|
|
203
|
+
fresh session JWT is stashed on the `oauth_states.result_token`
|
|
204
|
+
field and the SPA exchanges its state-id for the token via
|
|
205
|
+
`POST /oauth/exchange`. The exchange consumes the row atomically
|
|
206
|
+
(read + delete in one transaction); a second exchange call with
|
|
207
|
+
the same id returns 404. Tokens never appear in URLs longer than
|
|
208
|
+
the callback redirect, no cookies are set.
|
|
209
|
+
- **OAuth-issued sessions are normal session JWTs** signed with the
|
|
210
|
+
same `session`-purpose key. The `tokens_invalidated_after` bulk-
|
|
211
|
+
revoke applies — a password change or admin-disable kills any
|
|
212
|
+
OAuth-issued session too.
|
|
213
|
+
- **Open-redirect protection.** `redirect_to` on `/start` is
|
|
214
|
+
validated same-origin against `config.base_url`; a request with
|
|
215
|
+
an off-site target returns 400.
|
|
216
|
+
- **Identity-row uniqueness.** `(provider, subject_id)` is unique
|
|
217
|
+
so two regstack users can't share one external account; a
|
|
218
|
+
second-user link attempt returns `?error=identity_in_use`.
|
|
219
|
+
`(user_id, provider)` is also unique so re-linking the same
|
|
220
|
+
provider to the same user returns `?error=already_linked`
|
|
221
|
+
rather than silently succeeding.
|
|
222
|
+
- **OAuth-only users.** A Google sign-up creates a user with
|
|
223
|
+
`hashed_password=None`. Login with a password against such an
|
|
224
|
+
account returns the same generic 401 a wrong-password attempt
|
|
225
|
+
gets — never reveal that an account exists but has no password
|
|
226
|
+
set, so an attacker can't enumerate which accounts to phish via
|
|
227
|
+
OAuth. `change-password` / `change-email` / `delete-account`
|
|
228
|
+
return 400 with a pointer at the password-reset flow, which
|
|
229
|
+
doubles as a "set initial password" path.
|
|
230
|
+
- **Refuse to unlink the only auth method.**
|
|
231
|
+
`DELETE /oauth/{provider}/link` returns 400 if the user has no
|
|
232
|
+
password and only the one identity. Forces them to either set a
|
|
233
|
+
password (via reset) or link another provider first.
|
|
234
|
+
|
|
164
235
|
## CSP and the SSR layer
|
|
165
236
|
|
|
166
237
|
Content Security Policy (CSP) is a browser feature that restricts
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "regstack"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "Embeddable user registration, login, and account management for FastAPI apps. SQLite / Postgres / MongoDB."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -32,6 +32,11 @@ dependencies = [
|
|
|
32
32
|
"sqlalchemy[asyncio]>=2.0",
|
|
33
33
|
"aiosqlite>=0.20",
|
|
34
34
|
"alembic>=1.13",
|
|
35
|
+
# OAuth setup wizard (regstack oauth setup) — needs a webview shell,
|
|
36
|
+
# a small in-process FastAPI server, and round-trip-safe TOML editing.
|
|
37
|
+
"pywebview>=5.0",
|
|
38
|
+
"tomlkit>=0.13",
|
|
39
|
+
"uvicorn[standard]>=0.29",
|
|
35
40
|
]
|
|
36
41
|
|
|
37
42
|
[project.optional-dependencies]
|
|
@@ -65,6 +70,8 @@ dev = [
|
|
|
65
70
|
"asyncpg>=0.29",
|
|
66
71
|
# OAuth provider tests need the crypto bits to verify ID tokens.
|
|
67
72
|
"pyjwt[crypto]>=2.8",
|
|
73
|
+
# E2E tests for the OAuth setup wizard (drives the SPA in headless Chromium).
|
|
74
|
+
"pytest-playwright>=0.5",
|
|
68
75
|
]
|
|
69
76
|
|
|
70
77
|
[project.scripts]
|
|
@@ -98,11 +105,18 @@ markers = [
|
|
|
98
105
|
filterwarnings = [
|
|
99
106
|
"error",
|
|
100
107
|
"ignore::DeprecationWarning:passlib.*",
|
|
108
|
+
# uvicorn[standard] still imports websockets.legacy on shutdown,
|
|
109
|
+
# which fires a DeprecationWarning. The wizard's e2e fixture runs
|
|
110
|
+
# uvicorn in a thread and pytest's threadexception plugin promotes
|
|
111
|
+
# thread warnings to errors — silence at the source.
|
|
112
|
+
"ignore::DeprecationWarning:websockets.*",
|
|
113
|
+
"ignore::DeprecationWarning:uvicorn.*",
|
|
101
114
|
# CI surfaces PytestUnraisableExceptionWarning when async fixtures
|
|
102
115
|
# from a previous test leak sockets/event loops past their cleanup
|
|
103
116
|
# window — the sync UI tests shouldn't fail just because asyncpg or
|
|
104
117
|
# aiosqlite teardown was sloppy in a sibling test.
|
|
105
118
|
"ignore::pytest.PytestUnraisableExceptionWarning",
|
|
119
|
+
"ignore::pytest.PytestUnhandledThreadExceptionWarning",
|
|
106
120
|
]
|
|
107
121
|
|
|
108
122
|
[tool.ruff]
|