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 7 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
16 changes: 0 additions & 16 deletions .github/workflows/pull-request.yml

This file was deleted.

4 changes: 2 additions & 2 deletions .github/workflows/test-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
env:
COVERAGE_FILE: reports/.coverage.${{ matrix.python-version }}
- name: Upload pytest test results
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: reports
path: reports
Expand All @@ -61,7 +61,7 @@ jobs:
with:
python-version: '3.8'
- name: Get coverage files
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: reports
path: reports
Expand Down
80 changes: 56 additions & 24 deletions oauth2_lib/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,49 @@ async def extract(self, request: Request) -> str | None:
return credential.credentials if credential else None


class TokenExtractor(HTTPBearer):
"""Extracts tokens from HTTP requests.

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

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

async def __call__(self, request: Request, token: str | None = None) -> str | None:
"""Extract the token from the request.

Args:
request: Fastapi Request object.
token: token

Returns:
str: The token extracted from the request.

"""
# 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 token is None:
credentials = await super().__call__(request)
if not credentials:
return None

token_or_extracted_id_token = credentials.credentials
else:
token_or_extracted_id_token = token

return token_or_extracted_id_token


class OIDCAuth(Authentication):
"""Implements OIDC authentication.

Expand All @@ -181,14 +224,17 @@ 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, is_strawberry_request: bool = False
) -> OIDCUserModel | None:
"""Return the OIDC user from OIDC introspect endpoint.

This is used as a security module in Fastapi projects

Args:
request: Starlette request/websocket method.
token: Optional value to directly pass a token.
is_strawberry_request: argument to signify strawberry request.

Returns:
OIDCUserModel object.
Expand All @@ -197,33 +243,19 @@ 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 is_strawberry_request:
token = await self.id_token_extractor.extract(request)

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

try:
return await self.auth_manager.authentication.authenticate(self.request)
return await self.auth_manager.authentication.authenticate(self.request, is_strawberry_request=True)
except HTTPException as exc:
logger.debug("User is not authenticated", status_code=exc.status_code, detail=exc.detail)
return None
Expand Down
5 changes: 3 additions & 2 deletions tests/test_fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from httpx import AsyncClient, BasicAuth
from starlette.websockets import WebSocket

from oauth2_lib.fastapi import HttpBearerExtractor, OIDCAuth, OIDCConfig, OIDCUserModel
from oauth2_lib.fastapi import HttpBearerExtractor, OIDCAuth, OIDCConfig, OIDCUserModel, TokenExtractor
from oauth2_lib.settings import oauth2lib_settings
from tests.conftest import MockResponse

Expand Down Expand Up @@ -165,7 +165,8 @@ 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)
token = await TokenExtractor().__call__(request)
user = await oidc_auth.authenticate(request, token)
assert user == user_info_matching, "Authentication failed for a valid token"


Expand Down
Loading