OIDC Auth Paradox: "Identity Not Found" during login, but "Duplicate" when creating it

Hi everyone,

I’m stuck in a loop trying to set up Ziti Desktop Edge for Windows (ZDEW) with Microsoft Entra ID (Azure AD) using an External JWT Signer and the Network JWT method.

Summary / Paradox

  • During login (ZDEW Authorize): controller rejects auth with identity not found by externalId.

  • During manual creation: if I try to create the identity with that externalId, the controller rejects it as a duplicate (so it already exists).

So it looks like the identity exists in the DB, but the external JWT auth path can’t find it.

Evidence

A) Controller log during ZDEW login

After successful Microsoft login, controller logs:

authMethod="ext-jwt" msg="identity not found by externalId" claim_id="user@company.tld"
authMethod="ext-jwt" msg="failed to validate candidate JWT at index 0" error="INVALID_AUTH: The authentication request failed"

B) CLI says the externalId already exists

When trying to create the identity manually:

ziti edge create identity user "user-azure" --external-id "user@company.tld" ...

I get something like:

duplicate value 'user@company.tld' in unique index on identities store

Environment / Setup Notes

  • ZDEW external provider flow uses the Network JWT (my Network JWT payload shows em: "red" → not an OTT token).

  • Controller + ZAC are running in Docker.

  • I’m aware of the Docker advertised address issue (clients must reach the advertised controller hostname). ZDEW is configured to reach the controller at <controller-fqdn-or-ip>:1280.

External JWT Signer

issuer: https://login.microsoftonline.com/<tenant-id>/v2.0
jwksEndpoint: https://login.microsoftonline.com/<tenant-id>/discovery/v2.0/keys
clientId: <client-id-guid>
audience: <client-id-guid>
claimsProperty: email
useExternalId: true
targetToken: ID
scopes: [ api://<client-id-guid>/openziti, email, offline_access, openid, profile ]

Token

Decoded ID token contains:

  • iss = https://login.microsoftonline.com/<tenant-id>/v2.0 (matches signer)

  • aud = <client-id-guid> (matches signer) (Please confirm if Ziti strips 'api://' or requires exact string match)

  • email = user@company.tld (matches the identity externalId)

Questions

  1. In OpenZiti, can INVALID_AUTH be thrown for another reason (issuer/aud/signature) but still surface as “identity not found by externalId”? Or is that message strictly from the identity lookup step?

  2. Could this happen if ZDEW is authenticating against a different controller instance/advertised address than the one where the identity exists (so CLI shows the identity in one DB but ZDEW hits another)?

  3. Is there a recommended way to confirm which externalId the controller actually tries to map (e.g., log level / specific debug flags)?

Any pointers to break this loop would be appreciated. I can share additional sanitized logs (including timestamps and src.remote) if needed.

Thanks!

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

I'm going to guess you discovered auth policies and chose an auth policy as well for this user? Please reset the auth policy to the default policy if that's the case and try again. It's easy to get that wrong.

Yes invalid auth can be thrown for A TON of reasons. Most common is the issuer not actually matching. believe it or not, the traling slash on the audience is a constant frustration. For example if your token has an issuer of "https://keycloak.zrok.clint.demo.openziti.org:8446/" but you leave the slash off the end: "https://keycloak.zrok.clint.demo.openziti.org:8446" you'll get an error.

No, ziti won't change the audience nor strip the api://. In the docs we specifially mention api:// being necessary. (I wrote those docs fwiw). That's used when sending the request to entra to make sure you get the right token back.

The controller logs do indicate if the external id is being mapped and fails:

Jan 27 17:39:27 ip-172-31-47-200 ziti[3885570]: {"authMethod":"ext-jwt","authenticatorId":"","externalId":"clint.dovholuk@netfoundry.io","file":"github.com/openziti/ziti/controller/model/authenticator_mod_cert.go:536","func":"github.com/openziti/ziti/controller/model.getAuthPolicyByExternalId","level":"error","msg":"identity not found by externalId","time":"2026-01-27T17:39:27.421Z"}

My bet is the auth policy for now.