Authentication (Auth0)#
GeoAssistant uses Auth0 for authentication with a backend-driven OAuth flow. The API handles the entire auth exchange — not the frontend. This is the Regular Web Application pattern.
Why Backend-Driven Auth#
The SaaS frontend is a React Single Page Application, but authentication is intentionally handled by the API, not the browser. This enables:
Cookie-based sessions shared across all subdomains of
.geoassistant.orgCentralized auth — one auth endpoint serves all frontends
Security — the client secret never touches the browser
The flow is:
User clicks "Sign in"
│
▼
Browser → api.geoassistant.org/auth/login?redirect_to=https://geoassistant.org
│
▼
API redirects → Auth0 login page (auth.geoassistant.org)
│
▼
User logs in → Auth0 redirects → api.geoassistant.org/auth/callback?code=...
│
▼
API exchanges code for tokens → sets HttpOnly cookie
│
▼
Browser redirected to redirect_to URL (now authenticated)
Auth0 Application Settings#
Application type must be set to Regular Web Application — not Single Page Application. SPA type does not support client secrets, which the backend token exchange requires.
Allowed Callback URLs:
https://api.geoassistant.org/auth/callback,
https://api.geoassistant.localhost/auth/callback
Allowed Logout URLs:
https://geoassistant.org,
https://geoassistant.localhost
Allowed Web Origins:
https://app.geoassistant.org,
https://geoassistant.org,
https://app.geoassistant.localhost,
https://geoassistant.localhost
API Implementation#
Auth routes live in geoassistant_api/routers/auth.py.
Login — redirects to Auth0:
@router.get("/auth/login")
async def login(redirect_to: str):
params = {
"client_id": AUTH0_CLIENT_ID,
"redirect_uri": AUTH0_REDIRECT_URI,
"response_type": "code",
"scope": "openid profile email",
"state": redirect_to,
}
url = f"https://{AUTH0_DOMAIN}/authorize?" + urllib.parse.urlencode(params)
return RedirectResponse(url)
Callback — exchanges code for token and sets cookie:
@router.get("/auth/callback")
async def callback(request: Request, code: str, state: str):
# Exchange code for tokens
tokens = await exchange_code(code)
id_token = tokens.get("id_token")
# Set cookie and redirect to original URL
response = RedirectResponse(url=state)
response.set_cookie(
COOKIE_NAME, id_token,
httponly=True,
secure=COOKIE_SECURE,
samesite="lax",
domain=COOKIE_DOMAIN,
)
return response
Token Verification#
Auth0 signs tokens using RS256 (asymmetric). The API verifies them using Auth0’s public keys fetched from the JWKS endpoint — never trust an unverified token.
async def get_user_from_token(id_token: str):
jwks = await fetch_jwks() # https://{AUTH0_DOMAIN}/.well-known/jwks.json
payload = jwt.decode(
id_token,
rsa_key,
algorithms=["RS256"],
audience=AUTH0_CLIENT_ID,
)
return payload
Testing the Auth Flow#
Visit the following URL in your browser to trigger a full login cycle:
https://api.geoassistant.org/auth/login?redirect_to=https://geoassistant.org
After logging in, verify the session by calling:
https://api.geoassistant.org/user/me