T10.2: Okta login-redirect and callback routes
Adds GET /api/auth/okta/login (redirect to Okta's authorize endpoint) and GET /api/auth/okta/callback (exchange code, validate ID token, pull the identity claim) to server/app.py, using the oauth.okta client from okta_auth.py (T10.1). Access gating is Okta's job, not this route's: only accounts assigned to the app integration in Okta ever reach the callback, so there is no app-side group/claim check layered on top (D15, wave-10.md T10.2). Stops at NotImplementedError once the identity claim is in hand. Matching that claim to a local account and issuing the session cookie is T10.3, kept separate per the one-task-per-PR rule. wave-10.md T10.2 / D15
This commit is contained in:
@@ -9,6 +9,7 @@ Run (prod): gunicorn -k uvicorn.workers.UvicornWorker -b 127.0.0.1:8000 serve
|
||||
Interactive docs: http://<host>/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
|
||||
|
||||
Reference in New Issue
Block a user