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.org

  • Centralized 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