Skip to content

Secondary ext-jwt auth query: SDK obtains external IdP token but never presents it to controller (infinite loop) #927

@Izdeliye60

Description

@Izdeliye60

Summary

When using secondary-req-ext-jwt-signer in an auth policy with an external Keycloak IdP, the Desktop Edge SDK successfully:

  1. Authenticates via cert (primary) ✅
  2. Detects auth query for secondary ext-jwt ✅
  3. Shows "Authorize IdP" button ✅
  4. Opens browser → user logs into Keycloak ✅
  5. Receives Keycloak access token ✅
  6. Shows "Successfully authenticated with external provider" in browser ✅

But then the SDK never sends the token to the controller. Instead, it re-initializes the ext-oidc client and loops back to step 2. The controller receives zero requests from the client after the token is obtained.

Environment

Component Version
Ziti Controller v1.6.12
Ziti Edge Router v1.6.12
Desktop Edge (Windows) 2.9.3.0
ziti-edge-tunnel service v1.9.9+ (bundled)
ziti-sdk-c v0.40.9 (bundled)
External IdP Keycloak ~25.0.6
OS Windows 11

Configuration

ext-jwt-signer

{
  "name": "keycloak-xxx",
  "issuer": "https://auth.example.com/realms/MyRealm",
  "audience": "OpenZiti",
  "claimsProperty": "email",
  "useExternalId": true,
  "clientId": "OpenZiti",
  "scopes": ["openid", "email"],
  "externalAuthUrl": "https://auth.example.com/realms/MyRealm",
  "enabled": true
}

Auth Policy (secondary required)

{
  "name": "keycloak-required",
  "primary": {
    "cert": { "allowed": true },
    "extJwt": { "allowed": true, "allowedSigners": ["<signer-id>"] }
  },
  "secondary": {
    "requireExtJwtSigner": "<signer-id>"
  }
}

Identity

  • Type: Default
  • Has enrolled cert ✅
  • Has externalId set to user's email ✅
  • Auth policy: keycloak-required

Keycloak Client

  • Public client (no client secret)
  • Standard flow + Direct access grants enabled
  • Token contains correct iss, aud ("OpenZiti"), email claims ✅
  • Valid redirect URIs: http://localhost/*

Controller Config

  • edge-oidc API binding enabled
  • OIDC discovery endpoint responding ✅

What Happens (SDK service log)

Step 1: Cert auth → partially authenticated ✅

INFO  ziti.c:2076 version_pre_auth_cb() ztx[1] using OIDC authentication method
INFO  oidc.c:88 oidc_client_init() oidc[internal] initializing with provider[https://controller:1280/oidc]
WARN  ziti_tunnel_ctrl.c:1018 ziti_ctx controller connections failed: api session is partially authenticated, waiting for auth query resolution

Step 2: ext auth event → "Authorize IdP" shown ✅

INFO  ext_oidc.c:141 ext_oidc_client_init() oidc[keycloak-xxx] initializing with provider[https://auth.example.com/realms/MyRealm]
INFO  ziti_tunnel_ctrl.c:1139 ext auth event received
INFO  ziti-edge-tunnel.c:804 ext auth: login_with_ext_signer

Step 3: User clicks authorize → Keycloak login → token received ✅

INFO  external_auth.c:85 received link request: https://auth.example.com/realms/MyRealm/protocol/openid-connect/auth?client_id=OpenZiti&scope=openid%20email%20openid&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A20314%2Fauth%2Fcallback&...
INFO  ext_oidc.c:324 request_token() requesting token path[https://auth.example.com/realms/MyRealm/protocol/openid-connect/token] auth[...]

Browser shows: "Successfully authenticated with external provider. You may close this page."

Step 4: SDK LOOPS instead of presenting token to controller ❌

INFO  ext_oidc.c:141 ext_oidc_client_init() oidc[keycloak-xxx] initializing with provider[https://auth.example.com/realms/MyRealm]
INFO  ziti_tunnel_ctrl.c:1139 ext auth event received
INFO  ziti-edge-tunnel.c:804 ext auth: login_with_ext_signer

Time between token received and loop: ~0.6 seconds.
Controller log during this window: ZERO requests from client.

What I Verified

Direct ext-jwt auth via REST API — WORKS ✅

KC_TOKEN=$(curl -s -X POST "https://auth.example.com/realms/MyRealm/protocol/openid-connect/token" \
  -d "grant_type=password" -d "client_id=OpenZiti" \
  -d "username=testuser" -d "password=***" \
  -d "scope=openid email" | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")

curl -s -X POST "https://controller:1280/edge/client/v1/authenticate?method=ext-jwt" \
  -H "Authorization: Bearer $KC_TOKEN" -H "Content-Type: application/json" -d '{}'
# Result: ✅ 200 OK — session created, identity resolved via externalId

This proves the token is valid and the controller can authenticate with it.

Auth query present in API session ✅

GET /edge/client/v1/current-api-session
{
  "authQueries": [
    {
      "typeId": "EXT-JWT",
      "provider": "url",
      "httpUrl": "https://auth.example.com/realms/MyRealm",
      "clientId": "OpenZiti",
      "scopes": ["email", "openid"],
      "id": "<signer-id>"
    }
  ]
}

Primary ext-jwt (cert=false) also fails ❌

When auth policy has primary.cert.allowed=false, SDK still sends cert first → rejected:

ERROR authenticator_mod_cert.go:290 "invalid certificate authentication, not allowed by auth policy"

SDK never falls through to ext-jwt primary auth.

Expected Behavior

After SDK receives external IdP token (step 3), it should:

  1. Present the token to the controller to resolve the secondary auth query
  2. Controller validates JWT against ext-jwt-signer config
  3. Auth query resolved → session fully authenticated
  4. Services become available

Actual Behavior

After SDK receives external IdP token, it:

  1. Does NOT send token to controller (controller receives zero requests)
  2. Re-initializes ext_oidc_client
  3. Fires new ext auth event
  4. Shows "Authorize IdP" again → infinite loop

Questions

  1. Is there a known API endpoint for resolving secondary ext-jwt auth queries? I could not find one in the Swagger spec.
  2. Is secondary-req-ext-jwt-signer with external IdPs supported in v1.6.12, or is this a planned feature?
  3. Should this be filed against ziti-sdk-c or desktop-edge-win?

Workaround

Using auth policy with primary.cert.allowed=true + primary.extJwt.allowed=true + NO secondary requirement. Identity authenticates via cert only. Keycloak integration works at REST API level but not through the tunnel client.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions