Fix error handling in resource server
Some checks failed
/ build (push) Failing after 14s
/ test (push) Successful in 5s

This commit is contained in:
phil 2025-02-01 02:01:53 +01:00
parent f7ea132b7c
commit 17bf34a8a1
3 changed files with 78 additions and 59 deletions

View file

@ -13,6 +13,7 @@ logger = logging.getLogger(__name__)
class UserNotInDB(Exception): class UserNotInDB(Exception):
pass pass
class Database: class Database:
users: dict[str, User] = {} users: dict[str, User] = {}
tokens: dict[str, OAuth2Token] = {} tokens: dict[str, OAuth2Token] = {}

View file

@ -56,7 +56,6 @@ async def lifespan(app: FastAPI):
app = FastAPI(title="OIDC auth test", lifespan=lifespan) app = FastAPI(title="OIDC auth test", lifespan=lifespan)
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=settings.cors_origins, allow_origins=settings.cors_origins,

View file

@ -2,74 +2,93 @@ from datetime import datetime
import logging import logging
from httpx import AsyncClient from httpx import AsyncClient
from fastapi import HTTPException, status
from jwt import ExpiredSignatureError, InvalidKeyError, decode
from .models import User from .models import User
from .auth_utils import oidc_providers_settings
from .settings import settings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
async def get_resource(id: str, user: User) -> dict: async def get_resource(resource_id: str, user: User) -> dict:
"""
Resource processing: build an informative rely as a simple showcase
"""
pname = getattr(user.oidc_provider, "name", "?") pname = getattr(user.oidc_provider, "name", "?")
resp = { resp = {
"hello": f"Hi {user.name} from an OAuth resource provider.", "hello": f"Hi {user.name} from an OAuth resource provider",
"comment": f"I received a request for '{id}' with an access token signed by {pname}.", "comment": f"I received a request for '{resource_id}' "
+ f"with an access token signed by {pname}",
} }
scope = f"get:{id}" # For the demo, resource resource_id matches a scope get:resource_id,
user_scopes = user.userinfo["scope"].split(" ") # but this has to be refined for production
if scope in user_scopes: required_scope = f"get:{resource_id}"
if id == "time": # Check if the required scope is in the scopes allowed in userinfo
resp["time"] = datetime.now().strftime("%c") if "required_scope" in user.userinfo:
elif id == "bs": user_scopes = user.userinfo["required_scope"].split(" ")
async with AsyncClient() as client: if required_scope in user_scopes:
bs = await client.get( await process(user, required_scope, resp)
"https://corporatebs-generator.sameerkumar.website/"
)
resp["bs"] = bs.json().get("phrase", "Sorry, i am out of BS today.")
else:
resp["sorry"] = f"I don't known how to give '{id}' but i know corporate bs."
else: else:
## For the showcase, giving a explanation.
## Alternatively, raise HTTP_401_UNAUTHORIZED
resp["sorry"] = ( resp["sorry"] = (
f"I don't serve the ressource {id} to you because there is no scope {scope} in the access token," f"No scope {required_scope} in the access token "
+ "but it is required for accessing this resource."
) )
else:
resp["sorry"] = "There is no scope in id token"
return resp return resp
# assert user.oidc_provider is not None
### Get some info (TODO: refactor) async def process(user, resource_id, resp):
# if (auth_provider_id := user.oidc_provider.name) is None: """
# raise HTTPException( Too simple to be serious.
# status.HTTP_401_UNAUTHORIZED, It's a good fit for a plugin architecture for production
# "Request headers must have a 'auth_provider' field", """
# ) assert user is not None
# if ( if resource_id == "time":
# auth_provider_settings := oidc_providers_settings.get(auth_provider_id) resp["time"] = datetime.now().strftime("%c")
# ) is None: elif resource_id == "bs":
# raise HTTPException( async with AsyncClient() as client:
# status.HTTP_401_UNAUTHORIZED, f"Unknown auth provider '{auth_provider_id}'" bs = await client.get("https://corporatebs-generator.sameerkumar.website/")
# ) resp["bs"] = bs.json().get("phrase", "Sorry, i am out of BS today.")
# if (key := auth_provider_settings.get_public_key()) is None: else:
# raise HTTPException( resp["sorry"] = (
# status.HTTP_401_UNAUTHORIZED, f"I don't known how to give '{resource_id}' but i know corporate bs."
# f"Key for provider '{auth_provider_id}' unknown", )
# )
# logger.warn(f"refresh with scope {scope}")
# breakpoint() # assert user.oidc_provider is not None
# refreshed_auth_info = await user.oidc_provider.fetch_access_token(scope=scope) ### Get some info (TODO: refactor)
### Decode the new token # if (auth_provider_id := user.oidc_provider.name) is None:
# try: # raise HTTPException(
# payload = decode( # status.HTTP_401_UNAUTHORIZED,
# refreshed_auth_info["access_token"], # "Request headers must have a 'auth_provider' field",
# key=key, # )
# algorithms=["RS256"], # if (
# audience="account", # auth_provider_settings := oidc_providers_settings.get(auth_provider_id)
# options={"verify_signature": not settings.insecure.skip_verify_signature}, # ) is None:
# ) # raise HTTPException(
# except ExpiredSignatureError as err: # status.HTTP_401_UNAUTHORIZED, f"Unknown auth provider '{auth_provider_id}'"
# logger.info(f"Expired signature: {err}") # )
# raise HTTPException( # if (key := auth_provider_settings.get_public_key()) is None:
# status.HTTP_401_UNAUTHORIZED, # raise HTTPException(
# "Expired signature (refresh not implemented yet)", # status.HTTP_401_UNAUTHORIZED,
# ) # f"Key for provider '{auth_provider_id}' unknown",
# )
# logger.warn(f"refresh with scope {scope}")
# breakpoint()
# refreshed_auth_info = await user.oidc_provider.fetch_access_token(scope=scope)
### Decode the new token
# try:
# payload = decode(
# refreshed_auth_info["access_token"],
# key=key,
# algorithms=["RS256"],
# audience="account",
# options={"verify_signature": not settings.insecure.skip_verify_signature},
# )
# except ExpiredSignatureError as err:
# logger.info(f"Expired signature: {err}")
# raise HTTPException(
# status.HTTP_401_UNAUTHORIZED,
# "Expired signature (refresh not implemented yet)",
# )