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
|
Interactive docs: http://<host>/api/docs
|
||||||
"""
|
"""
|
||||||
import base64
|
import base64
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
@@ -17,6 +18,7 @@ from time import monotonic
|
|||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from authlib.integrations.base_client import OAuthError
|
||||||
from fastapi import FastAPI, Depends, HTTPException, Query, Request, Response, BackgroundTasks
|
from fastapi import FastAPI, Depends, HTTPException, Query, Request, Response, BackgroundTasks
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
@@ -26,7 +28,9 @@ from sqlalchemy import select, delete, func
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from .db import Base, engine, get_db
|
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:
|
# Schema management:
|
||||||
# • Local dev (SQLite) auto-creates tables for a zero-config run.
|
# • Local dev (SQLite) auto-creates tables for a zero-config run.
|
||||||
@@ -721,6 +725,41 @@ def logout(response: Response):
|
|||||||
return {"ok": True}
|
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) ──────────────────────
|
# ── Self-service password reset (needs email switched on) ──────────────────────
|
||||||
RESET_COOLDOWN_SECONDS = int(os.getenv("AUTH_RESET_COOLDOWN_SECONDS", "120"))
|
RESET_COOLDOWN_SECONDS = int(os.getenv("AUTH_RESET_COOLDOWN_SECONDS", "120"))
|
||||||
# In-process throttle: one reset mail per (account, client) per cooldown. Enough to
|
# In-process throttle: one reset mail per (account, client) per cooldown. Enough to
|
||||||
|
|||||||
Reference in New Issue
Block a user