buntu 0.1.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.
Files changed (136) hide show
  1. buntu-0.1.0/LICENSE +21 -0
  2. buntu-0.1.0/PKG-INFO +141 -0
  3. buntu-0.1.0/README.md +109 -0
  4. buntu-0.1.0/pyproject.toml +80 -0
  5. buntu-0.1.0/setup.cfg +4 -0
  6. buntu-0.1.0/src/buntu.egg-info/PKG-INFO +141 -0
  7. buntu-0.1.0/src/buntu.egg-info/SOURCES.txt +134 -0
  8. buntu-0.1.0/src/buntu.egg-info/dependency_links.txt +1 -0
  9. buntu-0.1.0/src/buntu.egg-info/requires.txt +21 -0
  10. buntu-0.1.0/src/buntu.egg-info/top_level.txt +1 -0
  11. buntu-0.1.0/src/idp/__init__.py +0 -0
  12. buntu-0.1.0/src/idp/app.py +114 -0
  13. buntu-0.1.0/src/idp/cli.py +340 -0
  14. buntu-0.1.0/src/idp/config.py +121 -0
  15. buntu-0.1.0/src/idp/db/__init__.py +76 -0
  16. buntu-0.1.0/src/idp/extensions.py +6 -0
  17. buntu-0.1.0/src/idp/models/__init__.py +54 -0
  18. buntu-0.1.0/src/idp/models/account.py +145 -0
  19. buntu-0.1.0/src/idp/models/clients.py +155 -0
  20. buntu-0.1.0/src/idp/models/grants.py +157 -0
  21. buntu-0.1.0/src/idp/models/platform.py +253 -0
  22. buntu-0.1.0/src/idp/models/users.py +87 -0
  23. buntu-0.1.0/src/idp/oauth/__init__.py +0 -0
  24. buntu-0.1.0/src/idp/oauth/claims.py +33 -0
  25. buntu-0.1.0/src/idp/oauth/grants.py +320 -0
  26. buntu-0.1.0/src/idp/oauth/jwt.py +101 -0
  27. buntu-0.1.0/src/idp/oauth/server.py +369 -0
  28. buntu-0.1.0/src/idp/plugins/__init__.py +270 -0
  29. buntu-0.1.0/src/idp/plugins/sync.py +152 -0
  30. buntu-0.1.0/src/idp/security/__init__.py +0 -0
  31. buntu-0.1.0/src/idp/security/crypto.py +30 -0
  32. buntu-0.1.0/src/idp/security/csrf.py +34 -0
  33. buntu-0.1.0/src/idp/security/data/breached.txt.gz +0 -0
  34. buntu-0.1.0/src/idp/security/keys.py +67 -0
  35. buntu-0.1.0/src/idp/security/passwords.py +48 -0
  36. buntu-0.1.0/src/idp/security/tokens.py +40 -0
  37. buntu-0.1.0/src/idp/security/totp.py +56 -0
  38. buntu-0.1.0/src/idp/services/__init__.py +0 -0
  39. buntu-0.1.0/src/idp/services/accounts.py +452 -0
  40. buntu-0.1.0/src/idp/services/activity.py +73 -0
  41. buntu-0.1.0/src/idp/services/audit.py +22 -0
  42. buntu-0.1.0/src/idp/services/backchannel.py +142 -0
  43. buntu-0.1.0/src/idp/services/closing.py +147 -0
  44. buntu-0.1.0/src/idp/services/connected.py +177 -0
  45. buntu-0.1.0/src/idp/services/consent.py +25 -0
  46. buntu-0.1.0/src/idp/services/credentials.py +58 -0
  47. buntu-0.1.0/src/idp/services/devices.py +30 -0
  48. buntu-0.1.0/src/idp/services/mail.py +86 -0
  49. buntu-0.1.0/src/idp/services/mfa.py +155 -0
  50. buntu-0.1.0/src/idp/services/own_apps.py +24 -0
  51. buntu-0.1.0/src/idp/services/passkeys.py +252 -0
  52. buntu-0.1.0/src/idp/services/platform.py +1024 -0
  53. buntu-0.1.0/src/idp/services/profile.py +190 -0
  54. buntu-0.1.0/src/idp/services/retention.py +90 -0
  55. buntu-0.1.0/src/idp/services/security_prompt.py +18 -0
  56. buntu-0.1.0/src/idp/services/session_check.py +91 -0
  57. buntu-0.1.0/src/idp/services/sessions.py +217 -0
  58. buntu-0.1.0/src/idp/services/setup.py +164 -0
  59. buntu-0.1.0/src/idp/static/idp/assets/account-D-MHfC1z.js +1 -0
  60. buntu-0.1.0/src/idp/static/idp/assets/account-DN_Addf0.js +1 -0
  61. buntu-0.1.0/src/idp/static/idp/assets/app-CzfYFveQ.js +147 -0
  62. buntu-0.1.0/src/idp/static/idp/assets/di-D0iZ5SU4.js +1 -0
  63. buntu-0.1.0/src/idp/static/idp/assets/di-dx_XE3Av.js +1 -0
  64. buntu-0.1.0/src/idp/static/idp/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  65. buntu-0.1.0/src/idp/static/idp/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  66. buntu-0.1.0/src/idp/static/idp/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  67. buntu-0.1.0/src/idp/static/idp/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  68. buntu-0.1.0/src/idp/static/idp/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  69. buntu-0.1.0/src/idp/static/idp/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  70. buntu-0.1.0/src/idp/static/idp/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  71. buntu-0.1.0/src/idp/static/idp/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  72. buntu-0.1.0/src/idp/static/idp/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  73. buntu-0.1.0/src/idp/static/idp/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  74. buntu-0.1.0/src/idp/static/idp/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  75. buntu-0.1.0/src/idp/static/idp/assets/i18n-BZyjOlbu.js +1 -0
  76. buntu-0.1.0/src/idp/static/idp/assets/i18n-Dog5o8z6.js +1 -0
  77. buntu-0.1.0/src/idp/static/idp/assets/roboto-latin-variable-ccAYIvAh.woff2 +0 -0
  78. buntu-0.1.0/src/idp/static/idp/assets/schemas-Bl_iabpv.js +65 -0
  79. buntu-0.1.0/src/idp/static/idp/assets/secure-account-Dl1pS_Wo.js +1 -0
  80. buntu-0.1.0/src/idp/static/idp/assets/service-BhG4-X2O.js +1 -0
  81. buntu-0.1.0/src/idp/static/idp/assets/service-vCuiuvPs.js +1 -0
  82. buntu-0.1.0/src/idp/static/idp/assets/theme-toggle-C07m3YIv.js +1 -0
  83. buntu-0.1.0/src/idp/static/idp/assets/two-step-section-DMQmuG2q.js +2 -0
  84. buntu-0.1.0/src/idp/static/idp/assets/webauthn-BQas4tAd.js +1 -0
  85. buntu-0.1.0/src/idp/static/idp/idp.css +2 -0
  86. buntu-0.1.0/src/idp/static/idp/idp.js +2 -0
  87. buntu-0.1.0/src/idp/templates/shell.html +15 -0
  88. buntu-0.1.0/src/idp/web/__init__.py +0 -0
  89. buntu-0.1.0/src/idp/web/admin.py +901 -0
  90. buntu-0.1.0/src/idp/web/api.py +717 -0
  91. buntu-0.1.0/src/idp/web/developer.py +628 -0
  92. buntu-0.1.0/src/idp/web/oidc.py +394 -0
  93. buntu-0.1.0/src/idp/web/openapi.py +1158 -0
  94. buntu-0.1.0/src/idp/web/pages.py +238 -0
  95. buntu-0.1.0/src/idp/web/validation.py +46 -0
  96. buntu-0.1.0/tests/test_account_area.py +68 -0
  97. buntu-0.1.0/tests/test_account_attacks.py +279 -0
  98. buntu-0.1.0/tests/test_account_closing.py +93 -0
  99. buntu-0.1.0/tests/test_account_connected.py +99 -0
  100. buntu-0.1.0/tests/test_account_profile.py +172 -0
  101. buntu-0.1.0/tests/test_account_security.py +64 -0
  102. buntu-0.1.0/tests/test_app_overview.py +203 -0
  103. buntu-0.1.0/tests/test_authorization_code.py +204 -0
  104. buntu-0.1.0/tests/test_authorization_code_attacks.py +196 -0
  105. buntu-0.1.0/tests/test_backchannel_logout.py +89 -0
  106. buntu-0.1.0/tests/test_boundary_attacks.py +84 -0
  107. buntu-0.1.0/tests/test_brand.py +45 -0
  108. buntu-0.1.0/tests/test_cli.py +162 -0
  109. buntu-0.1.0/tests/test_console_api.py +188 -0
  110. buntu-0.1.0/tests/test_console_users.py +144 -0
  111. buntu-0.1.0/tests/test_credentials.py +70 -0
  112. buntu-0.1.0/tests/test_data_retention.py +146 -0
  113. buntu-0.1.0/tests/test_deployment.py +88 -0
  114. buntu-0.1.0/tests/test_developer_api.py +298 -0
  115. buntu-0.1.0/tests/test_flows.py +433 -0
  116. buntu-0.1.0/tests/test_grant_attacks.py +302 -0
  117. buntu-0.1.0/tests/test_invitations.py +66 -0
  118. buntu-0.1.0/tests/test_members.py +187 -0
  119. buntu-0.1.0/tests/test_mfa.py +170 -0
  120. buntu-0.1.0/tests/test_offline_access.py +163 -0
  121. buntu-0.1.0/tests/test_openapi.py +250 -0
  122. buntu-0.1.0/tests/test_own_apps.py +87 -0
  123. buntu-0.1.0/tests/test_passkeys.py +157 -0
  124. buntu-0.1.0/tests/test_platform_gates.py +258 -0
  125. buntu-0.1.0/tests/test_platform_organization.py +168 -0
  126. buntu-0.1.0/tests/test_plugin_attacks.py +240 -0
  127. buntu-0.1.0/tests/test_plugins.py +234 -0
  128. buntu-0.1.0/tests/test_review_rules.py +125 -0
  129. buntu-0.1.0/tests/test_reviews.py +98 -0
  130. buntu-0.1.0/tests/test_role_matrix.py +265 -0
  131. buntu-0.1.0/tests/test_scope_rules.py +164 -0
  132. buntu-0.1.0/tests/test_security_prompt.py +57 -0
  133. buntu-0.1.0/tests/test_session_attacks.py +206 -0
  134. buntu-0.1.0/tests/test_session_check.py +85 -0
  135. buntu-0.1.0/tests/test_setup.py +104 -0
  136. buntu-0.1.0/tests/test_token_attacks.py +306 -0
buntu-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ibrahima Touré
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
buntu-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,141 @@
1
+ Metadata-Version: 2.4
2
+ Name: buntu
3
+ Version: 0.1.0
4
+ Summary: OAuth 2.1 and OpenID Connect provider
5
+ Author: Ibrahima Touré
6
+ License-Expression: MIT
7
+ Project-URL: Source, https://github.com/buntubi/buntu
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: flask>=3.1
12
+ Requires-Dist: authlib<1.9,>=1.8
13
+ Requires-Dist: sqlalchemy>=2.0.36
14
+ Requires-Dist: alembic>=1.14
15
+ Requires-Dist: psycopg[binary]>=3.2
16
+ Requires-Dist: pydantic-settings>=2.6
17
+ Requires-Dist: flask-limiter>=3.8
18
+ Requires-Dist: requests>=2.32
19
+ Requires-Dist: segno>=1.6
20
+ Requires-Dist: email-validator>=2.2
21
+ Requires-Dist: webauthn<4,>=3.0
22
+ Requires-Dist: cryptography>=44
23
+ Requires-Dist: joserfc<2,>=1.7
24
+ Requires-Dist: gunicorn>=23
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=8.3; extra == "dev"
27
+ Requires-Dist: ruff>=0.8; extra == "dev"
28
+ Requires-Dist: mypy>=1.13; extra == "dev"
29
+ Requires-Dist: openapi-spec-validator>=0.7; extra == "dev"
30
+ Requires-Dist: jsonschema>=4.23; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # The provider
34
+
35
+ The OAuth 2.1 and OpenID Connect server, in Flask. It owns every account, every session and
36
+ every token on the platform, and every rule about who may do what. The console, the portal and
37
+ the sign-in pages hold no rules of their own: they call this server and show what it answers.
38
+
39
+ Setup, deployment and the platform's concepts (accounts, organizations, apps, the catalogue) are
40
+ in the [repository README](../README.md). Plugins are in [docs/plugins.md](../docs/plugins.md).
41
+
42
+ ## What it serves
43
+
44
+ | Prefix | Who calls it | What |
45
+ |---|---|---|
46
+ | `/.well-known/openid-configuration`, `/.well-known/jwks.json` | Any client | Discovery and the signing keys |
47
+ | `/.well-known/brand.json` | The console and the portal | The platform's name, colour and links |
48
+ | `/oauth/*` | OAuth clients | `authorize`, `token`, `revoke`, `introspect`, `device_authorization`, `userinfo`, `logout`, `check-session` |
49
+ | `/signin`, `/signup`, `/mfa`, `/consent`, `/accounts`, `/account/*`, ... | People, in a browser | The pages built from `frontend/`, rendered into `templates/shell.html` |
50
+ | `/api/*` | Those pages | Sign-in steps, the account area, passkeys. Same-site, with a CSRF token |
51
+ | `/developer/api/*` | The portal | Organizations, members, apps, secrets, test users, API keys. Bearer token with the `developer` scope |
52
+ | `/admin/api/*` | The console | Users, organizations, reviews, the catalogue, the platform's apps. Bearer token with the `admin` scope and a two-step sign-in |
53
+ | `/api/plugins/<id>/*` | Plugin pages in the console and portal | Each plugin's own API |
54
+ | `/openapi.json` | Developers, Postman | The OAuth endpoints, the developer API and the admin API |
55
+
56
+ Grants: authorization code with PKCE, refresh token (rotated, with reuse detection), client
57
+ credentials, and the device code. Tokens are JWTs signed with the newest key in
58
+ `IDP_SIGNING_KEYS_DIR`; every key in the directory still verifies.
59
+
60
+ Sessions end everywhere they reached: a sign-out, a device signed out from the account area,
61
+ or a deleted account sends an OpenID back-channel logout token to every app with a
62
+ `backchannel_logout_uri` that took part in that session.
63
+
64
+ ## Layout
65
+
66
+ ```
67
+ src/idp/
68
+ app.py create_app: settings, extensions, blueprints, plugins
69
+ config.py every setting, read from IDP_* variables and .env
70
+ cli.py the flask commands
71
+ models/ SQLAlchemy models
72
+ db/ the session and utcnow
73
+ oauth/ Authlib server, grants, claims, JWT helpers
74
+ security/ passwords, TOTP, CSRF, signing keys, encryption at rest
75
+ services/ the rules: accounts, sessions, MFA, passkeys, platform, profile, ...
76
+ web/ the blueprints: pages, api, developer, admin, oidc, openapi
77
+ plugins/ the plugin interface and the catalogue sync
78
+ templates/ the page shell
79
+ static/idp/ the built sign-in pages (from frontend/, not edited here)
80
+ migrations/ Alembic revisions
81
+ tests/ pytest, against a real PostgreSQL database
82
+ platform.example.toml the platform's apps, copied to platform.toml per deployment
83
+ ```
84
+
85
+ The rule of thumb when changing something: a view in `web/` parses the request and shapes the
86
+ answer; the decision lives in `services/`. A rule that two endpoints share is one function in a
87
+ service, never two copies in two views.
88
+
89
+ ## Commands
90
+
91
+ Run from `idp/` as `../.venv/bin/flask --app idp.app:create_app <command>`.
92
+
93
+ | Command | Does |
94
+ |---|---|
95
+ | `keys generate` | Adds a signing key; the newest signs from then on |
96
+ | `admin create`, `admin check` | Creates the superadmin; checks one exists |
97
+ | `users admins`, `users promote`, `users demote` | Lists admins; gives or takes the admin role |
98
+ | `users delete <pattern> --yes` | Deletes accounts by email (a `*` pattern works), as deleting from the account page does: apps are signed out, plugins told, the audit trail kept without who it was |
99
+ | `prune` | Deletes expired codes and tokens, sign-ins ended more than `IDP_SESSION_RETENTION_DAYS` ago and audit rows older than `IDP_AUDIT_RETENTION_DAYS`; schedule it daily |
100
+ | `platform setup [--file] [--rotate-secrets]` | The platform organization, the apps `platform.toml` lists, and the plugins' catalogue |
101
+ | `clients create`, `clients homepage <client_id> [url]` | A client by hand; set or clear the address it lives at |
102
+ | `plugins list`, `plugins sync` | The plugins running; bring the catalogue in line with them |
103
+
104
+ Migrations: `../.venv/bin/alembic upgrade head`, with `IDP_DATABASE_URL` set. A new revision
105
+ goes in `migrations/versions/` and names its `down_revision`; there is one head.
106
+
107
+ ## Settings
108
+
109
+ All of them are in `src/idp/config.py`, each with a comment, and read from the environment with
110
+ the `IDP_` prefix or from `idp/.env` (see `.env.example`). The ones every deployment sets:
111
+
112
+ | Setting | Meaning |
113
+ |---|---|
114
+ | `IDP_ISSUER` | The public origin, exactly as clients see it, no trailing slash |
115
+ | `IDP_DATABASE_URL` | PostgreSQL |
116
+ | `IDP_SECRET_KEY` | Signs the session cookie and CSRF tokens; 32 characters or more |
117
+ | `IDP_SIGNING_KEYS_DIR` | The token signing keys |
118
+ | `IDP_EMAIL_TEST_MODE`, `IDP_RESEND_API_KEY`, `IDP_EMAIL_FROM` | Email: logged in test mode (development only), sent through Resend otherwise |
119
+ | `IDP_TRUSTED_PROXIES` | How many proxies in front set `X-Forwarded-For` and `X-Forwarded-Proto`; 0 when none |
120
+ | `IDP_RATE_LIMIT_STORAGE` | Where rate limits are counted: `redis://...` shared by every process, or `memory://` for one |
121
+ | `IDP_BRAND_*` | The platform's name, colour, logo and links |
122
+ | `IDP_CORS_ORIGINS` | Browser origins allowed on the token, revocation and userinfo endpoints |
123
+ | `IDP_PLUGINS`, `IDP_PLUGINS_SYNC_ON_START` | The plugins, as a JSON list (`["acme"]`); whether to sync the catalogue at start |
124
+
125
+ With an issuer that is not on this machine, the provider refuses to start when the issuer is
126
+ not https, email test mode is on, cookies are not Secure, or `IDP_RATE_LIMIT_STORAGE` is not
127
+ set. It then also sends `Strict-Transport-Security`.
128
+
129
+ Lifetimes (`IDP_ACCESS_TOKEN_TTL`, `IDP_REFRESH_TOKEN_TTL`, `IDP_SESSION_TTL`, ...) are in
130
+ seconds. What `prune` keeps is in days: `IDP_SESSION_RETENTION_DAYS` (30) for ended sign-ins,
131
+ `IDP_AUDIT_RETENTION_DAYS` (365) for the audit trail.
132
+
133
+ ## Checks
134
+
135
+ ```bash
136
+ ../.venv/bin/python -m pytest # needs the idp_test database on localhost
137
+ ../.venv/bin/ruff check src tests
138
+ ../.venv/bin/mypy src
139
+ ```
140
+
141
+ Every change to a rule comes with a test in `tests/` that fails without it.
buntu-0.1.0/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # The provider
2
+
3
+ The OAuth 2.1 and OpenID Connect server, in Flask. It owns every account, every session and
4
+ every token on the platform, and every rule about who may do what. The console, the portal and
5
+ the sign-in pages hold no rules of their own: they call this server and show what it answers.
6
+
7
+ Setup, deployment and the platform's concepts (accounts, organizations, apps, the catalogue) are
8
+ in the [repository README](../README.md). Plugins are in [docs/plugins.md](../docs/plugins.md).
9
+
10
+ ## What it serves
11
+
12
+ | Prefix | Who calls it | What |
13
+ |---|---|---|
14
+ | `/.well-known/openid-configuration`, `/.well-known/jwks.json` | Any client | Discovery and the signing keys |
15
+ | `/.well-known/brand.json` | The console and the portal | The platform's name, colour and links |
16
+ | `/oauth/*` | OAuth clients | `authorize`, `token`, `revoke`, `introspect`, `device_authorization`, `userinfo`, `logout`, `check-session` |
17
+ | `/signin`, `/signup`, `/mfa`, `/consent`, `/accounts`, `/account/*`, ... | People, in a browser | The pages built from `frontend/`, rendered into `templates/shell.html` |
18
+ | `/api/*` | Those pages | Sign-in steps, the account area, passkeys. Same-site, with a CSRF token |
19
+ | `/developer/api/*` | The portal | Organizations, members, apps, secrets, test users, API keys. Bearer token with the `developer` scope |
20
+ | `/admin/api/*` | The console | Users, organizations, reviews, the catalogue, the platform's apps. Bearer token with the `admin` scope and a two-step sign-in |
21
+ | `/api/plugins/<id>/*` | Plugin pages in the console and portal | Each plugin's own API |
22
+ | `/openapi.json` | Developers, Postman | The OAuth endpoints, the developer API and the admin API |
23
+
24
+ Grants: authorization code with PKCE, refresh token (rotated, with reuse detection), client
25
+ credentials, and the device code. Tokens are JWTs signed with the newest key in
26
+ `IDP_SIGNING_KEYS_DIR`; every key in the directory still verifies.
27
+
28
+ Sessions end everywhere they reached: a sign-out, a device signed out from the account area,
29
+ or a deleted account sends an OpenID back-channel logout token to every app with a
30
+ `backchannel_logout_uri` that took part in that session.
31
+
32
+ ## Layout
33
+
34
+ ```
35
+ src/idp/
36
+ app.py create_app: settings, extensions, blueprints, plugins
37
+ config.py every setting, read from IDP_* variables and .env
38
+ cli.py the flask commands
39
+ models/ SQLAlchemy models
40
+ db/ the session and utcnow
41
+ oauth/ Authlib server, grants, claims, JWT helpers
42
+ security/ passwords, TOTP, CSRF, signing keys, encryption at rest
43
+ services/ the rules: accounts, sessions, MFA, passkeys, platform, profile, ...
44
+ web/ the blueprints: pages, api, developer, admin, oidc, openapi
45
+ plugins/ the plugin interface and the catalogue sync
46
+ templates/ the page shell
47
+ static/idp/ the built sign-in pages (from frontend/, not edited here)
48
+ migrations/ Alembic revisions
49
+ tests/ pytest, against a real PostgreSQL database
50
+ platform.example.toml the platform's apps, copied to platform.toml per deployment
51
+ ```
52
+
53
+ The rule of thumb when changing something: a view in `web/` parses the request and shapes the
54
+ answer; the decision lives in `services/`. A rule that two endpoints share is one function in a
55
+ service, never two copies in two views.
56
+
57
+ ## Commands
58
+
59
+ Run from `idp/` as `../.venv/bin/flask --app idp.app:create_app <command>`.
60
+
61
+ | Command | Does |
62
+ |---|---|
63
+ | `keys generate` | Adds a signing key; the newest signs from then on |
64
+ | `admin create`, `admin check` | Creates the superadmin; checks one exists |
65
+ | `users admins`, `users promote`, `users demote` | Lists admins; gives or takes the admin role |
66
+ | `users delete <pattern> --yes` | Deletes accounts by email (a `*` pattern works), as deleting from the account page does: apps are signed out, plugins told, the audit trail kept without who it was |
67
+ | `prune` | Deletes expired codes and tokens, sign-ins ended more than `IDP_SESSION_RETENTION_DAYS` ago and audit rows older than `IDP_AUDIT_RETENTION_DAYS`; schedule it daily |
68
+ | `platform setup [--file] [--rotate-secrets]` | The platform organization, the apps `platform.toml` lists, and the plugins' catalogue |
69
+ | `clients create`, `clients homepage <client_id> [url]` | A client by hand; set or clear the address it lives at |
70
+ | `plugins list`, `plugins sync` | The plugins running; bring the catalogue in line with them |
71
+
72
+ Migrations: `../.venv/bin/alembic upgrade head`, with `IDP_DATABASE_URL` set. A new revision
73
+ goes in `migrations/versions/` and names its `down_revision`; there is one head.
74
+
75
+ ## Settings
76
+
77
+ All of them are in `src/idp/config.py`, each with a comment, and read from the environment with
78
+ the `IDP_` prefix or from `idp/.env` (see `.env.example`). The ones every deployment sets:
79
+
80
+ | Setting | Meaning |
81
+ |---|---|
82
+ | `IDP_ISSUER` | The public origin, exactly as clients see it, no trailing slash |
83
+ | `IDP_DATABASE_URL` | PostgreSQL |
84
+ | `IDP_SECRET_KEY` | Signs the session cookie and CSRF tokens; 32 characters or more |
85
+ | `IDP_SIGNING_KEYS_DIR` | The token signing keys |
86
+ | `IDP_EMAIL_TEST_MODE`, `IDP_RESEND_API_KEY`, `IDP_EMAIL_FROM` | Email: logged in test mode (development only), sent through Resend otherwise |
87
+ | `IDP_TRUSTED_PROXIES` | How many proxies in front set `X-Forwarded-For` and `X-Forwarded-Proto`; 0 when none |
88
+ | `IDP_RATE_LIMIT_STORAGE` | Where rate limits are counted: `redis://...` shared by every process, or `memory://` for one |
89
+ | `IDP_BRAND_*` | The platform's name, colour, logo and links |
90
+ | `IDP_CORS_ORIGINS` | Browser origins allowed on the token, revocation and userinfo endpoints |
91
+ | `IDP_PLUGINS`, `IDP_PLUGINS_SYNC_ON_START` | The plugins, as a JSON list (`["acme"]`); whether to sync the catalogue at start |
92
+
93
+ With an issuer that is not on this machine, the provider refuses to start when the issuer is
94
+ not https, email test mode is on, cookies are not Secure, or `IDP_RATE_LIMIT_STORAGE` is not
95
+ set. It then also sends `Strict-Transport-Security`.
96
+
97
+ Lifetimes (`IDP_ACCESS_TOKEN_TTL`, `IDP_REFRESH_TOKEN_TTL`, `IDP_SESSION_TTL`, ...) are in
98
+ seconds. What `prune` keeps is in days: `IDP_SESSION_RETENTION_DAYS` (30) for ended sign-ins,
99
+ `IDP_AUDIT_RETENTION_DAYS` (365) for the audit trail.
100
+
101
+ ## Checks
102
+
103
+ ```bash
104
+ ../.venv/bin/python -m pytest # needs the idp_test database on localhost
105
+ ../.venv/bin/ruff check src tests
106
+ ../.venv/bin/mypy src
107
+ ```
108
+
109
+ Every change to a rule comes with a test in `tests/` that fails without it.
@@ -0,0 +1,80 @@
1
+ [project]
2
+ name = "buntu"
3
+ version = "0.1.0"
4
+ description = "OAuth 2.1 and OpenID Connect provider"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [{ name = "Ibrahima Touré" }]
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "flask>=3.1",
12
+ # Pinned to a minor: 1.8 moved the ID token hooks, and a silent change there drops `kid`.
13
+ "authlib>=1.8,<1.9",
14
+ "sqlalchemy>=2.0.36",
15
+ "alembic>=1.14",
16
+ "psycopg[binary]>=3.2",
17
+ "pydantic-settings>=2.6",
18
+ "flask-limiter>=3.8",
19
+ "requests>=2.32",
20
+ "segno>=1.6",
21
+ "email-validator>=2.2",
22
+ "webauthn>=3.0,<4",
23
+ # Imported directly (keys, token checks), not only through authlib and webauthn.
24
+ "cryptography>=44",
25
+ "joserfc>=1.7,<2",
26
+ # The production server (the images run it).
27
+ "gunicorn>=23",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ dev = ["pytest>=8.3", "ruff>=0.8", "mypy>=1.13", "openapi-spec-validator>=0.7", "jsonschema>=4.23"]
32
+
33
+ [project.urls]
34
+ Source = "https://github.com/buntubi/buntu"
35
+
36
+ [build-system]
37
+ requires = ["setuptools>=77"]
38
+ build-backend = "setuptools.build_meta"
39
+
40
+ [tool.setuptools.packages.find]
41
+ where = ["src"]
42
+
43
+ [tool.setuptools.package-data]
44
+ idp = ["templates/*.html", "static/**/*", "security/data/*.gz"]
45
+
46
+ [tool.pytest.ini_options]
47
+ testpaths = ["tests"]
48
+ addopts = "-q"
49
+
50
+ [tool.ruff]
51
+ line-length = 110
52
+ target-version = "py311"
53
+
54
+ [tool.ruff.lint]
55
+ select = ["E", "F", "I", "B", "UP", "S", "SIM"]
56
+ ignore = ["S101"]
57
+
58
+ [tool.ruff.lint.isort]
59
+ known-local-folder = ["conftest"]
60
+
61
+ [tool.ruff.lint.per-file-ignores]
62
+ # Test accounts have passwords.
63
+ "tests/*" = ["S105", "S106"]
64
+
65
+ [tool.mypy]
66
+ python_version = "3.11"
67
+ strict = true
68
+ packages = ["idp"]
69
+ mypy_path = "src"
70
+
71
+ # Authlib and flask-limiter ship no type information.
72
+ [[tool.mypy.overrides]]
73
+ module = ["authlib.*", "flask_limiter.*"]
74
+ ignore_missing_imports = true
75
+
76
+ # The modules built on Authlib's classes and decorators get Any back from them.
77
+ [[tool.mypy.overrides]]
78
+ module = ["idp.models.*", "idp.oauth.*", "idp.web.*"]
79
+ warn_return_any = false
80
+ disallow_untyped_decorators = false
buntu-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,141 @@
1
+ Metadata-Version: 2.4
2
+ Name: buntu
3
+ Version: 0.1.0
4
+ Summary: OAuth 2.1 and OpenID Connect provider
5
+ Author: Ibrahima Touré
6
+ License-Expression: MIT
7
+ Project-URL: Source, https://github.com/buntubi/buntu
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: flask>=3.1
12
+ Requires-Dist: authlib<1.9,>=1.8
13
+ Requires-Dist: sqlalchemy>=2.0.36
14
+ Requires-Dist: alembic>=1.14
15
+ Requires-Dist: psycopg[binary]>=3.2
16
+ Requires-Dist: pydantic-settings>=2.6
17
+ Requires-Dist: flask-limiter>=3.8
18
+ Requires-Dist: requests>=2.32
19
+ Requires-Dist: segno>=1.6
20
+ Requires-Dist: email-validator>=2.2
21
+ Requires-Dist: webauthn<4,>=3.0
22
+ Requires-Dist: cryptography>=44
23
+ Requires-Dist: joserfc<2,>=1.7
24
+ Requires-Dist: gunicorn>=23
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=8.3; extra == "dev"
27
+ Requires-Dist: ruff>=0.8; extra == "dev"
28
+ Requires-Dist: mypy>=1.13; extra == "dev"
29
+ Requires-Dist: openapi-spec-validator>=0.7; extra == "dev"
30
+ Requires-Dist: jsonschema>=4.23; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # The provider
34
+
35
+ The OAuth 2.1 and OpenID Connect server, in Flask. It owns every account, every session and
36
+ every token on the platform, and every rule about who may do what. The console, the portal and
37
+ the sign-in pages hold no rules of their own: they call this server and show what it answers.
38
+
39
+ Setup, deployment and the platform's concepts (accounts, organizations, apps, the catalogue) are
40
+ in the [repository README](../README.md). Plugins are in [docs/plugins.md](../docs/plugins.md).
41
+
42
+ ## What it serves
43
+
44
+ | Prefix | Who calls it | What |
45
+ |---|---|---|
46
+ | `/.well-known/openid-configuration`, `/.well-known/jwks.json` | Any client | Discovery and the signing keys |
47
+ | `/.well-known/brand.json` | The console and the portal | The platform's name, colour and links |
48
+ | `/oauth/*` | OAuth clients | `authorize`, `token`, `revoke`, `introspect`, `device_authorization`, `userinfo`, `logout`, `check-session` |
49
+ | `/signin`, `/signup`, `/mfa`, `/consent`, `/accounts`, `/account/*`, ... | People, in a browser | The pages built from `frontend/`, rendered into `templates/shell.html` |
50
+ | `/api/*` | Those pages | Sign-in steps, the account area, passkeys. Same-site, with a CSRF token |
51
+ | `/developer/api/*` | The portal | Organizations, members, apps, secrets, test users, API keys. Bearer token with the `developer` scope |
52
+ | `/admin/api/*` | The console | Users, organizations, reviews, the catalogue, the platform's apps. Bearer token with the `admin` scope and a two-step sign-in |
53
+ | `/api/plugins/<id>/*` | Plugin pages in the console and portal | Each plugin's own API |
54
+ | `/openapi.json` | Developers, Postman | The OAuth endpoints, the developer API and the admin API |
55
+
56
+ Grants: authorization code with PKCE, refresh token (rotated, with reuse detection), client
57
+ credentials, and the device code. Tokens are JWTs signed with the newest key in
58
+ `IDP_SIGNING_KEYS_DIR`; every key in the directory still verifies.
59
+
60
+ Sessions end everywhere they reached: a sign-out, a device signed out from the account area,
61
+ or a deleted account sends an OpenID back-channel logout token to every app with a
62
+ `backchannel_logout_uri` that took part in that session.
63
+
64
+ ## Layout
65
+
66
+ ```
67
+ src/idp/
68
+ app.py create_app: settings, extensions, blueprints, plugins
69
+ config.py every setting, read from IDP_* variables and .env
70
+ cli.py the flask commands
71
+ models/ SQLAlchemy models
72
+ db/ the session and utcnow
73
+ oauth/ Authlib server, grants, claims, JWT helpers
74
+ security/ passwords, TOTP, CSRF, signing keys, encryption at rest
75
+ services/ the rules: accounts, sessions, MFA, passkeys, platform, profile, ...
76
+ web/ the blueprints: pages, api, developer, admin, oidc, openapi
77
+ plugins/ the plugin interface and the catalogue sync
78
+ templates/ the page shell
79
+ static/idp/ the built sign-in pages (from frontend/, not edited here)
80
+ migrations/ Alembic revisions
81
+ tests/ pytest, against a real PostgreSQL database
82
+ platform.example.toml the platform's apps, copied to platform.toml per deployment
83
+ ```
84
+
85
+ The rule of thumb when changing something: a view in `web/` parses the request and shapes the
86
+ answer; the decision lives in `services/`. A rule that two endpoints share is one function in a
87
+ service, never two copies in two views.
88
+
89
+ ## Commands
90
+
91
+ Run from `idp/` as `../.venv/bin/flask --app idp.app:create_app <command>`.
92
+
93
+ | Command | Does |
94
+ |---|---|
95
+ | `keys generate` | Adds a signing key; the newest signs from then on |
96
+ | `admin create`, `admin check` | Creates the superadmin; checks one exists |
97
+ | `users admins`, `users promote`, `users demote` | Lists admins; gives or takes the admin role |
98
+ | `users delete <pattern> --yes` | Deletes accounts by email (a `*` pattern works), as deleting from the account page does: apps are signed out, plugins told, the audit trail kept without who it was |
99
+ | `prune` | Deletes expired codes and tokens, sign-ins ended more than `IDP_SESSION_RETENTION_DAYS` ago and audit rows older than `IDP_AUDIT_RETENTION_DAYS`; schedule it daily |
100
+ | `platform setup [--file] [--rotate-secrets]` | The platform organization, the apps `platform.toml` lists, and the plugins' catalogue |
101
+ | `clients create`, `clients homepage <client_id> [url]` | A client by hand; set or clear the address it lives at |
102
+ | `plugins list`, `plugins sync` | The plugins running; bring the catalogue in line with them |
103
+
104
+ Migrations: `../.venv/bin/alembic upgrade head`, with `IDP_DATABASE_URL` set. A new revision
105
+ goes in `migrations/versions/` and names its `down_revision`; there is one head.
106
+
107
+ ## Settings
108
+
109
+ All of them are in `src/idp/config.py`, each with a comment, and read from the environment with
110
+ the `IDP_` prefix or from `idp/.env` (see `.env.example`). The ones every deployment sets:
111
+
112
+ | Setting | Meaning |
113
+ |---|---|
114
+ | `IDP_ISSUER` | The public origin, exactly as clients see it, no trailing slash |
115
+ | `IDP_DATABASE_URL` | PostgreSQL |
116
+ | `IDP_SECRET_KEY` | Signs the session cookie and CSRF tokens; 32 characters or more |
117
+ | `IDP_SIGNING_KEYS_DIR` | The token signing keys |
118
+ | `IDP_EMAIL_TEST_MODE`, `IDP_RESEND_API_KEY`, `IDP_EMAIL_FROM` | Email: logged in test mode (development only), sent through Resend otherwise |
119
+ | `IDP_TRUSTED_PROXIES` | How many proxies in front set `X-Forwarded-For` and `X-Forwarded-Proto`; 0 when none |
120
+ | `IDP_RATE_LIMIT_STORAGE` | Where rate limits are counted: `redis://...` shared by every process, or `memory://` for one |
121
+ | `IDP_BRAND_*` | The platform's name, colour, logo and links |
122
+ | `IDP_CORS_ORIGINS` | Browser origins allowed on the token, revocation and userinfo endpoints |
123
+ | `IDP_PLUGINS`, `IDP_PLUGINS_SYNC_ON_START` | The plugins, as a JSON list (`["acme"]`); whether to sync the catalogue at start |
124
+
125
+ With an issuer that is not on this machine, the provider refuses to start when the issuer is
126
+ not https, email test mode is on, cookies are not Secure, or `IDP_RATE_LIMIT_STORAGE` is not
127
+ set. It then also sends `Strict-Transport-Security`.
128
+
129
+ Lifetimes (`IDP_ACCESS_TOKEN_TTL`, `IDP_REFRESH_TOKEN_TTL`, `IDP_SESSION_TTL`, ...) are in
130
+ seconds. What `prune` keeps is in days: `IDP_SESSION_RETENTION_DAYS` (30) for ended sign-ins,
131
+ `IDP_AUDIT_RETENTION_DAYS` (365) for the audit trail.
132
+
133
+ ## Checks
134
+
135
+ ```bash
136
+ ../.venv/bin/python -m pytest # needs the idp_test database on localhost
137
+ ../.venv/bin/ruff check src tests
138
+ ../.venv/bin/mypy src
139
+ ```
140
+
141
+ Every change to a rule comes with a test in `tests/` that fails without it.
@@ -0,0 +1,134 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/buntu.egg-info/PKG-INFO
5
+ src/buntu.egg-info/SOURCES.txt
6
+ src/buntu.egg-info/dependency_links.txt
7
+ src/buntu.egg-info/requires.txt
8
+ src/buntu.egg-info/top_level.txt
9
+ src/idp/__init__.py
10
+ src/idp/app.py
11
+ src/idp/cli.py
12
+ src/idp/config.py
13
+ src/idp/extensions.py
14
+ src/idp/db/__init__.py
15
+ src/idp/models/__init__.py
16
+ src/idp/models/account.py
17
+ src/idp/models/clients.py
18
+ src/idp/models/grants.py
19
+ src/idp/models/platform.py
20
+ src/idp/models/users.py
21
+ src/idp/oauth/__init__.py
22
+ src/idp/oauth/claims.py
23
+ src/idp/oauth/grants.py
24
+ src/idp/oauth/jwt.py
25
+ src/idp/oauth/server.py
26
+ src/idp/plugins/__init__.py
27
+ src/idp/plugins/sync.py
28
+ src/idp/security/__init__.py
29
+ src/idp/security/crypto.py
30
+ src/idp/security/csrf.py
31
+ src/idp/security/keys.py
32
+ src/idp/security/passwords.py
33
+ src/idp/security/tokens.py
34
+ src/idp/security/totp.py
35
+ src/idp/security/data/breached.txt.gz
36
+ src/idp/services/__init__.py
37
+ src/idp/services/accounts.py
38
+ src/idp/services/activity.py
39
+ src/idp/services/audit.py
40
+ src/idp/services/backchannel.py
41
+ src/idp/services/closing.py
42
+ src/idp/services/connected.py
43
+ src/idp/services/consent.py
44
+ src/idp/services/credentials.py
45
+ src/idp/services/devices.py
46
+ src/idp/services/mail.py
47
+ src/idp/services/mfa.py
48
+ src/idp/services/own_apps.py
49
+ src/idp/services/passkeys.py
50
+ src/idp/services/platform.py
51
+ src/idp/services/profile.py
52
+ src/idp/services/retention.py
53
+ src/idp/services/security_prompt.py
54
+ src/idp/services/session_check.py
55
+ src/idp/services/sessions.py
56
+ src/idp/services/setup.py
57
+ src/idp/static/idp/idp.css
58
+ src/idp/static/idp/idp.js
59
+ src/idp/static/idp/assets/account-D-MHfC1z.js
60
+ src/idp/static/idp/assets/account-DN_Addf0.js
61
+ src/idp/static/idp/assets/app-CzfYFveQ.js
62
+ src/idp/static/idp/assets/di-D0iZ5SU4.js
63
+ src/idp/static/idp/assets/di-dx_XE3Av.js
64
+ src/idp/static/idp/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2
65
+ src/idp/static/idp/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2
66
+ src/idp/static/idp/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2
67
+ src/idp/static/idp/assets/geist-latin-wght-normal-BgDaEnEv.woff2
68
+ src/idp/static/idp/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2
69
+ src/idp/static/idp/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2
70
+ src/idp/static/idp/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2
71
+ src/idp/static/idp/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2
72
+ src/idp/static/idp/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2
73
+ src/idp/static/idp/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2
74
+ src/idp/static/idp/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2
75
+ src/idp/static/idp/assets/i18n-BZyjOlbu.js
76
+ src/idp/static/idp/assets/i18n-Dog5o8z6.js
77
+ src/idp/static/idp/assets/roboto-latin-variable-ccAYIvAh.woff2
78
+ src/idp/static/idp/assets/schemas-Bl_iabpv.js
79
+ src/idp/static/idp/assets/secure-account-Dl1pS_Wo.js
80
+ src/idp/static/idp/assets/service-BhG4-X2O.js
81
+ src/idp/static/idp/assets/service-vCuiuvPs.js
82
+ src/idp/static/idp/assets/theme-toggle-C07m3YIv.js
83
+ src/idp/static/idp/assets/two-step-section-DMQmuG2q.js
84
+ src/idp/static/idp/assets/webauthn-BQas4tAd.js
85
+ src/idp/templates/shell.html
86
+ src/idp/web/__init__.py
87
+ src/idp/web/admin.py
88
+ src/idp/web/api.py
89
+ src/idp/web/developer.py
90
+ src/idp/web/oidc.py
91
+ src/idp/web/openapi.py
92
+ src/idp/web/pages.py
93
+ src/idp/web/validation.py
94
+ tests/test_account_area.py
95
+ tests/test_account_attacks.py
96
+ tests/test_account_closing.py
97
+ tests/test_account_connected.py
98
+ tests/test_account_profile.py
99
+ tests/test_account_security.py
100
+ tests/test_app_overview.py
101
+ tests/test_authorization_code.py
102
+ tests/test_authorization_code_attacks.py
103
+ tests/test_backchannel_logout.py
104
+ tests/test_boundary_attacks.py
105
+ tests/test_brand.py
106
+ tests/test_cli.py
107
+ tests/test_console_api.py
108
+ tests/test_console_users.py
109
+ tests/test_credentials.py
110
+ tests/test_data_retention.py
111
+ tests/test_deployment.py
112
+ tests/test_developer_api.py
113
+ tests/test_flows.py
114
+ tests/test_grant_attacks.py
115
+ tests/test_invitations.py
116
+ tests/test_members.py
117
+ tests/test_mfa.py
118
+ tests/test_offline_access.py
119
+ tests/test_openapi.py
120
+ tests/test_own_apps.py
121
+ tests/test_passkeys.py
122
+ tests/test_platform_gates.py
123
+ tests/test_platform_organization.py
124
+ tests/test_plugin_attacks.py
125
+ tests/test_plugins.py
126
+ tests/test_review_rules.py
127
+ tests/test_reviews.py
128
+ tests/test_role_matrix.py
129
+ tests/test_scope_rules.py
130
+ tests/test_security_prompt.py
131
+ tests/test_session_attacks.py
132
+ tests/test_session_check.py
133
+ tests/test_setup.py
134
+ tests/test_token_attacks.py
@@ -0,0 +1,21 @@
1
+ flask>=3.1
2
+ authlib<1.9,>=1.8
3
+ sqlalchemy>=2.0.36
4
+ alembic>=1.14
5
+ psycopg[binary]>=3.2
6
+ pydantic-settings>=2.6
7
+ flask-limiter>=3.8
8
+ requests>=2.32
9
+ segno>=1.6
10
+ email-validator>=2.2
11
+ webauthn<4,>=3.0
12
+ cryptography>=44
13
+ joserfc<2,>=1.7
14
+ gunicorn>=23
15
+
16
+ [dev]
17
+ pytest>=8.3
18
+ ruff>=0.8
19
+ mypy>=1.13
20
+ openapi-spec-validator>=0.7
21
+ jsonschema>=4.23
@@ -0,0 +1 @@
1
+ idp
File without changes