Keycloak (IdP) as Secondary Auth

Hi guys,

I'm working on setting up a layered authentication scheme and I've run into a roadblock. I'm hoping someone can point out what I'm missing. I followed this documentation

My Goal:
I want to configure something like this:

  1. A device/user enrolls and connects to the network using a certificate

  2. Then the user must perform a secondary authentication against our IdP, which is Keycloak.

My understanding is that this should be possible? If I'm mistaken please correct me.

My Setup

I'm using a normal ziti setup with one controller, one router and ZDEW as the tunneler. The integration with Keycloak itself seems to be correct. My External JWT Signer is configured as follows and seems to work (more on that below)

External JWT Signer (keycloak2) (Changed Url's)

{
  "name": "keycloak2",
  "audience": "openziti",
  "issuer": "https://keycloakserver.com/realms/openziti-beta",
  "clientId": "openziti-client",
  "claimsProperty": "email",
  "enabled": true,
  "useExternalId": true,
  "kid": "",
  "externalAuthUrl": "https://keycloakserver.com/realms/openziti-beta",
  "scopes": [
    "email"
  ],
  "tags": {},
  "jwksEndpoint": "https://keycloakserver.com/realms/openziti-beta/protocol/openid-connect/certs",
  "targetToken": "ACCESS",
  "id": "3FpwHdgPw0kuP4fKDeAX7c"
}

The Authentication Policy

I seem to be stuck between two different problems depending on how I configure the primary section of my Authentication Policy.

Policy #1: extJwt: false

With this policy, the tunneler enrolls and connects with its certificate successfully. The client is stable. However, when I click "authorize IdP" in the tunneler, the OIDC flow completes, but the Ziti controller rejects the authentication with the following error.

Sep 26 10:02:12 ip-172-31-28-70 ziti[645448]: {"authMethod":"ext-jwt","file":"github.com/openziti/ziti/controller/model/authenticator_mod_ext_jwt.go:422","func":"github.com/openziti/ziti/controller/model.(*AuthModuleExtJwt).process","level":"error","msg":"encountered 1 candidate JWTs and all failed to validate for primary authentication, see the following log messages","time":"2025-09-26T10:02:12.224Z"}
Sep 26 10:02:12 ip-172-31-28-70 ziti[645448]: {"authMethod":"ext-jwt","authPolicyId":"4QWq74tBKgpVwUHb4HmGj9","error":"primary external jwt processing failed on authentication policy [4QWq74tBKgpVwUHb4HmGj9]: primary external jwt authentication on auth policy is disabled","expectedAudience":"openziti","extJwtSignerId":"3FpwHdgPw0kuP4fKDeAX7c","file":"github.com/openziti/ziti/controller/model/authenticator_mod_ext_jwt.go:88","func":"github.com/openziti/ziti/controller/model.(*candidateResult).LogResult","identityId":"FUOTKM5YDc","issuer":"https://keycloakserver.com/realms/openziti-beta","level":"error","msg":"failed to validate candidate JWT at index 0","time":"2025-09-26T10:02:12.224Z","tokenAudiences":"openziti"}

Policy Config:

{
  "name": "keycloak2",
  "primary": {
    "cert": {
      "allowExpiredCerts": false,
      "allowed": true
    },
    "extJwt": {
      "allowed": false,
      "allowedSigners": []
    },
    "updb": {
      "allowed": false,
      "lockoutDurationMinutes": 0,
      "maxAttempts": 5,
      "minPasswordLength": 5,
      "requireMixedCase": false,
      "requireNumberChar": false,
      "requireSpecialChar": false
    }
  },
  "secondary": {
    "requireExtJwtSigner": "3FpwHdgPw0kuP4fKDeAX7c",
    "requireTotp": false
  },
  "tags": {}
}

Policy #2: extJwt: true (Causes Tunneler to Freeze)

Based on the error above, I tried enabling extJwt as a primary method. While this seems like it should work, it immediately causes the Ziti Desktop Tunneler to freeze when I click "authorize IdP" in the tunneler. The controller logs are spammed with the error below until I close ZDEW.

Resulting Controller Error (repeating constantly):

Sep 26 10:00:41 ip-172-31-28-70 ziti[645448]: {"authMethod":"ext-jwt","file":"github.com/openziti/ziti/controller/model/authenticator_mod_ext_jwt.go:382","func":"github.com/openziti/ziti/controller/model.(*AuthModuleExtJwt).process","level":"error","msg":"encountered 0 candidate JWTs, verification cannot occur","time":"2025-09-26T10:00:41.902Z"}
Sep 26 10:00:41 ip-172-31-28-70 ziti[645448]: {"authMethod":"ext-jwt","file":"github.com/openziti/ziti/controller/model/authenticator_mod_ext_jwt.go:382","func":"github.com/openziti/ziti/controller/model.(*AuthModuleExtJwt).process","level":"error","msg":"encountered 0 candidate JWTs, verification cannot occur","time":"2025-09-26T10:00:41.923Z"}

What I've Verified

To ensure my Keycloak and JWT Signer settings are correct, I ran the following command: ziti ops verify ext-jwt-signer oidc keycloak2 --authenticate --controller-url ....

This command completes successfully when I have Ext JWT enabled as a Primary Authentication method.

and ziti ops verify ext-jwt-signer oidc keycloak2 --controller-url seems to give the correct tokens.

My Question

What is the correct authPolicy configuration to achieve something like this:

  • Enroll an identity using a certificate.
  • Connect with the Ziti Tunneler (authenticating with the cert).
  • Click the "authorize IdP" button, log in to Keycloak, and become fully authorized.
  • Access services.

And should it work with the keycloak setup shown in this documentation?

Environment Details:

  • Ziti Controller Version: v1.6.8
  • ZDEW version: 2.7.2.1
  • IdP: Keycloak
1 Like

Hi @montwepa, welcome to the community and to OpenZiti!

What you did sounds like it should have worked to me, so I gave this a try myself today. I configured an Auth policy that used cert-based auth and required a jwt signer of my choosing. If interested here's how it looked in ZAC:

After enrolling the identity with the Ziti Desktop Edge for Windows (which runs the ziti-edge-tunnel as a service), I can see the UI challenge the user (me) to auth, however after successfully authenticating with the keycloak controller I get a couple of logs in the controller:

Sep 26 11:48:36 ip-172-31-47-200 ziti[869574]: {"authMethod":"ext-jwt","file":"/mnt/d/git/github/openziti/nf/ziti/controller/model/authenticator_mod_ext_jwt.go:400","func":"github.com/openziti/ziti/controller/model.(*AuthModuleExtJwt).process","level":"error","msg":"encountered 1 candidate JWTs and all failed to validate for secondary authentication, see the following log messages","time":"2025-09-26T11:48:36.567Z"}

Sep 26 11:48:36 ip-172-31-47-200 ziti[869574]: {"authMethod":"ext-jwt","authPolicyId":"default","error":"jwt mapped to identity [G2SwK23MK - ClintAWS], which does not match the current sessions identity [MzpMCgBVP - ClintCertAuth]","expectedAudience":"browzerBootstrapClient","extJwtSignerId":"7cBXRKi4QQAiSpEJJR1ysJ","file":"/mnt/d/git/github/openziti/nf/ziti/controller/model/authenticator_mod_ext_jwt.go:88","func":"github.com/openziti/ziti/controller/model.(*candidateResult).LogResult","issuer":"https://keycloak.zrok.clint.demo.openziti.org:8446/realms/zitirealm","level":"error","msg":"failed to validate candidate JWT at index 0","time":"2025-09-26T11:48:36.567Z","tokenAudiences":"browzerBootstrapClient"}

I'll have to chat with @andrew.martinez on this one as he understands this subsystem best. It sure seems like a bug to me but it's possible I also did something wrong. We'll have a look and get back to you.

Hey,

Thanks for the quick response and also for the welcome. Been lurking here for a while but haven't had to post anything :grinning_face_with_smiling_eyes:

Your configuration in ZAC matches mine. Hopefully it's just something we both messed up then :face_with_peeking_eye:

I filed Secondary Auth via ext-jwt-signer fails · Issue #919 · openziti/ziti-sdk-c · GitHub. I'm not sure when it'll get fixed as it is a prioritization question, but it's definitely something we will get fixed.