Skip to content

Commit 62fd052

Browse files
committed
Iterate on caching strategy
1 parent 52fb81b commit 62fd052

File tree

4 files changed

+419
-10
lines changed

4 files changed

+419
-10
lines changed

FLEXIBLE_AUTH_README.md

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
# Flexible Authentication for OpenSensor API
2+
3+
This document describes the new flexible authentication system that allows users to access historical data endpoints using either Fief tokens (existing method) or device API keys (new method).
4+
5+
## Problem Solved
6+
7+
Previously, users authenticated in the web app had no way to get temporary tokens for direct API access outside the web interface. They could only:
8+
- Use Fief access tokens directly (not ideal for external tools)
9+
- Generate device-specific API keys (permanent, only for data submission)
10+
11+
## Solution
12+
13+
The new flexible authentication system allows users to use their existing device API keys to access historical data endpoints directly, providing a simple solution for data export without complex token management.
14+
15+
## Features
16+
17+
### ✅ Device API Key Authentication
18+
- Use existing device API keys for historical data retrieval
19+
- No need to manage separate temporary tokens
20+
- Automatic device access validation
21+
22+
### ✅ Fief Token Caching
23+
- Reduced load on Fief authentication server
24+
- 10-minute cache TTL for token validation
25+
- Automatic cache invalidation on errors
26+
27+
### ✅ Backward Compatibility
28+
- All existing Fief token authentication continues to work
29+
- No breaking changes to current API usage
30+
31+
### ✅ Security
32+
- API keys can only access their associated device data
33+
- Strict device ID and name matching
34+
- Same security model as existing system
35+
36+
## Usage Examples
37+
38+
### 1. Using Device API Key (New Method)
39+
40+
```bash
41+
# Get temperature data using device API key
42+
curl -H "X-API-Key: your_device_api_key_here" \
43+
"https://api.opensensor.io/temp/device123|MyDevice?page=1&size=10"
44+
```
45+
46+
```python
47+
import requests
48+
49+
headers = {"X-API-Key": "your_device_api_key_here"}
50+
response = requests.get(
51+
"https://api.opensensor.io/temp/device123|MyDevice",
52+
headers=headers
53+
)
54+
data = response.json()
55+
```
56+
57+
```javascript
58+
const response = await fetch('https://api.opensensor.io/temp/device123|MyDevice', {
59+
headers: {
60+
'X-API-Key': 'your_device_api_key_here'
61+
}
62+
});
63+
const data = await response.json();
64+
```
65+
66+
### 2. Using Fief Token (Existing Method)
67+
68+
```bash
69+
# Get temperature data using Fief token
70+
curl -H "Authorization: Bearer your_fief_token_here" \
71+
"https://api.opensensor.io/temp/device123|MyDevice?page=1&size=10"
72+
```
73+
74+
## Supported Endpoints
75+
76+
All historical data endpoints now support both authentication methods:
77+
78+
- `/temp/{device_id}` - Temperature data
79+
- `/humidity/{device_id}` - Humidity data
80+
- `/CO2/{device_id}` - CO2 data
81+
- `/moisture/{device_id}` - Moisture sensor data
82+
- `/pH/{device_id}` - pH data
83+
- `/VPD/{device_id}` - Vapor Pressure Deficit data
84+
- `/pressure/{device_id}` - Pressure data
85+
- `/lux/{device_id}` - Light intensity data
86+
- `/liquid/{device_id}` - Liquid level data
87+
- `/relays/{device_id}` - Relay status data
88+
- `/pumps/{device_id}` - Pump data
89+
90+
## Authentication Flow
91+
92+
### Device API Key Flow
93+
1. User provides API key in `X-API-Key` header
94+
2. System validates API key exists in database
95+
3. System checks if API key is authorized for requested device
96+
4. If authorized, data is returned
97+
98+
### Fief Token Flow (with caching)
99+
1. User provides token in `Authorization: Bearer` header
100+
2. System checks Redis cache for token validation
101+
3. If cache miss, validates with Fief server and caches result
102+
4. If valid, checks device access permissions
103+
5. If authorized, data is returned
104+
105+
## Error Responses
106+
107+
### 401 Unauthorized
108+
```json
109+
{
110+
"detail": "Authentication required"
111+
}
112+
```
113+
- No authentication provided (neither API key nor token)
114+
115+
### 403 Forbidden
116+
```json
117+
{
118+
"detail": "API key is not authorized to access device device123|MyDevice"
119+
}
120+
```
121+
- API key doesn't match the requested device
122+
123+
```json
124+
{
125+
"detail": "Invalid API key"
126+
}
127+
```
128+
- API key not found in database
129+
130+
## Getting Your API Key
131+
132+
1. Log into the web interface at https://opensensor.io
133+
2. Navigate to the sensor dashboard
134+
3. Your existing device API keys are displayed with masked values
135+
4. Use the full API key value for direct API access
136+
137+
## Implementation Details
138+
139+
### Files Modified
140+
141+
1. **`opensensor/users.py`**
142+
- Added flexible authentication functions
143+
- Added Fief token caching
144+
- Added device access validation for API keys
145+
146+
2. **`opensensor/collection_apis.py`**
147+
- Updated historical data routes to use flexible authentication
148+
- Improved error messages for different auth types
149+
150+
3. **`opensensor/cache_strategy.py`**
151+
- Added Fief token caching methods
152+
- 10-minute TTL for token validation cache
153+
154+
### Security Considerations
155+
156+
- API keys can only access data from their associated device
157+
- Device ID and name must exactly match the API key registration
158+
- Fief tokens maintain existing access control logic
159+
- Cache invalidation on authentication failures
160+
- No sensitive data stored in cache (tokens are hashed)
161+
162+
### Performance Improvements
163+
164+
- **Reduced Fief Server Load**: Token validation cached for 10 minutes
165+
- **Faster API Key Validation**: Direct database lookup without external calls
166+
- **Existing Caching**: Leverages existing Redis caching for data queries
167+
168+
## Testing
169+
170+
Use the provided test script to verify functionality:
171+
172+
```bash
173+
cd opensensor-api
174+
python test_flexible_auth.py
175+
```
176+
177+
Update the script with your actual API key and device ID before running.
178+
179+
## Migration Notes
180+
181+
- **No breaking changes**: All existing code continues to work
182+
- **Gradual adoption**: Users can start using API keys when convenient
183+
- **Monitoring**: Cache hit rates and authentication patterns can be monitored via Redis
184+
185+
## Benefits
186+
187+
1. **Immediate Solution**: Users can export data using existing API keys
188+
2. **Simple Integration**: Easy to use in scripts and external tools
189+
3. **Reduced Complexity**: No temporary token management needed
190+
4. **Better Performance**: Cached authentication reduces server load
191+
5. **Backward Compatible**: No impact on existing users
192+
193+
## Future Enhancements
194+
195+
Potential future improvements could include:
196+
- Bulk export endpoints for large data sets
197+
- API key usage analytics
198+
- Rate limiting per API key
199+
- API key expiration dates
200+
- Scoped permissions for API keys
201+
202+
---
203+
204+
**Note**: This implementation provides the simplest solution to the immediate problem while maintaining security and performance. The flexible authentication system can be extended in the future as needed.

opensensor/cache_strategy.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,49 @@ def invalidate_device_cache(self, device_id: str):
169169
logger.info(f"Invalidated {deleted_count} cache entries for device {device_id}")
170170
return deleted_count
171171

172+
# Fief token caching (to reduce Fief server load)
173+
def cache_fief_token_validation(self, token_hash: str, user_info: dict, ttl_minutes: int = 10):
174+
"""Cache Fief token validation results"""
175+
if not self.redis_client:
176+
return
177+
178+
cache_key = f"opensensor:fief_token:{token_hash}"
179+
try:
180+
self.redis_client.setex(cache_key, ttl_minutes * 60, json.dumps(user_info, default=str))
181+
logger.debug(f"Cached Fief token validation: {token_hash[:8]}...")
182+
except Exception as e:
183+
logger.warning(f"Failed to cache Fief token validation: {e}")
184+
185+
def get_cached_fief_token_validation(self, token_hash: str) -> Optional[dict]:
186+
"""Get cached Fief token validation result"""
187+
if not self.redis_client:
188+
return None
189+
190+
cache_key = f"opensensor:fief_token:{token_hash}"
191+
try:
192+
cached_data = self.redis_client.get(cache_key)
193+
if cached_data:
194+
logger.debug(f"Cache hit for Fief token: {token_hash[:8]}...")
195+
return json.loads(cached_data)
196+
except Exception as e:
197+
logger.warning(f"Failed to get cached Fief token validation: {e}")
198+
199+
return None
200+
201+
def invalidate_fief_token_cache(self, token_hash: str):
202+
"""Invalidate a specific Fief token cache entry"""
203+
if not self.redis_client:
204+
return False
205+
206+
cache_key = f"opensensor:fief_token:{token_hash}"
207+
try:
208+
result = self.redis_client.delete(cache_key)
209+
logger.debug(f"Invalidated Fief token cache: {token_hash[:8]}...")
210+
return result > 0
211+
except Exception as e:
212+
logger.warning(f"Failed to invalidate Fief token cache: {e}")
213+
return False
214+
172215

173216
# Global cache instance
174217
sensor_cache = SensorDataCache()

opensensor/collection_apis.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Response, status
88
from fastapi_pagination.default import Page as BasePage
99
from fastapi_pagination.default import Params as BaseParams
10-
from fief_client import FiefUserInfo
1110
from pydantic import BaseModel
1211

1312
from opensensor.cache_strategy import (
@@ -33,10 +32,11 @@
3332
)
3433
from opensensor.db import get_open_sensor_db
3534
from opensensor.users import (
35+
AuthInfo,
3636
User,
37-
auth,
38-
device_id_is_allowed_for_user,
37+
flexible_auth,
3938
migration_complete,
39+
validate_device_access_flexible,
4040
validate_environment,
4141
)
4242
from opensensor.utils.units import convert_temperature
@@ -653,7 +653,7 @@ def sample_and_paginate_collection(
653653

654654
def create_historical_data_route(entity: Type[T]):
655655
async def historical_data_route(
656-
fief_user: Optional[FiefUserInfo] = Depends(auth.current_user(optional=True)),
656+
auth_info: AuthInfo = Depends(flexible_auth),
657657
device_id: str = Path(
658658
title="The ID of the device chain for which to retrieve historical data."
659659
),
@@ -664,11 +664,14 @@ async def historical_data_route(
664664
size: int = Query(50, ge=1, le=1000, description="Page size"),
665665
unit: str | None = Query(None, description="Unit"),
666666
) -> Page[T]:
667-
if not device_id_is_allowed_for_user(device_id, user=fief_user):
668-
raise HTTPException(
669-
status_code=403,
670-
detail=f"User {fief_user} is not authorized to access device {device_id}",
671-
)
667+
# Use flexible authentication validation
668+
if not validate_device_access_flexible(auth_info, device_id):
669+
auth_type = auth_info.auth_type
670+
if auth_type == "fief":
671+
detail = f"User is not authorized to access device {device_id}"
672+
else:
673+
detail = f"API key is not authorized to access device {device_id}"
674+
raise HTTPException(status_code=403, detail=detail)
672675

673676
# TODO - Refactor this to support paid collections
674677
collection_name = "FreeTier"

0 commit comments

Comments
 (0)