Summary
On workos 10.0.0, user_management.get_authorization_url_with_pkce(client_id=...) accepts the
argument and ignores it. The URL it returns carries the client_id the WorkOSClient was
constructed with.
src/workos/user_management/_resource.py:2331 takes it and documents it:
def get_authorization_url_with_pkce(
self,
*,
redirect_uri: str,
client_id: Optional[str] = None,
...
client_id: The WorkOS client ID. Defaults to the client's configured ID.
Then at :2374 it calls get_authorization_url(...) without passing it, and that method has no
client_id parameter to pass it to. It reads the constructor value instead, at :2986:
params["response_type"] = "code"
if self._client.client_id is not None:
params["client_id"] = self._client.client_id
The async copy at :4690 is identical.
Two methods in the same hand-written block do honour the same argument:
:2277 resolved_client_id = client_id or self._client._require_client_id() # get_jwks_url
:2417 resolved_client_id = client_id or (self._client.client_id or "") # authenticate_with_code_pkce
authenticate_with_code_pkce is the other half of the PKCE pair. So a caller who passes a
per-tenant client_id to both gets an authorization URL for one application and a token exchange
for another.
Reproduction
workos 10.0.0, Python 3.14.0, fresh venv. Constructor client_id is client_CONSTRUCTOR, the
override passed on every call is client_OVERRIDE. base_url points at a loopback origin that
records the request body, so the token exchange row is read off the wire. Nothing leaves the
machine.
client = WorkOSClient(api_key="sk_test", client_id="client_CONSTRUCTOR", base_url=LOOPBACK)
um = client.user_management
um.get_authorization_url_with_pkce(redirect_uri=CB, client_id="client_OVERRIDE")
um.get_authorization_url_with_pkce(redirect_uri=CB)
um.get_jwks_url(client_id="client_OVERRIDE")
um.authenticate_with_code_pkce(code=..., code_verifier=..., client_id="client_OVERRIDE")
| call |
client_id actually used |
get_authorization_url_with_pkce(client_id=OVERRIDE) |
client_CONSTRUCTOR |
get_authorization_url_with_pkce() no override |
client_CONSTRUCTOR |
get_jwks_url(client_id=OVERRIDE) |
client_OVERRIDE |
authenticate_with_code_pkce(client_id=OVERRIDE) |
client_OVERRIDE |
AsyncWorkOSClient same call |
client_CONSTRUCTOR |
The second row is there so the first one means something. Without it, "the override is ignored"
would be indistinguishable from "the URL has no client_id at all". Rows three and four are the two
siblings that do the right thing.
Suggested fix
Either forward it, by giving get_authorization_url an optional client_id that falls back to
self._client.client_id, or drop the parameter from
get_authorization_url_with_pkce so the signature stops promising something it cannot do. The
first matches what authenticate_with_code_pkce already does, so the pair would agree.
This is a smaller problem than it looks at first glance. It only affects callers who set client_id
per request rather than on the client, which is the multi-tenant case the argument exists for.
Happy to open a PR either way, though I noticed most of this file is generated and the PKCE
methods sit inside the @oagen-ignore block at :2261 to :2444, so I wanted to check which
direction you would want before touching it.
Summary
On workos 10.0.0,
user_management.get_authorization_url_with_pkce(client_id=...)accepts theargument and ignores it. The URL it returns carries the client_id the
WorkOSClientwasconstructed with.
src/workos/user_management/_resource.py:2331takes it and documents it:Then at
:2374it callsget_authorization_url(...)without passing it, and that method has noclient_idparameter to pass it to. It reads the constructor value instead, at:2986:The async copy at
:4690is identical.Two methods in the same hand-written block do honour the same argument:
authenticate_with_code_pkceis the other half of the PKCE pair. So a caller who passes aper-tenant client_id to both gets an authorization URL for one application and a token exchange
for another.
Reproduction
workos 10.0.0, Python 3.14.0, fresh venv. Constructor client_id is
client_CONSTRUCTOR, theoverride passed on every call is
client_OVERRIDE.base_urlpoints at a loopback origin thatrecords the request body, so the token exchange row is read off the wire. Nothing leaves the
machine.
get_authorization_url_with_pkce(client_id=OVERRIDE)get_authorization_url_with_pkce()no overrideget_jwks_url(client_id=OVERRIDE)authenticate_with_code_pkce(client_id=OVERRIDE)AsyncWorkOSClientsame callThe second row is there so the first one means something. Without it, "the override is ignored"
would be indistinguishable from "the URL has no client_id at all". Rows three and four are the two
siblings that do the right thing.
Suggested fix
Either forward it, by giving
get_authorization_urlan optionalclient_idthat falls back toself._client.client_id, or drop the parameter fromget_authorization_url_with_pkceso the signature stops promising something it cannot do. Thefirst matches what
authenticate_with_code_pkcealready does, so the pair would agree.This is a smaller problem than it looks at first glance. It only affects callers who set client_id
per request rather than on the client, which is the multi-tenant case the argument exists for.
Happy to open a PR either way, though I noticed most of this file is generated and the PKCE
methods sit inside the
@oagen-ignoreblock at:2261to:2444, so I wanted to check whichdirection you would want before touching it.