Skip to content

Commit

Permalink
Merge pull request #32 from br3ndonland/update-http-basic-auth
Browse files Browse the repository at this point in the history
Update HTTP Basic auth for FastAPI and Starlette
  • Loading branch information
br3ndonland authored Apr 18, 2021
2 parents c9dd974 + 2630dea commit a0ab4d0
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 47 deletions.
23 changes: 16 additions & 7 deletions .github/workflows/builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,18 @@ jobs:
docker build . --rm --target fastapi --build-arg PYTHON_VERSION=${{ matrix.python-version }} -t ghcr.io/br3ndonland/inboard:fastapi
- name: Run Docker containers for testing
run: |
docker run -d -p 80:80 ghcr.io/br3ndonland/inboard:base
docker run -d -p 81:80 ghcr.io/br3ndonland/inboard:starlette
docker run -d -p 82:80 ghcr.io/br3ndonland/inboard:fastapi
docker run -d -p 80:80 \
-e "BASIC_AUTH_USERNAME=test_user" \
-e "BASIC_AUTH_PASSWORD=r4ndom_bUt_memorable" \
ghcr.io/br3ndonland/inboard:base
docker run -d -p 81:80 \
-e "BASIC_AUTH_USERNAME=test_user" \
-e "BASIC_AUTH_PASSWORD=r4ndom_bUt_memorable" \
ghcr.io/br3ndonland/inboard:starlette
docker run -d -p 82:80 \
-e "BASIC_AUTH_USERNAME=test_user" \
-e "BASIC_AUTH_PASSWORD=r4ndom_bUt_memorable" \
ghcr.io/br3ndonland/inboard:fastapi
- name: Smoke test Docker containers
run: |
handle_error_code() {
Expand Down Expand Up @@ -113,10 +122,10 @@ jobs:
smoke_test :80
smoke_test :81
smoke_test :82
smoke_test -a test_username:plunge-germane-tribal-pillar :81/status
smoke_test -a test_username:plunge-germane-tribal-pillar :82/status
smoke_test_xfail -a test_username:plunge-germane-tribal-incorrect :81/status
smoke_test_xfail -a test_username:plunge-germane-tribal-incorrect :82/status
smoke_test -a test_user:r4ndom_bUt_memorable :81/status
smoke_test -a test_user:r4ndom_bUt_memorable :82/status
smoke_test_xfail -a test_user:incorrect_password :81/status
smoke_test_xfail -a test_user:incorrect_password :82/status
smoke_test_xfail :81/status
smoke_test_xfail :82/status
- name: Push Docker images with latest Python version to registry
Expand Down
8 changes: 4 additions & 4 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
"module": "inboard.start",
"env": {
"APP_MODULE": "inboard.app.main_fastapi:app",
"BASIC_AUTH_USERNAME": "test_username",
"BASIC_AUTH_PASSWORD": "plunge-germane-tribal-pillar",
"BASIC_AUTH_USERNAME": "test_user",
"BASIC_AUTH_PASSWORD": "r4ndom_bUt_memorable",
"LOG_FORMAT": "uvicorn",
"LOG_LEVEL": "debug",
"PORT": "8000",
Expand All @@ -37,8 +37,8 @@
"inboard"
],
"env": {
"BASIC_AUTH_USERNAME": "test_username",
"BASIC_AUTH_PASSWORD": "plunge-germane-tribal-pillar",
"BASIC_AUTH_USERNAME": "test_user",
"BASIC_AUTH_PASSWORD": "r4ndom_bUt_memorable",
"WITH_RELOAD": "true"
},
"jinja": true
Expand Down
19 changes: 14 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -498,24 +498,33 @@ docker build . --rm --target starlette -t localhost/br3ndonland/inboard:starlett
cd inboard

docker run -d -p 80:80 \
-e "BASIC_AUTH_USERNAME=test_user" -e "BASIC_AUTH_PASSWORD=r4ndom_bUt_memorable" \
-e "LOG_LEVEL=debug" -e "PROCESS_MANAGER=uvicorn" -e "WITH_RELOAD=true" \
-v $(pwd)/inboard:/app/inboard localhost/br3ndonland/inboard:base

docker run -d -p 80:80 \
-e "BASIC_AUTH_USERNAME=test_user" -e "BASIC_AUTH_PASSWORD=r4ndom_bUt_memorable" \
-e "LOG_LEVEL=debug" -e "PROCESS_MANAGER=uvicorn" -e "WITH_RELOAD=true" \
-v $(pwd)/inboard:/app/inboard localhost/br3ndonland/inboard:fastapi

docker run -d -p 80:80 \
-e "BASIC_AUTH_USERNAME=test_user" -e "BASIC_AUTH_PASSWORD=r4ndom_bUt_memorable" \
-e "LOG_LEVEL=debug" -e "PROCESS_MANAGER=uvicorn" -e "WITH_RELOAD=true" \
-v $(pwd)/inboard:/app/inboard localhost/br3ndonland/inboard:starlette

# Run Docker container with Gunicorn and Uvicorn
docker run -d -p 80:80 localhost/br3ndonland/inboard:base
docker run -d -p 80:80 localhost/br3ndonland/inboard:fastapi
docker run -d -p 80:80 localhost/br3ndonland/inboard:starlette
docker run -d -p 80:80 \
-e "BASIC_AUTH_USERNAME=test_user" -e "BASIC_AUTH_PASSWORD=r4ndom_bUt_memorable" \
localhost/br3ndonland/inboard:base
docker run -d -p 80:80 \
-e "BASIC_AUTH_USERNAME=test_user" -e "BASIC_AUTH_PASSWORD=r4ndom_bUt_memorable" \
localhost/br3ndonland/inboard:fastapi
docker run -d -p 80:80 \
-e "BASIC_AUTH_USERNAME=test_user" -e "BASIC_AUTH_PASSWORD=r4ndom_bUt_memorable" \
localhost/br3ndonland/inboard:starlette

# Test HTTP Basic Auth when running the FastAPI or Starlette images:
http :80/status -a test_username:plunge-germane-tribal-pillar
# Test HTTP Basic auth when running the FastAPI or Starlette images:
http :80/status -a test_user:r4ndom_bUt_memorable
```

Change the port numbers to run multiple containers simultaneously (`-p 81:80`).
Expand Down
2 changes: 1 addition & 1 deletion inboard/app/main_starlette.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

def on_auth_error(request: Request, e: Exception) -> JSONResponse:
return JSONResponse(
{"detail": "Incorrect username or password", "error": str(e)}, status_code=401
{"error": "Incorrect username or password", "detail": str(e)}, status_code=401
)


Expand Down
22 changes: 13 additions & 9 deletions inboard/app/utilities_fastapi.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os
import secrets
from pathlib import Path
from secrets import compare_digest
from typing import List, Optional

import toml
Expand All @@ -10,17 +10,21 @@


async def basic_auth(credentials: HTTPBasicCredentials = Depends(HTTPBasic())) -> str:
correct_username = compare_digest(
credentials.username, str(os.getenv("BASIC_AUTH_USERNAME", "test_username"))
)
correct_password = compare_digest(
credentials.password,
str(os.getenv("BASIC_AUTH_PASSWORD", "plunge-germane-tribal-pillar")),
)
"""Authenticate a FastAPI request with HTTP Basic auth."""
basic_auth_username = os.getenv("BASIC_AUTH_USERNAME")
basic_auth_password = os.getenv("BASIC_AUTH_PASSWORD")
if not (basic_auth_username and basic_auth_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Server HTTP Basic auth credentials not set",
headers={"WWW-Authenticate": "Basic"},
)
correct_username = secrets.compare_digest(credentials.username, basic_auth_username)
correct_password = secrets.compare_digest(credentials.password, basic_auth_password)
if not (correct_username and correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
detail="HTTP Basic auth credentials not correct",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
Expand Down
23 changes: 12 additions & 11 deletions inboard/app/utilities_starlette.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import base64
import os
from secrets import compare_digest
import secrets
from typing import Optional, Tuple

from starlette.authentication import (
Expand All @@ -13,26 +13,27 @@


class BasicAuth(AuthenticationBackend):
"""Configure HTTP Basic auth for Starlette."""

async def authenticate(
self, request: HTTPConnection
) -> Optional[Tuple[AuthCredentials, SimpleUser]]:
"""Authenticate a Starlette request with HTTP Basic auth."""
if "Authorization" not in request.headers:
return None

auth = request.headers["Authorization"]
try:
auth = request.headers["Authorization"]
basic_auth_username = os.getenv("BASIC_AUTH_USERNAME")
basic_auth_password = os.getenv("BASIC_AUTH_PASSWORD")
if not (basic_auth_username and basic_auth_password):
raise AuthenticationError("Server HTTP Basic auth credentials not set")
scheme, credentials = auth.split()
decoded = base64.b64decode(credentials).decode("ascii")
username, _, password = decoded.partition(":")
correct_username = compare_digest(
username, str(os.getenv("BASIC_AUTH_USERNAME", "test_username"))
)
correct_password = compare_digest(
password,
str(os.getenv("BASIC_AUTH_PASSWORD", "plunge-germane-tribal-pillar")),
)
correct_username = secrets.compare_digest(username, basic_auth_username)
correct_password = secrets.compare_digest(password, basic_auth_password)
if not (correct_username and correct_password):
raise AuthenticationError("Invalid basic auth credentials")
raise AuthenticationError("HTTP Basic auth credentials not correct")
return AuthCredentials(["authenticated"]), SimpleUser(username)
except Exception:
raise
56 changes: 49 additions & 7 deletions tests/app/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ def test_get_root(self, clients: List[TestClient]) -> None:
def test_gets_with_basic_auth(
self, basic_auth: tuple, clients: List[TestClient], endpoint: str
) -> None:
"""Test `GET` requests to endpoints that require HTTP Basic Auth."""
"""Test `GET` requests to endpoints that require HTTP Basic auth."""
for client in clients:
assert client.get(endpoint).status_code in [401, 403]
response = client.get(endpoint, auth=basic_auth)
Expand All @@ -182,7 +182,7 @@ def test_gets_with_basic_auth(
def test_gets_with_basic_auth_incorrect(
self, basic_auth: tuple, clients: List[TestClient], endpoint: str
) -> None:
"""Test `GET` requests to Basic Auth endpoints with incorrect credentials."""
"""Test `GET` requests with incorrect HTTP Basic auth credentials."""
basic_auth_username, basic_auth_password = basic_auth
for client in clients:
assert client.get(endpoint).status_code in [401, 403]
Expand All @@ -197,17 +197,59 @@ def test_gets_with_basic_auth_incorrect(
assert response.status_code == 200

@pytest.mark.parametrize("endpoint", ["/health", "/status"])
def test_gets_with_starlette_auth_exception(
def test_gets_with_fastapi_auth_incorrect_credentials(
self, clients: List[TestClient], endpoint: str, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Test FastAPI `GET` requests with incorrect HTTP Basic auth credentials."""
monkeypatch.setenv("BASIC_AUTH_USERNAME", "test_user")
monkeypatch.setenv("BASIC_AUTH_PASSWORD", "r4ndom_bUt_memorable")
fastapi_client = clients[0]
response = fastapi_client.get(endpoint, auth=("user", "pass"))
assert isinstance(fastapi_client.app, FastAPI)
assert response.status_code in [401, 403]
assert response.json() == {"detail": "HTTP Basic auth credentials not correct"}

@pytest.mark.parametrize("endpoint", ["/health", "/status"])
def test_gets_with_fastapi_auth_no_credentials(
self, clients: List[TestClient], endpoint: str
) -> None:
"""Test Starlette `GET` requests with incorrect Basic Auth credentials."""
"""Test FastAPI `GET` requests without HTTP Basic auth credentials set."""
fastapi_client = clients[0]
response = fastapi_client.get(endpoint, auth=("user", "pass"))
assert isinstance(fastapi_client.app, FastAPI)
assert response.status_code in [401, 403]
assert response.json() == {
"detail": "Server HTTP Basic auth credentials not set"
}

@pytest.mark.parametrize("endpoint", ["/health", "/status"])
def test_gets_with_starlette_auth_incorrect_credentials(
self, clients: List[TestClient], endpoint: str, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Test Starlette `GET` requests with incorrect HTTP Basic auth credentials."""
monkeypatch.setenv("BASIC_AUTH_USERNAME", "test_user")
monkeypatch.setenv("BASIC_AUTH_PASSWORD", "r4ndom_bUt_memorable")
starlette_client = clients[1]
response = starlette_client.get(endpoint, auth=("user", "pass"))
assert isinstance(starlette_client.app, Starlette)
assert response.status_code in [401, 403]
assert response.json() == {
"detail": "HTTP Basic auth credentials not correct",
"error": "Incorrect username or password",
}

@pytest.mark.parametrize("endpoint", ["/health", "/status"])
def test_gets_with_starlette_auth_no_credentials(
self, clients: List[TestClient], endpoint: str
) -> None:
"""Test Starlette `GET` requests without HTTP Basic auth credentials set."""
starlette_client = clients[1]
response = starlette_client.get(endpoint, auth=("user", "pass"))
assert isinstance(starlette_client.app, Starlette)
assert response.status_code in [401, 403]
assert response.json() == {
"detail": "Incorrect username or password",
"error": "Invalid basic auth credentials",
"detail": "Server HTTP Basic auth credentials not set",
"error": "Incorrect username or password",
}

def test_get_status_message(
Expand Down Expand Up @@ -245,4 +287,4 @@ def test_get_user(
assert response.status_code == 200
assert "application" not in response.json().keys()
assert "status" not in response.json().keys()
assert response.json()["username"] == "test_username"
assert response.json()["username"] == "test_user"
6 changes: 3 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ def app_module_tmp_path(tmp_path_factory: pytest.TempPathFactory) -> Path:
@pytest.fixture
def basic_auth(
monkeypatch: pytest.MonkeyPatch,
username: str = "test_username",
password: str = "plunge-germane-tribal-pillar",
username: str = "test_user",
password: str = "r4ndom_bUt_memorable",
) -> tuple:
"""Set username and password for HTTP Basic Auth."""
"""Set username and password for HTTP Basic auth."""
monkeypatch.setenv("BASIC_AUTH_USERNAME", username)
monkeypatch.setenv("BASIC_AUTH_PASSWORD", password)
assert os.getenv("BASIC_AUTH_USERNAME") == username
Expand Down

0 comments on commit a0ab4d0

Please sign in to comment.