Thank you for your comment.
From the logs, it can be seen that Spawner.auth_state_hook is called twice: once before refresh_user and once after it.
During the first call to Spawner.auth_state_hook, the custom value ("Z2JH_TEST": "DUMMY DUMMY DUMMY") set in Authenticator.post_auth_hook is still present in auth_state.
However, after refresh_user is executed, the auth_state is replaced by the result returned from super().refresh_user(), and the custom attributes such as uidNumber, gidNumber, homeDirectory, and Z2JH_TEST are no longer present.
As a result, during the second call to Spawner.auth_state_hook, these custom values have disappeared from auth_state, which eventually leads to errors such as KeyError: 'username'.
1. c.Authenticator.post_auth_hook
1.1 Before set custom value
[D 2026-04-09 00:23:08.578 JupyterHub <string>:51] auth_model:
{
"name": "John Smith",
"admin": null,
"auth_state": {
"access_token": "",
"refresh_token": "",
"id_token": "",
"scope": [
""
],
"token_response": {
"token_type": "Bearer",
"expires_in": "3599",
"ext_expires_in": "3599",
"expires_on": "1775697788",
"access_token": "",
"refresh_token": "",
"id_token": ""
},
"user": {
"aud": "19d5279a-15a9-43bc-8d97-7990af52e5ff",
"iss": "https://sts.windows.net/3d7049f7-ce7c-4926-b7ae-2009ba790c24/",
"iat": 1775693888,
"nbf": 1775693888,
"exp": 1775697788,
"amr": [
"pwd",
"mfa"
],
"family_name": "Smith",
"given_name": "John",
"ipaddr": "2001:db8:1234:5678:9abc:def0:1234:5678",
"name": "John Smith",
"oid": "7ea0f9b7-d4a3-4ec1-a68e-2790e051c738",
"rh": "noCeichingu6shee9kaengaeghainguavugahgohloimuu8Weidaephai",
"sid": "8158ffc5-bb91-4598-a2d0-0f636023cba0",
"sub": "6a81d034-2e8d-4035-9b06-7b4968ccfc3b",
"tid": "3d7049f7-ce7c-4926-b7ae-2009ba790c24",
"unique_name": "jhon@example.com",
"upn": "jhon@example.com",
"uti": "eeng6Thu8thiewie0fux7p",
"ver": "1.0"
}
}
}
1.2 After set custom value
[D 2026-04-09 00:23:08.619 JupyterHub <string>:89] return auth_model:
{
"name": "jhon",
"admin": null,
"auth_state": {
"access_token": "",
"refresh_token": "",
"id_token": "",
"scope": [
""
],
"token_response": {
"token_type": "Bearer",
"expires_in": "3599",
"ext_expires_in": "3599",
"expires_on": "1775697788",
"access_token": "",
"id_token": ""
},
"user": {
"aud": "19d5279a-15a9-43bc-8d97-7990af52e5ff",
"iss": "https://sts.windows.net/3d7049f7-ce7c-4926-b7ae-2009ba790c24/",
"iat": 1775693888,
"nbf": 1775693888,
"exp": 1775697788,
"amr": [
"pwd",
"mfa"
],
"family_name": "Smith",
"given_name": "John",
"ipaddr": "2001:db8:1234:5678:9abc:def0:1234:5678",
"name": "John Smith",
"oid": "7ea0f9b7-d4a3-4ec1-a68e-2790e051c738",
"rh": "noCeichingu6shee9kaengaeghainguavugahgohloimuu8Weidaephai",
"sid": "8158ffc5-bb91-4598-a2d0-0f636023cba0",
"sub": "6a81d034-2e8d-4035-9b06-7b4968ccfc3b",
"tid": "3d7049f7-ce7c-4926-b7ae-2009ba790c24",
"unique_name": "jhon@example.com",
"upn": "jhon@example.com",
"uti": "eeng6Thu8thiewie0fux7p",
"ver": "1.0"
},
"username": "jhon",
"uidNumber": "123456",
"gidNumber": "987",
"homeDirectory": "/nosuchdir",
"Z2JH_TEST": "DUMMY DUMMY DUMMY" // <-- test value set
}
}
2. First Spawner.auth_state_hook
[D 2026-04-09 00:23:08.692 JupyterHub scopes:1013] Checking access to /hub/spawn via scope servers!server=jhon/
[D 2026-04-09 00:23:08.694 JupyterHub <string>:93] userdata_hook
[D 2026-04-09 00:23:08.694 JupyterHub <string>:94] {'access_token':
{
"access_token": "",
"refresh_token": "",
"id_token": "",
"scope": [
""
],
"token_response": {
"token_type": "Bearer",
"expires_in": "3599",
"ext_expires_in": "3599",
"expires_on": "1775697788",
"access_token": "",
"id_token": ""
},
"user": {
"aud": "19d5279a-15a9-43bc-8d97-7990af52e5ff",
"iss": "https://sts.windows.net/3d7049f7-ce7c-4926-b7ae-2009ba790c24/",
"iat": 1775693888,
"nbf": 1775693888,
"exp": 1775697788,
"amr": [
"pwd",
"mfa"
],
"family_name": "Smith",
"given_name": "John",
"ipaddr": "2001:db8:1234:5678:9abc:def0:1234:5678",
"name": "John Smith",
"oid": "7ea0f9b7-d4a3-4ec1-a68e-2790e051c738",
"rh": "noCeichingu6shee9kaengaeghainguavugahgohloimuu8Weidaephai",
"sid": "8158ffc5-bb91-4598-a2d0-0f636023cba0",
"sub": "6a81d034-2e8d-4035-9b06-7b4968ccfc3b",
"tid": "3d7049f7-ce7c-4926-b7ae-2009ba790c24",
"unique_name": "jhon@example.com",
"upn": "jhon@example.com",
"uti": "eeng6Thu8thiewie0fux7p",
"ver": "1.0"
},
"username": "jhon",
"uidNumber": "123456",
"gidNumber": "987",
"homeDirectory": "/nosuchdir",
"Z2JH_TEST": "DUMMY DUMMY DUMMY" // <-- Value stil exists
}
3. refresh user
3.1 Before refresh_user
[W 2026-04-09 00:23:08.695 JupyterHub <string>:105] BEFORE get_auth_state
[W 2026-04-09 00:23:08.695 JupyterHub <string>:107] BEFORE auth_state={'access_token':
{
"access_token": "",
"refresh_token": "",
"id_token": "",
"scope": [
""
],
"token_response": {
"token_type": "Bearer",
"expires_in": "3599",
"ext_expires_in": "3599",
"expires_on": "1775697788",
"access_token": "",
"refresh_token": "",
"id_token": ""
},
"user": {
"aud": "19d5279a-15a9-43bc-8d97-7990af52e5ff",
"iss": "https://sts.windows.net/3d7049f7-ce7c-4926-b7ae-2009ba790c24/",
"iat": 1775693888,
"nbf": 1775693888,
"exp": 1775697788,
"amr": [
"pwd",
"mfa"
],
"family_name": "Smith",
"given_name": "John",
"ipaddr": "2001:db8:1234:5678:9abc:def0:1234:5678",
"name": "John Smith",
"oid": "7ea0f9b7-d4a3-4ec1-a68e-2790e051c738",
"rh": "noCeichingu6shee9kaengaeghainguavugahgohloimuu8Weidaephai",
"sid": "8158ffc5-bb91-4598-a2d0-0f636023cba0",
"sub": "6a81d034-2e8d-4035-9b06-7b4968ccfc3b",
"tid": "3d7049f7-ce7c-4926-b7ae-2009ba790c24",
"unique_name": "jhon@example.com",
"upn": "jhon@example.com",
"uti": "eeng6Thu8thiewie0fux7p",
"ver": "1.0"
},
"username": "jhon",
"uidNumber": "123456",
"gidNumber": "987",
"homeDirectory": "/nosuchdir",
"Z2JH_TEST": "DUMMY DUMMY DUMMY" // < Custom value still present.
}
3.2 After refresh user
[W 2026-04-09 00:23:08.697 JupyterHub <string>:111] AFTER super()
[W 2026-04-09 00:23:08.697 JupyterHub <string>:116] AFTER auth_state={'access_token':
{
"access_token": "",
"refresh_token": "",
"id_token": "",
"scope": [
""
],
"token_response": {
"token_type": "Bearer",
"expires_in": "3599",
"ext_expires_in": "3599",
"expires_on": "1775697788",
"access_token": "",
"refresh_token": "",
"id_token": ""
},
"user": {
"aud": "19d5279a-15a9-43bc-8d97-7990af52e5ff",
"iss": "https://sts.windows.net/3d7049f7-ce7c-4926-b7ae-2009ba790c24/",
"iat": 1775693888,
"nbf": 1775693888,
"exp": 1775697788,
"amr": [
"pwd",
"mfa"
],
"family_name": "Smith",
"given_name": "John",
"ipaddr": "2001:db8:1234:5678:9abc:def0:1234:5678",
"name": "John Smith",
"oid": "7ea0f9b7-d4a3-4ec1-a68e-2790e051c738",
"rh": "noCeichingu6shee9kaengaeghainguavugahgohloimuu8Weidaephai",
"sid": "8158ffc5-bb91-4598-a2d0-0f636023cba0",
"sub": "6a81d034-2e8d-4035-9b06-7b4968ccfc3b",
"tid": "3d7049f7-ce7c-4926-b7ae-2009ba790c24",
"unique_name": "jhon@example.com",
"upn": "jhon@example.com",
"uti": "eeng6Thu8thiewie0fux7p",
"ver": "1.0"
}
// Custom value has been removed. "Z2JH_TEST": "DUMMY DUMMY DUMMY"
}
4. Second Spawner.auth_state_hook
[D 2026-04-09 00:23:08.767 JupyterHub <string>:93] userdata_hook
[D 2026-04-09 00:23:08.767 JupyterHub <string>:94] {'access_token':
{
"access_token": "",
"refresh_token": "",
"id_token": "",
"scope": [
""
],
"token_response": {
"token_type": "Bearer",
"expires_in": "3599",
"ext_expires_in": "3599",
"expires_on": "1775697788",
"access_token": "",
"id_token": ""
},
"user": {
"aud": "19d5279a-15a9-43bc-8d97-7990af52e5ff",
"iss": "https://sts.windows.net/3d7049f7-ce7c-4926-b7ae-2009ba790c24/",
"iat": 1775693888,
"nbf": 1775693888,
"exp": 1775697788,
"amr": [
"pwd",
"mfa"
],
"family_name": "Smith",
"given_name": "John",
"ipaddr": "2001:db8:1234:5678:9abc:def0:1234:5678",
"name": "John Smith",
"oid": "7ea0f9b7-d4a3-4ec1-a68e-2790e051c738",
"rh": "noCeichingu6shee9kaengaeghainguavugahgohloimuu8Weidaephai",
"sid": "8158ffc5-bb91-4598-a2d0-0f636023cba0",
"sub": "6a81d034-2e8d-4035-9b06-7b4968ccfc3b",
"tid": "3d7049f7-ce7c-4926-b7ae-2009ba790c24",
"unique_name": "jhon@example.com",
"upn": "jhon@example.com",
"uti": "eeng6Thu8thiewie0fux7p",
"ver": "1.0"
}
// Custom value removed "Z2JH_TEST": "DUMMY DUMMY DUMMY"
}
[E 2026-04-09 00:23:08.767 JupyterHub user:1004] Unhandled error starting jhon's server: 'username'
Traceback (most recent call last):
File "/usr/local/lib/python3.12/site-packages/jupyterhub/user.py", line 897, in spawn
await spawner.run_auth_state_hook(auth_state)
File "/usr/local/lib/python3.12/site-packages/jupyterhub/spawner.py", line 1575, in run_auth_state_hook
await maybe_future(self.auth_state_hook(self, auth_state))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<string>", line 98, in userdata_hook
KeyError: 'username'
5. Configuration
extraConfig:
SpawnerCustomConfig: |
from oauthenticator.azuread import AzureAdOAuthenticator
from ldap3 import Server, Connection, ALL, SUBTREE
from hashlib import md5
def my_post_auth_hook(authenticator, handler, auth_model):
authenticator.log.debug("my_post_auth_hook called")
authenticator.log.debug("auth_model: %s", auth_model)
auth_state = auth_model.get("auth_state") or {}
auth_state_user = auth_state.get("user") or {}
# snip
if conn.entries:
entry = conn.entries[0]
auth_state = auth_model["auth_state"]
# SNIP
auth_state["Z2JH_TEST"] = "DUMMY DUMMY DUMMY"
# SNIP
authenticator.log.debug("return auth_model: %s", auth_model)
return auth_model
def userdata_hook(spawner, auth_state):
spawner.log.debug("userdata_hook")
spawner.log.debug(auth_state)
if not auth_state:
return
spawner.environment["NB_USER"] = auth_state['username']
# snip
class AzureAdOAuthenticatorInfo(AzureAdOAuthenticator):
async def refresh_user(self, user, handler=None):
self.log.warning("BEFORE get_auth_state")
before_state = await user.get_auth_state() or {}
self.log.warning("BEFORE auth_state=%r", before_state)
refreshed = await super().refresh_user(user, handler)
self.log.warning("AFTER super()")
after_state = (
refreshed.get("auth_state") if isinstance(refreshed, dict)
else await user.get_auth_state()
) or {}
self.log.warning("AFTER auth_state=%r", after_state)
return refreshed
c.Authenticator.allow_all = True
c.Authenticator.enable_auth_state = True
c.Authenticator.post_auth_hook = my_post_auth_hook
c.Spawner.auth_state_hook = userdata_hook
c.JupyterHub.authenticator_class = AzureAdOAuthenticatorInfo
c.ServerApp.shutdown_no_activity_timeout = 150
Best regards,