Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTPBearer token is set, Auth button not shown on /api/docs #67

Merged
merged 27 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4ae5b2d
WIP: HTTPBearer token is set
Igoranze Oct 10, 2024
a611595
Fix: Authenticate refactor for GraphQL and other
Igoranze Oct 14, 2024
ee5085b
Fix: raise 403 on empty token
Igoranze Oct 14, 2024
23b0e16
Fix: Add test with token extractor
Igoranze Oct 14, 2024
bb5588b
Fix: changed bool to False auto_error
Igoranze Oct 14, 2024
8643e6c
Fix artefact package
pboers1988 Oct 14, 2024
8382c10
Linting
pboers1988 Oct 14, 2024
37433f3
Fix: Linting issues
Igoranze Oct 15, 2024
08c14b5
Fix: space removal black check .
Igoranze Oct 15, 2024
1239b56
Refactor: make it more abstract
Igoranze Oct 17, 2024
27e6a7f
Fix: linting
Igoranze Oct 17, 2024
effa3d8
Test: fix tests
Igoranze Oct 17, 2024
588231b
Fix: linting imports
Igoranze Oct 17, 2024
b3f4957
Fix: linting return type
Igoranze Oct 17, 2024
2e45752
Merge branch 'main' into auth-b-shown
Igoranze Oct 22, 2024
6079861
Add init to HTTPBearerExtractor
Igoranze Oct 22, 2024
f9e4f4c
Fix: Black --check
Igoranze Oct 22, 2024
a64b4ea
Fix typing decorator and removed unused code
Igoranze Oct 24, 2024
a7103f7
fix: black
Igoranze Oct 24, 2024
feac762
Update oauth2_lib/fastapi.py
Igoranze Oct 24, 2024
41b1cd0
Update oauth2_lib/fastapi.py
Igoranze Oct 24, 2024
576f944
Fix: ruff
Igoranze Oct 24, 2024
ce63c77
Merge branch 'auth-b-shown' of github.com:workfloworchestrator/oauth2…
Igoranze Oct 24, 2024
e163eac
Rollback Typing checks
Igoranze Oct 24, 2024
eeffe05
Add: typing ignore on decorator tests
Igoranze Oct 24, 2024
96ad751
bump version
Igoranze Oct 24, 2024
944473b
bump version
Igoranze Oct 24, 2024
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
52 changes: 21 additions & 31 deletions oauth2_lib/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

from fastapi import HTTPException
from fastapi.requests import Request
from fastapi.security.http import HTTPBearer
from fastapi.security.http import HTTPAuthorizationCredentials, HTTPBearer
from httpx import AsyncClient, NetworkError
from pydantic import BaseModel
from starlette.requests import ClientDisconnect, HTTPConnection
Expand Down Expand Up @@ -126,7 +126,7 @@ class Authentication(ABC):
"""

@abstractmethod
async def authenticate(self, request: HTTPConnection, token: str | None = None) -> dict | None:
async def authenticate(self, request: Request, token: str | None = None) -> dict | None:
"""Authenticate the user."""
pass

Expand All @@ -142,17 +142,24 @@ async def extract(self, request: Request) -> str | None:
pass


class HttpBearerExtractor(IdTokenExtractor):
class HttpBearerExtractor(HTTPBearer, IdTokenExtractor):
"""Extracts bearer tokens using FastAPI's HTTPBearer.

Specifically designed for HTTP Authorization header token extraction.
"""

def __init__(self, auto_error: bool = False):
super().__init__(auto_error=auto_error)

async def __call__(self, request: Request) -> Optional[HTTPAuthorizationCredentials]:
"""Extract the Credentials from the request."""
Igoranze marked this conversation as resolved.
Show resolved Hide resolved
return await super().__call__(request)

async def extract(self, request: Request) -> str | None:
http_bearer = HTTPBearer(auto_error=False)
credential = await http_bearer(request)
"""Extract the token from the http_auth_credentials."""
Igoranze marked this conversation as resolved.
Show resolved Hide resolved
http_auth_credentials = await super().__call__(request)

return credential.credentials if credential else None
return http_auth_credentials.credentials if http_auth_credentials else None


class OIDCAuth(Authentication):
Expand Down Expand Up @@ -181,7 +188,7 @@ def __init__(

self.openid_config: OIDCConfig | None = None

async def authenticate(self, request: HTTPConnection, token: str | None = None) -> OIDCUserModel | None:
async def authenticate(self, request: Request, token: str | None = None) -> OIDCUserModel | None:
"""Return the OIDC user from OIDC introspect endpoint.

This is used as a security module in Fastapi projects
Expand All @@ -197,33 +204,16 @@ async def authenticate(self, request: HTTPConnection, token: str | None = None)
if not oauth2lib_settings.OAUTH2_ACTIVE:
return None

async with AsyncClient(http1=True, verify=HTTPX_SSL_CONTEXT) as async_client:
await self.check_openid_config(async_client)

# Handle WebSocket requests separately only to check for token presence.
if isinstance(request, WebSocket):
if token is None:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="Not authenticated",
)
token_or_extracted_id_token = token
else:
request = cast(Request, request)

if await self.is_bypassable_request(request):
return None
if await self.is_bypassable_request(request):
return None

if token is None:
extracted_id_token = await self.id_token_extractor.extract(request)
if not extracted_id_token:
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Not authenticated")
if not token:
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Not authenticated")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you did this because the new WebSocket implementation passes the token in a header. Nice!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct :) And it no longer servers only for the websocket since that should not matter


token_or_extracted_id_token = extracted_id_token
else:
token_or_extracted_id_token = token
async with AsyncClient(http1=True, verify=HTTPX_SSL_CONTEXT) as async_client:
await self.check_openid_config(async_client)

user_info: OIDCUserModel = await self.userinfo(async_client, token_or_extracted_id_token)
user_info: OIDCUserModel = await self.userinfo(async_client, token)
logger.debug("OIDCUserModel object.", user_info=user_info)
return user_info

Expand Down
3 changes: 2 additions & 1 deletion oauth2_lib/strawberry.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ async def get_current_user(self) -> OIDCUserModel | None:
return None

try:
return await self.auth_manager.authentication.authenticate(self.request)
token = await self.auth_manager.authentication.id_token_extractor.extract(self.request)
return await self.auth_manager.authentication.authenticate(self.request, token)
except HTTPException as exc:
logger.debug("User is not authenticated", status_code=exc.status_code, detail=exc.detail)
return None
Expand Down
9 changes: 6 additions & 3 deletions tests/test_fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,15 +144,15 @@ def test_oidc_auth_initialization_default_extractor(oidc_auth):
async def test_extract_token_success():
request = mock.MagicMock()
request.headers = {"Authorization": "Bearer example_token"}
extractor = HttpBearerExtractor()
extractor = HttpBearerExtractor(auto_error=False)
assert await extractor.extract(request) == "example_token", "Token extraction failed"


@pytest.mark.asyncio
async def test_extract_token_returns_none():
request = mock.MagicMock()
request.headers = {}
extractor = HttpBearerExtractor()
extractor = HttpBearerExtractor(auto_error=False)
assert await extractor.extract(request) is None


Expand All @@ -165,7 +165,10 @@ async def test_authenticate_success(make_mock_async_client, discovery, oidc_auth
request = mock.MagicMock(spec=Request)
request.headers = {"Authorization": "Bearer valid_token"}

user = await oidc_auth.authenticate(request)
http_bearer_extractor = HttpBearerExtractor(auto_error=False)
token = await http_bearer_extractor(request)
# token = await oidc_auth.id_token_extractor.extract(request)
user = await oidc_auth.authenticate(request, token)
assert user == user_info_matching, "Authentication failed for a valid token"


Expand Down
Loading