Skip to main content

Enable Azure Entra ID for JupyterHub

JupyterHub can authenticate users with Microsoft Entra ID (Azure AD) using OAuth. This is separate from Penfield app SSO. Use a dedicated app registration for JupyterHub so access and redirect URIs stay independent.

tip

Use an email claim as the JupyterHub username so home directories and PVCs stay stable across auth methods (email-only, LDAP, or Entra).

Create a new App registration on Azure Entra ID

  1. Go to the Azure portalMicrosoft Entra ID.

  2. Make sure you are in the correct subscription and tenant.

  3. From Overview, open ManageApp registrations.

  4. Click New registration:

    • Name: for example Penfield JupyterHub (follow your naming standard).
    • Supported account types: typically Accounts in this organizational directory only (single tenant).
    • Redirect URI: select Web, then set:
      https://<FQDN>/jupyterhub/hub/oauth_callback
      Example: https://penfield.example.com/jupyterhub/hub/oauth_callback
    • Click Register.
    caution

    The redirect URI must match exactly (including /jupyterhub when that is the Hub baseUrl). A mismatch returns AADSTS50011.

  5. Open Certificates & secretsNew client secret. Copy the secret Value immediately (not the Secret ID). You will store this later on in the penfield-secrets Secret.

  6. Open Overview and copy:

    • Application (client) ID
    • Directory (tenant) ID

API permissions and email claim

JupyterHub must receive an email claim so usernames match other auth modes.

  1. Open API permissionsAdd a permissionMicrosoft GraphDelegated permissions.
  2. Add (if missing):
    • openid
    • email
    • profile
  3. Keep User.Read if it is already present.
  4. Click Grant admin consent for <your organization>.
  5. Open Token configurationAdd optional claim.
  6. Token type ID, select email, then add.
    If Azure warns that OpenID Connect scopes are required, confirm the API permissions above are granted.

Restrict who can sign in (Enterprise application)

  1. Under Microsoft Entra ID, open Enterprise applications.
  2. Search for the app name you registered and open it.
  3. Open Properties.
  4. Set Assignment required? to Yes and Save.
  5. Open Users and groupsAdd user/group.
  6. Assign users or (recommended) security groups who may use JupyterHub, then Assign.
note

Global Administrators may still be able to sign in even when they are not assigned. Validate access control with a normal user account. Unassigned non-admin users should see AADSTS50105.

Manage day-to-day access in Entra (users or groups). With the Penfield Helm defaults below (allow_all: true), JupyterHub accepts anyone Entra allows through.

Update Penfield app configuration

  1. Make sure you have these values from above steps:

    • CLIENT_ID (Application ID)
    • TENANT_ID (Directory ID)
    • CLIENT_SECRET (Client secret Value)
  2. Store credentials in penfield-secrets

    Add these keys under data: in the existing penfield-secrets Secret (same Secret used by other Penfield services). Values must be base64-encoded. Encrypt with Sealed Secrets / your secret manager before commit if required.

    Generate the base64 values:

    export CLIENT_ID="replace-with-your-client-id"
    export CLIENT_SECRET="replace-with-your-client-secret"

    printf '%s' "$CLIENT_ID" | base64 -w0; echo
    printf '%s' "$CLIENT_SECRET" | base64 -w0; echo
    # macOS / some WSL: printf '%s' "$CLIENT_ID" | base64 | tr -d '\n'; echo
    update penfield-secrets
    data:
    OAUTH_CLIENT_ID: <paste-base64-of-client-id>
    OAUTH_CLIENT_SECRET: <paste-base64-of-client-secret>

    Example with kubectl (namespace may differ):

    kubectl -n penfield-app patch secret penfield-secrets --type merge -p \
    "{\"stringData\":{\"OAUTH_CLIENT_ID\":\"$CLIENT_ID\",\"OAUTH_CLIENT_SECRET\":\"$CLIENT_SECRET\"}}"
  3. Enable Azure AD auth in values.yaml file

    Under jupyterhub.hub, enable extraEnv and the Azure AD authenticator (uncomment / set as below). Do not put the client secret in values.

    jupyterhub hub auth
    jupyterhub:
    enabled: true
    hub:
    extraEnv:
    OAUTH_CLIENT_ID:
    valueFrom:
    secretKeyRef:
    name: penfield-secrets
    key: OAUTH_CLIENT_ID
    OAUTH_CLIENT_SECRET:
    valueFrom:
    secretKeyRef:
    name: penfield-secrets
    key: OAUTH_CLIENT_SECRET
    config:
    JupyterHub:
    authenticator_class: azuread
    Authenticator:
    enable_auth_state: true
    AzureAdOAuthenticator:
    oauth_callback_url: https://<FQDN>/jupyterhub/hub/oauth_callback
    tenant_id: <Directory tenant ID>
    username_claim: email
    scope:
    - openid
    - email
    - profile
    allow_all: true
  4. Apply the change using ArgoCD or Helm

    • ArgoCD: commit and sync the app.
    • Helm:
    helm upgrade --install penfield-app penfieldai/penfieldai \
    --namespace penfield-app \
    --values penfield-values.yaml

    Wait for the Hub pod to restart, then open https://<FQDN>/jupyterhub/.

Verify

  1. Sign in with an assigned non-admin user → JupyterHub should open; the username should be the user’s email.
  2. Sign in with an unassigned non-admin user → Microsoft should block with AADSTS50105.
  3. If login fails with “No email found…”, confirm optional claim email, scopes openid / email / profile, admin consent, and that the Entra user has an email address populated.

Troubleshooting

SymptomWhat to check
AADSTS50011Redirect URI on the app registration must exactly match oauth_callback_url.
HTTP 401 on /hub/oauth_callbackWrong client secret (use the secret Value, not the Secret ID).
No email found in …Token optional claim email, Graph delegated scopes, and user mail attribute.
Unassigned user can still sign inThey may be a Global Administrator. Retest with a normal account.
Assigned user still blockedConfirm Enterprise applicationUsers and groups (not only App registration owners).

Internal / VPN-only FQDNs are fine: the browser completes the redirect while the user can reach JupyterHub (for example on VPN). Entra does not need to call your cluster directly.