diff --git a/examples/service-fastapi/app/security.py b/examples/service-fastapi/app/security.py index bfabaa3b..30d8b941 100644 --- a/examples/service-fastapi/app/security.py +++ b/examples/service-fastapi/app/security.py @@ -4,83 +4,55 @@ from fastapi import HTTPException from fastapi import Security from fastapi import status from fastapi.security import OAuth2AuthorizationCodeBearer -from fastapi.security.api_key import APIKeyCookie from fastapi.security.api_key import APIKeyQuery from .client import get_client +from .models import User -### For authenticated endpoints, we want to get auth from one of three ways -### 1. "token" in the url params -### 2. "jupyterhub-services" cookie (or config via env) -### 3. Authorization Bearer header (with oauth to Hub support) +### Endpoints can require authentication using Depends(get_current_user) +### get_current_user will look for a token in url params or +### Authorization: bearer token (header). +### Hub technically supports cookie auth too, but it is deprecated so +### not being included here. auth_by_param = APIKeyQuery(name="token", auto_error=False) -COOKIE_NAME = os.getenv("JUPYTERHUB_COOKIE_NAME", "jupyterhub-services") -auth_by_cookie = APIKeyCookie(name=COOKIE_NAME, auto_error=False) - -if "PUBLIC_HOST" in os.environ: - ### When running in Docker or maybe other infrastructure, - ### JUPYTERHUB_API_URL is "http://jupyterhub" but we need to give - ### clients (swagger webpage) a link to the public url - auth_url = os.environ["PUBLIC_HOST"] + "/hub/api/oauth2/authorize" -else: - auth_url = os.environ["JUPYTERHUB_API_URL"] + "/oauth2/authorize" +auth_url = os.environ["PUBLIC_HOST"] + "/hub/api/oauth2/authorize" auth_by_header = OAuth2AuthorizationCodeBearer( authorizationUrl=auth_url, tokenUrl="get_token", auto_error=False ) -### ^^ For Oauth in the Swagger webpage, we set the authorizationUrl -### to the Hub /oauth2/authorize endpoint, so browser does a GET there and -### receives a 'code' in return. Then the browser does a POST to our -### internal /get_token endpoint with that code, and our server subsequently -### POSTs to Hub /oauth2/token to get/return an acecss_token. -### The reason for the double POST is that the client (swagger ui) doesn't have -### the client_secret (JUPYTERHUB_API_TOKEN), but our server does. +### ^^ The flow for OAuth2 in Swagger is that the "authorize" button +### will redirect user (browser) to "auth_url", which is the Hub login page. +### After logging in, the browser will POST to our internal /get_token endpoint +### with the auth code. That endpoint POST's to Hub /oauth2/token with +### our client_secret (JUPYTERHUB_API_TOKEN) and that code to get an +### access_token, which it returns to browser, which places in Authorization header. -### For consideration: build a pydantic User model -### instead of just returning the dict from Hub api? -### Also: optimize performance with a cache instead of +### For consideration: optimize performance with a cache instead of ### always hitting the Hub api? async def get_current_user( - auth_by_cookie: str = Security(auth_by_cookie), auth_by_param: str = Security(auth_by_param), auth_by_header: str = Security(auth_by_header), ): - ### Note all three Security functions are auto_error=False, - ### meaning if the scheme (header/cookie/param) isn't present - ### then they return None. - ### The cookie can be tricky. Navigating to the Hub login - ### page but not logging in still sets a cookie, but - ### Hub API returns a 404 if you query that cookie - user = None - if auth_by_param is not None or auth_by_header is not None: - token = auth_by_param or auth_by_header - async with get_client() as client: - endpoint = "/authorizations/token/%s" % token - resp = await client.get(endpoint) - if resp.is_error: - raise HTTPException( - resp.status_code, - detail={ - "msg": "Error getting user info from token", - "request_url": str(resp.request.url), - "token": token, - "hub_response": resp.json(), - }, - ) - else: - user = resp.json() - - elif auth_by_cookie is not None: - async with get_client() as client: - endpoint = "/authorizations/cookie/%s/%s" % (COOKIE_NAME, auth_by_cookie) - resp = await client.get(endpoint) - if not resp.is_error: - user = resp.json() - - if user is None: + token = auth_by_param or auth_by_header + if token is None: raise HTTPException( status.HTTP_401_UNAUTHORIZED, - detail="Must login with token parameter, cookie, or header", + detail="Must login with token parameter or Authorization bearer header", ) - else: - return user + + async with get_client() as client: + endpoint = "/authorizations/token/%s" % token + resp = await client.get(endpoint) + if resp.is_error: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail={ + "msg": "Error getting user info from token", + "request_url": str(resp.request.url), + "token": token, + "response_code": resp.status_code, + "hub_response": resp.json(), + }, + ) + user = User(**resp.json()) + return user