jupyterhub / ltiauthenticator

A JupyterHub authenticator for LTI
https://ltiauthenticator.readthedocs.io
BSD 3-Clause "New" or "Revised" License
67 stars 54 forks source link

"Audience doesn't match" error if client_id is a string, not an iterable of strings #177

Closed jeflem closed 8 months ago

jeflem commented 11 months ago

Bug description

Starting with 1.6.1 ltiauthenticator supports iterables of strings for c.LTI13Authenticator.client_id in jupyterhub_config.py. According to the docs string should still work if only one client ID is required. But JHub shows "Audience doesn't match" on login via LMS/LTI.

If a string instead of an iterable is provided, the string seems to be cast to a set of single characters somehow. This makes validator.validate_auth_response(args) in lti13/handlers.py fail (each character is interpreted as a client ID).

Providing a list containing the string everything is fine.

Note that I have several hubs running. On some (the older ones) strings work fine (as they should corresponding to ltiauthenticator doc). On others (freshly installed) strings don't work. Maybe the problem is related to traitlets version. On the old hubs there is traitlets 5.9. On the fresh ones showing the bug it's 5.12. But not sure whether that's related. JHub version is the same (4.0.2) on all hubs.

Of course, a workaround is to avoid strings and to always use lists of strings.

How to reproduce

Setup JHub with ltiauthenticator and use a string for c.LTI13Authenticator.client_id in jupyterhub_config.py. This was the standard setup before ltiauthenticator 1.6.1.

Expected behaviour

No error. Client ID check does not fail.

Actual behaviour

Message "Audience doesn't match" and login to hub fails.

Your personal set up

Full environment ``` (jhub) /$ conda list # packages in environment at /opt/conda/envs/jhub: # # Name Version Build Channel _libgcc_mutex 0.1 conda_forge conda-forge _openmp_mutex 4.5 2_gnu conda-forge alembic 1.12.1 pyhd8ed1ab_0 conda-forge anyio 4.0.0 pyhd8ed1ab_0 conda-forge argon2-cffi 23.1.0 pyhd8ed1ab_0 conda-forge argon2-cffi-bindings 21.2.0 py311h459d7ec_4 conda-forge arrow 1.3.0 pyhd8ed1ab_0 conda-forge asttokens 2.4.1 pyhd8ed1ab_0 conda-forge async-lru 2.0.4 pyhd8ed1ab_0 conda-forge async_generator 1.10 py_0 conda-forge attrs 23.1.0 pyh71513ae_1 conda-forge babel 2.13.1 pyhd8ed1ab_0 conda-forge backcall 0.2.0 pyh9f0ad1d_0 conda-forge backports 1.0 pyhd8ed1ab_3 conda-forge backports.functools_lru_cache 1.6.5 pyhd8ed1ab_0 conda-forge beautifulsoup4 4.12.2 pyha770c72_0 conda-forge bleach 6.1.0 pyhd8ed1ab_0 conda-forge blinker 1.6.3 pyhd8ed1ab_0 conda-forge brotli-python 1.1.0 py311hb755f60_1 conda-forge bzip2 1.0.8 h7f98852_4 conda-forge c-ares 1.20.1 hd590300_1 conda-forge ca-certificates 2023.7.22 hbcca054_0 conda-forge cached-property 1.5.2 hd8ed1ab_1 conda-forge cached_property 1.5.2 pyha770c72_1 conda-forge certifi 2023.7.22 pyhd8ed1ab_0 conda-forge certipy 0.1.3 py_0 conda-forge cffi 1.16.0 py311hb3a22ac_0 conda-forge charset-normalizer 3.3.1 pyhd8ed1ab_0 conda-forge comm 0.1.4 pyhd8ed1ab_0 conda-forge configurable-http-proxy 4.6.0 he2f69ee_0 conda-forge cryptography 41.0.5 py311h63ff55d_0 conda-forge debugpy 1.8.0 py311hb755f60_1 conda-forge decorator 5.1.1 pyhd8ed1ab_0 conda-forge defusedxml 0.7.1 pyhd8ed1ab_0 conda-forge entrypoints 0.4 pyhd8ed1ab_0 conda-forge escapism 1.0.1 pypi_0 pypi exceptiongroup 1.1.3 pyhd8ed1ab_0 conda-forge executing 1.2.0 pyhd8ed1ab_0 conda-forge fqdn 1.5.1 pyhd8ed1ab_0 conda-forge greenlet 3.0.1 py311hb755f60_0 conda-forge icu 73.2 h59595ed_0 conda-forge idna 3.4 pyhd8ed1ab_0 conda-forge importlib-metadata 6.8.0 pyha770c72_0 conda-forge importlib_metadata 6.8.0 hd8ed1ab_0 conda-forge importlib_resources 6.1.0 pyhd8ed1ab_0 conda-forge ipykernel 6.26.0 pyhf8b6a83_0 conda-forge ipython 8.16.1 pyh0d859eb_0 conda-forge isoduration 20.11.0 pyhd8ed1ab_0 conda-forge jedi 0.19.1 pyhd8ed1ab_0 conda-forge jinja2 3.1.2 pyhd8ed1ab_1 conda-forge json5 0.9.14 pyhd8ed1ab_0 conda-forge jsonpointer 2.4 py311h38be061_3 conda-forge jsonschema 4.19.1 pyhd8ed1ab_0 conda-forge jsonschema-specifications 2023.7.1 pyhd8ed1ab_0 conda-forge jsonschema-with-format-nongpl 4.19.1 pyhd8ed1ab_0 conda-forge jupyter-lsp 2.2.0 pyhd8ed1ab_0 conda-forge jupyter_client 8.5.0 pyhd8ed1ab_0 conda-forge jupyter_core 5.4.0 py311h38be061_0 conda-forge jupyter_events 0.8.0 pyhd8ed1ab_0 conda-forge jupyter_server 2.9.1 pyhd8ed1ab_0 conda-forge jupyter_server_terminals 0.4.4 pyhd8ed1ab_1 conda-forge jupyter_telemetry 0.1.0 pyhd8ed1ab_1 conda-forge jupyterhub 4.0.2 pyh31011fe_0 conda-forge jupyterhub-base 4.0.2 pyh31011fe_0 conda-forge jupyterhub-idle-culler 1.2.1 pypi_0 pypi jupyterhub-ltiauthenticator 1.6.1 pypi_0 pypi jupyterhub-systemdspawner 1.0.1 pypi_0 pypi jupyterlab 4.0.7 pyhd8ed1ab_0 conda-forge jupyterlab_pygments 0.2.2 pyhd8ed1ab_0 conda-forge jupyterlab_server 2.25.0 pyhd8ed1ab_0 conda-forge keyutils 1.6.1 h166bdaf_0 conda-forge krb5 1.21.2 h659d440_0 conda-forge ld_impl_linux-64 2.40 h41732ed_0 conda-forge libcurl 8.4.0 hca28451_0 conda-forge libedit 3.1.20191231 he28a2e2_2 conda-forge libev 4.33 h516909a_1 conda-forge libexpat 2.5.0 hcb278e6_1 conda-forge libffi 3.4.2 h7f98852_5 conda-forge libgcc-ng 13.2.0 h807b86a_2 conda-forge libgomp 13.2.0 h807b86a_2 conda-forge libnghttp2 1.55.1 h47da74e_0 conda-forge libnsl 2.0.1 hd590300_0 conda-forge libsodium 1.0.18 h36c2ea0_1 conda-forge libsqlite 3.43.2 h2797004_0 conda-forge libssh2 1.11.0 h0841786_0 conda-forge libstdcxx-ng 13.2.0 h7e041cc_2 conda-forge libuuid 2.38.1 h0b41bf4_0 conda-forge libuv 1.46.0 hd590300_0 conda-forge libzlib 1.2.13 hd590300_5 conda-forge mako 1.2.4 pyhd8ed1ab_0 conda-forge markupsafe 2.1.3 py311h459d7ec_1 conda-forge matplotlib-inline 0.1.6 pyhd8ed1ab_0 conda-forge mistune 3.0.1 pyhd8ed1ab_0 conda-forge nbclient 0.8.0 pyhd8ed1ab_0 conda-forge nbconvert-core 7.9.2 pyhd8ed1ab_0 conda-forge nbformat 5.9.2 pyhd8ed1ab_0 conda-forge nbgitpuller 1.2.0 pypi_0 pypi ncurses 6.4 hcb278e6_0 conda-forge nest-asyncio 1.5.8 pyhd8ed1ab_0 conda-forge nodejs 18.17.1 h1990674_1 conda-forge notebook 7.0.6 pyhd8ed1ab_0 conda-forge notebook-shim 0.2.3 pyhd8ed1ab_0 conda-forge oauthlib 3.2.2 pyhd8ed1ab_0 conda-forge openssl 3.1.4 hd590300_0 conda-forge overrides 7.4.0 pyhd8ed1ab_0 conda-forge packaging 23.2 pyhd8ed1ab_0 conda-forge pamela 1.1.0 pyh1a96a4e_0 conda-forge pandocfilters 1.5.0 pyhd8ed1ab_0 conda-forge parso 0.8.3 pyhd8ed1ab_0 conda-forge pexpect 4.8.0 pyh1a96a4e_2 conda-forge pickleshare 0.7.5 py_1003 conda-forge pip 23.3.1 pyhd8ed1ab_0 conda-forge pkgutil-resolve-name 1.3.10 pyhd8ed1ab_1 conda-forge platformdirs 3.11.0 pyhd8ed1ab_0 conda-forge prometheus_client 0.17.1 pyhd8ed1ab_0 conda-forge prompt-toolkit 3.0.39 pyha770c72_0 conda-forge prompt_toolkit 3.0.39 hd8ed1ab_0 conda-forge psutil 5.9.5 py311h459d7ec_1 conda-forge ptyprocess 0.7.0 pyhd3deb0d_0 conda-forge pure_eval 0.2.2 pyhd8ed1ab_0 conda-forge pycparser 2.21 pyhd8ed1ab_0 conda-forge pycurl 7.45.1 py311hae980a4_3 conda-forge pygments 2.16.1 pyhd8ed1ab_0 conda-forge pyjwt 2.8.0 pyhd8ed1ab_0 conda-forge pyopenssl 23.2.0 pyhd8ed1ab_1 conda-forge pysocks 1.7.1 pyha2e5f31_6 conda-forge python 3.11.6 hab00c5b_0_cpython conda-forge python-dateutil 2.8.2 pyhd8ed1ab_0 conda-forge python-fastjsonschema 2.18.1 pyhd8ed1ab_0 conda-forge python-json-logger 2.0.7 pyhd8ed1ab_0 conda-forge python_abi 3.11 4_cp311 conda-forge pytz 2023.3.post1 pyhd8ed1ab_0 conda-forge pyyaml 6.0.1 py311h459d7ec_1 conda-forge pyzmq 25.1.1 py311h34ded2d_2 conda-forge readline 8.2 h8228510_1 conda-forge referencing 0.30.2 pyhd8ed1ab_0 conda-forge requests 2.31.0 pyhd8ed1ab_0 conda-forge rfc3339-validator 0.1.4 pyhd8ed1ab_0 conda-forge rfc3986-validator 0.1.1 pyh9f0ad1d_0 conda-forge rpds-py 0.10.6 py311h46250e7_0 conda-forge ruamel.yaml 0.18.2 py311h459d7ec_0 conda-forge ruamel.yaml.clib 0.2.7 py311h459d7ec_2 conda-forge send2trash 1.8.2 pyh41d4057_0 conda-forge setuptools 68.2.2 pyhd8ed1ab_0 conda-forge six 1.16.0 pyh6c4a22f_0 conda-forge sniffio 1.3.0 pyhd8ed1ab_0 conda-forge soupsieve 2.5 pyhd8ed1ab_1 conda-forge sqlalchemy 2.0.22 py311h459d7ec_0 conda-forge stack_data 0.6.2 pyhd8ed1ab_0 conda-forge terminado 0.17.1 pyh41d4057_0 conda-forge tinycss2 1.2.1 pyhd8ed1ab_0 conda-forge tk 8.6.13 h2797004_0 conda-forge tomli 2.0.1 pyhd8ed1ab_0 conda-forge tornado 6.3.3 py311h459d7ec_1 conda-forge traitlets 5.12.0 pyhd8ed1ab_0 conda-forge types-python-dateutil 2.8.19.14 pyhd8ed1ab_0 conda-forge typing-extensions 4.8.0 hd8ed1ab_0 conda-forge typing_extensions 4.8.0 pyha770c72_0 conda-forge typing_utils 0.1.0 pyhd8ed1ab_0 conda-forge tzdata 2023c h71feb2d_0 conda-forge uri-template 1.3.0 pyhd8ed1ab_0 conda-forge urllib3 2.0.7 pyhd8ed1ab_0 conda-forge wcwidth 0.2.8 pyhd8ed1ab_0 conda-forge webcolors 1.13 pyhd8ed1ab_0 conda-forge webencodings 0.5.1 pyhd8ed1ab_2 conda-forge websocket-client 1.6.4 pyhd8ed1ab_0 conda-forge wheel 0.41.2 pyhd8ed1ab_0 conda-forge xz 5.2.6 h166bdaf_0 conda-forge yaml 0.2.5 h7f98852_2 conda-forge zeromq 4.3.5 h59595ed_0 conda-forge zipp 3.17.0 pyhd8ed1ab_0 conda-forge zlib 1.2.13 hd590300_5 conda-forge zstd 1.5.5 hfc55251_0 conda-forge ```
Configuration ```python # jupyterhub_config.py # showing only the ltiauthenticator part base_url = 'http://192.168.178.28:9090' c.LTI13Authenticator.client_id = 'PUPg1NRDTBsicjZ' c.LTI13Authenticator.issuer = base_url c.LTI13Authenticator.authorize_url = f'{base_url}/mod/lti/auth.php' c.LTI13Authenticator.jwks_endpoint = f'{base_url}/mod/lti/certs.php' ```
Logs ``` Oct 29 15:35:15 5d838adff081 jupyterhub[33]: [D 2023-10-29 14:35:15.620 JupyterHub handlers:259] Initial login request args are {'iss': 'http://192.168.178.28:9090', 'target_link_uri': 'http://192.168.178.28:8000', 'login_hint': '3', 'lti_message_hint': '7', 'client_id': 'PUPg1NRDTBsicjZ', 'lti_deployment_id': '3'} Oct 29 15:35:15 5d838adff081 jupyterhub[33]: [D 2023-10-29 14:35:15.620 JupyterHub handlers:268] login_hint is 3 Oct 29 15:35:15 5d838adff081 jupyterhub[33]: [D 2023-10-29 14:35:15.620 JupyterHub handlers:333] lti_message_hint is 7 Oct 29 15:35:15 5d838adff081 jupyterhub[33]: [D 2023-10-29 14:35:15.620 JupyterHub handlers:333] client_id is PUPg1NRDTBsicjZ Oct 29 15:35:15 5d838adff081 jupyterhub[33]: [D 2023-10-29 14:35:15.620 JupyterHub handlers:279] redirect_uri is: http://192.168.178.28:8000/hub/lti13/oauth_callback Oct 29 15:35:15 5d838adff081 jupyterhub[33]: [W 2023-10-29 14:35:15.620 JupyterHub handlers:243] Ignoring next_url None, using '/' Oct 29 15:35:15 5d838adff081 jupyterhub[33]: [D 2023-10-29 14:35:15.620 JupyterHub base:587] Setting cookie lti13authenticator-state: {'httponly': True, 'expires_days': 1} Oct 29 15:35:15 5d838adff081 jupyterhub[33]: [D 2023-10-29 14:35:15.621 JupyterHub base:587] Setting cookie lti13authenticator-nonce-state: {'httponly': True, 'expires_days': 1} Oct 29 15:35:15 5d838adff081 jupyterhub[33]: [D 2023-10-29 14:35:15.621 JupyterHub handlers:286] nonce value: ef2ba7ab86763fbe7b5ac42a9736bf482ae30f3941d1f8502b67c18fcd0054b4 Oct 29 15:35:15 5d838adff081 jupyterhub[33]: [I 2023-10-29 14:35:15.621 JupyterHub log:191] 302 POST /hub/lti13/oauth_login -> http://192.168.178.28:9090/mod/lti/auth.php?response_type=id_token&scope=openid&response_mode=form_post&prompt=none&client_id=PUPg1NRDTBsicjZ&redirect_uri=http%3A%2F%2F192.168.178.28%3A8000%2Fhub%2Flti13%2Foauth_callback&login_hint=3&nonce=ef2ba7ab86763fbe7b5ac42a9736bf482ae30f3941d1f8502b67c18fcd0054b4&state=[secret]<i_message_hint=7 (@::ffff:10.0.2.100) 2.08ms Oct 29 15:35:15 5d838adff081 jupyterhub[33]: [D 2023-10-29 14:35:15.671 JupyterHub handlers:416] Initial launch request args are {'id_token': 'removed token here, token contains correct client id'} Oct 29 15:35:15 5d838adff081 jupyterhub[33]: [W 2023-10-29 14:35:15.704 JupyterHub web:1869] 401 POST /hub/lti13/oauth_callback (::ffff:10.0.2.100): Audience doesn't match Oct 29 15:35:15 5d838adff081 jupyterhub[33]: [D 2023-10-29 14:35:15.704 JupyterHub base:1371] No template for 401 Oct 29 15:35:15 5d838adff081 jupyterhub[33]: [W 2023-10-29 14:35:15.726 JupyterHub log:191] 401 POST /hub/lti13/oauth_callback (@::ffff:10.0.2.100) 54.72ms Oct 29 15:35:15 5d838adff081 jupyterhub[33]: [D 2023-10-29 14:35:15.750 JupyterHub log:191] 200 GET /hub/static/css/style.min.css?v=01598a5386176f0279952a3b9632a07e7fce9a12aa53108973c83be9ec3473e7a59354876fab64bfeb01892eb503870183707aa03f207d7a94845ca7980c3819 (@::ffff:10.0.2.100) 1.90ms Oct 29 15:35:15 5d838adff081 jupyterhub[33]: [D 2023-10-29 14:35:15.765 JupyterHub log:191] 200 GET /hub/logo (@::ffff:10.0.2.100) 1.39ms ```
jeflem commented 11 months ago

Further testing reveals that it's traitlets:

martinclaus commented 10 months ago

Hi @jeflem, thank you for spotting this and for the detailed analysis! This is really helpful. I have filed a bug in the traintlets repo (https://github.com/ipython/traitlets/issues/891) to tackle this upstream

Since two versions of traitlets are already released with this changed behaviour, I guess our best reaction is to change our docs such that only lists or sets shall be used as input for client_id.

jeflem commented 10 months ago

+1 for 'change our docs'

Every other solution would be too complex.