from typing import Optional
"""VerIQ — identity binding via OAuth (phase 2 scaffolding).
X (Twitter): OAuth 2.0 Authorization Code with PKCE.
- authorize: https://twitter.com/i/oauth2/authorize
- token: https://api.twitter.com/2/oauth2/token
- userinfo: https://api.twitter.com/2/users/me (user.fields=id,name,username,profile_image_url)
- scopes: users.read tweet.read (read-only; we never post)
Instagram: Facebook Login for Instagram (business/creator accounts).
- authorize: https://www.facebook.com/v19.0/dialog/oauth
- token: https://graph.facebook.com/v19.0/oauth/access_token
- userinfo: https://graph.facebook.com/me?fields=id,username,name,picture
Both are wired but return 503 until the owner registers the apps and fills
.env (X_CLIENT_ID/X_CLIENT_SECRET, IG_APP_ID/IG_APP_SECRET).
"""
import base64
import hashlib
import os
import secrets
import time
from urllib.parse import urlencode
X_AUTHORIZE = "https://twitter.com/i/oauth2/authorize"
X_TOKEN = "https://api.twitter.com/2/oauth2/token"
X_USERINFO = "https://api.twitter.com/2/users/me"
IG_AUTHORIZE = "https://www.facebook.com/v19.0/dialog/oauth"
IG_TOKEN = "https://graph.facebook.com/v19.0/oauth/access_token"
IG_USERINFO = "https://graph.facebook.com/me"
# in-memory PKCE/state store (single-process MVP; fine for v1)
FLOWS: dict[str, dict] = {}
FLOW_TTL_S = 600
def _env(name: str) -> Optional[str]:
v = os.environ.get(name, "").strip()
return v or None
def x_configured() -> bool:
return bool(_env("X_CLIENT_ID") and _env("X_CLIENT_SECRET"))
def ig_configured() -> bool:
return bool(_env("IG_APP_ID") and _env("IG_APP_SECRET"))
def public_base_url() -> str:
return _env("VERIQ_PUBLIC_BASE_URL") or "http://127.0.0.1:8801"
def _new_flow(provider: str, result_id: str) -> dict:
verifier = base64.urlsafe_b64encode(secrets.token_bytes(48)).decode().rstrip("=")
challenge = base64.urlsafe_b64encode(
hashlib.sha256(verifier.encode()).digest()).decode().rstrip("=")
state = secrets.token_urlsafe(24)
FLOWS[state] = {
"provider": provider,
"result_id": result_id,
"verifier": verifier,
"created_at": time.time(),
}
# prune
now = time.time()
for k in [k for k, v in FLOWS.items() if now - v["created_at"] > FLOW_TTL_S]:
FLOWS.pop(k, None)
return {"state": state, "challenge": challenge, "verifier": verifier}
def x_authorize_url(result_id: str) -> tuple[str, Optional[str]]:
"""Returns (authorize_url, state) or None if not configured."""
cid = _env("X_CLIENT_ID")
if not cid:
return None
flow = _new_flow("x", result_id)
params = {
"response_type": "code",
"client_id": cid,
"redirect_uri": public_base_url() + "/auth/x/callback",
"scope": "users.read tweet.read",
"state": flow["state"],
"code_challenge": flow["challenge"],
"code_challenge_method": "S256",
}
return X_AUTHORIZE + "?" + urlencode(params), flow["state"]
def ig_authorize_url(result_id: str) -> tuple[str, Optional[str]]:
app_id = _env("IG_APP_ID")
if not app_id:
return None
flow = _new_flow("ig", result_id)
params = {
"client_id": app_id,
"redirect_uri": public_base_url() + "/auth/ig/callback",
"state": flow["state"],
"scope": "instagram_basic,instagram_graph_user_profile",
"response_type": "code",
}
return IG_AUTHORIZE + "?" + urlencode(params), flow["state"]
def pop_flow(state: str) -> Optional[dict]:
flow = FLOWS.pop(state, None)
if flow and time.time() - flow["created_at"] <= FLOW_TTL_S:
return flow
return None