Skip to content

Fix: OAuth2 JWKS-fetch errors should be 401, not unhandled 500 - #337

Merged
cooper667 merged 3 commits into
developmentfrom
fix/oauth2-jwks-fetch-error-handling
Jul 9, 2026
Merged

cooper667 merged 3 commits into
developmentfrom
fix/oauth2-jwks-fetch-error-handling

Conversation

@cooper667

@cooper667 cooper667 commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Root cause of a live Naomi <-> ADR integration bug:
SSO/Bearer-token API calls (e.g. organization_list_for_user) were returning a generic 500 Internal Server Error.

Root cause

ckanext.unaids.auth0_domain and ckanext.unaids.oauth2_api_audience were not configured With AUTH0_DOMAIN unset:

jsonurl = urlopen("https://" + AUTH0_DOMAIN + "/.well-known/jwks.json")

raises an immediate TypeError (concatenating str + None), completely outside the try/except that only wrapped the later jwt.decode() call. Since TypeError isn't OAuth2AuthenticationError/OAuth2AuthorizationError, the registered Flask error handlers didn't catch it either, so it fell through to CKAN's generic unhandled-exception 500.

The missing config itself is fixed separately in adx_develop (deploy ini files). This PR fixes the underlying code fragility so the same class of issue (Auth0 outage, network blip, or any other future config mistake) degrades to a proper 401 instead of crashing.

Fix

  • Explicit early check: if AUTH0_DOMAIN/API_AUDIENCE aren't configured, raise OAuth2AuthenticationError (→ clean 401) instead of letting a TypeError propagate.
  • Wrap the JWKS fetch (urlopen + json.loads) in try/except, logging the real exception and raising OAuth2AuthenticationError on any failure.

If ckanext.unaids.auth0_domain/oauth2_api_audience are unset, or the
JWKS fetch fails for any reason (network issue, Auth0 outage), this
previously raised an uncaught TypeError/URLError that fell through to
a generic 500 Internal Server Error, since only jwt.decode() itself
was wrapped in error handling. Bearer-token requests now get a clean
401 with a useful message in both cases instead.

@ChasNelson1990 ChasNelson1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@A-Souhei A-Souhei left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good

The IAuthenticator.identify() hook only set g.userobj/g.user directly,
but CKAN's API action dispatcher (ckan/views/api.py) builds its context
from Flask-Login's current_user, not g.user/g.userobj -- and
identify_user() unconditionally re-derives g.user/g.userobj from
current_user right after every IAuthenticator.identify() call, silently
overwriting what this plugin set. Net effect: Bearer-token requests
authenticated successfully (no error) but ran as the anonymous user,
so e.g. organization_list_for_user returned an empty list instead of
the caller's actual orgs.

This is a real regression from the CKAN 2.11 migration: the original
pre-2.11 code had a g.current_user = payload line, which the migration's
post-migration cleanup (#331) removed as 'confirmed write-only' -- true,
but the actual fix needed was calling Flask-Login's login_user(), the
same pattern ckanext-saml2auth already uses for CKAN 2.10+.
@cooper667

Copy link
Copy Markdown
Contributor Author

The IAuthenticator.identify() hook only set g.userobj/g.user directly,
but CKAN's API action dispatcher (ckan/views/api.py) builds its context
from Flask-Login's current_user, not g.user/g.userobj -- and
identify_user() unconditionally re-derives g.user/g.userobj from
current_user right after every IAuthenticator.identify() call, silently
overwriting what this plugin set. Net effect: Bearer-token requests
authenticated successfully (no error) but ran as the anonymous user,
so e.g. organization_list_for_user returned an empty list instead of
the caller's actual orgs.

This is a real regression from the CKAN 2.11 migration: the original
pre-2.11 code had a g.current_user = payload line, which the migration's
post-migration cleanup (#331) removed as 'confirmed write-only' -- true,
but the actual fix needed was calling Flask-Login's login_user(), the
same pattern ckanext-saml2auth already uses for CKAN 2.10+.

@cooper667
cooper667 enabled auto-merge (squash) July 9, 2026 19:00
The bare mock User class only had .name, but login_user() (added in
the previous commit) also needs is_active and get_id() -- both
normally provided by flask_login's UserMixin, which this minimal test
double doesn't inherit.
@cooper667
cooper667 merged commit 1294b7d into development Jul 9, 2026
3 checks passed
@cooper667
cooper667 deleted the fix/oauth2-jwks-fetch-error-handling branch July 9, 2026 19:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants