Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
48dc555
user endpoints init
minh-biocommons Apr 24, 2025
845fa66
Merge branch 'auth0-integration' of https://github.com/AustralianBioC…
minh-biocommons Apr 24, 2025
f5cad69
Refactor user router to improve code organization
minh-biocommons Apr 24, 2025
67c3c84
Refactor user router to improve code organization and maintainability
minh-biocommons Apr 24, 2025
efe8965
Fix indentation in main.py for consistency
minh-biocommons Apr 24, 2025
064194b
Refactor test files for improved readability and consistency
minh-biocommons Apr 24, 2025
9231d18
Enhance JWT verification and admin access control; update tests for c…
minh-biocommons Apr 24, 2025
a37f55d
Add authentication tests and error handling for service endpoints
minh-biocommons Apr 24, 2025
6095199
Add mock for HTTP client in authentication tests to simulate API resp…
minh-biocommons Apr 24, 2025
7ab5a7c
Refactor mock user data and response fixtures for consistency; update…
minh-biocommons Apr 24, 2025
abc72a8
Update environment variables in GitHub Actions workflow for authentic…
minh-biocommons Apr 24, 2025
cb1710e
Refactor GitHub Actions workflow: update environment variables for cl…
minh-biocommons Apr 24, 2025
00acc6d
Merge remote-tracking branch 'origin/main' into user-endpoints
minh-biocommons May 7, 2025
9344a96
Merge remote-tracking branch 'origin/main' into user-endpoints
minh-biocommons May 7, 2025
3acad74
compily with PEP8
amandazhuyilan May 9, 2025
3531aa4
fix current tests and add more tests
amandazhuyilan May 9, 2025
384203f
add nit fixes
amandazhuyilan May 9, 2025
1c74221
refactor: clean up code formatting and improve readability across mul…
minh-biocommons May 12, 2025
07a0987
fix(tests): update service and resource tests to use new mock functions
minh-biocommons May 12, 2025
86e71d5
fix(tests): enhance service and resource tests with additional scenar…
minh-biocommons May 12, 2025
ace44c5
fix(tests): update resource request test to use mocked user data and …
minh-biocommons May 12, 2025
3d4223d
update tests
minh-biocommons May 12, 2025
1dcebe0
update models, routes & tests
minh-biocommons May 12, 2025
93a241b
Update tests to use automatically generated Auth0User
marius-mather May 13, 2025
c81d439
Remove unused import
marius-mather May 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-ecr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:

permissions:
contents: read
id-token: write # Required for OIDC
id-token: write # Required for OIDC
actions: read

env:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,4 @@ jobs:
EOF

- name: CDK Deploy
run: cdk deploy --require-approval never
run: cdk deploy --require-approval never
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ uv run pytest

# Deployment

Currently the service is deployed to AWS via the CDK scripts in `deploy/`,
Currently the service is deployed to AWS via the CDK scripts in `deploy/`,
and updated on each commit to `main`.

Secrets/configuration variables for the deployment are stored in the
GitHub Secrets for the repository.
GitHub Secrets for the repository.
2 changes: 1 addition & 1 deletion auth/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ class Settings(BaseSettings):

@lru_cache()
def get_settings():
return Settings()
return Settings()
12 changes: 6 additions & 6 deletions auth/management.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@

def get_management_token():
settings = get_settings()
url = f'https://{settings.auth0_domain}/oauth/token'
url = f"https://{settings.auth0_domain}/oauth/token"
payload = {
'grant_type': 'client_credentials',
'client_id': settings.auth0_management_id,
'client_secret': settings.auth0_management_secret,
'audience': f'https://{settings.auth0_domain}/api/v2/'
"grant_type": "client_credentials",
"client_id": settings.auth0_management_id,
"client_secret": settings.auth0_management_secret,
"audience": f"https://{settings.auth0_domain}/api/v2/",
}
response = httpx.post(url, json=payload)
response.raise_for_status()
return response.json()['access_token']
return response.json()["access_token"]
61 changes: 39 additions & 22 deletions auth/validator.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,14 @@
from typing import Dict

import httpx
from fastapi import HTTPException
from jose import jwt, jwk
from jose.exceptions import JWTError

from auth.config import get_settings
from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
from jose import jwt, jwk
from jose.exceptions import JWTError
from schemas.tokens import AccessTokenPayload
from schemas.user import User


def get_rsa_key(token: str) -> jwk.RSAKey | None:
settings = get_settings()
jwks_url = f"https://{settings.auth0_domain}/.well-known/jwks.json"
response = httpx.get(jwks_url)
jwks = response.json()
unverified_header = jwt.get_unverified_header(token)

for key in jwks["keys"]:
if key["kid"] == unverified_header["kid"]:
return jwk.construct(key)
return None
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


def verify_jwt(token: str) -> AccessTokenPayload:
Expand All @@ -28,26 +17,54 @@ def verify_jwt(token: str) -> AccessTokenPayload:
rsa_key = get_rsa_key(token)
except JWTError as e:
raise HTTPException(status_code=401, detail=f"Invalid token: {e}")

if rsa_key is None:
raise HTTPException(status_code=401, detail="Couldn't find a matching signing key.")
raise HTTPException(
status_code=401, detail="Couldn't find a matching signing key."
)

try:
payload = jwt.decode(
token,
rsa_key,
algorithms=settings.auth0_algorithms,
audience=settings.auth0_audience,
issuer=f"https://{settings.auth0_domain}/"
issuer=f"https://{settings.auth0_domain}/",
)
except JWTError as e:
raise HTTPException(status_code=401, detail=f"Invalid token: {e}")

roles_claim = "biocommons.org.au/roles"
if roles_claim not in payload:
raise HTTPException(status_code=403, detail=f"Missing required claim: {roles_claim}")
raise HTTPException(
status_code=403, detail=f"Missing required claim: {roles_claim}"
)

roles = payload[roles_claim]
if not isinstance(roles, list) or not any("admin" in role.lower() for role in roles):
raise HTTPException(status_code=403, detail=f"Access denied: Insufficient permissions")
if not isinstance(roles, list) or not any(
"admin" in role.lower() for role in roles
):
raise HTTPException(
status_code=403, detail="Access denied: Insufficient permissions"
)

return AccessTokenPayload(**payload)


def get_rsa_key(token: str) -> jwk.RSAKey | None: # type: ignore
settings = get_settings()
jwks_url = f"https://{settings.auth0_domain}/.well-known/jwks.json"
response = httpx.get(jwks_url)
jwks = response.json()
unverified_header = jwt.get_unverified_header(token)

for key in jwks["keys"]:
if key["kid"] == unverified_header["kid"]:
return jwk.construct(key)

return None


def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
access_token = verify_jwt(token)
return User(access_token=access_token)
20 changes: 5 additions & 15 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,12 @@
from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer

from auth.management import get_management_token
from auth.validator import verify_jwt
from schemas.user import User
from fastapi import FastAPI
from routers import user

app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
access_token = verify_jwt(token)
return User(access_token=access_token)

@app.get("/")
def public_route():
return {"message": "Public route"}
return {"message": "AAI Backend API"}


@app.get("/private")
def private_route(user=Depends(get_current_user)):
return {"message": "Private route", "user_claims": user, "management_token": get_management_token()}
app.include_router(user.router)
Loading