From b49152946e644eca0a749c142d983c61a18f396e Mon Sep 17 00:00:00 2001 From: Mireya Cueto Garrido Date: Wed, 29 Apr 2026 12:19:41 +0200 Subject: [PATCH] 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. --- backend/app/api/auth.py | 48 +++++++++++++++++----------- backend/app/main.py | 15 ++++++++- backend/app/services/orcid_client.py | 7 ++++ docker-compose.yml | 1 + 4 files changed, 52 insertions(+), 19 deletions(-) diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py index 99a1e7b..205cb95 100644 --- a/backend/app/api/auth.py +++ b/backend/app/api/auth.py @@ -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) + diff --git a/backend/app/main.py b/backend/app/main.py index 11c481a..1e5d6c8 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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 # --------------------------------------------------------- diff --git a/backend/app/services/orcid_client.py b/backend/app/services/orcid_client.py index 811a5ff..2e15add 100644 --- a/backend/app/services/orcid_client.py +++ b/backend/app/services/orcid_client.py @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index 45e2e0c..948de4a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,7 @@ services: environment: DATABASE_URL: postgresql://postgres:postgres@db:5432/orcid_db REDIS_URL: redis://redis:6379/0 + ORCID_REDIRECT_URI: https://willfully-brunette-antennae.ngrok-free.dev/callback depends_on: - db - redis