regstack 0.2.6__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.2.6 → regstack-0.4.0}/CHANGELOG.md +34 -0
- {regstack-0.2.6 → regstack-0.4.0}/CLAUDE.md +17 -2
- {regstack-0.2.6 → regstack-0.4.0}/PKG-INFO +15 -5
- {regstack-0.2.6 → regstack-0.4.0}/README.md +7 -4
- {regstack-0.2.6 → regstack-0.4.0}/docs/api.md +73 -0
- {regstack-0.2.6 → regstack-0.4.0}/docs/architecture.md +48 -2
- {regstack-0.2.6 → regstack-0.4.0}/docs/changelog.md +101 -0
- {regstack-0.2.6 → regstack-0.4.0}/docs/conf.py +8 -1
- {regstack-0.2.6 → regstack-0.4.0}/docs/configuration.md +34 -1
- {regstack-0.2.6 → regstack-0.4.0}/docs/embedding.md +58 -3
- {regstack-0.2.6 → regstack-0.4.0}/docs/index.md +6 -1
- regstack-0.4.0/docs/oauth.md +154 -0
- regstack-0.4.0/docs/security-reports/README.md +15 -0
- {regstack-0.2.6 → regstack-0.4.0}/docs/security.md +71 -0
- {regstack-0.2.6 → regstack-0.4.0}/pyproject.toml +18 -1
- regstack-0.4.0/scripts/security-review-prompt.md +583 -0
- regstack-0.4.0/src/regstack/__init__.py +12 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/app.py +29 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/base.py +4 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/mongo/backend.py +6 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/mongo/indexes.py +27 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/__init__.py +6 -0
- regstack-0.4.0/src/regstack/backends/mongo/repositories/oauth_identity_repo.py +63 -0
- regstack-0.4.0/src/regstack/backends/mongo/repositories/oauth_state_repo.py +45 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/protocols.py +89 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/backend.py +6 -0
- regstack-0.4.0/src/regstack/backends/sql/migrations/versions/0002_oauth.py +91 -0
- regstack-0.4.0/src/regstack/backends/sql/repositories/oauth_identity_repo.py +87 -0
- regstack-0.4.0/src/regstack/backends/sql/repositories/oauth_state_repo.py +66 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/schema.py +47 -1
- regstack-0.4.0/src/regstack/cli/__main__.py +49 -0
- regstack-0.4.0/src/regstack/config/__init__.py +4 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/config/schema.py +37 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/hooks/events.py +4 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/models/__init__.py +5 -0
- regstack-0.4.0/src/regstack/models/oauth_identity.py +55 -0
- regstack-0.4.0/src/regstack/models/oauth_state.py +82 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/models/user.py +5 -1
- regstack-0.4.0/src/regstack/oauth/__init__.py +42 -0
- regstack-0.4.0/src/regstack/oauth/base.py +186 -0
- regstack-0.4.0/src/regstack/oauth/errors.py +43 -0
- regstack-0.4.0/src/regstack/oauth/providers/__init__.py +6 -0
- regstack-0.4.0/src/regstack/oauth/providers/google.py +200 -0
- regstack-0.4.0/src/regstack/oauth/registry.py +74 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/routers/__init__.py +7 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/routers/account.py +28 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/routers/login.py +7 -1
- regstack-0.4.0/src/regstack/routers/oauth.py +536 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/pages.py +12 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/static/js/regstack.js +133 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/templates/auth/login.html +14 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/templates/auth/me.html +8 -0
- regstack-0.4.0/src/regstack/ui/templates/auth/oauth_complete.html +14 -0
- 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.4.0/tasks/oauth-design.md +729 -0
- {regstack-0.2.6 → regstack-0.4.0}/tasks.py +12 -1
- regstack-0.4.0/tests/_fake_google/__init__.py +14 -0
- regstack-0.4.0/tests/_fake_google/provider.py +166 -0
- 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/integration/test_oauth_google_router.py +583 -0
- regstack-0.4.0/tests/integration/test_oauth_repos.py +267 -0
- regstack-0.4.0/tests/integration/test_oauth_ui.py +168 -0
- regstack-0.4.0/tests/unit/__init__.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_base_install_imports.py +22 -12
- regstack-0.4.0/tests/unit/test_oauth_google.py +445 -0
- {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_ui_env.py +1 -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.2.6 → regstack-0.4.0}/uv.lock +340 -2
- regstack-0.2.6/src/regstack/__init__.py +0 -5
- regstack-0.2.6/src/regstack/cli/__main__.py +0 -29
- regstack-0.2.6/src/regstack/config/__init__.py +0 -4
- regstack-0.2.6/src/regstack/version.py +0 -1
- {regstack-0.2.6 → regstack-0.4.0}/.github/workflows/publish.yml +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/.github/workflows/test.yml +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/.gitignore +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/.python-version +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/.readthedocs.yaml +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/LICENSE +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/NOTICE +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/SECURITY.md +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/docs/_static/.gitkeep +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/docs/_templates/.gitkeep +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/docs/cli.md +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/docs/quickstart.md +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/docs/theming.md +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/examples/_common/__init__.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/examples/_common/app.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/examples/mongo/README.md +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/examples/mongo/branding/theme.css +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/examples/mongo/main.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/examples/mongo/regstack.toml +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/examples/postgres/README.md +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/examples/postgres/main.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/examples/postgres/regstack.toml +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/examples/sqlite/README.md +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/examples/sqlite/main.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/examples/sqlite/regstack.toml +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/regstack.toml.example +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/auth/__init__.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/auth/clock.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/auth/dependencies.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/auth/jwt.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/auth/lockout.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/auth/mfa.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/auth/password.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/auth/tokens.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/__init__.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/factory.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/mongo/__init__.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/mongo/client.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/blacklist_repo.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/login_attempt_repo.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/mfa_code_repo.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/pending_repo.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/user_repo.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/__init__.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/migrations/__init__.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/migrations/env.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/migrations/script.py.mako +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/migrations/versions/0001_initial_schema.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/repositories/__init__.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/repositories/blacklist_repo.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/repositories/login_attempt_repo.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/repositories/mfa_code_repo.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/repositories/pending_repo.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/repositories/user_repo.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/types.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/cli/__init__.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/cli/_runtime.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/cli/admin.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/cli/doctor.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/cli/init.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/cli/migrate.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/config/loader.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/config/secrets.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/__init__.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/base.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/composer.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/console.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/factory.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/ses.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/smtp.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/templates/email_change.html +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/templates/email_change.subject.txt +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/templates/email_change.txt +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/templates/password_reset.html +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/templates/password_reset.subject.txt +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/templates/password_reset.txt +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/templates/sms_login_mfa.txt +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/templates/sms_phone_setup.txt +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/templates/verification.html +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/templates/verification.subject.txt +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/templates/verification.txt +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/hooks/__init__.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/models/_objectid.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/models/login_attempt.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/models/mfa_code.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/models/pending_registration.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/routers/_schemas.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/routers/admin.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/routers/logout.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/routers/password.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/routers/phone.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/routers/register.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/routers/verify.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/sms/__init__.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/sms/base.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/sms/factory.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/sms/null.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/sms/sns.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/sms/twilio.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/__init__.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/static/css/core.css +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/static/css/theme.css +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/templates/auth/email_change_confirm.html +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/templates/auth/forgot.html +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/templates/auth/mfa_confirm.html +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/templates/auth/register.html +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/templates/auth/reset.html +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/templates/auth/verify.html +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/templates/base.html +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/tests/__init__.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/tests/conftest.py +0 -0
- {regstack-0.2.6/tests/integration → regstack-0.4.0/tests/e2e}/__init__.py +0 -0
- {regstack-0.2.6/tests/unit → regstack-0.4.0/tests/integration}/__init__.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/tests/integration/test_account_management.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/tests/integration/test_admin_router.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/tests/integration/test_happy_path.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/tests/integration/test_indexes.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/tests/integration/test_login_lockout.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/tests/integration/test_mfa.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/tests/integration/test_password_reset.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/tests/integration/test_sql_migrations.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/tests/integration/test_ui_router.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/tests/integration/test_verification.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_cli.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_cli_doctor.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_cli_init.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_config_loader.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_jwt.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_lockout.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_mail_composer.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_mfa_code_repo.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_password.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_ses_backend.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_sms.py +0 -0
- {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_smtp_backend.py +0 -0
|
@@ -5,6 +5,40 @@ authoritative copy lives at
|
|
|
5
5
|
[`docs/changelog.md`](docs/changelog.md) and is rendered into the
|
|
6
6
|
Sphinx docs.
|
|
7
7
|
|
|
8
|
+
## 0.3.0 — 2026-04-30
|
|
9
|
+
|
|
10
|
+
**OAuth — Sign in with Google.** Opt-in via the new `oauth` extra
|
|
11
|
+
and `enable_oauth=True`. Five JSON endpoints, an SSR
|
|
12
|
+
`/account/oauth-complete` page, "Sign in with Google" button on the
|
|
13
|
+
login page, and a Connected-accounts panel on `/account/me`.
|
|
14
|
+
|
|
15
|
+
Schema migration `0002_oauth.py` creates `oauth_identities` +
|
|
16
|
+
`oauth_states` and makes `users.hashed_password` nullable
|
|
17
|
+
(OAuth-only users have no password). Roll forward via
|
|
18
|
+
`regstack migrate` or first-boot `install_schema()` — no manual
|
|
19
|
+
intervention.
|
|
20
|
+
|
|
21
|
+
Account-linking policy defaults to **refuse**: if a Google sign-in
|
|
22
|
+
arrives carrying an email that already belongs to a password-
|
|
23
|
+
registered user, the callback returns `?error=email_in_use` and the
|
|
24
|
+
user must sign in then explicitly link from `/account/me`. Hosts
|
|
25
|
+
that consciously accept the email-recycling threat for UX can flip
|
|
26
|
+
`oauth.auto_link_verified_emails = true`. See
|
|
27
|
+
[`docs/oauth.md`](https://regstack.readthedocs.io/en/latest/oauth.html)
|
|
28
|
+
and [`tasks/oauth-design.md`](https://github.com/jdrumgoole/regstack/blob/main/tasks/oauth-design.md)
|
|
29
|
+
for the full threat model.
|
|
30
|
+
|
|
31
|
+
**Migration**
|
|
32
|
+
|
|
33
|
+
- Install the new extra: `uv add 'regstack[oauth]'`.
|
|
34
|
+
- Set `enable_oauth = true` and provide `oauth.google_client_id` +
|
|
35
|
+
`oauth.google_client_secret`.
|
|
36
|
+
- Run `regstack migrate` (SQL backends only) or rely on
|
|
37
|
+
`install_schema()` at first boot.
|
|
38
|
+
|
|
39
|
+
`BaseUser.hashed_password` is now `str | None`. Code that imported
|
|
40
|
+
the field type explicitly will need to widen it.
|
|
41
|
+
|
|
8
42
|
## 0.2.6 — 2026-04-28
|
|
9
43
|
|
|
10
44
|
Bug fix.
|
|
@@ -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,16 +29,21 @@ 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'
|
|
36
39
|
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
37
40
|
Requires-Dist: invoke>=2.2; extra == 'dev'
|
|
38
41
|
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
42
|
+
Requires-Dist: pyjwt[crypto]>=2.8; extra == 'dev'
|
|
39
43
|
Requires-Dist: pymongo>=4.9; extra == 'dev'
|
|
40
44
|
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
41
45
|
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
46
|
+
Requires-Dist: pytest-playwright>=0.5; extra == 'dev'
|
|
42
47
|
Requires-Dist: pytest-xdist>=3.5; extra == 'dev'
|
|
43
48
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
44
49
|
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
@@ -52,6 +57,8 @@ Requires-Dist: sphinx-copybutton>=0.5; extra == 'docs'
|
|
|
52
57
|
Requires-Dist: sphinx>=7.3; extra == 'docs'
|
|
53
58
|
Provides-Extra: mongo
|
|
54
59
|
Requires-Dist: pymongo>=4.9; extra == 'mongo'
|
|
60
|
+
Provides-Extra: oauth
|
|
61
|
+
Requires-Dist: pyjwt[crypto]>=2.8; extra == 'oauth'
|
|
55
62
|
Provides-Extra: postgres
|
|
56
63
|
Requires-Dist: asyncpg>=0.29; extra == 'postgres'
|
|
57
64
|
Provides-Extra: ses
|
|
@@ -75,9 +82,9 @@ auth bugs.**
|
|
|
75
82
|
|
|
76
83
|
`pip install regstack`, point it at SQLite (default), [PostgreSQL](https://www.postgresql.org/),
|
|
77
84
|
or [MongoDB](https://www.mongodb.com/), and you have register / login /
|
|
78
|
-
verify-email / reset-password / change-email / delete-account /
|
|
79
|
-
two-factor / admin endpoints / themable
|
|
80
|
-
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.
|
|
81
88
|
|
|
82
89
|
📚 **Docs:** <https://regstack.readthedocs.io>
|
|
83
90
|
·
|
|
@@ -129,6 +136,7 @@ result everywhere is what regstack is for.
|
|
|
129
136
|
✔ Forgot / reset password — anti-enumeration: identical responses
|
|
130
137
|
✔ Change password (revokes old tokens) / change email (re-verify)
|
|
131
138
|
✔ Delete account
|
|
139
|
+
✔ Sign in with Google (PKCE + ID-token verification, opt-in)
|
|
132
140
|
✔ Optional SMS two-factor (TOTP-style 6-digit codes over SMS)
|
|
133
141
|
✔ Server-side login lockout (HTTP 429 + Retry-After)
|
|
134
142
|
✔ Admin endpoints (list / disable / delete users, stats)
|
|
@@ -242,7 +250,9 @@ The same docs are also browsable as Markdown in [`docs/`](https://github.com/jdr
|
|
|
242
250
|
|
|
243
251
|
Alpha. Single-file SQLite is the default and runs with no infrastructure;
|
|
244
252
|
PostgreSQL and MongoDB backends pass the same parametrized integration
|
|
245
|
-
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
|
|
246
256
|
[changelog](https://regstack.readthedocs.io/en/latest/changelog.html)
|
|
247
257
|
for the per-release breakdown.
|
|
248
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,107 @@
|
|
|
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
|
+
|
|
24
|
+
## 0.3.0 — 2026-04-30
|
|
25
|
+
|
|
26
|
+
**OAuth — Sign in with Google.** Built across four PRs (M1–M4 of
|
|
27
|
+
[`tasks/oauth-design.md`](https://github.com/jdrumgoole/regstack/blob/main/tasks/oauth-design.md));
|
|
28
|
+
this is the release cut that wraps them up.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- New optional extra `oauth = ["pyjwt[crypto]>=2.8"]`.
|
|
33
|
+
- New `enable_oauth` flag and `OAuthConfig` sub-model
|
|
34
|
+
(`google_client_id`, `google_client_secret`, `google_redirect_uri`,
|
|
35
|
+
`auto_link_verified_emails`, `enforce_mfa_on_oauth_signin`,
|
|
36
|
+
`state_ttl_seconds`, `completion_ttl_seconds`).
|
|
37
|
+
- `regstack.oauth` package — `OAuthProvider` ABC, `OAuthRegistry`,
|
|
38
|
+
`OAuthTokens`, `OAuthUserInfo`, error hierarchy, and the concrete
|
|
39
|
+
`GoogleProvider` (Authorization Code with PKCE, ID-token
|
|
40
|
+
verification via `pyjwt[crypto]` + `PyJWKClient` against Google's
|
|
41
|
+
JWKS).
|
|
42
|
+
- Five JSON endpoints (mounted lazily when `enable_oauth=True` and a
|
|
43
|
+
provider is registered):
|
|
44
|
+
- `GET /oauth/{provider}/start`
|
|
45
|
+
- `GET /oauth/{provider}/callback`
|
|
46
|
+
- `POST /oauth/exchange`
|
|
47
|
+
- `POST /oauth/{provider}/link/start` (auth)
|
|
48
|
+
- `DELETE /oauth/{provider}/link` (auth)
|
|
49
|
+
- `GET /oauth/providers` (auth)
|
|
50
|
+
- New SSR page `/account/oauth-complete` (token-handoff round-trip).
|
|
51
|
+
- "Sign in with Google" button on `/account/login` and a Connected-
|
|
52
|
+
accounts panel on `/account/me`. Login page surfaces callback
|
|
53
|
+
errors via `?error=<code>` with translated banners.
|
|
54
|
+
- Two new repo protocols: `OAuthIdentityRepoProtocol`,
|
|
55
|
+
`OAuthStateRepoProtocol`. Mongo + SQL implementations with
|
|
56
|
+
parametrized integration tests over all three backends.
|
|
57
|
+
- Four new hook events: `oauth_signin_started`,
|
|
58
|
+
`oauth_signin_completed`, `oauth_account_linked`,
|
|
59
|
+
`oauth_account_unlinked`.
|
|
60
|
+
- `tests/_fake_google/` — in-process provider stub so the OAuth
|
|
61
|
+
test suite stays offline and parallel-safe.
|
|
62
|
+
- New docs page [`docs/oauth.md`](oauth.md) — host guide.
|
|
63
|
+
|
|
64
|
+
### Changed (potentially breaking)
|
|
65
|
+
|
|
66
|
+
- **`BaseUser.hashed_password: str` → `str | None`.** OAuth-only
|
|
67
|
+
users have no password. The login route rejects password attempts
|
|
68
|
+
on these accounts with the same generic 401 wrong-password gets
|
|
69
|
+
(no enumeration). `change-password`, `change-email`, and
|
|
70
|
+
`delete-account` all return 400 for OAuth-only users with a
|
|
71
|
+
pointer at the password-reset flow, which doubles as a "set
|
|
72
|
+
initial password" path.
|
|
73
|
+
- `users.hashed_password` is now nullable in the SQL schema —
|
|
74
|
+
migration `0002_oauth.py` flips the column via `batch_alter_table`
|
|
75
|
+
(SQLite-safe). Existing rows are unaffected.
|
|
76
|
+
- New SQL tables `oauth_identities` and `oauth_states`. Mongo
|
|
77
|
+
collections + indexes added by `install_schema()`.
|
|
78
|
+
|
|
79
|
+
### Security defaults
|
|
80
|
+
|
|
81
|
+
- **Account-linking policy defaults to refuse.** When a Google
|
|
82
|
+
sign-in carries an email that already belongs to a regstack user,
|
|
83
|
+
the callback returns `?error=email_in_use`. Hosts can opt into
|
|
84
|
+
auto-linking via `oauth.auto_link_verified_emails = true`, which
|
|
85
|
+
also requires `email_verified=true` on the ID token. The threat
|
|
86
|
+
model is in `tasks/oauth-design.md` § 1.
|
|
87
|
+
- **Server-side PKCE.** `code_verifier` is stored on the
|
|
88
|
+
`oauth_states` row and never enters the URL.
|
|
89
|
+
- **One-time token-handoff.** `/oauth/exchange` consumes the state
|
|
90
|
+
row atomically; second exchange returns 404.
|
|
91
|
+
- **Refuse to unlink the only sign-in method.** Returns 400 for
|
|
92
|
+
OAuth-only users attempting to unlink their only provider.
|
|
93
|
+
- **OAuth sessions are normal session JWTs** — the existing
|
|
94
|
+
`tokens_invalidated_after` bulk-revoke applies, so a password
|
|
95
|
+
change kills any OAuth-issued session too.
|
|
96
|
+
|
|
97
|
+
### Migration notes
|
|
98
|
+
|
|
99
|
+
- Install the extra: `uv add 'regstack[oauth]'`.
|
|
100
|
+
- Configure: set `enable_oauth = true` and provide
|
|
101
|
+
`oauth.google_client_id` + `oauth.google_client_secret` (the
|
|
102
|
+
secret in `regstack.secrets.env` as
|
|
103
|
+
`REGSTACK_OAUTH__GOOGLE_CLIENT_SECRET`).
|
|
104
|
+
- Schema: roll forward via `regstack migrate` or rely on
|
|
105
|
+
`install_schema()` at first boot.
|
|
106
|
+
|
|
6
107
|
## 0.2.6 — 2026-04-28
|
|
7
108
|
|
|
8
109
|
**Bug fix.**
|
|
@@ -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/),
|
|
@@ -120,6 +124,7 @@ configuration
|
|
|
120
124
|
architecture
|
|
121
125
|
security
|
|
122
126
|
embedding
|
|
127
|
+
oauth
|
|
123
128
|
theming
|
|
124
129
|
cli
|
|
125
130
|
```
|
|
@@ -134,7 +139,7 @@ changelog
|
|
|
134
139
|
|
|
135
140
|
## Project status
|
|
136
141
|
|
|
137
|
-
Alpha. Latest tagged release: `v0.
|
|
142
|
+
Alpha. Latest tagged release: `v0.4.0`. SQLite is the default and
|
|
138
143
|
runs with no infrastructure; PostgreSQL and MongoDB pass the same
|
|
139
144
|
parametrized integration suite. The full backend matrix runs in
|
|
140
145
|
parallel against every test, so a green CI on `main` is a strong
|