Skip to content

Commit

Permalink
feat: new auth to enable other instances auth (#199)
Browse files Browse the repository at this point in the history
* feat: new auth to enable other instances auth

That is if you deploy api.folksonomy.openbeautyfacts.org you will be able to authenticate against world.openbeautyfacts.org

* refactor: remove dead code

* test: fix tests
  • Loading branch information
alexgarel authored Sep 6, 2024
1 parent 653d3a6 commit b6729d3
Show file tree
Hide file tree
Showing 5 changed files with 41 additions and 16 deletions.
13 changes: 8 additions & 5 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,25 @@ psql folksonomy < db/db_setup.sql
```
uvicorn folksonomy.api:app --reload
```
or use `--host` if you want to make it available on your local network, eg.:
or use `--host` if you want to make it available on your local network:
```
uvicorn folksonomy.api:app --reload --host 192.168.0.100
uvicorn folksonomy.api:app --reload --host <you-ip-address>
```

## Run with a local instance of Product Opener

To deal with CORS and/or `401 Unauthorized` issues when running in a dev environment you have to deal with two things:

* both Folksonomy Engine server and Product Opener server have to run on the same domain (openfoodfacts.localhost by default for Product Opener)
* to allow authentication with the Product Opener cookie, you must tell Folksonomy Engine to use the local Product Opener instance as the authent server

To do so you can:
* add an environment variable, `AUTH_URL` to specify the auth server
* edit the `local_settings.py` (copying from `local_settings_example.py`) and uncomment proposed AUTH_PREFIX and FOLKSONOMY_PREFIX entries
* use a the same host name as Product Opener when launching Folksonomy Engine server

This should work:
This then should work:
```
AUTH_URL="http://fr.openfoodfacts.localhost" uvicorn folksonomy.api:app --host 'api.fr.openfoodfacts.localhost' --reload
uvicorn folksonomy.api:app --host 127.0.0.1 --reload --port 8888
```

You can then access the API at http://api.folksonomy.openfoodfacts.localhost:8888/docs
21 changes: 17 additions & 4 deletions folksonomy/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,21 @@ def check_owner_user(user: User, owner, allow_anonymous=False):
return


def get_auth_server(request: Request):
"""
Get auth server URL from request
We deduce it by changing part of the request base URL
according to FOLKSONOMY_PREFIX and AUTH_PREFIX settings
"""
base_url = f"{request.base_url.scheme}://{request.base_url.netloc}"
# remove folksonomy prefix and add AUTH prefix
base_url = base_url.replace(settings.FOLKSONOMY_PREFIX or "", settings.AUTH_PREFIX or "")
return base_url


@app.post("/auth")
async def authentication(response: Response, form_data: OAuth2PasswordRequestForm = Depends()):
async def authentication(request: Request, response: Response, form_data: OAuth2PasswordRequestForm = Depends()):
"""
Authentication: provide user/password and get a bearer token in return
Expand All @@ -143,7 +156,7 @@ async def authentication(response: Response, form_data: OAuth2PasswordRequestFor
user_id = form_data.username
password = form_data.password
token = user_id+'__U'+str(uuid.uuid4())
auth_url = settings.AUTH_SERVER + "/cgi/auth.pl"
auth_url = get_auth_server(request) + "/cgi/auth.pl"
auth_data={'user_id': user_id, 'password': password}
async with aiohttp.ClientSession() as http_session:
async with http_session.post(auth_url, data=auth_data) as resp:
Expand All @@ -167,7 +180,7 @@ async def authentication(response: Response, form_data: OAuth2PasswordRequestFor


@app.post("/auth_by_cookie")
async def authentication(response: Response, session: Optional[str] = Cookie(None)):
async def authentication(request: Request, response: Response, session: Optional[str] = Cookie(None)):
"""
Authentication: provide Open Food Facts session cookie and get a bearer token in return
Expand All @@ -187,7 +200,7 @@ async def authentication(response: Response, session: Optional[str] = Cookie(Non
raise HTTPException(
status_code=422, detail="Malformed 'session' cookie")

auth_url = settings.AUTH_SERVER + "/cgi/auth.pl"
auth_url = get_auth_server(request) + "/cgi/auth.pl"
async with aiohttp.ClientSession() as http_session:
async with http_session.post(auth_url, cookies={'session': session}) as resp:
status_code = resp.status
Expand Down
8 changes: 4 additions & 4 deletions folksonomy/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
POSTGRES_DATABASE = os.environ.get("POSTGRES_DATABASE", 'folksonomy')


# If you're in dev, you can specify another auth_server; eg.
# AUTH_URL="http://localhost.openfoodfacts" uvicorn folksonomy.api:app --host
# Otherwise it defaults to https://world.openfoodfacts.org
AUTH_SERVER = os.environ.get("AUTH_URL", "https://world.openfoodfacts.org")
# we deduce the URL to which to authenticate from the base url,
# with some changes in the prefixes, substituing FOLKSONOMY_PREFIX by AUTH_PREFIX
FOLKSONOMY_PREFIX = os.environ.get("FOLKSONOMY_PREFIX", "api.folksonomy")
AUTH_PREFIX = os.environ.get("AUTH_PREFIX", "world")

# time (in seconds) to wait for after a failed authentication attempt (to avoid brute force)
FAILED_AUTH_WAIT_TIME = 2 # this settings is meant to be overridden by tests only
Expand Down
7 changes: 6 additions & 1 deletion local_settings_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@
POSTGRES_USER = '' # Leave empty if no user exists for database
POSTGRES_PASSWORD = '' # Leave empty if no password exists for user
POSTGRES_HOST = '' # Change if necessary
POSTGRES_DATABASE = 'folksonomy/'
POSTGRES_DATABASE = 'folksonomy/'

# for dev using a local product opener instance you can use this
# base domain has to be the same (see INSTALL.md)
#FOLKSONOMY_PREFIX="api.folksonomy.openfoodfacts.localhost:8888"
#AUTH_PREFIX="world.openfoodfacts.localhost"
8 changes: 6 additions & 2 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
**Important:** you should run tests with PYTHONASYNCIODEBUG=1
"""
import asyncio
import collections
import contextlib
import json
import pytest
import time
Expand Down Expand Up @@ -126,7 +128,7 @@ async def __aexit__(self, *args, **kwargs):
pass

def dummy_auth(self, auth_url, data=None, cookies=None):
assert auth_url.endswith("/cgi/auth.pl")
assert auth_url == "http://authserver/cgi/auth.pl", "'test' replaced by 'auth' in URL"
success = False
# reject or not based on password, which should always be "test" :-)
if data is not None:
Expand All @@ -146,6 +148,8 @@ def dummy_auth(self, auth_url, data=None, cookies=None):
@pytest.fixture
def fake_authentication(monkeypatch):
"""Fake authentication using dummy_auth"""
monkeypatch.setattr(settings, "FOLKSONOMY_PREFIX", "test")
monkeypatch.setattr(settings, "AUTH_PREFIX", "auth")
monkeypatch.setattr(aiohttp.ClientSession, "post", dummy_auth)


Expand Down Expand Up @@ -414,7 +418,7 @@ def test_auth_empty():
assert response.status_code == 422


def test_auth_bad(monkeypatch):
def test_auth_bad(monkeypatch, fake_authentication):
# avoid waiting for 2 sec
monkeypatch.setattr(settings, 'FAILED_AUTH_WAIT_TIME', .1)
with TestClient(app) as client:
Expand Down

0 comments on commit b6729d3

Please sign in to comment.