Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
6 changes: 6 additions & 0 deletions docs/how-to/authenticate.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,9 @@ To log out and securely remove the cached access token, follow these steps:
```
Logged out
```


> [!NOTE]
> The login and logout instructions above apply to the CLI. If you are using `oauth2-proxy` to secure the Swagger
> UI documentation page, you can log out by visiting the `/logout` URL. For other OIDC providers, update the
> `oidc.logout_redirect_endpoint` configuration to the appropriate logout endpoint.
Comment thread
ZohebShaikh marked this conversation as resolved.
Outdated
7 changes: 6 additions & 1 deletion docs/reference/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ components:
description: Client ID
title: Client Id
type: string
logout_redirect_endpoint:
default: /oauth2/sign_out
description: The oidc endpoint required to logout
title: Logout Redirect Endpoint
type: string
well_known_url:
description: URL to fetch OIDC config from the provider
title: Well Known Url
Expand Down Expand Up @@ -350,7 +355,7 @@ info:
name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0.html
title: BlueAPI Control
version: 1.0.2
version: 1.0.3
openapi: 3.1.0
paths:
/config/oidc:
Expand Down
3 changes: 3 additions & 0 deletions src/blueapi/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ class OIDCConfig(BlueapiBaseModel):
)
client_id: str = Field(description="Client ID")
client_audience: str = Field(description="Client Audience(s)", default="blueapi")
logout_redirect_endpoint: str = Field(
description="The oidc endpoint required to logout", default="/oauth2/sign_out"
)
Comment thread
ZohebShaikh marked this conversation as resolved.

@cached_property
def _config_from_oidc_url(self) -> dict[str, Any]:
Expand Down
16 changes: 15 additions & 1 deletion src/blueapi/service/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
status,
)
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse
from fastapi.security import OAuth2AuthorizationCodeBearer
from observability_utils.tracing import (
add_span_attributes,
Expand Down Expand Up @@ -56,7 +57,7 @@
from .runner import WorkerDispatcher

#: API version to publish in OpenAPI schema
REST_API_VERSION = "1.0.2"
REST_API_VERSION = "1.0.3"
Comment thread
ZohebShaikh marked this conversation as resolved.
Outdated

LICENSE_INFO: dict[str, str] = {
"name": "Apache 2.0",
Expand Down Expand Up @@ -534,6 +535,19 @@ def health_probe() -> HealthProbeResponse:
return HealthProbeResponse(status=Health.OK)


@secure_router.get("/logout", status_code=status.HTTP_200_OK, include_in_schema=False)
Comment thread
ZohebShaikh marked this conversation as resolved.
Outdated
def logout(runner: Annotated[WorkerDispatcher, Depends(_runner)]) -> RedirectResponse:
Comment thread
ZohebShaikh marked this conversation as resolved.
Outdated
"""Redirect to logout url"""
config = runner.run(interface.get_oidc_config)
if config is None:
raise HTTPException(status_code=status.HTTP_204_NO_CONTENT)
Comment thread
ZohebShaikh marked this conversation as resolved.
Outdated
Comment thread
ZohebShaikh marked this conversation as resolved.
Outdated
return RedirectResponse(
status_code=status.HTTP_308_PERMANENT_REDIRECT,
url=config.logout_redirect_endpoint,
headers={"X-Auth-Request-Redirect": config.end_session_endpoint},
)


@start_as_current_span(TRACER, "config")
def start(config: ApplicationConfig):
import uvicorn
Expand Down
48 changes: 48 additions & 0 deletions tests/unit_tests/service/test_rest_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import pytest
from bluesky.protocols import Stoppable
from fastapi import status
from fastapi.routing import APIRoute
from fastapi.testclient import TestClient
from pydantic import BaseModel, ValidationError
from pydantic_core import InitErrorDetails
Expand Down Expand Up @@ -70,6 +71,29 @@ def client_with_auth(
main.teardown_runner()


@pytest.fixture
def client_authenticated(
mock_runner: Mock, oidc_config: OIDCConfig
) -> Iterator[TestClient]:
with patch("blueapi.service.interface.worker"):
main.setup_runner(runner=mock_runner)
app = main.get_app(ApplicationConfig(oidc=oidc_config))
dependant_dependencies = []
for route in app.routes:
if isinstance(route, APIRoute) and route.path == "/config/oidc":
dependant_dependencies = route.dependant.dependencies
break

for route in app.routes:
if isinstance(route, APIRoute):
route.dependencies = []
temp = dependant_dependencies
temp[0].path = route.path
route.dependant.dependencies = temp
yield TestClient(app)
main.teardown_runner()


@pytest.fixture
def rest_config_with_cors() -> RestConfig:
cors_config = CORSConfig(
Expand Down Expand Up @@ -720,3 +744,27 @@ def test_health_probe(client: TestClient):

assert response.status_code == status.HTTP_200_OK
assert response.json() == {"status": "ok"}


def test_logout(
mock_runner: Mock,
mock_authn_server,
oidc_config: OIDCConfig,
client_authenticated: TestClient,
):
mock_runner.run.return_value = oidc_config
client_authenticated.follow_redirects = False
response = client_authenticated.get("/logout")
assert response.status_code == status.HTTP_308_PERMANENT_REDIRECT
assert (
response.headers.get("X-Auth-Request-Redirect")
== oidc_config.end_session_endpoint
)


def test_logout_when_oidc_config_invalid(
mock_runner: Mock, mock_authn_server, client_authenticated: TestClient
):
mock_runner.run.return_value = None
response = client_authenticated.get("/logout")
assert response.status_code == status.HTTP_204_NO_CONTENT
2 changes: 2 additions & 0 deletions tests/unit_tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ def test_config_yaml_parsed(temp_yaml_config_file):
"well_known_url": "https://auth.example.com/realms/sample/.well-known/openid-configuration",
"client_id": "blueapi-client",
"client_audience": "aud",
"logout_redirect_endpoint": "/oauth2/sign_out",
},
"scratch": {
"root": "/tmp/scratch/blueapi",
Expand Down Expand Up @@ -353,6 +354,7 @@ def test_config_yaml_parsed(temp_yaml_config_file):
"well_known_url": "https://auth.example.com/realms/sample/.well-known/openid-configuration",
"client_id": "blueapi-client",
"client_audience": "aud",
"logout_redirect_endpoint": "/oauth2/sign_out",
},
"scratch": {
"root": "/tmp/scratch/blueapi",
Expand Down