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.
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
-
Go to the Azure portal → Microsoft Entra ID.
-
Make sure you are in the correct subscription and tenant.
-
From Overview, open Manage → App registrations.
-
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:
Example:
https://<FQDN>/jupyterhub/hub/oauth_callbackhttps://penfield.example.com/jupyterhub/hub/oauth_callback - Click Register.
cautionThe redirect URI must match exactly (including
/jupyterhubwhen that is the HubbaseUrl). A mismatch returnsAADSTS50011. - Name: for example
-
Open Certificates & secrets → New client secret. Copy the secret Value immediately (not the Secret ID). You will store this later on in the
penfield-secretsSecret. -
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.
- Open API permissions → Add a permission → Microsoft Graph → Delegated permissions.
- Add (if missing):
openidemailprofile
- Keep
User.Readif it is already present. - Click Grant admin consent for <your organization>.
- Open Token configuration → Add optional claim.
- 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)
- Under Microsoft Entra ID, open Enterprise applications.
- Search for the app name you registered and open it.
- Open Properties.
- Set Assignment required? to Yes and Save.
- Open Users and groups → Add user/group.
- Assign users or (recommended) security groups who may use JupyterHub, then Assign.
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
-
Make sure you have these values from above steps:
- CLIENT_ID (Application ID)
- TENANT_ID (Directory ID)
- CLIENT_SECRET (Client secret Value)
-
Store credentials in
penfield-secretsAdd these keys under
data:in the existingpenfield-secretsSecret (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'; echoupdate penfield-secretsdata:
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\"}}" -
Enable Azure AD auth in
values.yamlfileUnder
jupyterhub.hub, enableextraEnvand the Azure AD authenticator (uncomment / set as below). Do not put the client secret in values.jupyterhub hub authjupyterhub:
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 -
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.yamlWait for the Hub pod to restart, then open
https://<FQDN>/jupyterhub/.
Verify
- Sign in with an assigned non-admin user → JupyterHub should open; the username should be the user’s email.
- Sign in with an unassigned non-admin user → Microsoft should block with
AADSTS50105. - 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
| Symptom | What to check |
|---|---|
AADSTS50011 | Redirect URI on the app registration must exactly match oauth_callback_url. |
HTTP 401 on /hub/oauth_callback | Wrong 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 in | They may be a Global Administrator. Retest with a normal account. |
| Assigned user still blocked | Confirm Enterprise application → Users 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.