JWT Signer in ziti 2.0

Hey guys,

I experimented with "Add Identity by URL" about a year ago and came back to it now. I noticed there are now some new Auto Enrollment options in ZAC under JWT Signers, but I haven't been able to find much documentation describing how they work.

Based on the description, my understanding is that with Auto Enrollment enabled, I shouldn't need to manually create an identity in ZAC first. Instead, when a user authenticates via the JWT signer, the controller should automatically create the identity and enroll it.

Is that correct?

I wasn't able to get this working. When I tried the auto-enrollment approach, the controller logs showed:

identity not found by externalId

However, when I followed the "old" workflow:

  1. Create the identity manually in ZAC
  2. Set the identity's External ID to match the Keycloak user's email
  3. Add identity with URL and login with the flow

...everything worked as expected.

This makes me wonder if I'm misunderstanding how Auto Enrollment is supposed to work, or if I'm missing some required JWT signer configuration.

For reference, the ZAC UI describes Auto Enrollment as:

  • Auto-Enroll to Cert: Automatically creates an identity and enrolls it with a certificate when a user authenticates via JWT.
  • Auto-Enroll to Token: Automatically creates an identity and enrolls it with an authentication token when a user authenticates via JWT.

Is there any documentation, example configuration, or working JWT signer setup that demonstrates Auto Enrollment end-to-end?

Also, there is no way to close the description that opens when you click for more information which sucks when you just made a new signer and can't save it as the save button is covered as shown below D:

Thanks!

Controller: 2.0.4 ZAC: 4.5.1

Hi there @montwepa, welcome back to "Add Identity by URL" and now auto enrollment! :slight_smile:

It would be helpful to get an export of how you configured it or exact steps to reproduce. My guess is that you have enabled one of the toggles but chosen the opposite and the behavior didn't go as planned. Me and my favorite LLM setup a test and verified that most of the expected cases pass...

# Signer flags (ZAC) ZDEW radio Verdict / Result
1 token only none shown :white_check_mark: works: identity created, token auth
2 cert only Device certificate :white_check_mark: works: identity created with cert
3 cert + token User session (default) :white_check_mark: works: identity created, token auth
4 cert + token Device certificate :white_check_mark: works: identity created with cert
5 cert only User session (default) :cross_mark: ZDEW bug: auth failed, identity not found by externalId
6 none none shown :information_source: this is the normal operation :slight_smile:

The only problem I ran into was when I would do something like "cert only" for the signer while choosing "user session" in the ZDEW. I don't expect that to work since it's an invalid option but the ZDEW allows you to choose that invalid option. I'll file an enhancement for this so that you can't get that option.

Can you maybe show me how you created your signer? Or maybe share logs? I hate the "works on my machine" -- but it did :frowning: (mostly)

Hi @TheLumberjack!

Thanks for the reply!

Auto-enrollment sounds really cool, and I'd love to get it working.

The signer and the overall URL setup were based mainly on the video you made about a year ago here.

Signer:

{
  "_links": {
    "self": {
      "href": "./external-jwt-signers/jcqDYEdTSG7GCE6Gp8b8G"
    }
  },
  "createdAt": "2026-09-30T09:49:42.622Z",
  "id": "jcqDYEdTSG7GCE6Gp8b8G",
  "tags": {},
  "updatedAt": "2026-09-30T10:16:17.017Z",
  "audience": "openziti",
  "certPem": null,
  "claimsProperty": "email",
  "clientId": "openziti-client",
  "commonName": "",
  "enabled": true,
  "enrollAuthPolicyId": "default",
  "enrollNameClaimsSelector": "/sub",
  "enrollToCertEnabled": true,
  "externalAuthUrl": "https://auth.keycloak.com/realms/example",
  "fingerprint": null,
  "issuer": "https://auth.keycloak.com/realms/example",
  "jwksEndpoint": "https://auth.keycloak.com/realms/example/protocol/openid-connect/certs",
  "kid": "",
  "name": "Keycloak",
  "notAfter": "0001-01-01T00:00:00.000Z",
  "notBefore": "0001-01-01T00:00:00.000Z",
  "scopes": [
    "email"
  ],
  "targetToken": "ACCESS",
  "useExternalId": true
}

I tried it again just now and switched to token only, and it worked...

Previously, I had only the cert option enabled and was trying to use the user session flow, which is what I actually want. It looks like that's the only combination that currently has the bug :sweat_smile:

It's great to have it working now, and thanks for the help!

I also have a question about the new ZDEW radio option. Is there a way to enforce only one of the two flows? The user session flow is what I'm after, and it seems that setting the signer to token only achieves that. Am I understanding that correctly?

Yes this is the 'bug' I outlined that doesn't work, indeed! :slight_smile: The ZDEW UI on this one should not have even prompted you for the two radio buttons and this is the bug. You should have gotten a new identity with a cert straight up.

If you'd gotten that, you might have wondered why you never had to authenticate with your IdP. Then you'd have found the Auto-Enroll to Token option on the signer and maybe switched to that.

Is there a way to enforce only one of the two flows?

There is today for your case: token only forces the user session flow. Cert only should force device certificate, but the bug shows the radio buttons anyway. After the fix, you'll only see that choice when both options are checked on the signer.

Look for that in a release soon. Cheers

[mild edits for accuracy - first draft was misleading]