Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 4 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,7 @@ MONGODB_URI=mongodb://localhost:27017
# this — the built-in list already covers localhost ports. Deployments
# (kustomize overlays) set this to the deployed frontend host(s).
# CORS_EXTRA_ORIGINS=https://example.org,https://example-dev.org

AUTH0_DOMAIN=dev-bu7yo7484tyxu6a1.us.auth0.com
AUTH0_AUDIENCE=https://api.biosimulations.org
AUTH0_ISSUER=https://dev-bu7yo7484tyxu6a1.us.auth0.com/
75 changes: 75 additions & 0 deletions backend/biosim_server/common/auth/auth0.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import time
from functools import lru_cache

import httpx
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import jwt

from biosim_server.config import get_settings

bearer_scheme = HTTPBearer(auto_error=False)

_jwks_cache: dict = {"keys": None, "fetched_at": 0.0}
_JWKS_TTL_SECONDS = 3600


def _get_jwks() -> dict:
settings = get_settings().auth0
now = time.time()
if _jwks_cache["keys"] is None or now - _jwks_cache["fetched_at"] > _JWKS_TTL_SECONDS:
resp = httpx.get(f"https://{settings.domain}/.well-known/jwks.json", timeout=5.0)
resp.raise_for_status()
_jwks_cache["keys"] = resp.json()
_jwks_cache["fetched_at"] = now
return _jwks_cache["keys"]


class AuthenticatedUser:
def __init__(self, sub: str, email: str | None):
self.sub = sub # stable Auth0 user id, e.g. "auth0|abc123" or "google-oauth2|..."
self.email = email


def get_current_user(
credentials: HTTPAuthorizationCredentials | None = Depends(bearer_scheme),
) -> AuthenticatedUser:
if credentials is None:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Missing bearer token")

settings = get_settings().auth0
token = credentials.credentials

try:
unverified_header = jwt.get_unverified_header(token)
except Exception:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Malformed token")

jwks = _get_jwks()
rsa_key = next(
(
{"kty": k["kty"], "kid": k["kid"], "use": k["use"], "n": k["n"], "e": k["e"]}
for k in jwks["keys"]
if k["kid"] == unverified_header.get("kid")
),
None,
)
if rsa_key is None:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Unknown signing key")

try:
payload = jwt.decode(
token,
rsa_key,
algorithms=settings.algorithms,
audience=settings.audience,
issuer=f"https://{settings.domain}/",
)
except jwt.ExpiredSignatureError:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Token expired")
except jwt.JWTClaimsError:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid claims")
except Exception:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid token")

return AuthenticatedUser(sub=payload["sub"], email=payload.get("email"))
11 changes: 10 additions & 1 deletion backend/biosim_server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@
if os.getenv(ENV_SECRET_ENV_FILE) is not None and os.path.exists(str(os.getenv(ENV_SECRET_ENV_FILE))):
load_dotenv(os.getenv(ENV_SECRET_ENV_FILE))

class Auth0Settings(BaseSettings):
domain: str = Field(..., alias="AUTH0_DOMAIN"")
audience: str = Field(..., alias="AUTH0_AUDIENCE")
algorithms = list[str] = ["RS256"]

model_config = SettingsConfigDict(env_prefix"", extra="ignore")


class Settings(BaseSettings):
storage_backend: STORAGE_BACKEND = "gcs"
Expand Down Expand Up @@ -88,6 +95,9 @@ class Settings(BaseSettings):
simdata_api_base_url: str = "https://simdata.api.biosimulations.org"
biosimulators_api_base_url: str = "https://api.biosimulators.org"
biosimulations_api_base_url: str = "https://api.biosimulations.org"
auth0_domain: str = ""
auth0_audience: str = ""
auth0_issuer: str = ""

slurm_submit_host: str = "" # "hamantis.cam.uchc.edu"
slurm_submit_user: str = "" # "crbmapi"
Expand All @@ -108,4 +118,3 @@ def get_local_cache_dir() -> Path:
local_cache_dir = Path(settings.storage_local_cache_dir)
local_cache_dir.mkdir(parents=True, exist_ok=True)
return local_cache_dir

3 changes: 2 additions & 1 deletion backend/biosim_server/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from biosim_server.biosim_runs.database import DatabaseService, DatabaseServiceMongo
from biosim_server.common.storage import FileService, FileServiceGCS, FileServiceLocal, FileServiceMinio
from biosim_server.config import get_local_cache_dir, get_settings
from biosim_server.common.auth import get_current_user

if TYPE_CHECKING:
# Imported lazily at runtime (see init_standalone) to avoid an import cycle:
Expand Down Expand Up @@ -163,4 +164,4 @@ async def shutdown_standalone() -> None:
set_database_service(None)
# Shares the motor client closed via db_service above; just clear the handles.
set_simulation_run_database_service(None)
set_project_database_service(None)
set_project_database_service(None)
15 changes: 13 additions & 2 deletions backend/biosim_server/simulations/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
)
from biosim_server.simulations.workflow import SimulationRunWorkflow, SimulationRunWorkflowInput

from biosim_server.common.auth.auth0 import AuthenticatedUser, get_current_user

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/simulations", tags=["Simulations"])
Expand Down Expand Up @@ -140,7 +142,7 @@ async def run_simulations(request: RunSimulationRequest) -> ConglomerateStatus:
]
return ConglomerateStatus(processing_id=workflow_id, jobs=jobs)


'''
@router.post(
"/runs",
response_model=ListSimulationRunsResponse,
Expand All @@ -160,7 +162,16 @@ async def list_simulation_runs(request: ListSimulationRunsRequest) -> ListSimula
runs=[SimulationRun.from_record(record) for record in records],
pagination=request.pagination.model_copy(update={"total": total}),
)

'''

@router.post("/simulations/runs")
async def get_runs(
query: RunsQuery,
user: AuthenticatedUser = Depends(get_current_user),
):
if query.type == "user":
query.user = user.email
)

@router.get(
"/{processing_id}",
Expand Down
4 changes: 1 addition & 3 deletions backend/biosim_server/worker/worker_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,8 @@ async def main() -> None:


if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(main())
asyncio.run(main())
except KeyboardInterrupt:
interrupt_event.set()
loop.run_until_complete(loop.shutdown_asyncgens())
print("\nShutting down workers")
1 change: 1 addition & 0 deletions backend/node_modules/.bin/nanoid

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/node_modules/.bin/parser

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading