buntu 0.1.0__py3-none-any.whl

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 (90) hide show
  1. buntu-0.1.0.dist-info/METADATA +141 -0
  2. buntu-0.1.0.dist-info/RECORD +90 -0
  3. buntu-0.1.0.dist-info/WHEEL +5 -0
  4. buntu-0.1.0.dist-info/licenses/LICENSE +21 -0
  5. buntu-0.1.0.dist-info/top_level.txt +1 -0
  6. idp/__init__.py +0 -0
  7. idp/app.py +114 -0
  8. idp/cli.py +340 -0
  9. idp/config.py +121 -0
  10. idp/db/__init__.py +76 -0
  11. idp/extensions.py +6 -0
  12. idp/models/__init__.py +54 -0
  13. idp/models/account.py +145 -0
  14. idp/models/clients.py +155 -0
  15. idp/models/grants.py +157 -0
  16. idp/models/platform.py +253 -0
  17. idp/models/users.py +87 -0
  18. idp/oauth/__init__.py +0 -0
  19. idp/oauth/claims.py +33 -0
  20. idp/oauth/grants.py +320 -0
  21. idp/oauth/jwt.py +101 -0
  22. idp/oauth/server.py +369 -0
  23. idp/plugins/__init__.py +270 -0
  24. idp/plugins/sync.py +152 -0
  25. idp/security/__init__.py +0 -0
  26. idp/security/crypto.py +30 -0
  27. idp/security/csrf.py +34 -0
  28. idp/security/data/breached.txt.gz +0 -0
  29. idp/security/keys.py +67 -0
  30. idp/security/passwords.py +48 -0
  31. idp/security/tokens.py +40 -0
  32. idp/security/totp.py +56 -0
  33. idp/services/__init__.py +0 -0
  34. idp/services/accounts.py +452 -0
  35. idp/services/activity.py +73 -0
  36. idp/services/audit.py +22 -0
  37. idp/services/backchannel.py +142 -0
  38. idp/services/closing.py +147 -0
  39. idp/services/connected.py +177 -0
  40. idp/services/consent.py +25 -0
  41. idp/services/credentials.py +58 -0
  42. idp/services/devices.py +30 -0
  43. idp/services/mail.py +86 -0
  44. idp/services/mfa.py +155 -0
  45. idp/services/own_apps.py +24 -0
  46. idp/services/passkeys.py +252 -0
  47. idp/services/platform.py +1024 -0
  48. idp/services/profile.py +190 -0
  49. idp/services/retention.py +90 -0
  50. idp/services/security_prompt.py +18 -0
  51. idp/services/session_check.py +91 -0
  52. idp/services/sessions.py +217 -0
  53. idp/services/setup.py +164 -0
  54. idp/static/idp/assets/account-D-MHfC1z.js +1 -0
  55. idp/static/idp/assets/account-DN_Addf0.js +1 -0
  56. idp/static/idp/assets/app-CzfYFveQ.js +147 -0
  57. idp/static/idp/assets/di-D0iZ5SU4.js +1 -0
  58. idp/static/idp/assets/di-dx_XE3Av.js +1 -0
  59. idp/static/idp/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  60. idp/static/idp/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  61. idp/static/idp/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  62. idp/static/idp/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  63. idp/static/idp/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  64. idp/static/idp/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  65. idp/static/idp/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  66. idp/static/idp/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  67. idp/static/idp/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  68. idp/static/idp/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  69. idp/static/idp/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  70. idp/static/idp/assets/i18n-BZyjOlbu.js +1 -0
  71. idp/static/idp/assets/i18n-Dog5o8z6.js +1 -0
  72. idp/static/idp/assets/roboto-latin-variable-ccAYIvAh.woff2 +0 -0
  73. idp/static/idp/assets/schemas-Bl_iabpv.js +65 -0
  74. idp/static/idp/assets/secure-account-Dl1pS_Wo.js +1 -0
  75. idp/static/idp/assets/service-BhG4-X2O.js +1 -0
  76. idp/static/idp/assets/service-vCuiuvPs.js +1 -0
  77. idp/static/idp/assets/theme-toggle-C07m3YIv.js +1 -0
  78. idp/static/idp/assets/two-step-section-DMQmuG2q.js +2 -0
  79. idp/static/idp/assets/webauthn-BQas4tAd.js +1 -0
  80. idp/static/idp/idp.css +2 -0
  81. idp/static/idp/idp.js +2 -0
  82. idp/templates/shell.html +15 -0
  83. idp/web/__init__.py +0 -0
  84. idp/web/admin.py +901 -0
  85. idp/web/api.py +717 -0
  86. idp/web/developer.py +628 -0
  87. idp/web/oidc.py +394 -0
  88. idp/web/openapi.py +1158 -0
  89. idp/web/pages.py +238 -0
  90. idp/web/validation.py +46 -0
@@ -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,90 @@
1
+ buntu-0.1.0.dist-info/licenses/LICENSE,sha256=0i_cY3H9AZOVIDYj7922S_jN83vhQNdPg9sm2HqB4R8,1072
2
+ idp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ idp/app.py,sha256=ztmtbSu1mM2fnfEnB-3oUtbDcjGL_5gld7xX1etNLjo,4989
4
+ idp/cli.py,sha256=VsLT8ANUcaeYYAAE4MrS_pL7m2pSFpqTiDF18MRHFKQ,14995
5
+ idp/config.py,sha256=jYKMUOG7Tz4bbZRcBjgq37QHz2pA7fEcJH9LyKWql9A,5010
6
+ idp/extensions.py,sha256=XO3ARXfgVmdByvuZytY1dmMsNs5yaQweyAxjj3kztyE,211
7
+ idp/db/__init__.py,sha256=28UETZvOaB2CSlUQZz38ZdqZBgVHlK1vNHhuw5scBcM,2232
8
+ idp/models/__init__.py,sha256=B4k0NSAti0bfWyUUHsDKZ0XThE2CjNvvXdznjNga4Jc,1110
9
+ idp/models/account.py,sha256=Y9uhryZ77R_EBEjOguse1FeBbabYNCFpe68ttb3s6tQ,6417
10
+ idp/models/clients.py,sha256=cjAC9iZbYLea_Y1shC0z0UhoPq0_UC_WBI3RrI7MIbo,7750
11
+ idp/models/grants.py,sha256=TIbNw6LrjSAcIhGeiUa3VyHCO25JApLuqcvM6DdHnB0,6912
12
+ idp/models/platform.py,sha256=Ocwwnj9uOuef4W2akmBr6m1nJMMH5OKAX-5XDfUgK-c,11927
13
+ idp/models/users.py,sha256=z3s5P5qYDbAaQ0RuhFu8Bc5uy_qgAjIaMgf0ppbr1u0,4070
14
+ idp/oauth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ idp/oauth/claims.py,sha256=8Uv2i44ivuZwQ_Ts9Ga09GU3CpU_NjrkpYsH2r0djKU,1290
16
+ idp/oauth/grants.py,sha256=97fsLMALs0V0pJRnpLVPOmIYvKSTOunq_3R9pL0NHxE,12494
17
+ idp/oauth/jwt.py,sha256=7gNbEeVfI5j5Lhzjv-02G-LTtL4BPnbcb9dTVHnpLlw,2988
18
+ idp/oauth/server.py,sha256=Z4teDppC24gejuDmt9I0BQyFPW5iByJxVB3JmsxZLSQ,14716
19
+ idp/plugins/__init__.py,sha256=1Xr0cILaW0vPSZHTSCMaYDVt1SxV4gk3oHPoPF4ulKM,8771
20
+ idp/plugins/sync.py,sha256=oeF7HHh1zQBP6b5WDvVIuOptzOj-a_lk0uYeHN11kq4,6572
21
+ idp/security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ idp/security/crypto.py,sha256=7vzVGMni4lfFRrgz938aqbmXmzSxr1g2neytT-1eGsE,867
23
+ idp/security/csrf.py,sha256=BZ60FXYstIX6SXeQE7dFzZ_TLDquvfOPESQvoLh20VI,966
24
+ idp/security/keys.py,sha256=piOIBTtn0lCewBdhBveEr0qRXe_YMmGVS6Fi6JLRNKA,2330
25
+ idp/security/passwords.py,sha256=6lppGt-hqv4h-vES9LaFLj9lbyTyPFhHiNTTP9TfdYI,1517
26
+ idp/security/tokens.py,sha256=vL9YN_imgCexnyg8NhnH17ivc_ZVVNWDdHI0WDPP1QQ,1322
27
+ idp/security/totp.py,sha256=73XDq5kOrg4L4PMNW11YRDeRMd03ekmxabUDs-g8vxU,1940
28
+ idp/security/data/breached.txt.gz,sha256=ACk-h0bGJld_dE3pNxI2a0EDBGt4Hdj0hAMVmF_ekss,681212
29
+ idp/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
+ idp/services/accounts.py,sha256=BZBcoJx0GuwzchRrOyZMRQQPQDzRI10JfGTBginHtsg,17510
31
+ idp/services/activity.py,sha256=Wfet1b0o51N50cOCH0JAW2x7b-VMJOHqxW8XGIv2nHg,2605
32
+ idp/services/audit.py,sha256=xf_bVDvwxfN9vSSLZhrTJSVM_jaG5EI5gl21wkm8yrQ,805
33
+ idp/services/backchannel.py,sha256=oqX9MdIMQvuGuDNkXJOQDq6hFCWCDA1qb1EvheLv1Ec,5159
34
+ idp/services/closing.py,sha256=2v8SOWR-7J5RVy5QQWcHQK1HMg8eNbaQ6WttQYv-ZWQ,5573
35
+ idp/services/connected.py,sha256=rnn47m3Y1s2heNcTEr4celVkLIN_F-HSeGXKBHdFWTk,7387
36
+ idp/services/consent.py,sha256=OrtFtkqIhSOMdJluSBOZX0MGlHU4il5evTh2S4-mJXM,1038
37
+ idp/services/credentials.py,sha256=facYeHwlgOrMyxnE0o6o8X-XnvatzwEvS04HnFS-hdg,2033
38
+ idp/services/devices.py,sha256=KMNeeDJC91wIel5H_3qqUXJNK5RP_l2ow2FsHsAG6jk,1005
39
+ idp/services/mail.py,sha256=DNk0MOcHlu4EN7w_Y-Dr_sRriVTDGB4AaTl4W0siEd0,2902
40
+ idp/services/mfa.py,sha256=4lxgRoYePKdmQmIq6hRhNgLYwQaMRnSyYo6V9QT_bjk,5656
41
+ idp/services/own_apps.py,sha256=8Qdardppnyh7RvyMI-dn77xhXkY7wI8ota1aawxtIyk,787
42
+ idp/services/passkeys.py,sha256=HN5OujIgow8e-jt2NfwmOWyCjS9Nuyh3G1gSsg5TT9o,9333
43
+ idp/services/platform.py,sha256=IrgDowFrk-gO02w05eWDgA5QM0y6bCt3Mw64im-QO7U,42280
44
+ idp/services/profile.py,sha256=uwyP1HBZM2g4GDocowN-1NtQd9r-wiWhXPQ09sJ8kCA,7214
45
+ idp/services/retention.py,sha256=TeETx5o1EO6rsJxeLYKJlE9HeWZcEGk4FTYQ64M0k3s,3523
46
+ idp/services/security_prompt.py,sha256=ozRuF6s-RYbaISO1jgfiAx2Da6Uc_dAzk8EjbiqhZQk,660
47
+ idp/services/session_check.py,sha256=-Nubu-VjwKGfLAJfu-3TmgNH3FFA_sFHBwvIYTCaQzQ,3515
48
+ idp/services/sessions.py,sha256=uQHs9PrWNCjjufZMlft2z4TR8VJnD7YtR6UZfkm6-is,7541
49
+ idp/services/setup.py,sha256=14ThMoLzuGY00EpqrBmEVHrx9R3GYlvKp505v8_UPFU,6535
50
+ idp/static/idp/idp.css,sha256=ZQT1KQWJWUWUcAod-47KVN1h_n8wGm0kUMc_kRK2g84,154483
51
+ idp/static/idp/idp.js,sha256=AkOWkjlv6JWaR_dgsK8EyoujaSpXWikZekCWMPbohvc,62598
52
+ idp/static/idp/assets/account-D-MHfC1z.js,sha256=ovkoMR8zC8ixljQAwng45VAyt3en_TAdZpcZoLFnL50,66
53
+ idp/static/idp/assets/account-DN_Addf0.js,sha256=Q6qyIt_Dvxai63lQdP6FOSl7xe51QmCZaZGtMYTPSM4,48
54
+ idp/static/idp/assets/app-CzfYFveQ.js,sha256=XJTSpi8sU6e05Dr59sDrE8BJSTv8Bgv0FtTxqwckCJ8,194891
55
+ idp/static/idp/assets/di-D0iZ5SU4.js,sha256=8nDpOVoKz3qEbvh3vn1bpCdiYqgEMA6zilcRne2POZ4,75973
56
+ idp/static/idp/assets/di-dx_XE3Av.js,sha256=RyLaiVPqMklSjbVKnriDk7MSIfSyetI6B1v8-ZILgv8,190
57
+ idp/static/idp/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2,sha256=Ixf6S7KTycCxEOGDFdUpI1xHoN3TM4zqPYx5VekniZ4,7420
58
+ idp/static/idp/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2,sha256=aJRDlpSUalidFX7OADCGlgpqQBPXSoE9q3YC79s9jAk,15084
59
+ idp/static/idp/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2,sha256=gk9IW10m4vLaPCsjYTLs4byOTkM3NFKVC7DkBUi0MT8,16512
60
+ idp/static/idp/assets/geist-latin-wght-normal-BgDaEnEv.woff2,sha256=GfnJJUaqMAwxIjXjElrxuBOU2NuaS8SkJc1bZB0tVOE,29400
61
+ idp/static/idp/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2,sha256=zYgAmZBwtynhzAv3pI2mw6wJagRCURcbmnV0tW2W1LQ,6176
62
+ idp/static/idp/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2,sha256=SGZ4f8lS29vVkdaSPWe8IcLYlLk_NKtp0M3yXUe_st8,12940
63
+ idp/static/idp/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2,sha256=GhieuZfD4uzmg3Pjh6-uyehhdCQYbEsas8_3xUumIjs,14696
64
+ idp/static/idp/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2,sha256=aErVtTH4HUPB6McDgmLV23zcH2gAbgTWx3ae-o0zyMw,23128
65
+ idp/static/idp/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2,sha256=W7Ztgxm6FgK7LrZ9zo15wrRoe-jOGDoFdEM-lJs8YK0,5812
66
+ idp/static/idp/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2,sha256=05tgiJqUpSen9zx5iKLW77bAgWFK72g7CzhsAtdMIXU,7696
67
+ idp/static/idp/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2,sha256=j6QOXSSCR3NeuXoL1ZO4hSRAQwYA1roBNkwx_gq8H-E,8004
68
+ idp/static/idp/assets/i18n-BZyjOlbu.js,sha256=lZ_fa1mJ08lnL7-3VeHQ7nIgwPKgSBbix9XwB6by3PY,21873
69
+ idp/static/idp/assets/i18n-Dog5o8z6.js,sha256=-I0bXPYB401JQcWqjnypZSohpKJmWKw05D2Wfxvk-4g,50
70
+ idp/static/idp/assets/roboto-latin-variable-ccAYIvAh.woff2,sha256=FATKNIvXXvg29N2LbyzHGUWGQtEjfDaClrL8ZS3KR9w,43136
71
+ idp/static/idp/assets/schemas-Bl_iabpv.js,sha256=cTP5DkER2DtU8cSec9zPLKJoScFiAx116wkyr85L1Ww,125041
72
+ idp/static/idp/assets/secure-account-Dl1pS_Wo.js,sha256=Z5NW_nQ6I0CmCcyvS_M5Ri5ABziltF8A8cfK2NGFW7o,6925
73
+ idp/static/idp/assets/service-BhG4-X2O.js,sha256=PoHAfvw57_DnpP5_RytoB_Mk9uFVemNRjecZoQFdGAg,55
74
+ idp/static/idp/assets/service-vCuiuvPs.js,sha256=MZgvMN7xMF2DJu7lT4yW8ALTRzwoHt3NrxaT8BXUOWg,2140
75
+ idp/static/idp/assets/theme-toggle-C07m3YIv.js,sha256=YsNcs25XbcgBJ1dkxEL60ntVS4oVKRLXZmmND3Qs6lI,42495
76
+ idp/static/idp/assets/two-step-section-DMQmuG2q.js,sha256=eE-VtR4B8yMs0z3GAAgeYmxj84xx7EgLxCiBy_aMC7o,13404
77
+ idp/static/idp/assets/webauthn-BQas4tAd.js,sha256=dNBLDbM5QRyWuMvbfIcNG774ovX37fGc6mCp6byKQBU,2036
78
+ idp/templates/shell.html,sha256=bpsGx60ROA-1KXOpABIGqtqbB37ujE89visBUwLrSnQ,839
79
+ idp/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
80
+ idp/web/admin.py,sha256=hjUAsbVC67xTjjWx3gJjRJwP5KQ3_GuEP_MDs5ZEm30,34579
81
+ idp/web/api.py,sha256=CvRXtTf0m-TyLnnwCkg6BzKW6rWwm1EtXQK_SDAOwag,25094
82
+ idp/web/developer.py,sha256=SpfZdKaqVNtQW06uA2spLoeuvctFV-JVGYxAnWo_7YE,24589
83
+ idp/web/oidc.py,sha256=g8Xlbvd1g7ZGOX9xZBXkqKhReXyXGLfdHeztCj7DPwU,15107
84
+ idp/web/openapi.py,sha256=49Kz3SCJVC0Fm3uebqvVG_wtpd1bm4AMFSm6dRmEayQ,41127
85
+ idp/web/pages.py,sha256=VrQTLAL2womm8VqXJi2ChnhTut4v0po9tWROQO5i9Xw,7900
86
+ idp/web/validation.py,sha256=5Ky2kZo2d41RbtFgnqsK1ptvZ7zwvrOpYJ_AF7CbFYM,1873
87
+ buntu-0.1.0.dist-info/METADATA,sha256=DUEUW7k-Ltyh4mHhzkaAmXlGW10KVajzZCKVaZMODQk,7473
88
+ buntu-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
89
+ buntu-0.1.0.dist-info/top_level.txt,sha256=JJAnI16L4ryMUzpMjs3enjf_HDoDpuKsOBu4PHxbSng,4
90
+ buntu-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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.
@@ -0,0 +1 @@
1
+ idp
idp/__init__.py ADDED
File without changes
idp/app.py ADDED
@@ -0,0 +1,114 @@
1
+ """The application factory."""
2
+
3
+ import os
4
+ from datetime import timedelta
5
+
6
+ from flask import Flask, Response, jsonify, request
7
+ from werkzeug.middleware.proxy_fix import ProxyFix
8
+
9
+ from idp import cli, plugins
10
+ from idp.config import Settings, get_settings
11
+ from idp.db import db, init_db
12
+ from idp.extensions import limiter
13
+ from idp.oauth.server import init_oauth
14
+ from idp.services import backchannel, session_check
15
+ from idp.services.platform import PlatformError
16
+ from idp.web import admin, api, developer, oidc, openapi, pages
17
+
18
+ # Endpoints a browser application (SPA) calls cross-origin.
19
+ _CORS_PATHS = ("/oauth/token", "/oauth/revoke", "/oauth/userinfo")
20
+
21
+
22
+ def create_app(settings: Settings | None = None) -> Flask:
23
+ settings = settings or get_settings()
24
+ problems = settings.production_problems()
25
+ if problems:
26
+ raise RuntimeError("refusing to start:\n- " + "\n- ".join(problems))
27
+ # Authlib refuses plain HTTP; allowed only for an issuer on this machine (development, tests).
28
+ if settings.local:
29
+ os.environ["AUTHLIB_INSECURE_TRANSPORT"] = "1"
30
+ app = Flask(__name__)
31
+ if settings.trusted_proxies:
32
+ # The client's address and scheme as the proxies saw them, trusting only that many hops.
33
+ n = settings.trusted_proxies
34
+ app.wsgi_app = ProxyFix(app.wsgi_app, x_for=n, x_proto=n) # type: ignore[method-assign]
35
+ app.config.update(
36
+ SECRET_KEY=settings.secret_key.get_secret_value(),
37
+ SESSION_COOKIE_NAME="idp_session",
38
+ SESSION_COOKIE_HTTPONLY=True,
39
+ SESSION_COOKIE_SAMESITE="Lax",
40
+ SESSION_COOKIE_SECURE=settings.secure_cookies,
41
+ PERMANENT_SESSION_LIFETIME=timedelta(seconds=settings.session_ttl),
42
+ RATELIMIT_STORAGE_URI=settings.rate_limit_storage,
43
+ RATELIMIT_ENABLED=settings.rate_limit_enabled,
44
+ )
45
+ app.json.sort_keys = False # type: ignore[attr-defined]
46
+ init_db(app, settings.database_url)
47
+ init_oauth(app)
48
+ limiter.init_app(app)
49
+
50
+ app.register_blueprint(oidc.bp)
51
+ app.register_blueprint(api.bp)
52
+ app.register_blueprint(pages.bp)
53
+ app.register_blueprint(admin.bp)
54
+ app.register_blueprint(developer.bp)
55
+ app.register_blueprint(openapi.bp)
56
+ _init_plugins(app, settings)
57
+
58
+ # A plugin's routes refuse through the same helpers as the developer and admin APIs
59
+ # (require_developer, member_of): answered as those APIs answer, not as a server fault.
60
+ @app.errorhandler(PlatformError)
61
+ def _platform_refusal(error: PlatformError) -> tuple[Response, int]:
62
+ db().rollback()
63
+ return jsonify(error=error.code, **error.details), error.status
64
+
65
+ cli.register(app)
66
+
67
+ # Logout tokens for the sessions this request ended, once it is answered.
68
+ app.after_request(backchannel.flush)
69
+
70
+ @app.after_request
71
+ def _headers(response: Response) -> Response:
72
+ headers = response.headers
73
+ headers.setdefault(
74
+ "Content-Security-Policy",
75
+ "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; "
76
+ "img-src 'self' data: https:; connect-src 'self'; frame-ancestors 'none'; "
77
+ "base-uri 'none'; form-action 'self'",
78
+ )
79
+ # The session check page is meant to be framed by apps; nothing else is.
80
+ if request.path != "/oauth/check-session":
81
+ headers.setdefault("X-Frame-Options", "DENY")
82
+ session_check.sync_cookie(response)
83
+ headers.setdefault("X-Content-Type-Options", "nosniff")
84
+ headers.setdefault("Referrer-Policy", "no-referrer")
85
+ if settings.issuer.startswith("https://"):
86
+ # Not includeSubDomains: other services of the deployment's domain decide for themselves.
87
+ headers.setdefault("Strict-Transport-Security", "max-age=31536000")
88
+ # Avatars are public and their address changes with them: cached like static files.
89
+ if not request.path.startswith(("/static/", "/.well-known/", "/oauth/check-session.js", "/avatars/")):
90
+ headers["Cache-Control"] = "no-store"
91
+ origin = request.headers.get("Origin")
92
+ if origin and origin in settings.cors_origins and request.path in _CORS_PATHS:
93
+ headers["Access-Control-Allow-Origin"] = origin
94
+ headers["Access-Control-Allow-Headers"] = "Authorization, Content-Type"
95
+ headers["Access-Control-Allow-Methods"] = "GET, POST"
96
+ headers["Vary"] = "Origin"
97
+ return response
98
+
99
+ return app
100
+
101
+
102
+ def _init_plugins(app: Flask, settings: Settings) -> None:
103
+ """Load the deployment's plugins, mount their APIs, and sync their catalogue if asked."""
104
+ loaded = plugins.load(settings.plugins)
105
+ plugins.use(loaded)
106
+ for plugin in loaded:
107
+ blueprint = plugin.blueprint()
108
+ if blueprint is not None:
109
+ app.register_blueprint(blueprint, url_prefix=f"/api/plugins/{plugin.id}")
110
+ if loaded and settings.plugins_sync_on_start:
111
+ from idp.plugins.sync import sync
112
+
113
+ with app.app_context():
114
+ sync(loaded)