hotosm-auth 0.2.3__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.
- hotosm_auth-0.2.3/.gitignore +48 -0
- hotosm_auth-0.2.3/PKG-INFO +99 -0
- hotosm_auth-0.2.3/README.md +57 -0
- hotosm_auth-0.2.3/pyproject.toml +81 -0
- hotosm_auth-0.2.3/src/hotosm_auth/__init__.py +87 -0
- hotosm_auth-0.2.3/src/hotosm_auth/config.py +279 -0
- hotosm_auth-0.2.3/src/hotosm_auth/crypto.py +130 -0
- hotosm_auth-0.2.3/src/hotosm_auth/exceptions.py +39 -0
- hotosm_auth-0.2.3/src/hotosm_auth/jwt_validator.py +193 -0
- hotosm_auth-0.2.3/src/hotosm_auth/logger.py +173 -0
- hotosm_auth-0.2.3/src/hotosm_auth/models.py +91 -0
- hotosm_auth-0.2.3/src/hotosm_auth/osm_oauth.py +381 -0
- hotosm_auth-0.2.3/src/hotosm_auth/schemas/__init__.py +15 -0
- hotosm_auth-0.2.3/src/hotosm_auth/schemas/admin.py +45 -0
- hotosm_auth-0.2.3/src/hotosm_auth_django/__init__.py +88 -0
- hotosm_auth-0.2.3/src/hotosm_auth_django/admin_routes.py +356 -0
- hotosm_auth-0.2.3/src/hotosm_auth_django/apps.py +6 -0
- hotosm_auth-0.2.3/src/hotosm_auth_django/middleware.py +513 -0
- hotosm_auth-0.2.3/src/hotosm_auth_django/migrations/0001_initial.py +62 -0
- hotosm_auth-0.2.3/src/hotosm_auth_django/migrations/__init__.py +0 -0
- hotosm_auth-0.2.3/src/hotosm_auth_django/models.py +67 -0
- hotosm_auth-0.2.3/src/hotosm_auth_django/osm_views.py +292 -0
- hotosm_auth-0.2.3/src/hotosm_auth_fastapi/__init__.py +106 -0
- hotosm_auth-0.2.3/src/hotosm_auth_fastapi/admin.py +73 -0
- hotosm_auth-0.2.3/src/hotosm_auth_fastapi/admin_routes.py +248 -0
- hotosm_auth-0.2.3/src/hotosm_auth_fastapi/admin_routes_psycopg.py +274 -0
- hotosm_auth-0.2.3/src/hotosm_auth_fastapi/db_models.py +83 -0
- hotosm_auth-0.2.3/src/hotosm_auth_fastapi/dependencies.py +404 -0
- hotosm_auth-0.2.3/src/hotosm_auth_fastapi/osm_routes.py +216 -0
- hotosm_auth-0.2.3/src/hotosm_auth_fastapi/setup.py +142 -0
- hotosm_auth-0.2.3/uv.lock +1201 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
|
|
23
|
+
# Virtual environments
|
|
24
|
+
venv/
|
|
25
|
+
env/
|
|
26
|
+
ENV/
|
|
27
|
+
|
|
28
|
+
# Testing
|
|
29
|
+
.pytest_cache/
|
|
30
|
+
.coverage
|
|
31
|
+
htmlcov/
|
|
32
|
+
.tox/
|
|
33
|
+
|
|
34
|
+
# IDE
|
|
35
|
+
.vscode/
|
|
36
|
+
.idea/
|
|
37
|
+
*.swp
|
|
38
|
+
*.swo
|
|
39
|
+
*~
|
|
40
|
+
|
|
41
|
+
# OS
|
|
42
|
+
.DS_Store
|
|
43
|
+
Thumbs.db
|
|
44
|
+
|
|
45
|
+
# Mypy
|
|
46
|
+
.mypy_cache/
|
|
47
|
+
.dmypy.json
|
|
48
|
+
dmypy.json
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hotosm-auth
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: HOTOSM SSO authentication library for Hanko + OSM OAuth
|
|
5
|
+
Project-URL: Homepage, https://github.com/hotosm/login
|
|
6
|
+
Project-URL: Documentation, https://hotosm.github.io/auth-docs
|
|
7
|
+
Project-URL: Repository, https://github.com/hotosm/login/tree/main/auth-libs/python
|
|
8
|
+
Project-URL: Issues, https://github.com/hotosm/login/issues
|
|
9
|
+
Author-email: HOTOSM <tech@hotosm.org>
|
|
10
|
+
License: AGPL-3.0
|
|
11
|
+
Keywords: auth,django,fastapi,hanko,hotosm,oauth,osm,sso
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Framework :: Django
|
|
14
|
+
Classifier: Framework :: FastAPI
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: cryptography>=41.0.0
|
|
24
|
+
Requires-Dist: httpx>=0.25.0
|
|
25
|
+
Requires-Dist: pydantic>=2.0.0
|
|
26
|
+
Requires-Dist: pyjwt[crypto]>=2.8.0
|
|
27
|
+
Requires-Dist: python-dateutil>=2.8.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: black>=23.0.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: mypy>=1.5.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=7.4.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
35
|
+
Provides-Extra: django
|
|
36
|
+
Requires-Dist: django>=4.2.0; extra == 'django'
|
|
37
|
+
Requires-Dist: djangorestframework>=3.14.0; extra == 'django'
|
|
38
|
+
Provides-Extra: fastapi
|
|
39
|
+
Requires-Dist: fastapi>=0.104.0; extra == 'fastapi'
|
|
40
|
+
Requires-Dist: sqlalchemy>=2.0.0; extra == 'fastapi'
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
# HOTOSM Auth Library - Python
|
|
44
|
+
|
|
45
|
+
FastAPI/Django integration for Hanko authentication with OSM OAuth support.
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install hotosm-auth
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Usage
|
|
54
|
+
|
|
55
|
+
### FastAPI
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from fastapi import FastAPI, Depends
|
|
59
|
+
from hotosm_auth.integrations.fastapi import CurrentUser
|
|
60
|
+
|
|
61
|
+
app = FastAPI()
|
|
62
|
+
|
|
63
|
+
@app.get("/me")
|
|
64
|
+
async def get_me(user: CurrentUser):
|
|
65
|
+
return {"id": user.id, "email": user.email}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Django
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from django.http import JsonResponse
|
|
72
|
+
from hotosm_auth.integrations.django import require_auth
|
|
73
|
+
|
|
74
|
+
@require_auth
|
|
75
|
+
def my_view(request):
|
|
76
|
+
return JsonResponse({"user_id": request.user.id})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Features
|
|
80
|
+
|
|
81
|
+
- JWT validation for Hanko tokens
|
|
82
|
+
- OSM OAuth 2.0 integration
|
|
83
|
+
- User mapping between Hanko and application users
|
|
84
|
+
- FastAPI and Django integrations
|
|
85
|
+
|
|
86
|
+
## Configuration
|
|
87
|
+
|
|
88
|
+
Set these environment variables:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
HANKO_API_URL=https://login.hotosm.org
|
|
92
|
+
OSM_CLIENT_ID=your_osm_client_id
|
|
93
|
+
OSM_CLIENT_SECRET=your_osm_secret
|
|
94
|
+
COOKIE_SECRET=your_secret_key
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
AGPL-3.0
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# HOTOSM Auth Library - Python
|
|
2
|
+
|
|
3
|
+
FastAPI/Django integration for Hanko authentication with OSM OAuth support.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install hotosm-auth
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### FastAPI
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from fastapi import FastAPI, Depends
|
|
17
|
+
from hotosm_auth.integrations.fastapi import CurrentUser
|
|
18
|
+
|
|
19
|
+
app = FastAPI()
|
|
20
|
+
|
|
21
|
+
@app.get("/me")
|
|
22
|
+
async def get_me(user: CurrentUser):
|
|
23
|
+
return {"id": user.id, "email": user.email}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Django
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from django.http import JsonResponse
|
|
30
|
+
from hotosm_auth.integrations.django import require_auth
|
|
31
|
+
|
|
32
|
+
@require_auth
|
|
33
|
+
def my_view(request):
|
|
34
|
+
return JsonResponse({"user_id": request.user.id})
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- JWT validation for Hanko tokens
|
|
40
|
+
- OSM OAuth 2.0 integration
|
|
41
|
+
- User mapping between Hanko and application users
|
|
42
|
+
- FastAPI and Django integrations
|
|
43
|
+
|
|
44
|
+
## Configuration
|
|
45
|
+
|
|
46
|
+
Set these environment variables:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
HANKO_API_URL=https://login.hotosm.org
|
|
50
|
+
OSM_CLIENT_ID=your_osm_client_id
|
|
51
|
+
OSM_CLIENT_SECRET=your_osm_secret
|
|
52
|
+
COOKIE_SECRET=your_secret_key
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
AGPL-3.0
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hotosm-auth"
|
|
7
|
+
version = "0.2.3"
|
|
8
|
+
description = "HOTOSM SSO authentication library for Hanko + OSM OAuth"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "AGPL-3.0" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "HOTOSM", email = "tech@hotosm.org" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["hotosm", "auth", "hanko", "osm", "oauth", "sso", "fastapi", "django"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
|
+
"Framework :: FastAPI",
|
|
26
|
+
"Framework :: Django",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
dependencies = [
|
|
30
|
+
"pyjwt[crypto]>=2.8.0",
|
|
31
|
+
"httpx>=0.25.0",
|
|
32
|
+
"cryptography>=41.0.0",
|
|
33
|
+
"pydantic>=2.0.0",
|
|
34
|
+
"python-dateutil>=2.8.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
fastapi = [
|
|
39
|
+
"fastapi>=0.104.0",
|
|
40
|
+
"sqlalchemy>=2.0.0",
|
|
41
|
+
]
|
|
42
|
+
django = [
|
|
43
|
+
"django>=4.2.0",
|
|
44
|
+
"djangorestframework>=3.14.0",
|
|
45
|
+
]
|
|
46
|
+
dev = [
|
|
47
|
+
"pytest>=7.4.0",
|
|
48
|
+
"pytest-asyncio>=0.21.0",
|
|
49
|
+
"pytest-cov>=4.1.0",
|
|
50
|
+
"black>=23.0.0",
|
|
51
|
+
"ruff>=0.1.0",
|
|
52
|
+
"mypy>=1.5.0",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[project.urls]
|
|
56
|
+
Homepage = "https://github.com/hotosm/login"
|
|
57
|
+
Documentation = "https://hotosm.github.io/auth-docs"
|
|
58
|
+
Repository = "https://github.com/hotosm/login/tree/main/auth-libs/python"
|
|
59
|
+
Issues = "https://github.com/hotosm/login/issues"
|
|
60
|
+
|
|
61
|
+
[tool.hatch.build.targets.wheel]
|
|
62
|
+
packages = ["src/hotosm_auth", "src/hotosm_auth_fastapi", "src/hotosm_auth_django"]
|
|
63
|
+
|
|
64
|
+
[tool.pytest.ini_options]
|
|
65
|
+
asyncio_mode = "auto"
|
|
66
|
+
testpaths = ["tests"]
|
|
67
|
+
|
|
68
|
+
[tool.black]
|
|
69
|
+
line-length = 88
|
|
70
|
+
target-version = ['py310']
|
|
71
|
+
|
|
72
|
+
[tool.ruff]
|
|
73
|
+
line-length = 88
|
|
74
|
+
target-version = "py310"
|
|
75
|
+
select = ["E", "F", "I", "N", "W"]
|
|
76
|
+
|
|
77
|
+
[tool.mypy]
|
|
78
|
+
python_version = "3.10"
|
|
79
|
+
warn_return_any = true
|
|
80
|
+
warn_unused_configs = true
|
|
81
|
+
disallow_untyped_defs = true
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
hotosm-auth: HOTOSM SSO Authentication Library
|
|
3
|
+
|
|
4
|
+
This library provides authentication for HOTOSM applications using:
|
|
5
|
+
- Hanko v2.1.0 for base SSO (Google, GitHub, Email/Password)
|
|
6
|
+
- OpenStreetMap OAuth 2.0 for OSM authorization
|
|
7
|
+
|
|
8
|
+
Core Package Structure:
|
|
9
|
+
- hotosm_auth: Core functionality (this package)
|
|
10
|
+
- hotosm_auth_fastapi: FastAPI integration
|
|
11
|
+
- hotosm_auth_django: Django integration
|
|
12
|
+
|
|
13
|
+
Quick Start (FastAPI):
|
|
14
|
+
from hotosm_auth_fastapi import setup_auth, Auth
|
|
15
|
+
|
|
16
|
+
app = FastAPI()
|
|
17
|
+
setup_auth(app)
|
|
18
|
+
|
|
19
|
+
@app.get("/me")
|
|
20
|
+
async def me(auth: Auth):
|
|
21
|
+
return {"user": auth.user.email}
|
|
22
|
+
|
|
23
|
+
Quick Start (Django):
|
|
24
|
+
# settings.py
|
|
25
|
+
MIDDLEWARE = ['hotosm_auth_django.HankoAuthMiddleware']
|
|
26
|
+
|
|
27
|
+
# views.py
|
|
28
|
+
from hotosm_auth_django import login_required
|
|
29
|
+
|
|
30
|
+
@login_required
|
|
31
|
+
def my_view(request):
|
|
32
|
+
return JsonResponse({"email": request.hotosm.user.email})
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
__version__ = "0.2.0"
|
|
36
|
+
|
|
37
|
+
# Core models and configuration
|
|
38
|
+
from hotosm_auth.models import HankoUser, OSMConnection, OSMScope
|
|
39
|
+
from hotosm_auth.config import AuthConfig
|
|
40
|
+
from hotosm_auth.exceptions import (
|
|
41
|
+
AuthenticationError,
|
|
42
|
+
TokenExpiredError,
|
|
43
|
+
TokenInvalidError,
|
|
44
|
+
CookieDecryptionError,
|
|
45
|
+
OSMOAuthError,
|
|
46
|
+
)
|
|
47
|
+
from hotosm_auth.jwt_validator import JWTValidator
|
|
48
|
+
from hotosm_auth.crypto import CookieCrypto
|
|
49
|
+
from hotosm_auth.osm_oauth import OSMOAuthClient
|
|
50
|
+
from hotosm_auth.logger import get_logger, log_auth_event
|
|
51
|
+
|
|
52
|
+
# Admin schemas (used by both FastAPI and Django)
|
|
53
|
+
from hotosm_auth.schemas.admin import (
|
|
54
|
+
MappingResponse,
|
|
55
|
+
MappingListResponse,
|
|
56
|
+
MappingCreate,
|
|
57
|
+
MappingUpdate,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
# Version
|
|
62
|
+
"__version__",
|
|
63
|
+
# Models
|
|
64
|
+
"HankoUser",
|
|
65
|
+
"OSMConnection",
|
|
66
|
+
"OSMScope",
|
|
67
|
+
# Configuration
|
|
68
|
+
"AuthConfig",
|
|
69
|
+
# Exceptions
|
|
70
|
+
"AuthenticationError",
|
|
71
|
+
"TokenExpiredError",
|
|
72
|
+
"TokenInvalidError",
|
|
73
|
+
"CookieDecryptionError",
|
|
74
|
+
"OSMOAuthError",
|
|
75
|
+
# Core classes
|
|
76
|
+
"JWTValidator",
|
|
77
|
+
"CookieCrypto",
|
|
78
|
+
"OSMOAuthClient",
|
|
79
|
+
# Logging
|
|
80
|
+
"get_logger",
|
|
81
|
+
"log_auth_event",
|
|
82
|
+
# Admin schemas
|
|
83
|
+
"MappingResponse",
|
|
84
|
+
"MappingListResponse",
|
|
85
|
+
"MappingCreate",
|
|
86
|
+
"MappingUpdate",
|
|
87
|
+
]
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration for HOTOSM authentication.
|
|
3
|
+
|
|
4
|
+
Apps should create one AuthConfig instance at startup with their settings.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from pydantic import BaseModel, Field, HttpUrl
|
|
10
|
+
from hotosm_auth.logger import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AuthConfig(BaseModel):
|
|
16
|
+
"""Configuration for hotosm-auth library.
|
|
17
|
+
|
|
18
|
+
Example usage:
|
|
19
|
+
config = AuthConfig(
|
|
20
|
+
hanko_api_url="https://login.hotosm.org",
|
|
21
|
+
cookie_secret="your-secret-key-32-bytes-min",
|
|
22
|
+
cookie_domain=".hotosm.org",
|
|
23
|
+
osm_enabled=True,
|
|
24
|
+
)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
# Hanko configuration
|
|
28
|
+
hanko_api_url: HttpUrl = Field(
|
|
29
|
+
...,
|
|
30
|
+
description="Hanko API URL (e.g., https://login.hotosm.org)",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Cookie configuration
|
|
34
|
+
cookie_secret: str = Field(
|
|
35
|
+
...,
|
|
36
|
+
min_length=32,
|
|
37
|
+
description="Secret key for encrypting OSM cookies (min 32 bytes)",
|
|
38
|
+
)
|
|
39
|
+
cookie_domain: Optional[str] = Field(
|
|
40
|
+
None,
|
|
41
|
+
description="Domain for cookies (e.g., '.hotosm.org' for all subdomains)",
|
|
42
|
+
)
|
|
43
|
+
cookie_secure: bool = Field(
|
|
44
|
+
True,
|
|
45
|
+
description="Use secure cookies (HTTPS only). Set False for local dev.",
|
|
46
|
+
)
|
|
47
|
+
cookie_samesite: str = Field(
|
|
48
|
+
"lax",
|
|
49
|
+
description="SameSite cookie policy: 'lax', 'strict', or 'none'",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# OSM OAuth configuration
|
|
53
|
+
osm_enabled: bool = Field(
|
|
54
|
+
False,
|
|
55
|
+
description="Enable OSM OAuth integration",
|
|
56
|
+
)
|
|
57
|
+
osm_client_id: Optional[str] = Field(
|
|
58
|
+
None,
|
|
59
|
+
description="OSM OAuth client ID (required if osm_enabled=True)",
|
|
60
|
+
)
|
|
61
|
+
osm_client_secret: Optional[str] = Field(
|
|
62
|
+
None,
|
|
63
|
+
description="OSM OAuth client secret (required if osm_enabled=True)",
|
|
64
|
+
)
|
|
65
|
+
osm_redirect_uri: Optional[str] = Field(
|
|
66
|
+
None,
|
|
67
|
+
description="OSM OAuth redirect URI (e.g., https://app.hotosm.org/auth/osm/callback)",
|
|
68
|
+
)
|
|
69
|
+
osm_scopes: list[str] = Field(
|
|
70
|
+
default_factory=lambda: ["read_prefs"],
|
|
71
|
+
description="Default OSM OAuth scopes to request",
|
|
72
|
+
)
|
|
73
|
+
osm_api_url: HttpUrl = Field(
|
|
74
|
+
default="https://www.openstreetmap.org",
|
|
75
|
+
description="OSM API base URL (use https://master.apis.dev.openstreetmap.org for dev)",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# JWT validation
|
|
79
|
+
jwt_audience: Optional[str] = Field(
|
|
80
|
+
None,
|
|
81
|
+
description="Expected JWT audience claim (optional)",
|
|
82
|
+
)
|
|
83
|
+
jwt_issuer: Optional[str] = Field(
|
|
84
|
+
"auto",
|
|
85
|
+
description="Expected JWT issuer claim (optional, 'auto' defaults to hanko_api_url, None skips validation)",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# JWKS caching
|
|
89
|
+
jwks_cache_ttl: int = Field(
|
|
90
|
+
3600,
|
|
91
|
+
description="JWKS cache TTL in seconds (default 1 hour)",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Admin configuration
|
|
95
|
+
admin_emails: str = Field(
|
|
96
|
+
"",
|
|
97
|
+
description="Comma-separated list of admin email addresses",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def admin_email_list(self) -> list[str]:
|
|
102
|
+
"""Parse admin_emails into a list of lowercase email addresses."""
|
|
103
|
+
return [e.strip().lower() for e in self.admin_emails.split(",") if e.strip()]
|
|
104
|
+
|
|
105
|
+
def model_post_init(self, __context) -> None:
|
|
106
|
+
"""Validate configuration after initialization."""
|
|
107
|
+
# If OSM is enabled, require client credentials
|
|
108
|
+
if self.osm_enabled:
|
|
109
|
+
if not self.osm_client_id:
|
|
110
|
+
raise ValueError("osm_client_id is required when osm_enabled=True")
|
|
111
|
+
if not self.osm_client_secret:
|
|
112
|
+
raise ValueError("osm_client_secret is required when osm_enabled=True")
|
|
113
|
+
|
|
114
|
+
# Auto-generate osm_redirect_uri if not provided
|
|
115
|
+
if not self.osm_redirect_uri:
|
|
116
|
+
# Default pattern: {HANKO_API_URL}/auth/osm/callback
|
|
117
|
+
auto_uri = f"{str(self.hanko_api_url).rstrip('/')}/auth/osm/callback"
|
|
118
|
+
logger.info(f"OSM_REDIRECT_URI not set, auto-generating: {auto_uri}")
|
|
119
|
+
logger.info("If this doesn't match your app's path, set OSM_REDIRECT_URI explicitly.")
|
|
120
|
+
object.__setattr__(self, 'osm_redirect_uri', auto_uri)
|
|
121
|
+
|
|
122
|
+
# Default JWT issuer to Hanko API URL if set to "auto"
|
|
123
|
+
# Use object.__setattr__ because model is frozen
|
|
124
|
+
if self.jwt_issuer == "auto":
|
|
125
|
+
# Strip trailing slash from HttpUrl (Pydantic adds it automatically)
|
|
126
|
+
issuer_url = str(self.hanko_api_url).rstrip('/')
|
|
127
|
+
object.__setattr__(self, 'jwt_issuer', issuer_url)
|
|
128
|
+
logger.info(f"JWT issuer set to: {issuer_url} (from hanko_api_url)")
|
|
129
|
+
elif self.jwt_issuer:
|
|
130
|
+
logger.info(f"JWT issuer explicitly set to: {self.jwt_issuer}")
|
|
131
|
+
else:
|
|
132
|
+
logger.warning("JWT issuer validation is DISABLED (jwt_issuer=None)")
|
|
133
|
+
# If None, issuer validation is skipped
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def from_env(cls) -> "AuthConfig":
|
|
137
|
+
"""
|
|
138
|
+
Load configuration from environment variables.
|
|
139
|
+
|
|
140
|
+
This is the recommended way to configure hotosm-auth in production.
|
|
141
|
+
|
|
142
|
+
Required environment variables:
|
|
143
|
+
HANKO_API_URL: Hanko API URL (e.g., https://login.hotosm.org)
|
|
144
|
+
COOKIE_SECRET: Secret key for encrypting cookies (min 32 bytes)
|
|
145
|
+
|
|
146
|
+
Optional environment variables:
|
|
147
|
+
COOKIE_DOMAIN: Cookie domain (e.g., ".hotosm.org")
|
|
148
|
+
COOKIE_SECURE: Use secure cookies (default: "true")
|
|
149
|
+
COOKIE_SAMESITE: SameSite policy (default: "lax")
|
|
150
|
+
JWT_AUDIENCE: Expected JWT audience
|
|
151
|
+
JWT_ISSUER: Expected JWT issuer (default: "auto")
|
|
152
|
+
OSM_CLIENT_ID: OSM OAuth client ID
|
|
153
|
+
OSM_CLIENT_SECRET: OSM OAuth client secret
|
|
154
|
+
OSM_REDIRECT_URI: OSM OAuth redirect URI (optional, auto-generated from HANKO_API_URL if not set)
|
|
155
|
+
OSM_SCOPES: Space-separated OSM scopes (default: "read_prefs")
|
|
156
|
+
OSM_API_URL: OSM API URL (default: https://www.openstreetmap.org)
|
|
157
|
+
ADMIN_EMAILS: Comma-separated list of admin email addresses
|
|
158
|
+
|
|
159
|
+
Example:
|
|
160
|
+
# .env file
|
|
161
|
+
HANKO_API_URL=https://login.hotosm.org
|
|
162
|
+
COOKIE_SECRET=your-secret-key-min-32-bytes
|
|
163
|
+
OSM_CLIENT_ID=your-osm-client-id
|
|
164
|
+
OSM_CLIENT_SECRET=your-osm-client-secret
|
|
165
|
+
|
|
166
|
+
# Python code
|
|
167
|
+
config = AuthConfig.from_env()
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
AuthConfig: Configuration loaded from environment
|
|
171
|
+
|
|
172
|
+
Raises:
|
|
173
|
+
ValueError: If required environment variables are missing
|
|
174
|
+
"""
|
|
175
|
+
# Try to load .env file from current working directory
|
|
176
|
+
try:
|
|
177
|
+
from dotenv import load_dotenv
|
|
178
|
+
import os as _os
|
|
179
|
+
# Load from current directory and walk up parent directories
|
|
180
|
+
load_dotenv(dotenv_path=_os.path.join(_os.getcwd(), '.env'), verbose=False)
|
|
181
|
+
# Also try parent directories
|
|
182
|
+
load_dotenv(verbose=False)
|
|
183
|
+
except ImportError:
|
|
184
|
+
# dotenv not installed, rely on environment variables being set
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
# Required variables
|
|
188
|
+
hanko_api_url = os.getenv("HANKO_API_URL")
|
|
189
|
+
if not hanko_api_url:
|
|
190
|
+
raise ValueError(
|
|
191
|
+
"HANKO_API_URL environment variable is required. "
|
|
192
|
+
"Set it to your Hanko API URL (e.g., https://login.hotosm.org)"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
cookie_secret = os.getenv("COOKIE_SECRET")
|
|
196
|
+
if not cookie_secret:
|
|
197
|
+
raise ValueError(
|
|
198
|
+
"COOKIE_SECRET environment variable is required. "
|
|
199
|
+
"Generate a secure secret with: python -c \"import secrets; print(secrets.token_urlsafe(32))\""
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Optional variables with smart defaults
|
|
203
|
+
cookie_domain = os.getenv("COOKIE_DOMAIN")
|
|
204
|
+
cookie_secure_env = os.getenv("COOKIE_SECURE")
|
|
205
|
+
cookie_samesite = os.getenv("COOKIE_SAMESITE", "lax")
|
|
206
|
+
|
|
207
|
+
# Smart default for COOKIE_DOMAIN (based on HANKO_API_URL)
|
|
208
|
+
if not cookie_domain:
|
|
209
|
+
from urllib.parse import urlparse
|
|
210
|
+
parsed = urlparse(hanko_api_url)
|
|
211
|
+
hostname = parsed.hostname or "localhost"
|
|
212
|
+
|
|
213
|
+
if hostname in ["localhost", "127.0.0.1"]:
|
|
214
|
+
cookie_domain = "localhost"
|
|
215
|
+
logger.info("COOKIE_DOMAIN not set, auto-detected: localhost")
|
|
216
|
+
else:
|
|
217
|
+
# Production: extract root domain with dot prefix
|
|
218
|
+
# e.g., "login.hotosm.org" → ".hotosm.org"
|
|
219
|
+
parts = hostname.split(".")
|
|
220
|
+
if len(parts) >= 2:
|
|
221
|
+
cookie_domain = f".{'.'.join(parts[-2:])}"
|
|
222
|
+
logger.info(f"COOKIE_DOMAIN not set, auto-detected: {cookie_domain}")
|
|
223
|
+
else:
|
|
224
|
+
cookie_domain = None # No domain restriction
|
|
225
|
+
logger.info("COOKIE_DOMAIN not set, using no domain restriction")
|
|
226
|
+
|
|
227
|
+
# Smart default for COOKIE_SECURE (based on HANKO_API_URL scheme)
|
|
228
|
+
if cookie_secure_env is None:
|
|
229
|
+
cookie_secure = hanko_api_url.startswith("https://")
|
|
230
|
+
logger.info(f"COOKIE_SECURE not set, auto-detected: {cookie_secure} (from {hanko_api_url})")
|
|
231
|
+
else:
|
|
232
|
+
cookie_secure = cookie_secure_env.lower() == "true"
|
|
233
|
+
|
|
234
|
+
# JWT configuration
|
|
235
|
+
jwt_audience = os.getenv("JWT_AUDIENCE") or None
|
|
236
|
+
# If JWT_ISSUER is not set or empty, default to "auto"
|
|
237
|
+
# This ensures issuer validation is enabled by default
|
|
238
|
+
jwt_issuer_env = os.getenv("JWT_ISSUER")
|
|
239
|
+
if jwt_issuer_env and jwt_issuer_env.strip():
|
|
240
|
+
jwt_issuer = jwt_issuer_env
|
|
241
|
+
logger.info(f"JWT_ISSUER from env: {jwt_issuer}")
|
|
242
|
+
else:
|
|
243
|
+
jwt_issuer = "auto" # Default to auto instead of None
|
|
244
|
+
logger.info("JWT_ISSUER not set, defaulting to 'auto'")
|
|
245
|
+
|
|
246
|
+
# OSM configuration
|
|
247
|
+
osm_client_id = os.getenv("OSM_CLIENT_ID")
|
|
248
|
+
osm_client_secret = os.getenv("OSM_CLIENT_SECRET")
|
|
249
|
+
osm_redirect_uri = os.getenv("OSM_REDIRECT_URI")
|
|
250
|
+
osm_scopes_str = os.getenv("OSM_SCOPES", "read_prefs")
|
|
251
|
+
osm_scopes = osm_scopes_str.split() if osm_scopes_str else ["read_prefs"]
|
|
252
|
+
osm_api_url = os.getenv("OSM_API_URL", "https://www.openstreetmap.org")
|
|
253
|
+
|
|
254
|
+
# Determine if OSM is enabled
|
|
255
|
+
osm_enabled = bool(osm_client_id and osm_client_secret)
|
|
256
|
+
|
|
257
|
+
# Admin configuration
|
|
258
|
+
admin_emails = os.getenv("ADMIN_EMAILS", "")
|
|
259
|
+
|
|
260
|
+
return cls(
|
|
261
|
+
hanko_api_url=hanko_api_url,
|
|
262
|
+
cookie_secret=cookie_secret,
|
|
263
|
+
cookie_domain=cookie_domain,
|
|
264
|
+
cookie_secure=cookie_secure,
|
|
265
|
+
cookie_samesite=cookie_samesite,
|
|
266
|
+
jwt_audience=jwt_audience,
|
|
267
|
+
jwt_issuer=jwt_issuer,
|
|
268
|
+
osm_enabled=osm_enabled,
|
|
269
|
+
osm_client_id=osm_client_id if osm_enabled else None,
|
|
270
|
+
osm_client_secret=osm_client_secret if osm_enabled else None,
|
|
271
|
+
osm_redirect_uri=osm_redirect_uri if osm_enabled else None,
|
|
272
|
+
osm_scopes=osm_scopes if osm_enabled else [],
|
|
273
|
+
osm_api_url=osm_api_url,
|
|
274
|
+
admin_emails=admin_emails,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
class Config:
|
|
278
|
+
"""Pydantic configuration."""
|
|
279
|
+
frozen = True # Make config immutable after creation
|