Skip to content

OneAPI client constructs invalid OAuth and API base URLs for GOV and GOV-US clouds #526

@mmoulton-zscaler

Description

@mmoulton-zscaler

OneAPI client constructs invalid OAuth and API base URLs for GOV and GOV-US clouds

Description

The ZscalerClient (OneAPI client) fails entirely when cloud is set to gov or govus. Two methods produce incorrect URLs for these clouds:

  1. OAuth._get_auth_url — appends .zslogin{cloud}.net to the vanity domain regardless of cloud type. For government clouds, the identity provider uses .zidentitygov.net (GOV) and .zidentitygov.us (GOV-US), not the standard .zslogin.net family.

  2. RequestExecutor.get_base_url — constructs https://api.{cloud}.zsapi.net for non-production clouds. The GOV OneAPI gateway is at https://api.zscalergov.net and GOV-US is at https://api.zscalergov.us; neither matches the .zsapi.net pattern.

The result is an immediate SSLError / ConnectionError on every API call because the constructed hostnames do not exist.


Reproduction

from zscaler import ZscalerClient

# GOV cloud — fails immediately on any API call
client = ZscalerClient({
    "clientId": "<your-gov-client-id>",
    "clientSecret": "<your-gov-client-secret>",
    "vanityDomain": "zsgovlab-net",      # short-form prefix (without .zidentitygov.net)
    "cloud": "gov",
    "customerId": "<your-zpa-customer-id>",
})

rules, response, error = client.zpa.policies.list_rules("access")
# Error: SSLError connecting to zsgovlab-net.zslogingov.net (does not exist)

# GOV-US cloud — same failure
client_us = ZscalerClient({
    "clientId": "<your-govus-client-id>",
    "clientSecret": "<your-govus-client-secret>",
    "vanityDomain": "zsgovlab-us",       # short-form prefix (without .zidentitygov.us)
    "cloud": "govus",
    "customerId": "<your-zpa-customer-id>",
})

rules, response, error = client_us.zpa.policies.list_rules("access")
# Error: SSLError connecting to zsgovlab-us.zslogingovus.net (does not exist)

Exact URLs the SDK constructs vs. what they should be:

Cloud Component SDK produces (broken) Correct URL
gov Auth token endpoint https://zsgovlab-net.zslogingov.net/oauth2/v1/token https://zsgovlab-net.zidentitygov.net/oauth2/v1/token
gov API base URL https://api.gov.zsapi.net https://api.zscalergov.net
govus Auth token endpoint https://zsgovlab-us.zslogingovus.net/oauth2/v1/token https://zsgovlab-us.zidentitygov.us/oauth2/v1/token
govus API base URL https://api.govus.zsapi.net https://api.zscalergov.us

Both broken hostnames (zslogingov.net, zslogingovus.net, api.gov.zsapi.net, api.govus.zsapi.net) do not resolve in DNS.


Expected behavior

ZscalerClient with cloud="gov" or cloud="govus" should authenticate against the correct government identity provider and route API calls to the correct government OneAPI gateway:

  • GOV — Auth: https://{vanityDomain}.zidentitygov.net/oauth2/v1/token, API: https://api.zscalergov.net
  • GOV-US — Auth: https://{vanityDomain}.zidentitygov.us/oauth2/v1/token, API: https://api.zscalergov.us

Is it a regression?

Unknown. The gov and govus cloud values exist in the legacy ZPA client (LegacyZPAClientHelper) but appear never to have been implemented in the OneAPI (ZscalerClient) path. This is a missing feature that manifests as a broken URL rather than a clear error.


Debug Logs

With ZSCALER_SDK_LOG=true and ZSCALER_SDK_VERBOSE=true, the following error is emitted immediately on the first API call:

ERROR:root:Failed to get access token: HTTPSConnectionPool(host='zsgovlab-net.zslogingov.net', port=443):
Max retries exceeded with url: /oauth2/v1/token
(Caused by SSLError(SSLError(1, '[SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure (_ssl.c:1129)')))

For GOV-US:

ERROR:root:Failed to get access token: HTTPSConnectionPool(host='zsgovlab-us.zslogingovus.net', port=443):
Max retries exceeded with url: /oauth2/v1/token
(Caused by NewConnectionError('Failed to establish a new connection: [Errno 8] nodename nor servname provided'))

Proposed Fix

File: zscaler/oneapi_oauth_client.pyOAuth._get_auth_url

# Current (broken)
def _get_auth_url(self, vanity_domain: str, cloud: str) -> str:
    if cloud == "production":
        return f"https://{vanity_domain}.zslogin.net/oauth2/v1/token"
    else:
        return f"https://{vanity_domain}.zslogin{cloud}.net/oauth2/v1/token"

# Fixed
def _get_auth_url(self, vanity_domain: str, cloud: str) -> str:
    if cloud == "production":
        return f"https://{vanity_domain}.zslogin.net/oauth2/v1/token"
    elif cloud == "gov":
        return f"https://{vanity_domain}.zidentitygov.net/oauth2/v1/token"
    elif cloud == "govus":
        return f"https://{vanity_domain}.zidentitygov.us/oauth2/v1/token"
    else:
        return f"https://{vanity_domain}.zslogin{cloud}.net/oauth2/v1/token"

File: zscaler/request_executor.pyRequestExecutor.get_base_url

# Current (broken) — the relevant branch
if self.cloud and self.cloud != "production":
    return f"https://api.{self.cloud}.zsapi.net"

# Fixed — add gov/govus cases before the generic branch
if self.cloud == "gov":
    return "https://api.zscalergov.net"
elif self.cloud == "govus":
    return "https://api.zscalergov.us"
elif self.cloud and self.cloud != "production":
    return f"https://api.{self.cloud}.zsapi.net"

The same gov/govus guard is also needed in the zidentity and zins branches of get_base_url for completeness:

def get_base_url(self, endpoint: str) -> str:
    GOV_BASE_URLS = {
        "gov":   "https://api.zscalergov.net",
        "govus": "https://api.zscalergov.us",
    }

    if "/zscsb" in endpoint:
        return f"https://csbapi.{self.sandbox_cloud}.net"

    if "/zidentity" in endpoint or "/admin/api/v1" in endpoint:
        if not self.vanity_domain:
            raise ValueError("vanityDomain is required for zidentity service")
        if self.cloud in GOV_BASE_URLS:
            # GOV identity admin uses the vanity domain directly (no zslogin suffix)
            return f"https://{self.vanity_domain}-admin/admin/api/v1"
        if self.cloud and self.cloud != "production":
            return f"https://{self.vanity_domain}-admin.zslogin{self.cloud}.net/admin/api/v1"
        return f"https://{self.vanity_domain}-admin.zslogin.net/admin/api/v1"

    if self.cloud in GOV_BASE_URLS:
        return GOV_BASE_URLS[self.cloud]

    if "/zins" in endpoint:
        if self.cloud and self.cloud != "production":
            return f"https://api.{self.cloud}.zsapi.net"
        return self.BASE_URL

    if self.cloud and self.cloud != "production":
        return f"https://api.{self.cloud}.zsapi.net"

    return self.BASE_URL

Other Information

  • OS: macOS 14 (Darwin 24.6.0)
  • Python: 3.9
  • SDK Version: 1.9.16 (latest)
  • Clouds affected: gov (Zscaler GOV — api.zscalergov.net) and govus (Zscaler GOV-US — api.zscalergov.us)
  • Clouds unaffected: production and all other standard clouds (.zsapi.net family)

Additional Context

Government cloud tenants use a separate identity provider (zidentitygov.net / zidentitygov.us) rather than the standard zslogin.net provider. The vanity domain for these tenants is in the form {org-name}.zidentitygov.{tld}, not {org-name}.zslogin.net.

Workaround (until fixed): monkey-patch both methods at import time before constructing ZscalerClient.

from zscaler.oneapi_oauth_client import OAuth
from zscaler.request_executor import RequestExecutor

_orig_auth = OAuth._get_auth_url
def _patched_auth(self, vanity_domain, cloud):
    if cloud == "gov":
        return f"https://{vanity_domain}.zidentitygov.net/oauth2/v1/token"
    if cloud == "govus":
        return f"https://{vanity_domain}.zidentitygov.us/oauth2/v1/token"
    return _orig_auth(self, vanity_domain, cloud)
OAuth._get_auth_url = _patched_auth

_orig_base = RequestExecutor.get_base_url
def _patched_base(self, endpoint):
    cloud = getattr(self, "cloud", "")
    if cloud == "gov":
        return "https://api.zscalergov.net"
    if cloud == "govus":
        return "https://api.zscalergov.us"
    return _orig_base(self, endpoint)
RequestExecutor.get_base_url = _patched_base

Metadata

Metadata

Assignees

No fields configured for Feature.

Projects

Status
⚙️ In development

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions