pico-auth 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.
- pico_auth-0.1.0/LICENSE +21 -0
- pico_auth-0.1.0/MANIFEST.in +18 -0
- pico_auth-0.1.0/PKG-INFO +246 -0
- pico_auth-0.1.0/README.md +203 -0
- pico_auth-0.1.0/application.yaml +22 -0
- pico_auth-0.1.0/pico_auth/__init__.py +29 -0
- pico_auth-0.1.0/pico_auth/config.py +33 -0
- pico_auth-0.1.0/pico_auth/errors.py +42 -0
- pico_auth-0.1.0/pico_auth/jwt_provider.py +137 -0
- pico_auth-0.1.0/pico_auth/local_auth_configurer.py +55 -0
- pico_auth-0.1.0/pico_auth/local_jwks_provider.py +28 -0
- pico_auth-0.1.0/pico_auth/main.py +50 -0
- pico_auth-0.1.0/pico_auth/models.py +28 -0
- pico_auth-0.1.0/pico_auth/passwords.py +24 -0
- pico_auth-0.1.0/pico_auth/repository.py +91 -0
- pico_auth-0.1.0/pico_auth/routes.py +147 -0
- pico_auth-0.1.0/pico_auth/schema.py +11 -0
- pico_auth-0.1.0/pico_auth/service.py +185 -0
- pico_auth-0.1.0/pico_auth.egg-info/PKG-INFO +246 -0
- pico_auth-0.1.0/pico_auth.egg-info/SOURCES.txt +27 -0
- pico_auth-0.1.0/pico_auth.egg-info/dependency_links.txt +1 -0
- pico_auth-0.1.0/pico_auth.egg-info/requires.txt +21 -0
- pico_auth-0.1.0/pico_auth.egg-info/top_level.txt +1 -0
- pico_auth-0.1.0/pyproject.toml +96 -0
- pico_auth-0.1.0/setup.cfg +4 -0
- pico_auth-0.1.0/tests/__init__.py +0 -0
- pico_auth-0.1.0/tests/conftest.py +56 -0
- pico_auth-0.1.0/tests/test_auth_e2e.py +399 -0
- pico_auth-0.1.0/tests/test_coverage_gaps.py +327 -0
pico_auth-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 David Perez
|
|
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,18 @@
|
|
|
1
|
+
include README.md
|
|
2
|
+
include LICENSE
|
|
3
|
+
include application.yaml
|
|
4
|
+
|
|
5
|
+
recursive-include pico_auth *.py
|
|
6
|
+
|
|
7
|
+
recursive-include tests *.py
|
|
8
|
+
|
|
9
|
+
exclude .gitignore
|
|
10
|
+
exclude Dockerfile*
|
|
11
|
+
exclude docker-compose*.yml
|
|
12
|
+
exclude Makefile
|
|
13
|
+
|
|
14
|
+
prune build
|
|
15
|
+
prune dist
|
|
16
|
+
prune .tox
|
|
17
|
+
prune .pytest_cache
|
|
18
|
+
prune __pycache__
|
pico_auth-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pico-auth
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Minimal JWT auth server for the pico ecosystem
|
|
5
|
+
Author: David Perez
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dperezcabrera/pico-auth
|
|
8
|
+
Project-URL: Documentation, https://dperezcabrera.github.io/pico-auth/
|
|
9
|
+
Project-URL: Repository, https://github.com/dperezcabrera/pico-auth
|
|
10
|
+
Project-URL: Issues, https://github.com/dperezcabrera/pico-auth/issues
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Security
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: pico-ioc>=2.1.0
|
|
24
|
+
Requires-Dist: pico-boot>=0.1.0
|
|
25
|
+
Requires-Dist: pico-fastapi>=0.1.0
|
|
26
|
+
Requires-Dist: pico-sqlalchemy>=0.1.0
|
|
27
|
+
Requires-Dist: pico-client-auth>=0.1.0
|
|
28
|
+
Requires-Dist: python-jose[cryptography]>=3.3
|
|
29
|
+
Requires-Dist: bcrypt>=4.0
|
|
30
|
+
Requires-Dist: aiosqlite>=0.19
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
34
|
+
Requires-Dist: httpx>=0.24; extra == "dev"
|
|
35
|
+
Requires-Dist: ruff; extra == "dev"
|
|
36
|
+
Requires-Dist: tox; extra == "dev"
|
|
37
|
+
Provides-Extra: docs
|
|
38
|
+
Requires-Dist: mkdocs-material>=9.0; extra == "docs"
|
|
39
|
+
Requires-Dist: mkdocstrings[python]>=0.24; extra == "docs"
|
|
40
|
+
Requires-Dist: mkdocs-minify-plugin; extra == "docs"
|
|
41
|
+
Requires-Dist: mkdocs-git-revision-date-localized-plugin; extra == "docs"
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
|
|
44
|
+
# Pico-Auth
|
|
45
|
+
|
|
46
|
+
[](https://pypi.org/project/pico-auth/)
|
|
47
|
+
[](https://deepwiki.com/dperezcabrera/pico-auth)
|
|
48
|
+
[](https://opensource.org/licenses/MIT)
|
|
49
|
+

|
|
50
|
+
[](https://codecov.io/gh/dperezcabrera/pico-auth)
|
|
51
|
+
[](https://dperezcabrera.github.io/pico-auth/)
|
|
52
|
+
|
|
53
|
+
**Minimal JWT auth server for the Pico ecosystem.**
|
|
54
|
+
|
|
55
|
+
Pico-Auth is a ready-to-run authentication server built on top of the [pico-framework](https://github.com/dperezcabrera/pico-ioc) stack. It provides:
|
|
56
|
+
|
|
57
|
+
- **RS256 JWT tokens** with auto-generated RSA key pairs
|
|
58
|
+
- **Refresh token rotation** with SHA-256 hashed storage
|
|
59
|
+
- **RBAC** with four built-in roles: `superadmin`, `org_admin`, `operator`, `viewer`
|
|
60
|
+
- **OIDC discovery** endpoints (`.well-known/openid-configuration`, JWKS)
|
|
61
|
+
- **Bcrypt password hashing** (72-byte input limit enforced)
|
|
62
|
+
- **Zero-config startup** with auto-created admin user
|
|
63
|
+
|
|
64
|
+
> Requires Python 3.11+
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Architecture
|
|
69
|
+
|
|
70
|
+
Pico-Auth uses the full Pico stack with dependency injection:
|
|
71
|
+
|
|
72
|
+
| Layer | Component | Decorator |
|
|
73
|
+
|-------|-----------|-----------|
|
|
74
|
+
| Config | `AuthSettings` | `@configured(prefix="auth")` |
|
|
75
|
+
| Models | `User`, `RefreshToken` | SQLAlchemy `AppBase` |
|
|
76
|
+
| Repository | `UserRepository`, `RefreshTokenRepository` | `@component` |
|
|
77
|
+
| Service | `AuthService` | `@component` |
|
|
78
|
+
| Security | `JWTProvider`, `PasswordService`, `LocalJWKSProvider` | `@component` |
|
|
79
|
+
| Routes | `AuthController`, `OIDCController` | `@controller` |
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Installation
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install -e ".[dev]"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Quick Start
|
|
92
|
+
|
|
93
|
+
### 1. Run the Server
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
python -m pico_auth.main
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The server starts on `http://localhost:8100` with:
|
|
100
|
+
- An auto-created admin user (`admin@pico.local` / `admin`)
|
|
101
|
+
- SQLite database at `auth.db`
|
|
102
|
+
- RSA keys at `~/.pico-auth/`
|
|
103
|
+
|
|
104
|
+
### 2. Register a User
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
curl -X POST http://localhost:8100/api/v1/auth/register \
|
|
108
|
+
-H "Content-Type: application/json" \
|
|
109
|
+
-d '{"email": "alice@example.com", "password": "secret123", "display_name": "Alice"}'
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 3. Login
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
curl -X POST http://localhost:8100/api/v1/auth/login \
|
|
116
|
+
-H "Content-Type: application/json" \
|
|
117
|
+
-d '{"email": "alice@example.com", "password": "secret123"}'
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"access_token": "eyJhbGciOiJSUzI1NiIs...",
|
|
124
|
+
"refresh_token": "a1b2c3d4...",
|
|
125
|
+
"token_type": "Bearer",
|
|
126
|
+
"expires_in": 900
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 4. Access Protected Endpoint
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
curl http://localhost:8100/api/v1/auth/me \
|
|
134
|
+
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## API Endpoints
|
|
140
|
+
|
|
141
|
+
| Method | Path | Auth | Description |
|
|
142
|
+
|--------|------|------|-------------|
|
|
143
|
+
| POST | `/api/v1/auth/register` | No | Register a new user |
|
|
144
|
+
| POST | `/api/v1/auth/login` | No | Login and get tokens |
|
|
145
|
+
| POST | `/api/v1/auth/refresh` | No | Refresh access token |
|
|
146
|
+
| GET | `/api/v1/auth/me` | Bearer | Get current user profile |
|
|
147
|
+
| POST | `/api/v1/auth/me/password` | Bearer | Change password |
|
|
148
|
+
| GET | `/api/v1/auth/users` | Admin | List all users |
|
|
149
|
+
| PUT | `/api/v1/auth/users/{id}/role` | Admin | Update user role |
|
|
150
|
+
| GET | `/api/v1/auth/jwks` | No | JSON Web Key Set |
|
|
151
|
+
| GET | `/.well-known/openid-configuration` | No | OIDC discovery |
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Configuration
|
|
156
|
+
|
|
157
|
+
All settings are loaded from `application.yaml` and can be overridden with environment variables:
|
|
158
|
+
|
|
159
|
+
```yaml
|
|
160
|
+
auth:
|
|
161
|
+
data_dir: "~/.pico-auth" # RSA key storage
|
|
162
|
+
access_token_expire_minutes: 15 # JWT lifetime
|
|
163
|
+
refresh_token_expire_days: 7 # Refresh token lifetime
|
|
164
|
+
issuer: "http://localhost:8100" # JWT issuer claim
|
|
165
|
+
audience: "pico-bot" # JWT audience claim
|
|
166
|
+
auto_create_admin: true # Create admin on startup
|
|
167
|
+
admin_email: "admin@pico.local" # Default admin email
|
|
168
|
+
admin_password: "admin" # Default admin password
|
|
169
|
+
|
|
170
|
+
database:
|
|
171
|
+
url: "sqlite+aiosqlite:///auth.db" # Database URL
|
|
172
|
+
echo: false # SQL logging
|
|
173
|
+
|
|
174
|
+
auth_client:
|
|
175
|
+
enabled: true # Enable auth middleware
|
|
176
|
+
issuer: "http://localhost:8100" # Must match auth.issuer
|
|
177
|
+
audience: "pico-bot" # Must match auth.audience
|
|
178
|
+
|
|
179
|
+
fastapi:
|
|
180
|
+
title: "Pico Auth API"
|
|
181
|
+
version: "0.1.0"
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Environment variable override example:
|
|
185
|
+
```bash
|
|
186
|
+
AUTH_ISSUER=https://auth.myapp.com AUTH_ADMIN_PASSWORD=strong-password python -m pico_auth.main
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## JWT Token Claims
|
|
192
|
+
|
|
193
|
+
Access tokens include:
|
|
194
|
+
|
|
195
|
+
| Claim | Description |
|
|
196
|
+
|-------|-------------|
|
|
197
|
+
| `sub` | User ID |
|
|
198
|
+
| `email` | User email |
|
|
199
|
+
| `role` | User role (`superadmin`, `org_admin`, `operator`, `viewer`) |
|
|
200
|
+
| `org_id` | Organization ID |
|
|
201
|
+
| `iss` | Issuer URL |
|
|
202
|
+
| `aud` | Audience |
|
|
203
|
+
| `iat` | Issued at (Unix timestamp) |
|
|
204
|
+
| `exp` | Expiration (Unix timestamp) |
|
|
205
|
+
| `jti` | Unique token ID |
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Ecosystem
|
|
210
|
+
|
|
211
|
+
Pico-Auth is built on:
|
|
212
|
+
|
|
213
|
+
| Package | Role |
|
|
214
|
+
|---------|------|
|
|
215
|
+
| [pico-ioc](https://github.com/dperezcabrera/pico-ioc) | Dependency injection container |
|
|
216
|
+
| [pico-boot](https://github.com/dperezcabrera/pico-boot) | Bootstrap and plugin discovery |
|
|
217
|
+
| [pico-fastapi](https://github.com/dperezcabrera/pico-fastapi) | FastAPI integration with `@controller` |
|
|
218
|
+
| [pico-sqlalchemy](https://github.com/dperezcabrera/pico-sqlalchemy) | Async SQLAlchemy with `SessionManager` |
|
|
219
|
+
| [pico-client-auth](https://github.com/dperezcabrera/pico-client-auth) | JWT auth middleware with `SecurityContext` |
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Development
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
# Install in dev mode
|
|
227
|
+
pip install -e ".[dev]"
|
|
228
|
+
|
|
229
|
+
# Run tests
|
|
230
|
+
pytest tests/ -v
|
|
231
|
+
|
|
232
|
+
# Run with coverage
|
|
233
|
+
pytest --cov=pico_auth --cov-report=term-missing tests/
|
|
234
|
+
|
|
235
|
+
# Full test matrix
|
|
236
|
+
tox
|
|
237
|
+
|
|
238
|
+
# Lint
|
|
239
|
+
ruff check pico_auth/ tests/
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## License
|
|
245
|
+
|
|
246
|
+
MIT - [LICENSE](./LICENSE)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Pico-Auth
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/pico-auth/)
|
|
4
|
+
[](https://deepwiki.com/dperezcabrera/pico-auth)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+

|
|
7
|
+
[](https://codecov.io/gh/dperezcabrera/pico-auth)
|
|
8
|
+
[](https://dperezcabrera.github.io/pico-auth/)
|
|
9
|
+
|
|
10
|
+
**Minimal JWT auth server for the Pico ecosystem.**
|
|
11
|
+
|
|
12
|
+
Pico-Auth is a ready-to-run authentication server built on top of the [pico-framework](https://github.com/dperezcabrera/pico-ioc) stack. It provides:
|
|
13
|
+
|
|
14
|
+
- **RS256 JWT tokens** with auto-generated RSA key pairs
|
|
15
|
+
- **Refresh token rotation** with SHA-256 hashed storage
|
|
16
|
+
- **RBAC** with four built-in roles: `superadmin`, `org_admin`, `operator`, `viewer`
|
|
17
|
+
- **OIDC discovery** endpoints (`.well-known/openid-configuration`, JWKS)
|
|
18
|
+
- **Bcrypt password hashing** (72-byte input limit enforced)
|
|
19
|
+
- **Zero-config startup** with auto-created admin user
|
|
20
|
+
|
|
21
|
+
> Requires Python 3.11+
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Architecture
|
|
26
|
+
|
|
27
|
+
Pico-Auth uses the full Pico stack with dependency injection:
|
|
28
|
+
|
|
29
|
+
| Layer | Component | Decorator |
|
|
30
|
+
|-------|-----------|-----------|
|
|
31
|
+
| Config | `AuthSettings` | `@configured(prefix="auth")` |
|
|
32
|
+
| Models | `User`, `RefreshToken` | SQLAlchemy `AppBase` |
|
|
33
|
+
| Repository | `UserRepository`, `RefreshTokenRepository` | `@component` |
|
|
34
|
+
| Service | `AuthService` | `@component` |
|
|
35
|
+
| Security | `JWTProvider`, `PasswordService`, `LocalJWKSProvider` | `@component` |
|
|
36
|
+
| Routes | `AuthController`, `OIDCController` | `@controller` |
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install -e ".[dev]"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
### 1. Run the Server
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
python -m pico_auth.main
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The server starts on `http://localhost:8100` with:
|
|
57
|
+
- An auto-created admin user (`admin@pico.local` / `admin`)
|
|
58
|
+
- SQLite database at `auth.db`
|
|
59
|
+
- RSA keys at `~/.pico-auth/`
|
|
60
|
+
|
|
61
|
+
### 2. Register a User
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
curl -X POST http://localhost:8100/api/v1/auth/register \
|
|
65
|
+
-H "Content-Type: application/json" \
|
|
66
|
+
-d '{"email": "alice@example.com", "password": "secret123", "display_name": "Alice"}'
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 3. Login
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
curl -X POST http://localhost:8100/api/v1/auth/login \
|
|
73
|
+
-H "Content-Type: application/json" \
|
|
74
|
+
-d '{"email": "alice@example.com", "password": "secret123"}'
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"access_token": "eyJhbGciOiJSUzI1NiIs...",
|
|
81
|
+
"refresh_token": "a1b2c3d4...",
|
|
82
|
+
"token_type": "Bearer",
|
|
83
|
+
"expires_in": 900
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 4. Access Protected Endpoint
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
curl http://localhost:8100/api/v1/auth/me \
|
|
91
|
+
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## API Endpoints
|
|
97
|
+
|
|
98
|
+
| Method | Path | Auth | Description |
|
|
99
|
+
|--------|------|------|-------------|
|
|
100
|
+
| POST | `/api/v1/auth/register` | No | Register a new user |
|
|
101
|
+
| POST | `/api/v1/auth/login` | No | Login and get tokens |
|
|
102
|
+
| POST | `/api/v1/auth/refresh` | No | Refresh access token |
|
|
103
|
+
| GET | `/api/v1/auth/me` | Bearer | Get current user profile |
|
|
104
|
+
| POST | `/api/v1/auth/me/password` | Bearer | Change password |
|
|
105
|
+
| GET | `/api/v1/auth/users` | Admin | List all users |
|
|
106
|
+
| PUT | `/api/v1/auth/users/{id}/role` | Admin | Update user role |
|
|
107
|
+
| GET | `/api/v1/auth/jwks` | No | JSON Web Key Set |
|
|
108
|
+
| GET | `/.well-known/openid-configuration` | No | OIDC discovery |
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Configuration
|
|
113
|
+
|
|
114
|
+
All settings are loaded from `application.yaml` and can be overridden with environment variables:
|
|
115
|
+
|
|
116
|
+
```yaml
|
|
117
|
+
auth:
|
|
118
|
+
data_dir: "~/.pico-auth" # RSA key storage
|
|
119
|
+
access_token_expire_minutes: 15 # JWT lifetime
|
|
120
|
+
refresh_token_expire_days: 7 # Refresh token lifetime
|
|
121
|
+
issuer: "http://localhost:8100" # JWT issuer claim
|
|
122
|
+
audience: "pico-bot" # JWT audience claim
|
|
123
|
+
auto_create_admin: true # Create admin on startup
|
|
124
|
+
admin_email: "admin@pico.local" # Default admin email
|
|
125
|
+
admin_password: "admin" # Default admin password
|
|
126
|
+
|
|
127
|
+
database:
|
|
128
|
+
url: "sqlite+aiosqlite:///auth.db" # Database URL
|
|
129
|
+
echo: false # SQL logging
|
|
130
|
+
|
|
131
|
+
auth_client:
|
|
132
|
+
enabled: true # Enable auth middleware
|
|
133
|
+
issuer: "http://localhost:8100" # Must match auth.issuer
|
|
134
|
+
audience: "pico-bot" # Must match auth.audience
|
|
135
|
+
|
|
136
|
+
fastapi:
|
|
137
|
+
title: "Pico Auth API"
|
|
138
|
+
version: "0.1.0"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Environment variable override example:
|
|
142
|
+
```bash
|
|
143
|
+
AUTH_ISSUER=https://auth.myapp.com AUTH_ADMIN_PASSWORD=strong-password python -m pico_auth.main
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## JWT Token Claims
|
|
149
|
+
|
|
150
|
+
Access tokens include:
|
|
151
|
+
|
|
152
|
+
| Claim | Description |
|
|
153
|
+
|-------|-------------|
|
|
154
|
+
| `sub` | User ID |
|
|
155
|
+
| `email` | User email |
|
|
156
|
+
| `role` | User role (`superadmin`, `org_admin`, `operator`, `viewer`) |
|
|
157
|
+
| `org_id` | Organization ID |
|
|
158
|
+
| `iss` | Issuer URL |
|
|
159
|
+
| `aud` | Audience |
|
|
160
|
+
| `iat` | Issued at (Unix timestamp) |
|
|
161
|
+
| `exp` | Expiration (Unix timestamp) |
|
|
162
|
+
| `jti` | Unique token ID |
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Ecosystem
|
|
167
|
+
|
|
168
|
+
Pico-Auth is built on:
|
|
169
|
+
|
|
170
|
+
| Package | Role |
|
|
171
|
+
|---------|------|
|
|
172
|
+
| [pico-ioc](https://github.com/dperezcabrera/pico-ioc) | Dependency injection container |
|
|
173
|
+
| [pico-boot](https://github.com/dperezcabrera/pico-boot) | Bootstrap and plugin discovery |
|
|
174
|
+
| [pico-fastapi](https://github.com/dperezcabrera/pico-fastapi) | FastAPI integration with `@controller` |
|
|
175
|
+
| [pico-sqlalchemy](https://github.com/dperezcabrera/pico-sqlalchemy) | Async SQLAlchemy with `SessionManager` |
|
|
176
|
+
| [pico-client-auth](https://github.com/dperezcabrera/pico-client-auth) | JWT auth middleware with `SecurityContext` |
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Development
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
# Install in dev mode
|
|
184
|
+
pip install -e ".[dev]"
|
|
185
|
+
|
|
186
|
+
# Run tests
|
|
187
|
+
pytest tests/ -v
|
|
188
|
+
|
|
189
|
+
# Run with coverage
|
|
190
|
+
pytest --cov=pico_auth --cov-report=term-missing tests/
|
|
191
|
+
|
|
192
|
+
# Full test matrix
|
|
193
|
+
tox
|
|
194
|
+
|
|
195
|
+
# Lint
|
|
196
|
+
ruff check pico_auth/ tests/
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
MIT - [LICENSE](./LICENSE)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
auth:
|
|
2
|
+
data_dir: "~/.pico-auth"
|
|
3
|
+
access_token_expire_minutes: 15
|
|
4
|
+
refresh_token_expire_days: 7
|
|
5
|
+
issuer: "http://localhost:8100"
|
|
6
|
+
audience: "pico-bot"
|
|
7
|
+
auto_create_admin: true
|
|
8
|
+
admin_email: "admin@pico.local"
|
|
9
|
+
admin_password: "admin"
|
|
10
|
+
|
|
11
|
+
database:
|
|
12
|
+
url: "sqlite+aiosqlite:///auth.db"
|
|
13
|
+
echo: false
|
|
14
|
+
|
|
15
|
+
auth_client:
|
|
16
|
+
enabled: true
|
|
17
|
+
issuer: "http://localhost:8100"
|
|
18
|
+
audience: "pico-bot"
|
|
19
|
+
|
|
20
|
+
fastapi:
|
|
21
|
+
title: "Pico Auth API"
|
|
22
|
+
version: "0.1.0"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""pico-auth -- minimal JWT auth server for the pico ecosystem."""
|
|
2
|
+
|
|
3
|
+
import pico_auth.local_auth_configurer as local_auth_configurer # noqa: F401 — patches AuthFastapiConfigurer
|
|
4
|
+
from pico_auth.config import AuthSettings
|
|
5
|
+
from pico_auth.errors import AuthError
|
|
6
|
+
from pico_auth.jwt_provider import JWTProvider
|
|
7
|
+
from pico_auth.local_jwks_provider import LocalJWKSProvider
|
|
8
|
+
from pico_auth.models import RefreshToken, User
|
|
9
|
+
from pico_auth.passwords import PasswordService
|
|
10
|
+
from pico_auth.repository import RefreshTokenRepository, UserRepository
|
|
11
|
+
from pico_auth.routes import AuthController, OIDCController
|
|
12
|
+
from pico_auth.schema import create_tables
|
|
13
|
+
from pico_auth.service import AuthService
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"AuthController",
|
|
17
|
+
"AuthError",
|
|
18
|
+
"AuthSettings",
|
|
19
|
+
"AuthService",
|
|
20
|
+
"JWTProvider",
|
|
21
|
+
"LocalJWKSProvider",
|
|
22
|
+
"OIDCController",
|
|
23
|
+
"PasswordService",
|
|
24
|
+
"RefreshToken",
|
|
25
|
+
"RefreshTokenRepository",
|
|
26
|
+
"User",
|
|
27
|
+
"UserRepository",
|
|
28
|
+
"create_tables",
|
|
29
|
+
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Auth server configuration using pico-ioc @configured."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from pico_ioc import configured
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@configured(target="self", prefix="auth", mapping="tree")
|
|
10
|
+
@dataclass
|
|
11
|
+
class AuthSettings:
|
|
12
|
+
"""Auth server settings from application.yaml / env vars."""
|
|
13
|
+
|
|
14
|
+
data_dir: str = "~/.pico-auth"
|
|
15
|
+
access_token_expire_minutes: int = 15
|
|
16
|
+
refresh_token_expire_days: int = 7
|
|
17
|
+
issuer: str = "http://localhost:8100"
|
|
18
|
+
audience: str = "pico-bot"
|
|
19
|
+
auto_create_admin: bool = True
|
|
20
|
+
admin_email: str = "admin@pico.local"
|
|
21
|
+
admin_password: str = "admin"
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def data_path(self) -> Path:
|
|
25
|
+
return Path(self.data_dir).expanduser()
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def private_key_path(self) -> Path:
|
|
29
|
+
return self.data_path / "private.pem"
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def public_key_path(self) -> Path:
|
|
33
|
+
return self.data_path / "public.pem"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Auth error hierarchy."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AuthError(Exception):
|
|
5
|
+
def __init__(self, message: str):
|
|
6
|
+
self.message = message
|
|
7
|
+
super().__init__(message)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class UserExistsError(AuthError):
|
|
11
|
+
def __init__(self, email: str):
|
|
12
|
+
super().__init__(f"User already exists: {email}")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InvalidCredentialsError(AuthError):
|
|
16
|
+
def __init__(self):
|
|
17
|
+
super().__init__("Invalid email or password")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TokenExpiredError(AuthError):
|
|
21
|
+
def __init__(self):
|
|
22
|
+
super().__init__("Token has expired")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TokenInvalidError(AuthError):
|
|
26
|
+
def __init__(self):
|
|
27
|
+
super().__init__("Invalid token")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class UserNotFoundError(AuthError):
|
|
31
|
+
def __init__(self, user_id: str):
|
|
32
|
+
super().__init__(f"User not found: {user_id}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class InsufficientPermissionsError(AuthError):
|
|
36
|
+
def __init__(self):
|
|
37
|
+
super().__init__("Insufficient permissions")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class UserSuspendedError(AuthError):
|
|
41
|
+
def __init__(self):
|
|
42
|
+
super().__init__("User account is suspended")
|