Switch Unsandbox API auth to HMAC-SHA256 with public/secret keys
Replace simple Bearer token auth with HMAC-SHA256 signature scheme: - UNSANDBOX_PUBLIC_KEY for account identification (Bearer token) - UNSANDBOX_SECRET_KEY for request signing (never transmitted) - X-Timestamp header for replay attack prevention - X-Signature header with HMAC-SHA256(secret, ts:method:path:body)
This commit is contained in:
parent
331c11e8f7
commit
0d5d2c27be
2 changed files with 74 additions and 26 deletions
15
CLAUDE.md
15
CLAUDE.md
|
|
@ -166,12 +166,21 @@ POST https://api.unsandbox.com/run
|
||||||
|
|
||||||
#### Authentication
|
#### Authentication
|
||||||
|
|
||||||
Use Bearer token authentication:
|
Uses HMAC-SHA256 authentication with public/secret key pairs:
|
||||||
|
|
||||||
|
**Environment Variables:**
|
||||||
|
- `UNSANDBOX_PUBLIC_KEY` - Public key (unsb-pk-xxxx) used as Bearer token to identify account
|
||||||
|
- `UNSANDBOX_SECRET_KEY` - Secret key (unsb-sk-xxxx) used for HMAC signing, never transmitted
|
||||||
|
|
||||||
|
**Request Headers:**
|
||||||
```
|
```
|
||||||
Authorization: Bearer unsb-sk-xxxx-xxxx-xxxx-xxxx
|
Authorization: Bearer <public_key>
|
||||||
|
X-Timestamp: <unix_seconds>
|
||||||
|
X-Signature: HMAC-SHA256(secret_key, timestamp:method:path:body)
|
||||||
```
|
```
|
||||||
|
|
||||||
API keys start with `unsb-sk-` prefix.
|
The secret key is never transmitted - server verifies HMAC using its stored copy.
|
||||||
|
Timestamp must be within ±5 minutes of server time (replay attack prevention).
|
||||||
|
|
||||||
#### Supported Languages
|
#### Supported Languages
|
||||||
|
|
||||||
|
|
|
||||||
85
app.py
85
app.py
|
|
@ -1242,35 +1242,76 @@ Examples:
|
||||||
return jsonify({"filename": "compiled_binary"})
|
return jsonify({"filename": "compiled_binary"})
|
||||||
|
|
||||||
|
|
||||||
# Unsandbox API proxy endpoints - keeps API key server-side
|
# Unsandbox API proxy endpoints - keeps API keys server-side
|
||||||
UNSANDBOX_API_URL = "https://api.unsandbox.com"
|
UNSANDBOX_API_URL = "https://api.unsandbox.com"
|
||||||
|
|
||||||
|
|
||||||
|
def get_unsandbox_auth_headers(method, path, body=None):
|
||||||
|
"""Generate HMAC authentication headers for Unsandbox API.
|
||||||
|
|
||||||
|
Authentication scheme:
|
||||||
|
Authorization: Bearer <public_key> - identifies account
|
||||||
|
X-Timestamp: <unix_seconds> - replay prevention
|
||||||
|
X-Signature: HMAC-SHA256(secret_key, ts:method:path:body) - proves secret
|
||||||
|
|
||||||
|
The secret key is NEVER transmitted. Server verifies HMAC with stored secret.
|
||||||
|
"""
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
import time
|
||||||
|
|
||||||
|
public_key = os.environ.get("UNSANDBOX_PUBLIC_KEY")
|
||||||
|
secret_key = os.environ.get("UNSANDBOX_SECRET_KEY")
|
||||||
|
|
||||||
|
if not public_key or not secret_key:
|
||||||
|
return None
|
||||||
|
|
||||||
|
timestamp = int(time.time())
|
||||||
|
body_str = body if body else ""
|
||||||
|
|
||||||
|
# Build message: "timestamp:method:path:body"
|
||||||
|
message = f"{timestamp}:{method}:{path}:{body_str}"
|
||||||
|
|
||||||
|
# Compute HMAC-SHA256
|
||||||
|
signature = hmac.new(
|
||||||
|
secret_key.encode("utf-8"),
|
||||||
|
message.encode("utf-8"),
|
||||||
|
hashlib.sha256,
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"Authorization": f"Bearer {public_key}",
|
||||||
|
"X-Timestamp": str(timestamp),
|
||||||
|
"X-Signature": signature,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/code/execute", methods=["POST"])
|
@app.route("/api/code/execute", methods=["POST"])
|
||||||
def proxy_code_execute():
|
def proxy_code_execute():
|
||||||
"""Proxy code execution requests to Unsandbox API.
|
"""Proxy code execution requests to Unsandbox API.
|
||||||
|
|
||||||
Keeps the UNSANDBOX_API_KEY secure on the server side.
|
Keeps UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY secure on the server side.
|
||||||
|
Uses HMAC-SHA256 authentication.
|
||||||
"""
|
"""
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
api_key = os.environ.get("UNSANDBOX_API_KEY")
|
|
||||||
if not api_key:
|
|
||||||
return jsonify({"error": "Code execution not configured"}), 503
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
if not data:
|
if not data:
|
||||||
return jsonify({"error": "Request body required"}), 400
|
return jsonify({"error": "Request body required"}), 400
|
||||||
|
|
||||||
|
body_json = json.dumps(data, separators=(",", ":"))
|
||||||
|
auth_headers = get_unsandbox_auth_headers("POST", "/execute/async", body_json)
|
||||||
|
if not auth_headers:
|
||||||
|
return jsonify({"error": "Code execution not configured"}), 503
|
||||||
|
|
||||||
|
headers = {"Content-Type": "application/json", **auth_headers}
|
||||||
|
|
||||||
with httpx.Client(timeout=30.0) as client:
|
with httpx.Client(timeout=30.0) as client:
|
||||||
response = client.post(
|
response = client.post(
|
||||||
f"{UNSANDBOX_API_URL}/execute/async",
|
f"{UNSANDBOX_API_URL}/execute/async",
|
||||||
headers={
|
headers=headers,
|
||||||
"Content-Type": "application/json",
|
content=body_json,
|
||||||
"Authorization": f"Bearer {api_key}",
|
|
||||||
},
|
|
||||||
json=data,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Return the response from Unsandbox
|
# Return the response from Unsandbox
|
||||||
|
|
@ -1288,17 +1329,16 @@ def proxy_job_status(job_id):
|
||||||
"""Proxy job status requests to Unsandbox API."""
|
"""Proxy job status requests to Unsandbox API."""
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
api_key = os.environ.get("UNSANDBOX_API_KEY")
|
path = f"/jobs/{job_id}"
|
||||||
if not api_key:
|
auth_headers = get_unsandbox_auth_headers("GET", path, None)
|
||||||
|
if not auth_headers:
|
||||||
return jsonify({"error": "Code execution not configured"}), 503
|
return jsonify({"error": "Code execution not configured"}), 503
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with httpx.Client(timeout=30.0) as client:
|
with httpx.Client(timeout=30.0) as client:
|
||||||
response = client.get(
|
response = client.get(
|
||||||
f"{UNSANDBOX_API_URL}/jobs/{job_id}",
|
f"{UNSANDBOX_API_URL}{path}",
|
||||||
headers={
|
headers=auth_headers,
|
||||||
"Authorization": f"Bearer {api_key}",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return jsonify(response.json()), response.status_code
|
return jsonify(response.json()), response.status_code
|
||||||
|
|
@ -1315,17 +1355,16 @@ def proxy_job_cancel(job_id):
|
||||||
"""Proxy job cancellation requests to Unsandbox API."""
|
"""Proxy job cancellation requests to Unsandbox API."""
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
api_key = os.environ.get("UNSANDBOX_API_KEY")
|
path = f"/jobs/{job_id}"
|
||||||
if not api_key:
|
auth_headers = get_unsandbox_auth_headers("DELETE", path, None)
|
||||||
|
if not auth_headers:
|
||||||
return jsonify({"error": "Code execution not configured"}), 503
|
return jsonify({"error": "Code execution not configured"}), 503
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with httpx.Client(timeout=30.0) as client:
|
with httpx.Client(timeout=30.0) as client:
|
||||||
response = client.delete(
|
response = client.delete(
|
||||||
f"{UNSANDBOX_API_URL}/jobs/{job_id}",
|
f"{UNSANDBOX_API_URL}{path}",
|
||||||
headers={
|
headers=auth_headers,
|
||||||
"Authorization": f"Bearer {api_key}",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# DELETE may return empty body on success
|
# DELETE may return empty body on success
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue