Skip to content

Commit

Permalink
Implement HTTPBasic security (#564)
Browse files Browse the repository at this point in the history
* Implement HTTPBasic security

* pre-commit fix

---------

Co-authored-by: Davor Runje <davor@airt.ai>
  • Loading branch information
sternakt and davorrunje authored Nov 12, 2024
1 parent f861081 commit b781e16
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 3 deletions.
1 change: 1 addition & 0 deletions docs/docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ search:
- [APIKeyQuery](api/fastagency/api/openapi/security/APIKeyQuery.md)
- [BaseSecurity](api/fastagency/api/openapi/security/BaseSecurity.md)
- [BaseSecurityParameters](api/fastagency/api/openapi/security/BaseSecurityParameters.md)
- [HTTPBasic](api/fastagency/api/openapi/security/HTTPBasic.md)
- [HTTPBearer](api/fastagency/api/openapi/security/HTTPBearer.md)
- [OAuth2PasswordBearer](api/fastagency/api/openapi/security/OAuth2PasswordBearer.md)
- [UnsuportedSecurityStub](api/fastagency/api/openapi/security/UnsuportedSecurityStub.md)
Expand Down
11 changes: 11 additions & 0 deletions docs/docs/en/api/fastagency/api/openapi/security/HTTPBasic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
# 0.5 - API
# 2 - Release
# 3 - Contributing
# 5 - Template Page
# 10 - Default
search:
boost: 0.5
---

::: fastagency.api.openapi.security.HTTPBasic
28 changes: 26 additions & 2 deletions docs/docs/en/user-guide/api/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ FastAgency currently supports the following security schemas:
1. **HTTP Bearer Token**
An HTTP authentication scheme using Bearer tokens, commonly used for securing RESTful APIs.

2. **API Key**
2. **HTTP Basic Auth**
HTTP authenticcation scheme using username/password pair.

3. **API Key**
API keys can be provided in:
- HTTP header
- Query parameters
- Cookies

3. **OAuth2 (Password Flow)**
4. **OAuth2 (Password Flow)**
A flow where the token authority and API service reside on the same address. This is useful for scenarios where the client credentials are exchanged for a token directly.

## Defining Security Schemas in OpenAPI
Expand All @@ -37,6 +40,21 @@ In your OpenAPI schema:
}
```

### HTTP Basic auth
In your OpenAPI schema:
```json
{
"components": {
"securitySchemes": {
"BasicAuth": {
"type": "http",
"scheme": "basic"
}
}
}
}
```

### API Key (in Header, Query, or Cookie)
In your OpenAPI schema:
```json
Expand Down Expand Up @@ -96,6 +114,12 @@ To configure bearer token authentication, provide the token when initializing th
{! docs_src/user_guide/external_rest_apis/security_examples.py [ln:66-70] !}
```

### Using HTTP Basic Auth
To configure basic authentication, provide the username and password when initializing the API client:
```python hl_lines="5"
{! docs_src/user_guide/external_rest_apis/security_examples.py [ln:77-81] !}
```

### Using API Key (in Header, Query, or Cookie)
You can configure the client to send an API key in the appropriate location (header, query, or cookie):

Expand Down
11 changes: 11 additions & 0 deletions docs/docs_src/user_guide/external_rest_apis/security_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,14 @@ def configure_http_bearer_client(
api_client.set_security_params(HTTPBearer.Parameters(value=api_key)) # API key

return api_client

def configure_http_basic_client(
openapi_url: str, username: str, password: str
) -> OpenAPI:
from fastagency.api.openapi import OpenAPI
from fastagency.api.openapi.security import HTTPBasic

api_client = OpenAPI.create(openapi_url=openapi_url) # API openapi specification url
api_client.set_security_params(HTTPBasic.Parameters(username=username, password=password)) # username/password

return api_client
37 changes: 37 additions & 0 deletions fastagency/api/openapi/security.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import base64
import logging
from typing import Any, ClassVar, Literal, Optional, Protocol

Expand Down Expand Up @@ -229,6 +230,42 @@ def get_security_class(self) -> type[BaseSecurity]:
return HTTPBearer


class HTTPBasic(BaseSecurity):
"""HTTP Bearer security class."""

type: ClassVar[Literal["http"]] = "http"
in_value: ClassVar[Literal["basic"]] = "basic"

@classmethod
def is_supported(cls, type: str, schema_parameters: dict[str, Any]) -> bool:
return cls.type == type and cls.in_value == schema_parameters.get("scheme")

class Parameters(BaseModel): # BaseSecurityParameters
"""HTTP Basic security parameters class."""

username: str
password: str

def apply(
self,
q_params: dict[str, Any],
body_dict: dict[str, Any],
security: BaseSecurity,
) -> None:
if "headers" not in body_dict:
body_dict["headers"] = {}

credentials = f"{self.username}:{self.password}"
encoded_credentials = base64.b64encode(credentials.encode("utf-8")).decode(
"utf-8"
)

body_dict["headers"]["Authorization"] = f"Basic {encoded_credentials}"

def get_security_class(self) -> type[BaseSecurity]:
return HTTPBasic


class OAuth2PasswordBearer(BaseSecurity):
"""OAuth2 Password Bearer security class."""

Expand Down
147 changes: 147 additions & 0 deletions tests/api/openapi/security/test_http_basic_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import json
from typing import Annotated

import pytest
import requests
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials

from docs.docs_src.user_guide.external_rest_apis.security_examples import (
configure_http_basic_client,
)


def create_http_basic_fastapi_app(host: str, port: int) -> FastAPI:
app = FastAPI(
title="OAuth2",
servers=[
{"url": f"http://{host}:{port}", "description": "Local development server"}
],
)

security = HTTPBasic()

# Dependency to verify the username/password
def verify_auth(
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
) -> None:
if (credentials.username != "john") or (
credentials.password != "supersecret" # pragma: allowlist secret
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Invalid username or password",
)

# Secured endpoint
@app.get("/hello")
def get_hello(
credentials: HTTPBasicCredentials = Depends(verify_auth), # noqa: B008
) -> dict[str, str]:
return {"message": "Hello, authenticated user!"}

return app


@pytest.mark.parametrize(
"fastapi_openapi_url",
[(create_http_basic_fastapi_app)],
indirect=["fastapi_openapi_url"],
)
def test_openapi_schema(fastapi_openapi_url: str) -> None:
with requests.get(fastapi_openapi_url, timeout=10) as response:
response.raise_for_status()
openapi_json = json.loads(response.text)

expected_schema = {
"openapi": "3.1.0",
"info": {"title": "OAuth2", "version": "0.1.0"},
"servers": [
{
"url": f"{fastapi_openapi_url.split('/openapi.json')[0]}",
"description": "Local development server",
}
],
"paths": {
"/hello": {
"get": {
"summary": "Get Hello",
"operationId": "get_hello_hello_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"additionalProperties": {"type": "string"},
"type": "object",
"title": "Response Get Hello Hello Get",
}
}
},
}
},
"security": [{"HTTPBasic": []}],
}
}
},
"components": {
"securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}}
},
}

assert openapi_json == expected_schema


@pytest.mark.parametrize(
"fastapi_openapi_url",
[
(create_http_basic_fastapi_app),
],
indirect=["fastapi_openapi_url"],
)
def test_http_bearer_token_correct(fastapi_openapi_url: str) -> None:
api_client = configure_http_basic_client(
fastapi_openapi_url,
username="john",
password="supersecret", # pragma: allowlist secret
)

expected = [
"get_hello_hello_get",
]

functions = list(api_client._get_functions_to_register())
assert [f.__name__ for f in functions] == expected

hello_get_f = functions[0]

response = hello_get_f()

assert response == {"message": "Hello, authenticated user!"}


@pytest.mark.parametrize(
"fastapi_openapi_url",
[
(create_http_basic_fastapi_app),
],
indirect=["fastapi_openapi_url"],
)
def test_http_bearer_token_wrong(fastapi_openapi_url: str) -> None:
api_client = configure_http_basic_client(
fastapi_openapi_url,
username="john",
password="wrongsecret", # pragma: allowlist secret
)

expected = [
"get_hello_hello_get",
]

functions = list(api_client._get_functions_to_register())
assert [f.__name__ for f in functions] == expected

hello_get_f = functions[0]

assert hello_get_f() == {"detail": "Invalid username or password"}
2 changes: 1 addition & 1 deletion tests/api/openapi/security/test_unsupported_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from fastagency.api.openapi.client import OpenAPI
from fastagency.api.openapi.security import HTTPBearer, UnsuportedSecurityStub

from .test_http_client import create_http_bearer_fastapi_app
from .test_http_bearer_client import create_http_bearer_fastapi_app


@pytest.fixture
Expand Down

0 comments on commit b781e16

Please sign in to comment.