feat: implement ORCID OAuth callback and update environment configuration

- Added ORCID_REDIRECT_URI to docker-compose for OAuth integration.
- Introduced a new callback endpoint to handle ORCID login and token exchange.
- Refactored authentication logic to streamline the OAuth process.
- Ensured local environment variables are loaded for development.
This commit is contained in:
Mireya Cueto Garrido
2026-04-29 12:19:41 +02:00
parent fec26089ed
commit b49152946e
4 changed files with 52 additions and 19 deletions
+30 -18
View File
@@ -1,5 +1,7 @@
import httpx
import os
from pathlib import Path
from dotenv import load_dotenv
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.responses import RedirectResponse
from sqlalchemy.orm import Session
@@ -11,6 +13,10 @@ from app.security.jwt import create_access_token
from app.services.orcid_client import ORCIDClient
from app.utils.orcid_validator import is_valid_orcid
# Asegura que al ejecutar `uvicorn` local también se carga `backend/.env`.
_ENV_PATH = Path(__file__).resolve().parents[2] / ".env"
load_dotenv(dotenv_path=_ENV_PATH, override=False)
router = APIRouter(prefix="/auth", tags=["auth"])
@@ -29,25 +35,12 @@ def _orcid_redirect_uri() -> str:
return os.getenv("ORCID_REDIRECT_URI") or "http://localhost:8000/api/auth/orcid/callback"
@router.get("/orcid/authorize")
def authorize_orcid():
def _complete_oauth_login(*, code: str, db: Session) -> OrcidLoginResponseSchema:
"""
Inicia el flujo OAuth 3-legged (authorization code) hacia ORCID.
"""
client = ORCIDClient()
authorize_url = client.build_authorize_url(
redirect_uri=_orcid_redirect_uri(),
# Solo necesitamos el Authenticated iD del usuario.
scope="/authenticate",
)
return RedirectResponse(authorize_url)
@router.get("/orcid/callback", response_model=OrcidLoginResponseSchema)
def orcid_callback(code: str, db: Session = Depends(get_db)):
"""
Recibe el `code` devuelto por ORCID, lo intercambia por tokens en el servidor
y emite nuestro JWT solo para el ORCID autenticado por ORCID.
Completa el login OAuth:
1) intercambio del `code` en ORCID (server-side)
2) crea/actualiza el investigador
3) emite nuestro JWT
"""
if not code:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Missing ORCID authorization code")
@@ -95,3 +88,22 @@ def orcid_callback(code: str, db: Session = Depends(get_db)):
token = create_access_token(subject=orcid_id, extra={"rid": str(researcher.id)})
return OrcidLoginResponseSchema(access_token=token)
@router.get("/orcid/authorize")
def authorize_orcid():
"""
Inicia el flujo OAuth 3-legged (authorization code) hacia ORCID.
"""
client = ORCIDClient()
authorize_url = client.build_authorize_url(
redirect_uri=_orcid_redirect_uri(),
# Solo necesitamos el Authenticated iD del usuario.
scope="/authenticate",
)
return RedirectResponse(authorize_url)
@router.get("/orcid/callback", response_model=OrcidLoginResponseSchema)
def orcid_callback(code: str, db: Session = Depends(get_db)):
return _complete_oauth_login(code=code, db=db)
+14 -1
View File
@@ -1,10 +1,14 @@
from fastapi import FastAPI
from fastapi import Depends, FastAPI
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
from app.db.session import init_db
from app.db.session import get_db
from app.api.researchers import router as researchers_router
from app.api.export import router as export_router
from app.api.auth import router as auth_router
from app.api.auth import _complete_oauth_login
from app.schema.auth import OrcidLoginResponseSchema
from app.scheduler.sync_scheduler import start_scheduler
@@ -35,6 +39,15 @@ def health():
return {"status": "ok"}
@app.get("/callback", response_model=OrcidLoginResponseSchema)
def oauth_callback_root(code: str, db: Session = Depends(get_db)):
"""
Alias para probar redirect URIs como `https://127.0.0.1/callback` en local.
Intercambia el code con ORCID y emite el JWT.
"""
return _complete_oauth_login(code=code, db=db)
# ---------------------------------------------------------
# Registrar routers
# ---------------------------------------------------------
+7
View File
@@ -1,7 +1,9 @@
import os
import urllib.parse
from pathlib import Path
from typing import Any, Optional
from dotenv import load_dotenv
import httpx
TOKEN_URL_SANDBOX = "https://sandbox.orcid.org/oauth/token"
@@ -15,6 +17,11 @@ BASE_URL_SANDBOX = "https://pub.sandbox.orcid.org/v3.0"
class ORCIDClient:
def __init__(self):
# Asegura que al ejecutar `uvicorn` local también se carga `backend/.env`.
# (En docker `ORCID_REDIRECT_URI` y secretos llegan por env_file, así que esto no molesta.)
_env_path = Path(__file__).resolve().parents[2] / ".env"
load_dotenv(dotenv_path=_env_path, override=False)
self.client_id = os.getenv("ORCID_CLIENT_ID")
self.client_secret = os.getenv("ORCID_CLIENT_SECRET")
self._token_cache: Optional[str] = None