diff --git a/gateway/README.md b/gateway/README.md index 74a8152dd..791cbe9b5 100644 --- a/gateway/README.md +++ b/gateway/README.md @@ -11,19 +11,21 @@ docker build -t qiskit/quantum-serverless-gateway: . ### Env variables for container -| Variable | Description | -|---------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| DEBUG | run application on debug mode | -| SITE_HOST | host of site that will be created for Django application | -| RAY_HOST | Host of Ray head node that will be assigned to default created compute resource | -| CLIENT_ID | Keycloak client id that will be created for social integrations | -| DJANGO_SUPERUSER_USERNAME | username for admin user that is created on launch of container | -| DJANGO_SUPERUSER_PASSWORD | password for admin user that is created on launch of container | -| DJANGO_SUPERUSER_EMAIL | email for admin user that is created on launch of container | -| SETTING_KEYCLOAK_URL | url to keycloak instance | -| SETTING_KEYCLOAK_REALM | Realm of keycloak to authenticate with | -| SETTINGS_KEYCLOAK_CLIENT_SECRET | client secret | -| SETTINGS_TOKEN_AUTH_URL | URL for custom token authentication | -| SETTINGS_TOKEN_AUTH_USER_FIELD | user field name for custom token authentication mechanism. Default `userId`. | -| SETTINGS_TOKEN_AUTH_TOKEN_FIELD | user field name for custom token authentication mechanism. Default `apiToken`. | -| SETTINGS_AUTH_MECHANISM | authentication backend mechanism. Default `default`. Options: `default` and `custom_token`. If `custom_token` is selected then `SETTINGS_TOKEN_AUTH_URL` must be set. | +| Variable | Description | +|----------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| DEBUG | run application on debug mode | +| SITE_HOST | host of site that will be created for Django application | +| RAY_HOST | Host of Ray head node that will be assigned to default created compute resource | +| CLIENT_ID | Keycloak client id that will be created for social integrations | +| DJANGO_SUPERUSER_USERNAME | username for admin user that is created on launch of container | +| DJANGO_SUPERUSER_PASSWORD | password for admin user that is created on launch of container | +| DJANGO_SUPERUSER_EMAIL | email for admin user that is created on launch of container | +| SETTING_KEYCLOAK_URL | url to keycloak instance | +| SETTING_KEYCLOAK_REALM | Realm of keycloak to authenticate with | +| SETTINGS_KEYCLOAK_CLIENT_SECRET | client secret | +| SETTINGS_TOKEN_AUTH_URL | URL for custom token authentication | +| SETTINGS_TOKEN_AUTH_USER_FIELD | user field name for custom token authentication mechanism. Default `userId`. | +| SETTINGS_TOKEN_AUTH_TOKEN_FIELD | user field name for custom token authentication mechanism. Default `apiToken`. | +| SETTINGS_AUTH_MECHANISM | authentication backend mechanism. Default `default`. Options: `default` and `custom_token`. If `custom_token` is selected then `SETTINGS_TOKEN_AUTH_URL` must be set. | +| SETTINGS_TOKEN_AUTH_VERIFICATION_URL | URL for custom token verificaiton | +| SETTINGS_TOKEN_AUTH_VERIFICATION_FIELD | name of a field to use for token verification | \ No newline at end of file diff --git a/gateway/api/authentication.py b/gateway/api/authentication.py index 8a5c2708a..5235e954f 100644 --- a/gateway/api/authentication.py +++ b/gateway/api/authentication.py @@ -3,6 +3,7 @@ import json import logging from dataclasses import dataclass +from typing import Callable, Any, Dict, Optional import requests from django.conf import settings @@ -19,11 +20,30 @@ class CustomToken: token: str +def safe_request(request: Callable) -> Optional[Dict[str, Any]]: + """Makes safe request and parses json response.""" + result = None + response = None + try: + response = request() + except Exception as request_exception: # pylint: disable=broad-exception-caught + logging.error(request_exception) + + if response is not None and response.ok: + try: + result = json.loads(response.text) + except Exception as json_exception: # pylint: disable=broad-exception-caught + logging.error(json_exception) + + return result + + class CustomTokenBackend(authentication.BaseAuthentication): """Custom token backend for authentication against 3rd party auth service.""" def authenticate(self, request): auth_url = settings.SETTINGS_TOKEN_AUTH_URL + verification_url = settings.SETTINGS_TOKEN_AUTH_VERIFICATION_URL auth_header = request.META.get("HTTP_AUTHORIZATION") user = None @@ -31,26 +51,34 @@ def authenticate(self, request): if auth_header is not None and auth_url is not None: token = auth_header.split(" ")[-1] - response = requests.post( - auth_url, - json={settings.SETTINGS_TOKEN_AUTH_TOKEN_FIELD: token}, - timeout=60, + auth_data = safe_request( + request=lambda: requests.post( + auth_url, + json={settings.SETTINGS_TOKEN_AUTH_TOKEN_FIELD: token}, + timeout=60, + ) ) - if response.ok: - try: - json_response = json.loads(response.text) - user_id = json_response.get(settings.SETTINGS_TOKEN_AUTH_USER_FIELD) - except ValueError as exception: - logging.error( - "Encountered exception on parsing response from %s", exception + if auth_data is not None: + user_id = auth_data.get(settings.SETTINGS_TOKEN_AUTH_USER_FIELD) + + verification_data = safe_request( + request=lambda: requests.get( + verification_url, + headers={"Authorization": auth_data.get("id")}, + timeout=60, + ) + ) + + if verification_data is not None: + verified = verification_data.get( + settings.SETTINGS_TOKEN_AUTH_VERIFICATION_FIELD, False ) - user_id = None - - if user_id is not None: - try: - user = User.objects.get(username=user_id) - except User.DoesNotExist: - user = User(username=user_id) - user.save() + + if user_id is not None and verified: + try: + user = User.objects.get(username=user_id) + except User.DoesNotExist: + user = User(username=user_id) + user.save() return user, CustomToken(token.encode()) if token else None diff --git a/gateway/main/settings.py b/gateway/main/settings.py index 4dc1259e4..d46e1edba 100644 --- a/gateway/main/settings.py +++ b/gateway/main/settings.py @@ -223,13 +223,6 @@ SETTINGS_KEYCLOAK_REQUESTS_TIMEOUT = int( os.environ.get("SETTINGS_KEYCLOAK_REQUESTS_TIMEOUT", 15) ) -SETTINGS_TOKEN_AUTH_URL = os.environ.get("SETTINGS_TOKEN_AUTH_URL", None) -SETTINGS_TOKEN_AUTH_USER_FIELD = os.environ.get( - "SETTINGS_TOKEN_AUTH_USER_FIELD", "userId" -) -SETTINGS_TOKEN_AUTH_TOKEN_FIELD = os.environ.get( - "SETTINGS_TOKEN_AUTH_TOKEN_FIELD", "apiToken" -) SOCIALACCOUNT_PROVIDERS = { "keycloak": { "KEYCLOAK_URL": os.environ.get(SETTING_KEYCLOAK_URL, "http://localhost:8085"), @@ -244,3 +237,18 @@ MEDIA_ROOT = os.path.join(BASE_DIR, "media") MEDIA_URL = "/media/" + +# custom token auth +SETTINGS_TOKEN_AUTH_URL = os.environ.get("SETTINGS_TOKEN_AUTH_URL", None) +SETTINGS_TOKEN_AUTH_USER_FIELD = os.environ.get( + "SETTINGS_TOKEN_AUTH_USER_FIELD", "userId" +) +SETTINGS_TOKEN_AUTH_TOKEN_FIELD = os.environ.get( + "SETTINGS_TOKEN_AUTH_TOKEN_FIELD", "apiToken" +) +SETTINGS_TOKEN_AUTH_VERIFICATION_URL = os.environ.get( + "SETTINGS_TOKEN_AUTH_VERIFICATION_URL", None +) +SETTINGS_TOKEN_AUTH_VERIFICATION_FIELD = os.environ.get( + "SETTINGS_TOKEN_AUTH_VERIFICATION_FIELD", None +)