pico-server-auth 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.
@@ -0,0 +1,15 @@
1
+ """pico-server-auth: embeddable auth server module.
2
+
3
+ Auto-discovered by pico-boot via the ``pico_boot.modules`` entry point.
4
+ Provides JWT issuance, wallet challenge/verify login, password login,
5
+ and JWKS endpoint as FastAPI controllers.
6
+
7
+ Compatible with pico-client-auth — tokens issued here are validated
8
+ by pico-client-auth in the same process or across services.
9
+ """
10
+
11
+ from .challenge_store import ChallengeStore as ChallengeStore
12
+ from .challenge_store import InMemoryChallengeStore as InMemoryChallengeStore
13
+ from .config import ServerAuthSettings as ServerAuthSettings
14
+ from .token_issuer import TokenIssuer as TokenIssuer
15
+ from .wallet_verifier import WalletVerifier as WalletVerifier
@@ -0,0 +1 @@
1
+ __version__ = '0.1.0'
@@ -0,0 +1,57 @@
1
+ import secrets
2
+ import time
3
+ from typing import Protocol
4
+
5
+ from pico_ioc import component
6
+
7
+ from pico_server_auth.config import ServerAuthSettings
8
+
9
+
10
+ class ChallengeStore(Protocol):
11
+ """Protocol for storing and validating auth challenges.
12
+
13
+ Default implementation is in-memory with TTL expiry.
14
+ Replace with a Redis or DB-backed implementation by registering
15
+ your own @component that implements this protocol.
16
+ """
17
+
18
+ def create(self, address: str) -> str: ...
19
+
20
+ def validate(self, address: str, nonce: str) -> bool: ...
21
+
22
+ def cleanup(self) -> int: ...
23
+
24
+
25
+ @component(on_missing_selector=ChallengeStore)
26
+ class InMemoryChallengeStore:
27
+ """In-memory challenge store with TTL-based expiry.
28
+
29
+ Suitable for single-process deployments. For multi-instance setups,
30
+ register a shared-storage implementation of ChallengeStore.
31
+ """
32
+
33
+ def __init__(self, settings: ServerAuthSettings):
34
+ self._ttl = settings.challenge_ttl_seconds
35
+ self._challenges: dict[str, tuple[str, float]] = {}
36
+
37
+ def create(self, address: str) -> str:
38
+ self.cleanup()
39
+ nonce = secrets.token_hex(32)
40
+ self._challenges[address] = (nonce, time.time())
41
+ return nonce
42
+
43
+ def validate(self, address: str, nonce: str) -> bool:
44
+ entry = self._challenges.pop(address, None)
45
+ if entry is None:
46
+ return False
47
+ stored_nonce, created_at = entry
48
+ if time.time() - created_at > self._ttl:
49
+ return False
50
+ return secrets.compare_digest(stored_nonce, nonce)
51
+
52
+ def cleanup(self) -> int:
53
+ now = time.time()
54
+ expired = [k for k, (_, t) in self._challenges.items() if now - t > self._ttl]
55
+ for k in expired:
56
+ del self._challenges[k]
57
+ return len(expired)
@@ -0,0 +1,24 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ from pico_ioc import configured
4
+
5
+
6
+ @configured(target="self", prefix="server_auth", mapping="tree")
7
+ @dataclass
8
+ class ServerAuthSettings:
9
+ """Configuration for pico-server-auth.
10
+
11
+ When embedded alongside pico-client-auth, set ``issuer`` and ``audience``
12
+ to the same values so tokens issued here are accepted by the client middleware.
13
+ """
14
+
15
+ issuer: str = "http://localhost:8100"
16
+ audience: str = "pico"
17
+ algorithm: str = "RS256"
18
+ access_token_expire_minutes: int = 15
19
+ refresh_token_expire_days: int = 7
20
+ challenge_ttl_seconds: int = 60
21
+ auto_create_admin: bool = False
22
+ admin_email: str = "admin@pico.local"
23
+ admin_password: str = "admin"
24
+ supported_wallet_algorithms: list[str] = field(default_factory=lambda: ["ML-DSA-65", "Ed25519", "secp256k1"])
@@ -0,0 +1,129 @@
1
+ from typing import Any
2
+
3
+ from fastapi import HTTPException
4
+ from pico_client_auth import allow_anonymous
5
+ from pico_fastapi import controller, get, post
6
+
7
+ from pico_server_auth.challenge_store import ChallengeStore
8
+ from pico_server_auth.config import ServerAuthSettings
9
+ from pico_server_auth.token_issuer import TokenIssuer
10
+ from pico_server_auth.wallet_verifier import WalletVerifier
11
+
12
+
13
+ @controller(prefix="/auth", tags=["Auth"])
14
+ class AuthController:
15
+ """Auth endpoints — compatible with pico-client-auth validation.
16
+
17
+ JWKS endpoint allows pico-client-auth to validate tokens whether
18
+ this module runs in the same process or as a separate service.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ settings: ServerAuthSettings,
24
+ challenges: ChallengeStore,
25
+ verifier: WalletVerifier,
26
+ issuer: TokenIssuer,
27
+ ):
28
+ self._settings = settings
29
+ self._challenges = challenges
30
+ self._verifier = verifier
31
+ self._issuer = issuer
32
+
33
+ @allow_anonymous
34
+ @get("/jwks")
35
+ async def jwks(self):
36
+ """JWKS endpoint — pico-client-auth fetches this to validate tokens."""
37
+ return self._issuer.jwks()
38
+
39
+ @allow_anonymous
40
+ @post("/challenge")
41
+ async def challenge(self, body: dict[str, Any]):
42
+ """Request a challenge nonce for wallet auth.
43
+
44
+ Body: { "address": "0x..." }
45
+ Returns: { "challenge": "<nonce>", "ttl": 60 }
46
+ """
47
+ address = body.get("address")
48
+ if not address:
49
+ raise HTTPException(status_code=400, detail="address required")
50
+ nonce = self._challenges.create(str(address))
51
+ return {
52
+ "challenge": nonce,
53
+ "ttl": self._settings.challenge_ttl_seconds,
54
+ }
55
+
56
+ @allow_anonymous
57
+ @post("/wallet")
58
+ async def wallet_login(self, body: dict[str, Any]):
59
+ """Verify wallet signature and issue JWT.
60
+
61
+ Body: {
62
+ "address": "0x...",
63
+ "public_key": "<hex>",
64
+ "signature": "<hex>",
65
+ "challenge": "<nonce>",
66
+ "algorithm": "ML-DSA-65" | "Ed25519" | "secp256k1"
67
+ }
68
+ Returns: { "access_token": "...", "refresh_token": "...", "address": "..." }
69
+ """
70
+ address = str(body.get("address", ""))
71
+ public_key_hex = str(body.get("public_key", ""))
72
+ signature_hex = str(body.get("signature", ""))
73
+ challenge_nonce = str(body.get("challenge", ""))
74
+ algorithm = str(body.get("algorithm", "ML-DSA-65"))
75
+
76
+ if not all([address, public_key_hex, signature_hex, challenge_nonce]):
77
+ raise HTTPException(status_code=400, detail="address, public_key, signature, and challenge required")
78
+
79
+ if not self._challenges.validate(address, challenge_nonce):
80
+ raise HTTPException(status_code=401, detail="invalid or expired challenge")
81
+
82
+ try:
83
+ public_key = bytes.fromhex(public_key_hex)
84
+ signature = bytes.fromhex(signature_hex)
85
+ message = challenge_nonce.encode("utf-8")
86
+ except ValueError:
87
+ raise HTTPException(status_code=400, detail="invalid hex encoding")
88
+
89
+ if not self._verifier.verify(algorithm, public_key, message, signature):
90
+ raise HTTPException(status_code=401, detail="signature verification failed")
91
+
92
+ access_token = self._issuer.issue_access_token(
93
+ subject=address,
94
+ role="wallet",
95
+ extra_claims={"algorithm": algorithm, "wallet_address": address},
96
+ )
97
+ refresh_token = self._issuer.issue_refresh_token(subject=address)
98
+
99
+ return {
100
+ "access_token": access_token,
101
+ "refresh_token": refresh_token,
102
+ "address": address,
103
+ "algorithm": algorithm,
104
+ }
105
+
106
+ @allow_anonymous
107
+ @post("/login")
108
+ async def password_login(self, body: dict[str, Any]):
109
+ """Password-based login (for admin bootstrap).
110
+
111
+ Body: { "email": "...", "password": "..." }
112
+ Returns: { "access_token": "...", "refresh_token": "..." }
113
+ """
114
+ email = str(body.get("email", ""))
115
+ password = str(body.get("password", ""))
116
+
117
+ if not self._settings.auto_create_admin:
118
+ raise HTTPException(status_code=403, detail="password login disabled")
119
+
120
+ if email != self._settings.admin_email or password != self._settings.admin_password:
121
+ raise HTTPException(status_code=401, detail="invalid credentials")
122
+
123
+ access_token = self._issuer.issue_access_token(subject=email, role="admin")
124
+ refresh_token = self._issuer.issue_refresh_token(subject=email)
125
+
126
+ return {
127
+ "access_token": access_token,
128
+ "refresh_token": refresh_token,
129
+ }
File without changes
@@ -0,0 +1,100 @@
1
+ import time
2
+ from typing import Any
3
+
4
+ from cryptography.hazmat.primitives import serialization
5
+ from pico_ioc import component
6
+
7
+ from pico_server_auth.config import ServerAuthSettings
8
+
9
+
10
+ @component
11
+ class TokenIssuer:
12
+ """Issues JWT tokens compatible with pico-client-auth validation.
13
+
14
+ Tokens use the same issuer/audience as configured in pico-client-auth,
15
+ so they are accepted transparently whether issued in-process or remotely.
16
+ """
17
+
18
+ def __init__(self, settings: ServerAuthSettings):
19
+ self._settings = settings
20
+ self._private_key: Any = None
21
+ self._public_key: Any = None
22
+ self._jwk: dict | None = None
23
+ self._kid: str = "pico-server-auth-1"
24
+ self._init_keys()
25
+
26
+ def _init_keys(self) -> None:
27
+ alg = self._settings.algorithm
28
+ if alg == "RS256":
29
+ from cryptography.hazmat.primitives.asymmetric import rsa
30
+
31
+ self._private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
32
+ self._public_key = self._private_key.public_key()
33
+ elif alg in ("ML-DSA-65", "ML-DSA-87"):
34
+ pass # TODO: post-quantum key generation
35
+ else:
36
+ raise ValueError(f"unsupported token algorithm: {alg}")
37
+
38
+ def issue_access_token(self, subject: str, role: str = "user", extra_claims: dict | None = None) -> str:
39
+ from jose import jwt
40
+
41
+ now = int(time.time())
42
+ payload = {
43
+ "sub": subject,
44
+ "iss": self._settings.issuer,
45
+ "aud": self._settings.audience,
46
+ "iat": now,
47
+ "exp": now + self._settings.access_token_expire_minutes * 60,
48
+ "role": role,
49
+ **(extra_claims or {}),
50
+ }
51
+
52
+ pem = self._private_key.private_bytes(
53
+ encoding=serialization.Encoding.PEM,
54
+ format=serialization.PrivateFormat.PKCS8,
55
+ encryption_algorithm=serialization.NoEncryption(),
56
+ )
57
+ return jwt.encode(payload, pem, algorithm="RS256", headers={"kid": self._kid})
58
+
59
+ def issue_refresh_token(self, subject: str) -> str:
60
+ from jose import jwt
61
+
62
+ now = int(time.time())
63
+ payload = {
64
+ "sub": subject,
65
+ "iss": self._settings.issuer,
66
+ "aud": self._settings.audience,
67
+ "iat": now,
68
+ "exp": now + self._settings.refresh_token_expire_days * 86400,
69
+ "type": "refresh",
70
+ }
71
+
72
+ pem = self._private_key.private_bytes(
73
+ encoding=serialization.Encoding.PEM,
74
+ format=serialization.PrivateFormat.PKCS8,
75
+ encryption_algorithm=serialization.NoEncryption(),
76
+ )
77
+ return jwt.encode(payload, pem, algorithm="RS256", headers={"kid": self._kid})
78
+
79
+ def jwks(self) -> dict:
80
+ """Returns JWKS for pico-client-auth to validate tokens.
81
+
82
+ When running in the same process, pico-client-auth can fetch
83
+ this from the /auth/jwks endpoint or directly from this component.
84
+ """
85
+ if self._jwk is None:
86
+ pub_numbers = self._public_key.public_numbers()
87
+ import base64
88
+
89
+ def _b64url(n: int, length: int) -> str:
90
+ return base64.urlsafe_b64encode(n.to_bytes(length, "big")).rstrip(b"=").decode()
91
+
92
+ self._jwk = {
93
+ "kty": "RSA",
94
+ "kid": self._kid,
95
+ "alg": "RS256",
96
+ "use": "sig",
97
+ "n": _b64url(pub_numbers.n, 256),
98
+ "e": _b64url(pub_numbers.e, 3),
99
+ }
100
+ return {"keys": [self._jwk]}
@@ -0,0 +1,81 @@
1
+ from pico_ioc import component
2
+
3
+ from pico_server_auth.config import ServerAuthSettings
4
+
5
+
6
+ @component
7
+ class WalletVerifier:
8
+ """Verifies wallet signatures for challenge-response auth.
9
+
10
+ Supports ML-DSA-65 (FIPS 204), Ed25519, and secp256k1.
11
+ Each backend is loaded on first use to avoid hard dependencies.
12
+ """
13
+
14
+ def __init__(self, settings: ServerAuthSettings):
15
+ self._supported = set(settings.supported_wallet_algorithms)
16
+
17
+ def verify(self, algorithm: str, public_key: bytes, message: bytes, signature: bytes) -> bool:
18
+ if algorithm not in self._supported:
19
+ raise ValueError(f"unsupported wallet algorithm: {algorithm}")
20
+
21
+ if algorithm == "ML-DSA-65":
22
+ return self._verify_mldsa65(public_key, message, signature)
23
+ elif algorithm == "Ed25519":
24
+ return self._verify_ed25519(public_key, message, signature)
25
+ elif algorithm == "secp256k1":
26
+ return self._verify_secp256k1(public_key, message, signature)
27
+ else:
28
+ raise ValueError(f"no verifier for algorithm: {algorithm}")
29
+
30
+ @staticmethod
31
+ def _verify_mldsa65(public_key: bytes, message: bytes, signature: bytes) -> bool:
32
+ # Try native SDK backend first
33
+ try:
34
+ from dilithia_sdk.crypto import load_native_crypto_adapter
35
+
36
+ adapter = load_native_crypto_adapter()
37
+ if adapter is not None:
38
+ return adapter.verify_message(
39
+ public_key.hex(),
40
+ message.decode("utf-8", errors="replace"),
41
+ signature.hex(),
42
+ )
43
+ except ImportError:
44
+ pass
45
+
46
+ # Fallback: cryptography lib (when FIPS 204 support is available)
47
+ try:
48
+ from cryptography.hazmat.primitives.asymmetric import mldsa
49
+
50
+ pk = mldsa.MLDSA65PublicKey.from_public_bytes(public_key)
51
+ pk.verify(signature, message)
52
+ return True
53
+ except (ImportError, AttributeError):
54
+ pass
55
+ except Exception:
56
+ return False
57
+
58
+ raise RuntimeError("ML-DSA-65 verification requires a compatible crypto backend")
59
+
60
+ @staticmethod
61
+ def _verify_ed25519(public_key: bytes, message: bytes, signature: bytes) -> bool:
62
+ try:
63
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
64
+
65
+ pk = Ed25519PublicKey.from_public_bytes(public_key)
66
+ pk.verify(signature, message)
67
+ return True
68
+ except Exception:
69
+ return False
70
+
71
+ @staticmethod
72
+ def _verify_secp256k1(public_key: bytes, message: bytes, signature: bytes) -> bool:
73
+ try:
74
+ from cryptography.hazmat.primitives import hashes
75
+ from cryptography.hazmat.primitives.asymmetric import ec
76
+
77
+ pk = ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256K1(), public_key)
78
+ pk.verify(signature, message, ec.ECDSA(hashes.SHA256()))
79
+ return True
80
+ except Exception:
81
+ return False
@@ -0,0 +1,169 @@
1
+ Metadata-Version: 2.4
2
+ Name: pico-server-auth
3
+ Version: 0.1.0
4
+ Summary: Embeddable auth server module for the pico ecosystem — JWT issuance, wallet login, JWKS endpoint
5
+ Author-email: David Perez Cabrera <dperezcabrera@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/dperezcabrera/pico-server-auth
8
+ Project-URL: Documentation, https://dperezcabrera.github.io/pico-server-auth/
9
+ Project-URL: Repository, https://github.com/dperezcabrera/pico-server-auth
10
+ Project-URL: Changelog, https://github.com/dperezcabrera/pico-server-auth/blob/main/CHANGELOG.md
11
+ Project-URL: Issue Tracker, https://github.com/dperezcabrera/pico-server-auth/issues
12
+ Keywords: auth,jwt,wallet,challenge-response,jwks,ml-dsa-65,ed25519,secp256k1,post-quantum,pico-ioc,pico-boot,dependency-injection
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Framework :: AsyncIO
16
+ Classifier: Typing :: Typed
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: License :: OSI Approved :: MIT License
23
+ Classifier: Operating System :: OS Independent
24
+ Requires-Python: >=3.11
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: pico-ioc[yaml]>=2.2.0
28
+ Requires-Dist: pico-boot>=0.1.0
29
+ Requires-Dist: pico-fastapi>=0.1.0
30
+ Requires-Dist: python-jose[cryptography]>=3.3.0
31
+ Requires-Dist: cryptography>=42.0.0
32
+ Requires-Dist: pydantic>=2.0.0
33
+ Provides-Extra: dev
34
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
35
+ Requires-Dist: pytest-asyncio>=0.24.0; extra == "dev"
36
+ Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
37
+ Requires-Dist: ruff>=0.9.0; extra == "dev"
38
+ Requires-Dist: httpx; extra == "dev"
39
+ Dynamic: license-file
40
+
41
+ # pico-server-auth
42
+
43
+ [![PyPI](https://img.shields.io/pypi/v/pico-server-auth.svg)](https://pypi.org/project/pico-server-auth/)
44
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/dperezcabrera/pico-server-auth)
45
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
46
+ ![CI](https://github.com/dperezcabrera/pico-server-auth/actions/workflows/ci.yml/badge.svg)
47
+ [![codecov](https://codecov.io/gh/dperezcabrera/pico-server-auth/branch/main/graph/badge.svg)](https://codecov.io/gh/dperezcabrera/pico-server-auth)
48
+ [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=dperezcabrera_pico-server-auth&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=dperezcabrera_pico-server-auth)
49
+ [![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=dperezcabrera_pico-server-auth&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=dperezcabrera_pico-server-auth)
50
+ [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=dperezcabrera_pico-server-auth&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=dperezcabrera_pico-server-auth)
51
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/pico-server-auth?period=monthly&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=Monthly+downloads)](https://pepy.tech/projects/pico-server-auth)
52
+ [![Docs](https://img.shields.io/badge/Docs-pico--server--auth-blue?style=flat&logo=readthedocs&logoColor=white)](https://dperezcabrera.github.io/pico-server-auth/)
53
+ [![Interactive Lab](https://img.shields.io/badge/Learn-online-green?style=flat&logo=python&logoColor=white)](https://dperezcabrera.github.io/pico-learn/)
54
+
55
+ Embeddable auth server module for the [pico-boot](https://github.com/dperezcabrera/pico-boot) ecosystem.
56
+
57
+ Issues JWT tokens, handles wallet challenge-response login, and exposes JWKS — all compatible with [pico-client-auth](https://github.com/dperezcabrera/pico-client-auth) validation.
58
+
59
+ ## Two deployment modes
60
+
61
+ **Embedded** — add to any pico-boot app, auth runs in the same process:
62
+
63
+ ```python
64
+ container = init(modules=["myapp", "pico_server_auth"], config=config)
65
+ # /auth/jwks, /auth/challenge, /auth/wallet, /auth/login — all available
66
+ # pico-client-auth validates tokens from the same JWKS
67
+ ```
68
+
69
+ **Standalone** — deploy as a separate auth service (like pico-auth):
70
+
71
+ ```python
72
+ container = init(modules=["pico_server_auth"], config=config)
73
+ app = container.get(FastAPI)
74
+ # Other services point pico-client-auth JWKS to this service's /auth/jwks
75
+ ```
76
+
77
+ ## Endpoints
78
+
79
+ ```
80
+ GET /auth/jwks JWKS public keys (pico-client-auth fetches this)
81
+ POST /auth/challenge Request nonce for wallet login
82
+ POST /auth/wallet Verify wallet signature, issue JWT
83
+ POST /auth/login Password login (admin bootstrap)
84
+ ```
85
+
86
+ ## Wallet login flow
87
+
88
+ ```
89
+ Client pico-server-auth
90
+ │ │
91
+ │ POST /auth/challenge │
92
+ │ { address: "0x..." } │
93
+ │───────────────────────────>│
94
+ │ { challenge: "<nonce>" } │
95
+ │<───────────────────────────│
96
+ │ │
97
+ │ sign(nonce) with wallet │
98
+ │ │
99
+ │ POST /auth/wallet │
100
+ │ { address, public_key, │
101
+ │ signature, challenge, │
102
+ │ algorithm: "ML-DSA-65" } │
103
+ │───────────────────────────>│
104
+ │ { access_token, address } │
105
+ │<───────────────────────────│
106
+ ```
107
+
108
+ ## Supported wallet algorithms
109
+
110
+ | Algorithm | Type | Library |
111
+ |-----------|------|---------|
112
+ | ML-DSA-65 | Post-quantum lattice (FIPS 204) | `cryptography` |
113
+ | Ed25519 | Edwards curve | `cryptography` |
114
+ | secp256k1 | Elliptic curve (ECDSA) | `cryptography` |
115
+
116
+ ## Compatibility with pico-client-auth
117
+
118
+ Tokens issued by pico-server-auth are standard JWT (RS256). pico-client-auth validates them by fetching JWKS from the `/auth/jwks` endpoint.
119
+
120
+ **Same process**: pico-client-auth discovers the JWKS endpoint automatically (same FastAPI app).
121
+
122
+ **Separate processes**: configure pico-client-auth to point to the server:
123
+
124
+ ```yaml
125
+ auth_client:
126
+ issuer: "http://auth-server:8100"
127
+ audience: "pico"
128
+ # JWKS fetched from http://auth-server:8100/auth/jwks
129
+ ```
130
+
131
+ ## Challenge store
132
+
133
+ By default, challenges are stored in memory with TTL expiry. For multi-instance deployments, register a custom `ChallengeStore` component:
134
+
135
+ ```python
136
+ @component
137
+ class RedisChallengeStore:
138
+ async def create(self, address: str) -> str: ...
139
+ async def validate(self, address: str, nonce: str) -> bool: ...
140
+ async def cleanup(self) -> int: ...
141
+ ```
142
+
143
+ The in-memory default is replaced automatically via `on_missing_selector`.
144
+
145
+ ## Configuration
146
+
147
+ ```yaml
148
+ server_auth:
149
+ issuer: "http://localhost:8100"
150
+ audience: "pico"
151
+ algorithm: "RS256"
152
+ access_token_expire_minutes: 15
153
+ challenge_ttl_seconds: 60
154
+ supported_wallet_algorithms:
155
+ - "ML-DSA-65"
156
+ - "Ed25519"
157
+ - "secp256k1"
158
+ ```
159
+
160
+ ## Stack
161
+
162
+ - [pico-ioc](https://github.com/dperezcabrera/pico-ioc) — dependency injection
163
+ - [pico-boot](https://github.com/dperezcabrera/pico-boot) — auto-discovery
164
+ - [pico-fastapi](https://github.com/dperezcabrera/pico-fastapi) — controllers
165
+ - [pico-client-auth](https://github.com/dperezcabrera/pico-client-auth) — token validation
166
+
167
+ ## License
168
+
169
+ MIT
@@ -0,0 +1,14 @@
1
+ pico_server_auth/__init__.py,sha256=nxqUtnZj5BU61JTqapLnMYcqQ0mgbMyONiFWvakAarE,689
2
+ pico_server_auth/_version.py,sha256=IMjkMO3twhQzluVTo8Z6rE7Eg-9U79_LGKMcsWLKBkY,22
3
+ pico_server_auth/challenge_store.py,sha256=2bCFn82UrRmQDfrtxOUa0jXBxcAhTy_1rVg0oohjTh4,1771
4
+ pico_server_auth/config.py,sha256=5Qw2hijL3bAeWkwJBMQ062GzEp6aehgBz56DGc_tLw4,834
5
+ pico_server_auth/controllers.py,sha256=tRnDSbmfmaAxzezXMC6v4kaEpYDCLGhbKEQLYG_l4CQ,4692
6
+ pico_server_auth/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ pico_server_auth/token_issuer.py,sha256=LdcvUmvnEOorqu232Hukuz3-_KciajwwxNL62IKojuM,3540
8
+ pico_server_auth/wallet_verifier.py,sha256=4Hgp-eynA6AS3-txaZaH9sRFGg533uh9HMIR3R-OtOw,3025
9
+ pico_server_auth-0.1.0.dist-info/licenses/LICENSE,sha256=N1_nOvHTM6BobYnOTNXiQkroDqCEi6EzfGBv8lWtyZ0,1077
10
+ pico_server_auth-0.1.0.dist-info/METADATA,sha256=RQffrhF89tPNKiKdUw_k029AEZMAIfI6cXW38F6NV20,7461
11
+ pico_server_auth-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
12
+ pico_server_auth-0.1.0.dist-info/entry_points.txt,sha256=7KkDhxu1ehMGvMXuCalYAZhjQ5VBT8GyMrZQNZAshzE,56
13
+ pico_server_auth-0.1.0.dist-info/top_level.txt,sha256=gSTx--rTRb0UA4YxYvCaraOTIHAuTN8OeSobhj7jgDE,17
14
+ pico_server_auth-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [pico_boot.modules]
2
+ pico_server_auth = pico_server_auth
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 David Pérez Cabrera
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
+ pico_server_auth