Skip to content

Commit

Permalink
Issue #90 disable basic auth by default
Browse files Browse the repository at this point in the history
  • Loading branch information
soxofaan committed Nov 7, 2023
1 parent c04ab47 commit 7edd78b
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 9 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ and start a new "In Progress" section above it.

## In progress

## 0.75.1

- Disable basic auth support by default ([#90](https://github.com/Open-EO/openeo-python-driver/issues/90))

## 0.75.0

- Move `enable_basic_auth`/`enable_oidc_auth` to `OpenEoBackendConfig`
Expand Down
2 changes: 1 addition & 1 deletion openeo_driver/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.75.0a1"
__version__ = "0.75.1a1"
8 changes: 4 additions & 4 deletions openeo_driver/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ class OpenEoBackendConfig:
processing_facility: str = "openEO"
processing_software: str = "openeo-python-driver"

enable_basic_auth: bool = True
enable_basic_auth: bool = False
# `valid_basic_auth`: function that takes a username and password and returns a boolean indicating if password is correct.
valid_basic_auth: Optional[Callable[[str, str], bool]] = None

enable_oidc_auth: bool = True

oidc_providers: List[OidcProvider] = attrs.Factory(list)
Expand All @@ -50,9 +53,6 @@ class OpenEoBackendConfig:
# TODO: allow it to be a callable instead of a dictionary?
oidc_user_map: Dict[Tuple[str, str], dict] = attrs.Factory(dict)

# TODO #90 #186: eliminate simple password scheme
valid_basic_auth: Callable[[str, str], bool] = lambda u, p: p == f"{u}123"

# General Flask related settings
# (e.g. see https://flask.palletsprojects.com/en/2.3.x/config/#builtin-configuration-values)
flask_settings: dict = attrs.Factory(
Expand Down
1 change: 1 addition & 0 deletions openeo_driver/dummy/dummy_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,6 @@ def _valid_basic_auth(username: str, password: str) -> bool:
capabilities_deploy_metadata=build_backend_deploy_metadata(packages=["openeo", "openeo_driver"]),
processing_facility="Dummy openEO API",
oidc_providers=oidc_providers,
enable_basic_auth=True,
valid_basic_auth=_valid_basic_auth,
)
11 changes: 9 additions & 2 deletions openeo_driver/users/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,12 @@ def _get_user_from_bearer_token(self, bearer: str) -> User:
_log.warning("Invalid bearer token {b!r}".format(b=bearer))
raise TokenInvalidException
if bearer_type == 'basic':
if not self._config.enable_basic_auth:
raise AuthenticationSchemeInvalidException(message="Basic authentication is not supported.")
return self.resolve_basic_access_token(access_token=access_token)
elif bearer_type == 'oidc' and provider_id in self._oidc_providers:
if not self._config.enable_oidc_auth:
raise AuthenticationSchemeInvalidException(message="OIDC authentication is not supported.")
oidc_provider = self._oidc_providers[provider_id]
return self.resolve_oidc_access_token(oidc_provider=oidc_provider, access_token=access_token)
else:
Expand Down Expand Up @@ -148,7 +152,9 @@ def authenticate_basic(self, request: flask.Request) -> Tuple[str, str]:
"""
username, password = self.parse_basic_auth_header(request)
_log.info(f"Handling basic auth for user {username!r}")
if not get_backend_config().valid_basic_auth(username, password):
if not (self._config.enable_basic_auth and self._config.valid_basic_auth):
raise AuthenticationSchemeInvalidException(message="Basic authentication is not supported.")
if not self._config.valid_basic_auth(username, password):
raise CredentialsInvalidException
# TODO real resolving of given user name to user_id?
user_id = username
Expand All @@ -157,12 +163,13 @@ def authenticate_basic(self, request: flask.Request) -> Tuple[str, str]:

@staticmethod
def build_basic_access_token(user_id: str) -> str:
# TODO: generate real access token and link to user in some key value store
# TODO: generate real verifiable access token and link to user in some key value store
return base64.urlsafe_b64encode(user_id.encode("utf-8")).decode("ascii")

def resolve_basic_access_token(self, access_token: str) -> User:
try:
# Resolve token to user id
# TODO: verify that access token is valid, has not expired, etc.
user_id = base64.urlsafe_b64decode(access_token.encode('ascii')).decode('utf-8')
except Exception:
raise TokenInvalidException
Expand Down
88 changes: 86 additions & 2 deletions tests/users/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from flask import Flask, jsonify, Response, request

from openeo_driver.backend import OidcProvider
from openeo_driver.config import OpenEoBackendConfig
from openeo_driver.errors import OpenEOApiException, PermissionsInsufficientException, TokenInvalidException
from openeo_driver.testing import build_basic_http_auth_header, DictSubSet
from openeo_driver.users import User
Expand All @@ -21,10 +22,10 @@ def oidc_provider(requests_mock) -> OidcProvider:


@pytest.fixture()
def app(oidc_provider):
def app(oidc_provider, backend_config):
"""Fixture for a flask app with some public and some auth requiring handlers"""
app = Flask("__test__")
auth = HttpAuthHandler(oidc_providers=[oidc_provider])
auth = HttpAuthHandler(oidc_providers=[oidc_provider], config=backend_config)
app.config["auth_handler"] = auth

@app.route("/public/hello")
Expand Down Expand Up @@ -118,6 +119,31 @@ def test_basic_auth_invalid_password(app):
assert_invalid_credentials_failure(response)


@pytest.mark.parametrize(
["backend_config_overrides", "expected"],
[
(
{"enable_basic_auth": False},
(
403,
DictSubSet(
{"code": "AuthenticationSchemeInvalid", "message": "Basic authentication is not supported."}
),
),
),
(
{"enable_basic_auth": True},
(200, b"hello basic"),
),
],
)
def test_basic_auth_disabled(app, backend_config_overrides, expected):
with app.test_client() as client:
headers = {"Authorization": build_basic_http_auth_header("Alice", "alice123")}
resp = client.get("/basic/hello", headers=headers)
assert (resp.status_code, resp.json or resp.data) == expected


@pytest.mark.parametrize(
["username", "password"],
[
Expand Down Expand Up @@ -172,6 +198,31 @@ def test_bearer_auth_basic_invalid_token_prefix(app, url):
assert_invalid_token_failure(response)


@pytest.mark.parametrize(
["backend_config_overrides", "expected"],
[
(
{"enable_basic_auth": False},
(
403,
DictSubSet(
{"code": "AuthenticationSchemeInvalid", "message": "Basic authentication is not supported."}
),
),
),
(
{"enable_basic_auth": True},
(200, b"hello Alice"),
),
],
)
def test_bearer_auth_basic_disabled(app, backend_config_overrides, expected):
with app.test_client() as client:
access_token = HttpAuthHandler.build_basic_access_token(user_id="Alice")
headers = {"Authorization": f"Bearer basic//{access_token}"}
resp = client.get("/personal/hello", headers=headers)
assert (resp.status_code, resp.json or resp.data) == expected

@pytest.mark.parametrize(
["url", "expected_data"],
[
Expand Down Expand Up @@ -232,6 +283,39 @@ def test_bearer_auth_oidc_token_resolve_problems(app, requests_mock, oidc_provid
assert resp.json["message"] == api_error.message


@pytest.mark.parametrize(
["backend_config_overrides", "expected"],
[
(
{"enable_oidc_auth": False},
(
403,
DictSubSet({"code": "AuthenticationSchemeInvalid", "message": "OIDC authentication is not supported."}),
),
),
(
{"enable_oidc_auth": True},
(200, b"hello oidcuser"),
),
],
)
def test_bearer_auth_oidc_disabled(app, requests_mock, oidc_provider, expected):
def userinfo(request, context):
"""Fake OIDC /userinfo endpoint handler"""
_, _, token = request.headers["Authorization"].partition("Bearer ")
user_id = token.split(".")[1]
return json.dumps({"sub": user_id})

requests_mock.get(oidc_provider.issuer + "/userinfo", text=userinfo)

with app.test_client() as client:
# Note: user id is "hidden" in access token
oidc_access_token = "kcneududhey8rmxje3uhs.oidcuser.o94h4oe9djdndjeu3rkrnmlxpds834r"
headers = {"Authorization": "Bearer oidc/{p}/{a}".format(p=oidc_provider.id, a=oidc_access_token)}
resp = client.get("/personal/hello", headers=headers)
assert (resp.status_code, resp.json or resp.data) == expected


@pytest.mark.parametrize(["url", "expected_data"], [
("/private/hello", b"hello you"),
("/personal/hello", b"hello oidcuser"),
Expand Down

0 comments on commit 7edd78b

Please sign in to comment.