Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
9 changes: 4 additions & 5 deletions bluesky_httpserver/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from fastapi.middleware.cors import CORSMiddleware
from fastapi.openapi.utils import get_openapi

from .authentication import Mode
from .authentication import ExternalAuthenticator, InternalAuthenticator
from .console_output import CollectPublishedConsoleOutput, ConsoleOutputStream, SystemInfoStream
from .core import PatchedStreamingResponse
from .database.core import purge_expired
Expand Down Expand Up @@ -179,20 +179,19 @@ def build_app(authentication=None, api_access=None, resource_access=None, server
for spec in authentication["providers"]:
provider = spec["provider"]
authenticator = spec["authenticator"]
mode = authenticator.mode
if mode == Mode.password:
if isinstance(authenticator, InternalAuthenticator):
authentication_router.post(f"/provider/{provider}/token")(
build_handle_credentials_route(authenticator, provider)
)
elif mode == Mode.external:
elif isinstance(authenticator, ExternalAuthenticator):
authentication_router.get(f"/provider/{provider}/code")(
build_auth_code_route(authenticator, provider)
)
authentication_router.post(f"/provider/{provider}/code")(
build_auth_code_route(authenticator, provider)
)
else:
raise ValueError(f"unknown authentication mode {mode}")
raise ValueError(f"unknown authenticator type {type(authenticator)}")
for custom_router in getattr(authenticator, "include_routers", []):
authentication_router.include_router(custom_router, prefix=f"/provider/{provider}")

Expand Down
17 changes: 9 additions & 8 deletions bluesky_httpserver/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
from pydantic_settings import BaseSettings

from . import schemas
from .authentication.authenticator_base import (
ExternalAuthenticator,
InternalAuthenticator,
UserSessionState,
)
from .authorization._defaults import _DEFAULT_ANONYMOUS_PROVIDER_NAME
from .core import json_or_msgpack
from .database import orm
Expand All @@ -54,12 +59,6 @@ def utcnow():
"UTC now with second resolution"
return datetime.utcnow().replace(microsecond=0)


class Mode(enum.Enum):
password = "password"
external = "external"


class Token(BaseModel):
access_token: str
token_type: str
Expand Down Expand Up @@ -421,7 +420,8 @@ async def auth_code(
api_access_manager=Depends(get_api_access_manager),
):
request.state.endpoint = "auth"
username = await authenticator.authenticate(request)
user_session_state = await authenticator.authenticate(request)
username = user_session_state.user_name if user_session_state else None

if username and api_access_manager.is_user_known(username):
scopes = api_access_manager.get_user_scopes(username)
Expand Down Expand Up @@ -450,7 +450,8 @@ async def handle_credentials(
api_access_manager=Depends(get_api_access_manager),
):
request.state.endpoint = "auth"
username = await authenticator.authenticate(username=form_data.username, password=form_data.password)
user_session_state = await authenticator.authenticate(username=form_data.username, password=form_data.password)
username = user_session_state.user_name if user_session_state else None

err_msg = None
if not username:
Expand Down
11 changes: 11 additions & 0 deletions bluesky_httpserver/authentication/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from .authenticator_base import (
ExternalAuthenticator,
InternalAuthenticator,
UserSessionState,
)

__all__ = [
"ExternalAuthenticator",
"InternalAuthenticator",
"UserSessionState",
]
39 changes: 39 additions & 0 deletions bluesky_httpserver/authentication/authenticator_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from abc import ABC
from dataclasses import dataclass
from typing import Optional

from fastapi import Request


@dataclass
class UserSessionState:
"""Data transfer class to communicate custom session state information."""

user_name: str
state: dict = None


class InternalAuthenticator(ABC):
"""
Base class for authenticators that use username/password credentials.

Subclasses must implement the authenticate method which takes a username
and password and returns a UserSessionState on success or None on failure.
"""

async def authenticate(
self, username: str, password: str
) -> Optional[UserSessionState]:
raise NotImplementedError


class ExternalAuthenticator(ABC):
"""
Base class for authenticators that use external identity providers.

Subclasses must implement the authenticate method which takes a FastAPI
Request object and returns a UserSessionState on success or None on failure.
"""

async def authenticate(self, request: Request) -> Optional[UserSessionState]:
raise NotImplementedError
Loading
Loading