feat(auth): OIDC login + identity binding + HARBORFORGE_OIDC_ONLY
- Generic OIDC (Authlib discovery) Authorization Code flow; backend
issues the existing HS256 JWT on success. Unbound identities are
rejected (no auto-provisioning).
- User.oidc_issuer/oidc_subject (unique together) + startup migration.
- PUT/DELETE /users/{id}/oidc-binding (admin or account-manager;
JWT or API key; 409 on conflict). Self-link /auth/oidc/link
(non-OIDC_ONLY only). Public GET /auth/config.
- HARBORFORGE_OIDC_ONLY: /auth/token rejected, create/update ignore
password (passwordless users; API keys + OIDC still work).
- Dockerfile ARG/ENV HARBORFORGE_OIDC_ONLY; authlib+itsdangerous deps;
SessionMiddleware for OIDC state. Fixed _user_response to expose
the new binding fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
24
app/main.py
24
app/main.py
@@ -1,6 +1,9 @@
|
||||
"""HarborForge API — Agent/人类协同任务管理平台"""
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
app = FastAPI(
|
||||
title="HarborForge API",
|
||||
@@ -17,6 +20,17 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Short-lived signed session cookie — only used to carry the OIDC
|
||||
# state/nonce between /auth/oidc/login and the callback.
|
||||
app.add_middleware(
|
||||
SessionMiddleware,
|
||||
secret_key=settings.SECRET_KEY,
|
||||
session_cookie="hf_oidc",
|
||||
same_site="lax",
|
||||
https_only=False,
|
||||
max_age=600,
|
||||
)
|
||||
|
||||
# Health & version (kept at top level)
|
||||
@app.get("/health", tags=["System"])
|
||||
def health_check():
|
||||
@@ -65,8 +79,10 @@ from app.api.routers.meetings import router as meetings_router
|
||||
from app.api.routers.essentials import router as essentials_router
|
||||
from app.api.routers.schedule_type import router as schedule_type_router
|
||||
from app.api.routers.calendar import router as calendar_router
|
||||
from app.api.routers.oidc import router as oidc_router
|
||||
|
||||
app.include_router(auth_router)
|
||||
app.include_router(oidc_router)
|
||||
app.include_router(tasks_router)
|
||||
app.include_router(projects_router)
|
||||
app.include_router(users_router)
|
||||
@@ -277,6 +293,14 @@ def _migrate_schema():
|
||||
if _has_table(db, "users") and not _has_column(db, "users", "discord_user_id"):
|
||||
db.execute(text("ALTER TABLE users ADD COLUMN discord_user_id VARCHAR(32) NULL"))
|
||||
|
||||
# --- users OIDC binding (issuer + subject), unique together ---
|
||||
if _has_table(db, "users") and not _has_column(db, "users", "oidc_issuer"):
|
||||
db.execute(text("ALTER TABLE users ADD COLUMN oidc_issuer VARCHAR(255) NULL"))
|
||||
if _has_table(db, "users") and not _has_column(db, "users", "oidc_subject"):
|
||||
db.execute(text("ALTER TABLE users ADD COLUMN oidc_subject VARCHAR(255) NULL"))
|
||||
if _has_table(db, "users") and _has_column(db, "users", "oidc_subject"):
|
||||
_ensure_unique_index(db, "users", "uq_users_oidc_identity", "oidc_issuer, oidc_subject")
|
||||
|
||||
# --- monitored_servers.api_key for heartbeat v2 ---
|
||||
if _has_table(db, "monitored_servers") and not _has_column(db, "monitored_servers", "api_key"):
|
||||
db.execute(text("ALTER TABLE monitored_servers ADD COLUMN api_key VARCHAR(64) NULL"))
|
||||
|
||||
Reference in New Issue
Block a user