diff --git a/src/oidc_test/auth_utils.py b/src/oidc_test/auth_utils.py index 7f0d278..880c111 100644 --- a/src/oidc_test/auth_utils.py +++ b/src/oidc_test/auth_utils.py @@ -19,18 +19,22 @@ logger = logging.getLogger(__name__) OIDC_PROVIDERS = set([provider.id for provider in settings.oidc.providers]) -def get_oidc_provider(request: Request) -> StarletteOAuth2App: +def get_oidc_provider_or_none(request: Request) -> StarletteOAuth2App | None: """Return the oidc_provider from a request object, from the session. It can be used in Depends()""" if (oidc_provider_id := request.session.get("oidc_provider_id")) is None: - raise HTTPException( - status.HTTP_503_SERVICE_UNAVAILABLE, - "Not logged in (no provider in session)", - ) + return try: return getattr(authlib_oauth, str(oidc_provider_id)) except AttributeError: + return + + +def get_oidc_provider(request: Request) -> StarletteOAuth2App: + if (oidc_provider := get_oidc_provider_or_none(request)) is None: raise HTTPException(status.HTTP_401_UNAUTHORIZED, "No such provider") + else: + return oidc_provider async def get_current_user(request: Request) -> User: diff --git a/src/oidc_test/main.py b/src/oidc_test/main.py index cfed74a..c835b7c 100644 --- a/src/oidc_test/main.py +++ b/src/oidc_test/main.py @@ -21,10 +21,11 @@ from authlib.integrations.httpx_client import AsyncOAuth2Client from authlib.oauth2.rfc6749 import OAuth2Token from pkce import generate_code_verifier, generate_pkce_pair -from .settings import settings +from .settings import settings, OIDCProvider from .models import User from .auth_utils import ( get_oidc_provider, + get_oidc_provider_or_none, hasrole, get_current_user_or_none, get_current_user, @@ -55,8 +56,8 @@ app.add_middleware( # Add oidc providers to authlib from the settings -fastapi_providers = {} -_providers = {} +# fastapi_providers: dict[str, OpenIdConnect] = {} +providers_settings: dict[str, OIDCProvider] = {} for provider in settings.oidc.providers: authlib_oauth.register( @@ -74,17 +75,27 @@ for provider in settings.oidc.providers: # update_token=update_token, # client_id="some-client-id", # if enabled, authlib will also check that the access token belongs to this client id (audience) ) - fastapi_providers[provider.id] = OpenIdConnect( - openIdConnectUrl=provider.openid_configuration - ) - _providers[provider.id] = provider + # fastapi_providers[provider.id] = OpenIdConnect( + # openIdConnectUrl=provider.openid_configuration + # ) + providers_settings[provider.id] = provider @app.get("/") async def home( - request: Request, user: Annotated[User, Depends(get_current_user_or_none)] + request: Request, + user: Annotated[User, Depends(get_current_user_or_none)], + oidc_provider: Annotated[ + StarletteOAuth2App | None, Depends(get_oidc_provider_or_none) + ], ) -> HTMLResponse: now = datetime.now() + if oidc_provider and ( + (provider := providers_settings.get(oidc_provider.name)) is not None + ): + resources = provider.resources + else: + resources = [] return templates.TemplateResponse( name="home.html", request=request, @@ -92,6 +103,7 @@ async def home( "settings": settings.model_dump(), "user": user, "now": now, + "resources": resources, "user_info_details": ( pretty_details(user, now) if user and settings.oidc.show_session_details @@ -112,11 +124,13 @@ async def login(request: Request, oidc_provider_id: str) -> RedirectResponse: """ redirect_uri = request.url_for("auth", oidc_provider_id=oidc_provider_id) try: - provider_: StarletteOAuth2App = getattr(authlib_oauth, oidc_provider_id) + provider: StarletteOAuth2App = getattr(authlib_oauth, oidc_provider_id) except AttributeError: raise HTTPException(status.HTTP_401_UNAUTHORIZED, "No such provider") if ( - code_challenge_method := _providers[oidc_provider_id].code_challenge_method + code_challenge_method := providers_settings[ + oidc_provider_id + ].code_challenge_method ) is not None: client = AsyncOAuth2Client(..., code_challenge_method=code_challenge_method) code_verifier = generate_code_verifier() @@ -124,7 +138,7 @@ async def login(request: Request, oidc_provider_id: str) -> RedirectResponse: else: code_verifier = None try: - response = await provider_.authorize_redirect( + response = await provider.authorize_redirect( request, redirect_uri, access_type="offline", @@ -238,26 +252,37 @@ async def non_compliant_logout( # Route for OAuth resource server -@app.get("/resource/{name}") +@app.get("/resource/{id}") async def get_resource( - name: str, + id: str, request: Request, user: Annotated[User, Depends(get_current_user)], oidc_provider: Annotated[StarletteOAuth2App, Depends(get_oidc_provider)], token: Annotated[OAuth2Token, Depends(get_token)], -) -> HTMLResponse: +) -> JSONResponse: """Generic path for testing a resource provided by a provider""" - provider = _providers[oidc_provider.name] + if oidc_provider is None: + raise HTTPException( + status.HTTP_406_NOT_ACCEPTABLE, detail="No such oidc provider" + ) + if (provider := providers_settings.get(oidc_provider.name)) is None: + raise HTTPException( + status.HTTP_406_NOT_ACCEPTABLE, detail="No oidc provider setting" + ) + try: + resource = next(x for x in provider.resources if x.id == id) + except StopIteration: + raise HTTPException( + status.HTTP_406_NOT_ACCEPTABLE, detail="No such resource for this provider" + ) if ( response := await oidc_provider.get( - "/api/v1/user/repos", + resource.url, # headers={"Authorization": f"token {token['access_token']}"}, token=token, ) ).is_success: - repos = response.json() - names = [repo["name"] for repo in repos] - return HTMLResponse(f"{user.name} has {len(repos)} repos: {', '.join(names)}") + return JSONResponse(response.json()) else: raise HTTPException(status_code=response.status_code, detail=response.text) diff --git a/src/oidc_test/settings.py b/src/oidc_test/settings.py index bf823ba..3a9447c 100644 --- a/src/oidc_test/settings.py +++ b/src/oidc_test/settings.py @@ -16,6 +16,7 @@ from pydantic_settings import ( class Resource(BaseModel): """A resource with an URL that can be accessed with an OAuth2 access token""" + id: str name: str url: str diff --git a/src/oidc_test/static/styles.css b/src/oidc_test/static/styles.css index 8c6961a..6065a91 100644 --- a/src/oidc_test/static/styles.css +++ b/src/oidc_test/static/styles.css @@ -57,7 +57,7 @@ hr { .user-info a.logout:hover { background-color: orange; } -debug-auth { +.debug-auth { font-size: 90%; background-color: #d8bebc75; padding: 6px; @@ -152,14 +152,14 @@ debug-auth { font-weight: bold; flex: 1 1 auto; } -.content #links-to-check { +.content .links-to-check { display: flex; text-align: center; justify-content: center; gap: 0.5em; flex-flow: wrap; } -.content #links-to-check a { +.content .links-to-check a { color: black; padding: 5px 10px; text-decoration: none; diff --git a/src/oidc_test/static/utils.js b/src/oidc_test/static/utils.js index ec9bcfc..6b40d3d 100644 --- a/src/oidc_test/static/utils.js +++ b/src/oidc_test/static/utils.js @@ -11,7 +11,9 @@ function checkHref(elem) { xmlHttp.send(null) } -function checkPerms(rootId) { - var rootElem = document.getElementById(rootId) - Array.from(rootElem.children).forEach(elem => checkHref(elem)) +function checkPerms(className) { + var rootElems = document.getElementsByClassName(className) + Array.from(rootElems).forEach(elem => + Array.from(elem.children).forEach(elem => checkHref(elem)) + ) } diff --git a/src/oidc_test/templates/home.html b/src/oidc_test/templates/home.html index d20f74c..ab4dc77 100644 --- a/src/oidc_test/templates/home.html +++ b/src/oidc_test/templates/home.html @@ -50,7 +50,7 @@

These links should get different response codes depending on the authorization:

- {% endif %} {% endblock %}