diff --git a/server/app.py b/server/app.py index c12afa3..209b75d 100644 --- a/server/app.py +++ b/server/app.py @@ -9,6 +9,7 @@ Run (prod): gunicorn -k uvicorn.workers.UvicornWorker -b 127.0.0.1:8000 serve Interactive docs: http:///api/docs """ import base64 +import logging import os import re import uuid @@ -17,6 +18,7 @@ from time import monotonic from typing import Any, Optional from urllib.parse import urlparse +from authlib.integrations.base_client import OAuthError from fastapi import FastAPI, Depends, HTTPException, Query, Request, Response, BackgroundTasks from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse @@ -26,7 +28,9 @@ from sqlalchemy import select, delete, func from sqlalchemy.orm import Session from .db import Base, engine, get_db -from . import models, auth, notify, assets_db +from . import models, auth, notify, assets_db, okta_auth + +log = logging.getLogger("wpsuite.app") # Schema management: # • Local dev (SQLite) auto-creates tables for a zero-config run. @@ -721,6 +725,41 @@ def logout(response: Response): return {"ok": True} +# ── Okta OIDC sign-in (T10.2, wave 10 / D15) ──────────────────────────────────── +# Access gating is Okta's job: only accounts assigned to this app integration in Okta +# can complete authorize_redirect at all. No app-side group/claim check is layered on +# top here — see okta_auth.py's docstring and wave-10.md T10.2 for why. + +@app.get("/api/auth/okta/login") +async def okta_login(request: Request): + """Send the browser to Okta's authorize endpoint.""" + if not okta_auth.oauth: + raise HTTPException(status_code=503, detail="Sign-in is temporarily unavailable. Contact IT.") + return await okta_auth.oauth.okta.authorize_redirect(request, okta_auth.REDIRECT_URI) + + +@app.get("/api/auth/okta/callback") +async def okta_callback(request: Request, response: Response, db: Session = Depends(get_db)): + """Exchange the authorization code for tokens and validate the ID token. Identity + matching and session issuance are T10.3 — this route stops once the claim is in + hand, same one-task-per-PR split the wave file records.""" + if not okta_auth.oauth: + raise HTTPException(status_code=503, detail="Sign-in is temporarily unavailable. Contact IT.") + try: + token = await okta_auth.oauth.okta.authorize_access_token(request) + except OAuthError as exc: + log.warning("Okta callback rejected: %s", exc) + raise HTTPException(status_code=401, detail="Sign-in was not completed.") + claims = token.get("userinfo") or {} + identity = (claims.get(okta_auth.IDENTITY_CLAIM) or "").strip() + if not identity: + log.error("Okta ID token had no %r claim — check OKTA_IDENTITY_CLAIM", okta_auth.IDENTITY_CLAIM) + raise HTTPException(status_code=503, detail="Sign-in is temporarily unavailable. Contact IT.") + # T10.3 picks up here: match `identity` to a local account (or JIT-provision one), + # then auth.create_token / auth.set_session_cookie the same way login() does today. + raise NotImplementedError("T10.3: identity matching and JIT provisioning") + + # ── Self-service password reset (needs email switched on) ────────────────────── RESET_COOLDOWN_SECONDS = int(os.getenv("AUTH_RESET_COOLDOWN_SECONDS", "120")) # In-process throttle: one reset mail per (account, client) per cooldown. Enough to