108 lines
3.7 KiB
Markdown
108 lines
3.7 KiB
Markdown
# Apodex Passwordless Auth Flow
|
|
|
|
> Discovered 2026-06-26. This is the CORRECT auth flow, NOT the now-dead Supabase flow.
|
|
|
|
## Overview
|
|
|
|
Apodex uses a passwordless auth system proxied through `www.apodex.ai`, NOT direct Supabase calls. The old Supabase project (`hgejqhubxceqppqkgqgx.supabase.co`) is dead — DNS fails everywhere.
|
|
|
|
## Endpoints
|
|
|
|
Both work from Python WITHOUT cookies, CSRF tokens, or special headers:
|
|
|
|
| Endpoint | Method | Returns |
|
|
|----------|--------|---------|
|
|
| `https://www.apodex.ai/api/auth/passwordless/send-code` | POST | 200 + `{"success":true,"message":"Login code sent..."}` |
|
|
| `https://www.apodex.ai/api/auth/passwordless/verify-login` | POST | 200 + `{"success":true,"data":{"access_token":"eyJ...","user":{...}}}` |
|
|
|
|
## Full Flow (Python)
|
|
|
|
```python
|
|
import requests
|
|
import imaplib, ssl, re, time, email
|
|
|
|
EMAIL = '[REDACTED-EMAIL]'
|
|
PASSWORD = '[REDACTED]'
|
|
|
|
# Step 1: Send OTP
|
|
s = requests.Session()
|
|
r = s.post('https://www.apodex.ai/api/auth/passwordless/send-code',
|
|
json={'email': EMAIL}, timeout=15)
|
|
assert r.status_code == 200, f'Send code failed: {r.status_code} {r.text}'
|
|
|
|
# Step 2: Get OTP from IMAP
|
|
time.sleep(3)
|
|
ctx = ssl.create_default_context()
|
|
imap = imaplib.IMAP4_SSL('secureimap.t-online.de', 993, ssl_context=ctx, timeout=15)
|
|
imap.login(EMAIL, PASSWORD)
|
|
imap.select('INBOX')
|
|
_, msgs = imap.search(None, 'FROM', 'apodex')
|
|
_, data = imap.fetch(msgs[0].split()[-1], '(RFC822)')
|
|
raw = data[0][1].decode('utf-8', errors='ignore')
|
|
msg = email.message_from_string(raw)
|
|
body = '' if msg.is_multipart() else msg.get_payload(decode=True).decode('utf-8', errors='ignore')
|
|
if msg.is_multipart():
|
|
for part in msg.walk():
|
|
if part.get_content_type() == 'text/plain':
|
|
body = part.get_payload(decode=True).decode('utf-8', errors='ignore')
|
|
break
|
|
otp = re.findall(r'\b\d{6}\b', body)[0]
|
|
imap.logout()
|
|
|
|
# Step 3: Verify OTP
|
|
r = s.post('https://www.apodex.ai/api/auth/passwordless/verify-login',
|
|
json={'email': EMAIL, 'code': otp}, timeout=15)
|
|
data = r.json()
|
|
access_token = data['data']['access_token'] # Apodex JWT
|
|
|
|
# Step 4: Use token (limited)
|
|
r = requests.get('https://www.apodex.ai/api/auth/get_user_info',
|
|
headers={'Authorization': f'Bearer {access_token}'})
|
|
# ✅ Works — returns user profile
|
|
|
|
# ❌ What does NOT work:
|
|
r = requests.get('https://api.apodex.ai/v1/api-keys',
|
|
headers={'Authorization': f'Bearer {access_token}'})
|
|
# Returns 401 {"error":"invalid token"} — need Supabase JWT instead
|
|
|
|
# But the existing sk- keys DO work:
|
|
r = requests.get('https://api.apodex.ai/v1/models',
|
|
headers={'Authorization': 'Bearer [REDACTED-KEY]='})
|
|
# ✅ Returns model list
|
|
```
|
|
|
|
## Token Comparison
|
|
|
|
```
|
|
Apodex JWT (from verify-login):
|
|
Header: {"alg":"HS256","typ":"JWT"}
|
|
Payload: {"user_id":"email_xxx","exp":1785xxxxxx,"session_id":"uuid"}
|
|
→ Works for: www.apodex.ai/api/auth/*
|
|
→ Fails for: api.apodex.ai/v1/api-keys
|
|
|
|
Supabase JWT (from Supabase auth, now DEAD):
|
|
Header: {"alg":"HS256","typ":"JWT"}
|
|
Payload: {"iss":"supabase","ref":"hgejqhubxceqppqkgqgx","role":"anon","sub":"uuid","aud":"authenticated"}
|
|
→ Works for: api.apodex.ai/v1/api-keys (but Supabase project is dead)
|
|
|
|
API Key (sk-...):
|
|
opaque base64-encoded string
|
|
→ Works for: api.apodex.ai/v1/chat/completions, api.apodex.ai/v1/models
|
|
```
|
|
|
|
## Rate Limit
|
|
|
|
- Endpoint: `POST /api/auth/passwordless/send-code`
|
|
- After ~8-10 requests: HTTP 429
|
|
- Body: `{"code":"PASSWORDLESS_SEND_RATE_LIMITED","message":"Operation too frequent, please try again later"}`
|
|
- Scope: IP-based (not cookie-based)
|
|
- Recovery: Wait 5-10 minutes
|
|
- Bypass: Rotate IP via VPN/proxy
|
|
|
|
## Headers Observed in Response
|
|
|
|
```
|
|
set-cookie: session=<uuid>; HttpOnly; Max-Age=3600; Path=/; SameSite=Lax
|
|
```
|
|
|
|
This session cookie is set on send-code and may be needed for verify-login. |