Skip to content

Commit 6adba36

Browse files
committed
Add external auth contract integration
1 parent 75d8a41 commit 6adba36

3 files changed

Lines changed: 71 additions & 2 deletions

File tree

config/jupyterhub/01-spawner.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,19 @@
88
import urllib.error
99
import urllib.request
1010
from pathlib import Path
11-
from urllib.parse import urlencode
11+
from urllib.parse import quote, urlencode
1212
from urllib.request import Request, urlopen
1313

1414
from z2jh import get_config
1515

1616
log = logging.getLogger(__name__)
1717

18+
external_auth_enabled = "__EXTERNAL_AUTH_ENABLED__".lower() == "true"
19+
external_auth_broker_url = "__EXTERNAL_AUTH_BROKER_URL__".strip()
20+
external_auth_providers = json.loads(r'''__EXTERNAL_AUTH_PROVIDERS_JSON__''')
21+
external_auth_env_var_map = json.loads(r'''__EXTERNAL_AUTH_ENV_VAR_MAP_JSON__''')
22+
external_auth_timeout_seconds = int("__EXTERNAL_AUTH_TIMEOUT_SECONDS__" or "10")
23+
1824

1925
# ---------------------------------------------------------------------------
2026
# Storage
@@ -726,6 +732,54 @@ async def _setup_nss_wrapper(spawner, username, groups):
726732
log.info("nss-wrapper: postStart lifecycle hook registered for %s", username)
727733

728734

735+
# ---------------------------------------------------------------------------
736+
# External auth pack integration
737+
# ---------------------------------------------------------------------------
738+
739+
def _external_auth_provider_token(broker_url, provider, bearer_token):
740+
"""Fetch one provider token from the external-auth broker."""
741+
req = Request(
742+
f"{broker_url.rstrip('/')}/external-auth/{quote(provider)}/token",
743+
headers={"Authorization": f"Bearer {bearer_token}", "Accept": "application/json"},
744+
method="GET",
745+
)
746+
with urlopen(req, timeout=external_auth_timeout_seconds) as resp:
747+
return json.loads(resp.read().decode("utf-8"))
748+
749+
750+
async def _external_auth_pre_spawn_hook(spawner, auth_state):
751+
"""Deliver external-auth provider tokens from configured contract fields."""
752+
if not external_auth_enabled or not external_auth_broker_url:
753+
return
754+
access_token = (auth_state or {}).get("access_token")
755+
if not access_token:
756+
log.debug("external-auth: no access_token for %s; skipping", spawner.user.name)
757+
return
758+
759+
providers = external_auth_providers or list(external_auth_env_var_map)
760+
for provider in providers:
761+
try:
762+
payload = await asyncio.to_thread(
763+
_external_auth_provider_token,
764+
external_auth_broker_url,
765+
provider,
766+
access_token,
767+
)
768+
except urllib.error.HTTPError as exc:
769+
log.debug("external-auth: token request failed provider=%s status=%s", provider, exc.code)
770+
continue
771+
except Exception as exc:
772+
log.debug("external-auth: token request failed provider=%s error=%s", provider, exc)
773+
continue
774+
775+
token = str(payload.get("access_token", "")).strip()
776+
if payload.get("status") != "token_valid" or not token:
777+
continue
778+
779+
env_var = str(external_auth_env_var_map.get(provider) or f"{provider.upper()}_TOKEN").strip()
780+
spawner.environment = {**spawner.environment, env_var: token}
781+
782+
729783
# ---------------------------------------------------------------------------
730784
# Pre-spawn hook orchestrator
731785
# ---------------------------------------------------------------------------
@@ -791,6 +845,12 @@ async def _pre_spawn_hook(spawner):
791845
except Exception:
792846
log.exception("pre-spawn: NSS wrapper setup FAILED for %s", username)
793847

848+
# 5. External auth provider tokens (non-fatal)
849+
try:
850+
await _external_auth_pre_spawn_hook(spawner, auth_state)
851+
except Exception:
852+
log.exception("pre-spawn: external-auth setup FAILED for %s", username)
853+
794854
log.info("pre-spawn: hook complete for user %s", username)
795855

796856

templates/hub-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ data:
99
00-gateway-auth.py: |
1010
{{ .Files.Get "config/jupyterhub/00-gateway-auth.py" | indent 4 }}
1111
01-spawner.py: |
12-
{{ .Files.Get "config/jupyterhub/01-spawner.py" | replace "__SINGLEUSER_CONFIG_CM__" (include "nebari-data-science-pack.singleuser-config" .) | indent 4 }}
12+
{{ .Files.Get "config/jupyterhub/01-spawner.py" | replace "__SINGLEUSER_CONFIG_CM__" (include "nebari-data-science-pack.singleuser-config" .) | replace "__EXTERNAL_AUTH_ENABLED__" (toString .Values.externalAuth.enabled) | replace "__EXTERNAL_AUTH_BROKER_URL__" .Values.externalAuth.brokerURL | replace "__EXTERNAL_AUTH_PROVIDERS_JSON__" (toJson .Values.externalAuth.providers) | replace "__EXTERNAL_AUTH_ENV_VAR_MAP_JSON__" (toJson .Values.externalAuth.envVarMap) | replace "__EXTERNAL_AUTH_TIMEOUT_SECONDS__" (toString .Values.externalAuth.timeoutSeconds) | indent 4 }}
1313
02-jhub-apps.py: |
1414
{{ .Files.Get "config/jupyterhub/02-jhub-apps.py" | indent 4 }}
1515
03-nebi-envs.py: |

values.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,15 @@ sharedStorage:
128128
# of `/` is broken — see shared-pvc.yaml for details.
129129
mountOptions: []
130130

131+
# External auth pack integration. Copy these values from the
132+
# ExternalAuthIntegration contract rendered by nebari-external-auth-pack.
133+
externalAuth:
134+
enabled: false
135+
brokerURL: ""
136+
providers: []
137+
envVarMap: {}
138+
timeoutSeconds: 10
139+
131140
# =============================================================================
132141
# Nebi Integration
133142
# =============================================================================

0 commit comments

Comments
 (0)