3tears-iam 0.20.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 (50) hide show
  1. 3tears_iam-0.20.0/.gitignore +220 -0
  2. 3tears_iam-0.20.0/LICENSE +21 -0
  3. 3tears_iam-0.20.0/PKG-INFO +141 -0
  4. 3tears_iam-0.20.0/README.md +105 -0
  5. 3tears_iam-0.20.0/pyproject.toml +107 -0
  6. 3tears_iam-0.20.0/src/threetears/iam/__init__.py +20 -0
  7. 3tears_iam-0.20.0/src/threetears/iam/_digest.py +33 -0
  8. 3tears_iam-0.20.0/src/threetears/iam/apikeys.py +89 -0
  9. 3tears_iam-0.20.0/src/threetears/iam/breach.py +185 -0
  10. 3tears_iam-0.20.0/src/threetears/iam/claim_mapping.py +129 -0
  11. 3tears_iam-0.20.0/src/threetears/iam/clientip.py +204 -0
  12. 3tears_iam-0.20.0/src/threetears/iam/dpop.py +179 -0
  13. 3tears_iam-0.20.0/src/threetears/iam/github.py +279 -0
  14. 3tears_iam-0.20.0/src/threetears/iam/oauth_state.py +324 -0
  15. 3tears_iam-0.20.0/src/threetears/iam/oidc.py +317 -0
  16. 3tears_iam-0.20.0/src/threetears/iam/passwords.py +294 -0
  17. 3tears_iam-0.20.0/src/threetears/iam/pkce.py +129 -0
  18. 3tears_iam-0.20.0/src/threetears/iam/py.typed +0 -0
  19. 3tears_iam-0.20.0/src/threetears/iam/rotation.py +328 -0
  20. 3tears_iam-0.20.0/src/threetears/iam/saml.py +255 -0
  21. 3tears_iam-0.20.0/src/threetears/iam/stepup.py +105 -0
  22. 3tears_iam-0.20.0/src/threetears/iam/stores/__init__.py +59 -0
  23. 3tears_iam-0.20.0/src/threetears/iam/stores/base.py +164 -0
  24. 3tears_iam-0.20.0/src/threetears/iam/stores/memory.py +125 -0
  25. 3tears_iam-0.20.0/src/threetears/iam/stores/nats_kv.py +340 -0
  26. 3tears_iam-0.20.0/src/threetears/iam/stores/postgres.py +234 -0
  27. 3tears_iam-0.20.0/src/threetears/iam/tokens.py +652 -0
  28. 3tears_iam-0.20.0/src/threetears/iam/totp.py +141 -0
  29. 3tears_iam-0.20.0/src/threetears/iam/webauthn.py +75 -0
  30. 3tears_iam-0.20.0/tests/enforcement/__init__.py +0 -0
  31. 3tears_iam-0.20.0/tests/enforcement/test_tokens_alg_pinning.py +53 -0
  32. 3tears_iam-0.20.0/tests/unit/test_apikeys.py +75 -0
  33. 3tears_iam-0.20.0/tests/unit/test_breach.py +102 -0
  34. 3tears_iam-0.20.0/tests/unit/test_claim_mapping.py +109 -0
  35. 3tears_iam-0.20.0/tests/unit/test_clientip.py +159 -0
  36. 3tears_iam-0.20.0/tests/unit/test_dpop.py +323 -0
  37. 3tears_iam-0.20.0/tests/unit/test_github.py +183 -0
  38. 3tears_iam-0.20.0/tests/unit/test_oauth_state.py +246 -0
  39. 3tears_iam-0.20.0/tests/unit/test_oidc.py +217 -0
  40. 3tears_iam-0.20.0/tests/unit/test_passwords.py +166 -0
  41. 3tears_iam-0.20.0/tests/unit/test_pkce.py +107 -0
  42. 3tears_iam-0.20.0/tests/unit/test_rotation.py +522 -0
  43. 3tears_iam-0.20.0/tests/unit/test_saml.py +200 -0
  44. 3tears_iam-0.20.0/tests/unit/test_stepup.py +100 -0
  45. 3tears_iam-0.20.0/tests/unit/test_stores_memory.py +171 -0
  46. 3tears_iam-0.20.0/tests/unit/test_stores_nats_kv.py +474 -0
  47. 3tears_iam-0.20.0/tests/unit/test_stores_postgres.py +239 -0
  48. 3tears_iam-0.20.0/tests/unit/test_tokens.py +360 -0
  49. 3tears_iam-0.20.0/tests/unit/test_totp.py +144 -0
  50. 3tears_iam-0.20.0/tests/unit/test_webauthn.py +69 -0
@@ -0,0 +1,220 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ # Anchored: these name top-level build output. Unanchored, `lib/` matches at ANY depth --
18
+ # it swallowed a vendored `.../pako/lib/` tree, and hatchling reads this file with its own
19
+ # matcher that does NOT honour `!` re-inclusion, so the miss reached built artifacts.
20
+ /lib/
21
+ /lib64/
22
+ parts/
23
+ sdist/
24
+ var/
25
+ wheels/
26
+ share/python-wheels/
27
+ *.egg-info/
28
+ .installed.cfg
29
+ *.egg
30
+ MANIFEST
31
+
32
+ # PyInstaller
33
+ # Usually these files are written by a python script from a template
34
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
35
+ *.manifest
36
+ *.spec
37
+
38
+ # Installer logs
39
+ pip-log.txt
40
+ pip-delete-this-directory.txt
41
+
42
+ # Unit test / coverage reports
43
+ htmlcov/
44
+ .tox/
45
+ .nox/
46
+ .coverage
47
+ .coverage.*
48
+ .cache
49
+ nosetests.xml
50
+ coverage.xml
51
+ *.cover
52
+ *.py.cover
53
+ .hypothesis/
54
+ .pytest_cache/
55
+ cover/
56
+
57
+ # Translations
58
+ *.mo
59
+ *.pot
60
+
61
+ # Django stuff:
62
+ *.log
63
+ local_settings.py
64
+ db.sqlite3
65
+ db.sqlite3-journal
66
+
67
+ # Flask stuff:
68
+ instance/
69
+ .webassets-cache
70
+
71
+ # Scrapy stuff:
72
+ .scrapy
73
+
74
+ # Sphinx documentation
75
+ docs/_build/
76
+
77
+ # PyBuilder
78
+ .pybuilder/
79
+ target/
80
+
81
+ # Jupyter Notebook
82
+ .ipynb_checkpoints
83
+
84
+ # IPython
85
+ profile_default/
86
+ ipython_config.py
87
+
88
+ # pyenv
89
+ # For a library or package, you might want to ignore these files since the code is
90
+ # intended to run in multiple environments; otherwise, check them in:
91
+ # .python-version
92
+
93
+ # pipenv
94
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
95
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
96
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
97
+ # install all needed dependencies.
98
+ #Pipfile.lock
99
+
100
+ # UV
101
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
102
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
103
+ # commonly ignored for libraries.
104
+ #uv.lock
105
+
106
+ # poetry
107
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
108
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
109
+ # commonly ignored for libraries.
110
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
111
+ #poetry.lock
112
+ #poetry.toml
113
+
114
+ # pdm
115
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
116
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
117
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
118
+ #pdm.lock
119
+ #pdm.toml
120
+ .pdm-python
121
+ .pdm-build/
122
+
123
+ # pixi
124
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
125
+ #pixi.lock
126
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
127
+ # in the .venv directory. It is recommended not to include this directory in version control.
128
+ .pixi
129
+
130
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
131
+ __pypackages__/
132
+
133
+ # Celery stuff
134
+ celerybeat-schedule
135
+ celerybeat.pid
136
+
137
+ # SageMath parsed files
138
+ *.sage.py
139
+
140
+ # Environments
141
+ .env
142
+ .envrc
143
+ .venv
144
+ env/
145
+ venv/
146
+ ENV/
147
+ env.bak/
148
+ venv.bak/
149
+
150
+ # Spyder project settings
151
+ .spyderproject
152
+ .spyproject
153
+
154
+ # Rope project settings
155
+ .ropeproject
156
+
157
+ # mkdocs documentation
158
+ /site
159
+
160
+ # mypy
161
+ .mypy_cache/
162
+ .dmypy.json
163
+ dmypy.json
164
+
165
+ # Pyre type checker
166
+ .pyre/
167
+
168
+ # pytype static type analyzer
169
+ .pytype/
170
+
171
+ # Cython debug symbols
172
+ cython_debug/
173
+
174
+ # PyCharm
175
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
176
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
177
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
178
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
179
+ #.idea/
180
+
181
+ # Abstra
182
+ # Abstra is an AI-powered process automation framework.
183
+ # Ignore directories containing user credentials, local state, and settings.
184
+ # Learn more at https://abstra.io/docs
185
+ .abstra/
186
+
187
+ # Visual Studio Code
188
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
189
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
190
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
191
+ # you could uncomment the following to ignore the entire vscode folder
192
+ # .vscode/
193
+
194
+ # Ruff stuff:
195
+ .ruff_cache/
196
+
197
+ # PyPI configuration file
198
+ .pypirc
199
+
200
+ # Cursor
201
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
202
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
203
+ # refer to https://docs.cursor.com/context/ignore-files
204
+ .cursorignore
205
+ .cursorindexingignore
206
+
207
+ # Marimo
208
+ marimo/_static/
209
+ marimo/_lsp/
210
+ __marimo__/
211
+
212
+ # Claude Code local state
213
+ .claude/
214
+
215
+ # prawduct session evidence (local governance artifacts, never shipped)
216
+ .prawduct/
217
+
218
+ # macOS folder metadata
219
+ .DS_Store
220
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mark Pace
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,141 @@
1
+ Metadata-Version: 2.4
2
+ Name: 3tears-iam
3
+ Version: 0.20.0
4
+ Summary: Identity and access primitives: OAuth2/OIDC, SAML, passwords, JWT sessions, DPoP, TOTP, WebAuthn, and the anti-automation controls that guard them
5
+ Project-URL: Repository, https://github.com/pacepace/3tears
6
+ Author: pace
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Framework :: AsyncIO
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Topic :: Security
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.14
18
+ Requires-Dist: 3tears-agent-acl<0.21.0,>=0.20.0
19
+ Requires-Dist: 3tears-nats[client]<0.21.0,>=0.20.0
20
+ Requires-Dist: 3tears-observe<0.21.0,>=0.20.0
21
+ Requires-Dist: 3tears<0.21.0,>=0.20.0
22
+ Requires-Dist: argon2-cffi>=25.1.0
23
+ Requires-Dist: bcrypt>=5.0.0
24
+ Requires-Dist: cryptography>=43.0
25
+ Requires-Dist: httpx>=0.27
26
+ Requires-Dist: joserfc>=1.0
27
+ Requires-Dist: pydantic>=2
28
+ Requires-Dist: pyjwt[crypto]>=2.8
29
+ Requires-Dist: pyotp>=2.9
30
+ Provides-Extra: saml
31
+ Requires-Dist: defusedxml>=0.7; extra == 'saml'
32
+ Requires-Dist: pysaml2>=7.5.0; extra == 'saml'
33
+ Provides-Extra: webauthn
34
+ Requires-Dist: webauthn>=3.0; extra == 'webauthn'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # 3tears-iam
38
+
39
+ `threetears.iam` -- the identity and access primitives every authenticating
40
+ service in the platform needs: password handling, OAuth2/OIDC, SAML, GitHub
41
+ sign-in, session tokens, DPoP, TOTP, WebAuthn, and the anti-automation
42
+ controls that keep all of it from being brute-forced.
43
+
44
+ ## Why this exists
45
+
46
+ Two services in this ecosystem grew their own identity layers independently.
47
+ Both wrote argon2id password hashing with anti-enumeration timing. Both wrote
48
+ a GitHub OAuth2 authorization-code flow. Both wrote a NATS-KV login throttle,
49
+ a single-use SHA-256 ticket store, and a JWT mint/verify pair that pins its
50
+ claim set. Neither could use the other's, because each was welded to its own
51
+ database schema, its own transport, and its own config prefix.
52
+
53
+ That is the failure this package exists to stop. The protocol work -- RFC 7636
54
+ PKCE, RFC 9449 DPoP, RFC 6238 TOTP, OIDC discovery and `id_token` verification,
55
+ SAML assertion handling, the OAuth2 code exchange -- is the same everywhere.
56
+ Getting it subtly wrong is a security bug, and getting it subtly wrong twice
57
+ means fixing it twice, in two repos, on two schedules, and finding out the
58
+ second one was missed during an incident.
59
+
60
+ ## Model
61
+
62
+ The package owns **protocol, crypto, and policy**. It owns nobody's database
63
+ schema and nobody's wire DTOs.
64
+
65
+ That line is deliberate. The two services that seeded this package disagree on
66
+ almost everything below the protocol layer -- one is NATS-RPC-native with a
67
+ multi-tenant Postgres `identity` schema, the other is a FastAPI app with its own
68
+ control plane -- and any attempt to unify their persistence would have produced
69
+ an abstraction neither could use. So state lives behind narrow Protocols
70
+ (`SingleUseTicketStore`, `AttemptLimiter`, `StateStore`), with a NATS-KV
71
+ implementation shipped for the common case and nothing stopping a caller from
72
+ supplying its own.
73
+
74
+ Everything else follows from that:
75
+
76
+ - **Pure functions where the protocol allows it.** PKCE verification, password
77
+ policy, step-up freshness, claim mapping, and API-key hashing take arguments
78
+ and return answers. No I/O, no clock you cannot inject, no global state.
79
+ - **Algorithms are pinned from literals, never read from the input.** A DPoP
80
+ proof does not get to say which algorithm verifies it. An `id_token` does not
81
+ get to select `none`. This mirrors `threetears.core.security.identity_token`'s
82
+ discipline, and the pins are written so a static reader can audit them.
83
+ - **Fail closed by default, and without a side channel.** A malformed stored hash is an
84
+ authentication failure, not a 500. The one place a caller may choose otherwise is
85
+ `NatsKvAttemptLimiter`'s `fail_open`, which exists for a cheap throttle sitting in front
86
+ of an authoritative check -- it defaults to closed, and a counter with nothing behind it
87
+ must leave it that way. A rejected password never says *which*
88
+ rule it broke when saying so would build an oracle. Errors carry structural
89
+ reasons only -- never token strings, key material, or credentials -- so they
90
+ are safe to log at a verification boundary.
91
+ - **Builds on core, does not fork it.** `jwk_thumbprint`, `build_jwks`,
92
+ `generate_signing_keypair`, `ReplayGuard`, `RevocationGuard`, `WindowedCounter`
93
+ and `seal`/`open_secret` already exist in `threetears.core`. This package
94
+ imports them.
95
+
96
+ ## Public surface
97
+
98
+ Imported per module -- `threetears.iam` itself exports only `__version__`, so reach for the
99
+ submodule that owns the thing:
100
+
101
+ ```python
102
+ from threetears.iam.passwords import hash_password
103
+ from threetears.iam.tokens import SessionClaims, mint_session_token
104
+ from threetears.iam.stores.nats_kv import state_store, ticket_store
105
+ ```
106
+
107
+ - **Passwords** (`.passwords`, `.breach`) -- `hash_password`, `verify_password`, `validate_new_password`,
108
+ `normalize_password`, `PasswordVerifyResult`, `PasswordPolicyError`, plus
109
+ `BreachCorpus` for k-anonymity breach screening. argon2id for new hashes,
110
+ bcrypt verify-then-upgrade for migrated ones, NFKC normalization always.
111
+ - **OAuth2 / OIDC** (`.pkce`, `.oidc`, `.github`) -- `PkceChallenge` and the RFC 7636 verifier, `OidcDiscoveryClient`,
112
+ `verify_id_token`, `OidcIdentity`, `GithubOAuth2Client`, `GithubProfile`.
113
+ - **SAML** (`.saml`, extra: `saml`) -- `SamlMetadataResolver`, assertion identity
114
+ extraction, relay-state validation.
115
+ - **Sessions** (`.tokens`, `.rotation`) -- `SessionClaims`, `mint_session_token`,
116
+ `verify_session_token` over EdDSA or HS256, `mint_token_pair`, `TokenPair`,
117
+ `sole_audience`, and `rotate_refresh_token` with reuse detection.
118
+ - **Proof of possession** (`.dpop`) -- `validate_dpop_proof` (RFC 9449, ES256/P-256).
119
+ - **Second factors** (`.totp`, `.webauthn`) -- TOTP enrolment and verification, backup codes, and
120
+ (extra: `webauthn`) passkey registration/assertion helpers.
121
+ - **Anti-automation** (`.stores`, `.clientip`) -- the `AttemptLimiter` Protocol and its
122
+ `NatsKvAttemptLimiter` implementation over `threetears.core.coordination.WindowedCounter`,
123
+ plus `resolve_client_ip` for trusted-proxy-aware rate-limit keying.
124
+ - **Storage seams** (`.stores`) -- `SingleUseTicketStore` and `StateStore` Protocols,
125
+ `hash_ticket`/`new_ticket_secret`, the `threetears.iam.stores.nats_kv` implementations with
126
+ their `state_store`/`ticket_store` factories, and in-memory doubles in
127
+ `threetears.iam.stores.memory` for consumer tests.
128
+
129
+ ## Install
130
+
131
+ ```bash
132
+ pip install 3tears-iam
133
+ pip install '3tears-iam[saml]' # adds pysaml2; needs the xmlsec1 system binary
134
+ pip install '3tears-iam[webauthn]' # adds passkey support
135
+ ```
136
+
137
+ ## Versioning policy
138
+
139
+ `3tears-iam` versions in lockstep with the rest of the 3tears monorepo: every
140
+ package shares one version, tracking the framework git tag. All packages move
141
+ together.
@@ -0,0 +1,105 @@
1
+ # 3tears-iam
2
+
3
+ `threetears.iam` -- the identity and access primitives every authenticating
4
+ service in the platform needs: password handling, OAuth2/OIDC, SAML, GitHub
5
+ sign-in, session tokens, DPoP, TOTP, WebAuthn, and the anti-automation
6
+ controls that keep all of it from being brute-forced.
7
+
8
+ ## Why this exists
9
+
10
+ Two services in this ecosystem grew their own identity layers independently.
11
+ Both wrote argon2id password hashing with anti-enumeration timing. Both wrote
12
+ a GitHub OAuth2 authorization-code flow. Both wrote a NATS-KV login throttle,
13
+ a single-use SHA-256 ticket store, and a JWT mint/verify pair that pins its
14
+ claim set. Neither could use the other's, because each was welded to its own
15
+ database schema, its own transport, and its own config prefix.
16
+
17
+ That is the failure this package exists to stop. The protocol work -- RFC 7636
18
+ PKCE, RFC 9449 DPoP, RFC 6238 TOTP, OIDC discovery and `id_token` verification,
19
+ SAML assertion handling, the OAuth2 code exchange -- is the same everywhere.
20
+ Getting it subtly wrong is a security bug, and getting it subtly wrong twice
21
+ means fixing it twice, in two repos, on two schedules, and finding out the
22
+ second one was missed during an incident.
23
+
24
+ ## Model
25
+
26
+ The package owns **protocol, crypto, and policy**. It owns nobody's database
27
+ schema and nobody's wire DTOs.
28
+
29
+ That line is deliberate. The two services that seeded this package disagree on
30
+ almost everything below the protocol layer -- one is NATS-RPC-native with a
31
+ multi-tenant Postgres `identity` schema, the other is a FastAPI app with its own
32
+ control plane -- and any attempt to unify their persistence would have produced
33
+ an abstraction neither could use. So state lives behind narrow Protocols
34
+ (`SingleUseTicketStore`, `AttemptLimiter`, `StateStore`), with a NATS-KV
35
+ implementation shipped for the common case and nothing stopping a caller from
36
+ supplying its own.
37
+
38
+ Everything else follows from that:
39
+
40
+ - **Pure functions where the protocol allows it.** PKCE verification, password
41
+ policy, step-up freshness, claim mapping, and API-key hashing take arguments
42
+ and return answers. No I/O, no clock you cannot inject, no global state.
43
+ - **Algorithms are pinned from literals, never read from the input.** A DPoP
44
+ proof does not get to say which algorithm verifies it. An `id_token` does not
45
+ get to select `none`. This mirrors `threetears.core.security.identity_token`'s
46
+ discipline, and the pins are written so a static reader can audit them.
47
+ - **Fail closed by default, and without a side channel.** A malformed stored hash is an
48
+ authentication failure, not a 500. The one place a caller may choose otherwise is
49
+ `NatsKvAttemptLimiter`'s `fail_open`, which exists for a cheap throttle sitting in front
50
+ of an authoritative check -- it defaults to closed, and a counter with nothing behind it
51
+ must leave it that way. A rejected password never says *which*
52
+ rule it broke when saying so would build an oracle. Errors carry structural
53
+ reasons only -- never token strings, key material, or credentials -- so they
54
+ are safe to log at a verification boundary.
55
+ - **Builds on core, does not fork it.** `jwk_thumbprint`, `build_jwks`,
56
+ `generate_signing_keypair`, `ReplayGuard`, `RevocationGuard`, `WindowedCounter`
57
+ and `seal`/`open_secret` already exist in `threetears.core`. This package
58
+ imports them.
59
+
60
+ ## Public surface
61
+
62
+ Imported per module -- `threetears.iam` itself exports only `__version__`, so reach for the
63
+ submodule that owns the thing:
64
+
65
+ ```python
66
+ from threetears.iam.passwords import hash_password
67
+ from threetears.iam.tokens import SessionClaims, mint_session_token
68
+ from threetears.iam.stores.nats_kv import state_store, ticket_store
69
+ ```
70
+
71
+ - **Passwords** (`.passwords`, `.breach`) -- `hash_password`, `verify_password`, `validate_new_password`,
72
+ `normalize_password`, `PasswordVerifyResult`, `PasswordPolicyError`, plus
73
+ `BreachCorpus` for k-anonymity breach screening. argon2id for new hashes,
74
+ bcrypt verify-then-upgrade for migrated ones, NFKC normalization always.
75
+ - **OAuth2 / OIDC** (`.pkce`, `.oidc`, `.github`) -- `PkceChallenge` and the RFC 7636 verifier, `OidcDiscoveryClient`,
76
+ `verify_id_token`, `OidcIdentity`, `GithubOAuth2Client`, `GithubProfile`.
77
+ - **SAML** (`.saml`, extra: `saml`) -- `SamlMetadataResolver`, assertion identity
78
+ extraction, relay-state validation.
79
+ - **Sessions** (`.tokens`, `.rotation`) -- `SessionClaims`, `mint_session_token`,
80
+ `verify_session_token` over EdDSA or HS256, `mint_token_pair`, `TokenPair`,
81
+ `sole_audience`, and `rotate_refresh_token` with reuse detection.
82
+ - **Proof of possession** (`.dpop`) -- `validate_dpop_proof` (RFC 9449, ES256/P-256).
83
+ - **Second factors** (`.totp`, `.webauthn`) -- TOTP enrolment and verification, backup codes, and
84
+ (extra: `webauthn`) passkey registration/assertion helpers.
85
+ - **Anti-automation** (`.stores`, `.clientip`) -- the `AttemptLimiter` Protocol and its
86
+ `NatsKvAttemptLimiter` implementation over `threetears.core.coordination.WindowedCounter`,
87
+ plus `resolve_client_ip` for trusted-proxy-aware rate-limit keying.
88
+ - **Storage seams** (`.stores`) -- `SingleUseTicketStore` and `StateStore` Protocols,
89
+ `hash_ticket`/`new_ticket_secret`, the `threetears.iam.stores.nats_kv` implementations with
90
+ their `state_store`/`ticket_store` factories, and in-memory doubles in
91
+ `threetears.iam.stores.memory` for consumer tests.
92
+
93
+ ## Install
94
+
95
+ ```bash
96
+ pip install 3tears-iam
97
+ pip install '3tears-iam[saml]' # adds pysaml2; needs the xmlsec1 system binary
98
+ pip install '3tears-iam[webauthn]' # adds passkey support
99
+ ```
100
+
101
+ ## Versioning policy
102
+
103
+ `3tears-iam` versions in lockstep with the rest of the 3tears monorepo: every
104
+ package shares one version, tracking the framework git tag. All packages move
105
+ together.
@@ -0,0 +1,107 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "3tears-iam"
7
+ version = "0.20.0"
8
+ description = "Identity and access primitives: OAuth2/OIDC, SAML, passwords, JWT sessions, DPoP, TOTP, WebAuthn, and the anti-automation controls that guard them"
9
+ readme = "README.md"
10
+ requires-python = ">=3.14"
11
+ authors = [{name = "pace"}]
12
+ license = "MIT"
13
+ license-files = ["LICENSE"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Framework :: AsyncIO",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.14",
20
+ "Topic :: Security",
21
+ "Topic :: Software Development :: Libraries",
22
+ "Typing :: Typed",
23
+ ]
24
+ dependencies = [
25
+ # core supplies the crypto this package builds ON rather than beside:
26
+ # jwk_thumbprint/build_jwks/generate_signing_keypair (identity_token),
27
+ # ReplayGuard/RevocationGuard/WindowedCounter (coordination), and
28
+ # seal/open_secret (encryption) for TOTP seed material at rest. Forking
29
+ # any of those would give the platform two answers to the same question.
30
+ "3tears>=0.20.0,<0.21.0",
31
+ "3tears-observe>=0.20.0,<0.21.0",
32
+ # the sensitive-action taxonomy. agent-acl's ImpersonationCategory and
33
+ # identity-core's SensitiveActionCategory declared the same six strings
34
+ # independently, each documenting that the copy existed only because no
35
+ # import path connected them. This package is that path, so the enum is
36
+ # imported here and the second declaration goes away.
37
+ "3tears-agent-acl>=0.20.0,<0.21.0",
38
+ # the KV adapters in threetears.iam.stores.nats_kv. Every state this
39
+ # package keeps (auth codes, OAuth state, single-use tickets, attempt
40
+ # counters) is short-lived and TTL'd, which is a KV bucket's job, not a
41
+ # table's -- and both first consumers already run NATS.
42
+ "3tears-nats[client]>=0.20.0,<0.21.0",
43
+ # argon2id for password hashing, bcrypt for verify-then-upgrade of hashes
44
+ # migrated in from an older system. bcrypt is verify-only here: nothing in
45
+ # this package ever WRITES a bcrypt hash.
46
+ "argon2-cffi>=25.1.0",
47
+ "bcrypt>=5.0.0",
48
+ # session tokens (EdDSA or HS256), DPoP proofs, and OIDC id_token
49
+ # verification. [crypto] pulls the asymmetric backends.
50
+ "pyjwt[crypto]>=2.8",
51
+ # Ed25519/EC key handling, and AES-256-GCM for TOTP seeds at rest.
52
+ "cryptography>=43.0",
53
+ # OIDC discovery, the OAuth2 token exchange, provider userinfo/profile
54
+ # fetches, and the k-anonymity breach-corpus range query. Used directly
55
+ # rather than through authlib: the flows here are small enough that a
56
+ # second OAuth framework would be more surface than saved code.
57
+ "httpx>=0.27",
58
+ # TOTP code generation/verification (RFC 6238).
59
+ "pyotp>=2.9",
60
+ # OIDC id_token verification. joserfc rather than pyjwt for this one path: it models a
61
+ # JWKS KeySet and a claims registry directly, which is what an id_token needs, and
62
+ # authlib's own jose module -- the obvious alternative -- is deprecated.
63
+ "joserfc>=1.0",
64
+ # SecretStr on the seed-sealing surface, and the type core's seal/open_secret speak.
65
+ # Declared directly rather than leaned on transitively: this package names it in a
66
+ # public signature, so it is a real dependency of this API, not an implementation detail.
67
+ "pydantic>=2",
68
+ ]
69
+
70
+ [project.optional-dependencies]
71
+ # SAML drags pysaml2 AND the xmlsec1 SYSTEM binary in with it. A consumer that
72
+ # only needs OAuth/OIDC should not inherit an apt-get line, so the SAML service
73
+ # provider lives behind this extra and threetears.iam.saml raises a pointed
74
+ # ImportError when it is missing.
75
+ saml = [
76
+ "pysaml2>=7.5.0",
77
+ "defusedxml>=0.7",
78
+ ]
79
+ # WebAuthn/passkey registration + assertion verification. Optional for the same
80
+ # reason: a consumer doing password + OIDC only should not carry it.
81
+ webauthn = [
82
+ "webauthn>=3.0",
83
+ ]
84
+
85
+ [project.urls]
86
+ Repository = "https://github.com/pacepace/3tears"
87
+
88
+ [tool.hatch.build.targets.wheel]
89
+ packages = ["src/threetears"]
90
+
91
+ [tool.uv.sources]
92
+ 3tears = { workspace = true }
93
+ 3tears-observe = { workspace = true }
94
+ 3tears-agent-acl = { workspace = true }
95
+ 3tears-nats = { workspace = true }
96
+
97
+ [tool.mypy]
98
+ strict = true
99
+ mypy_path = "src"
100
+ packages = ["threetears.iam"]
101
+ explicit_package_bases = true
102
+ ignore_missing_imports = true
103
+
104
+ # no [tool.pytest.ini_options] block: pytest's rootdir detection picks the
105
+ # closest pyproject with that block, so a per-package one would hide the
106
+ # workspace conftest.py and unregister the canonical pytest_plugins line.
107
+ # inherit from the workspace instead.
@@ -0,0 +1,20 @@
1
+ """3tears-iam: identity and access primitives.
2
+
3
+ Protocol, crypto, and policy for authenticating callers -- passwords, OAuth2/
4
+ OIDC, SAML, session tokens, DPoP, TOTP, WebAuthn, and the anti-automation
5
+ controls that guard them.
6
+
7
+ This package owns no database schema and no wire DTOs. State lives behind the
8
+ Protocols in :mod:`threetears.iam.stores`, with a NATS-KV implementation
9
+ supplied for the common case.
10
+ """
11
+
12
+ from importlib.metadata import PackageNotFoundError as _PackageNotFoundError
13
+ from importlib.metadata import version as _version
14
+
15
+ try:
16
+ __version__ = _version("3tears-iam")
17
+ except _PackageNotFoundError: # pragma: no cover - dev fallback
18
+ __version__ = "unknown"
19
+
20
+ __all__ = ["__version__"]
@@ -0,0 +1,33 @@
1
+ """The one place this package turns a high-entropy secret into its stored form.
2
+
3
+ Two public functions need it -- :func:`threetears.iam.stores.base.hash_ticket` and
4
+ :func:`threetears.iam.apikeys.hash_api_key_secret` -- and they had the same body written
5
+ out twice. They keep separate public names because they are separate contracts with
6
+ separate call sites, and because a future decision to pepper one is a decision about that
7
+ one; what they share is the digest, and sharing it means a change to how this package
8
+ hashes cannot land in one of them and miss the other.
9
+
10
+ **SHA-256, not a password KDF, and that is deliberate for both.** Every value passed here
11
+ is 256 bits of generated randomness, so there is no dictionary for an attacker to run and
12
+ nothing for a slow KDF to slow down. What the stores need instead is an equality lookup,
13
+ which a per-candidate KDF run would make impossible. A user-chosen password is the opposite
14
+ case in every respect and goes through :mod:`threetears.iam.passwords`, which uses
15
+ argon2id. Nothing in this module is appropriate for one.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import hashlib
21
+
22
+ __all__ = ["sha256_hex"]
23
+
24
+
25
+ def sha256_hex(secret: str) -> str:
26
+ """SHA-256 hex digest of ``secret``'s UTF-8 bytes.
27
+
28
+ :param secret: the raw, high-entropy secret.
29
+ :ptype secret: str
30
+ :return: the lowercase hex digest.
31
+ :rtype: str
32
+ """
33
+ return hashlib.sha256(secret.encode("utf-8")).hexdigest()