Skip to content

fix: a_exchange_token sends None values as empty strings in form-encoded body, causing Keycloak invalid_issuer error#709

Open
adriagarp wants to merge 1 commit intomarcospereirampj:masterfrom
adriagarp:fix-none-values-in-a-exchange-token
Open

fix: a_exchange_token sends None values as empty strings in form-encoded body, causing Keycloak invalid_issuer error#709
adriagarp wants to merge 1 commit intomarcospereirampj:masterfrom
adriagarp:fix-none-values-in-a-exchange-token

Conversation

@adriagarp
Copy link

In my team we found out while migrating from sync to async that the async a_exchange_token method fails with KeycloakPostError: 400: b'{"error":"invalid_issuer", "error_description": "Invalid subject_issuer parameter"}' while the sync exchange_token works correctly with identical arguments.

The root cause is that both exchange_token and a_exchange_token build the same payload dict, which includes keys with None values for optional parameters that weren't provided (e.g. subject_issuer, requested_issuer, requested_subject).

The sync path uses requests, which silently drops keys with None values from form-encoded bodies:

  >>> import requests
  >>> r = requests.Request('POST', 'http://example.com', data={'a': '1', 'subject_issuer': None})
  >>> r.prepare().body
  'a=1'

The async path uses httpx, which serializes None as empty strings.

  >>> async with httpx.AsyncClient() as client:
  ...     req = client.build_request('POST', 'http://example.com', data={'a': '1', 'subject_issuer': None})
  ...     req.content
  b'a=1&subject_issuer='

This was already identified for query parameters (ConnectionManager._filter_query_params already addresses this). The proposed fix renames that method to_filter_none_values and makes it a @classmethod so that it can be reused from _prepare_httpx_request_content (which is now also a @classmethod).

How to reproduce:

  from keycloak import KeycloakOpenID
  import asyncio

  kc = KeycloakOpenID(
      server_url="https://your-keycloak/auth/",
      client_id="your-client",
      realm_name="your-realm",
      client_secret_key="your-secret",
  )

  token = "a-valid-access-token"

  # Works
  kc.exchange_token(token=token, audience="target-client")

  # Fails with: invalid_issuer / Invalid subject_issuer parameter
  asyncio.run(kc.a_exchange_token(token=token, audience="target-client"))

Tested with:

python-keycloak==7.1.1
requests==2.32.5
httpx==0.28.1

@adriagarp adriagarp force-pushed the fix-none-values-in-a-exchange-token branch from cb5d6ba to c931e16 Compare March 6, 2026 14:38
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.

1 participant