Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gateway: custom auth, token verification #545

Merged
merged 1 commit into from
May 15, 2023
Merged
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
34 changes: 18 additions & 16 deletions gateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,21 @@ docker build -t qiskit/quantum-serverless-gateway:<VERSION> .

### 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 |
66 changes: 47 additions & 19 deletions gateway/api/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -19,38 +20,65 @@ 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
token = None
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
22 changes: 15 additions & 7 deletions gateway/main/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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
)