Summary
In the edge client POST /sessions (legacy) flow, the service-access JWT is signed with the session id before the durable session is created. When the create dedups to a pre-existing session for the same (api-session, type, service), the JWT keeps the freshly generated id while the stored record uses the existing id. The client is left holding a token whose jti matches no stored session, so every subsequent create-circuit / create-terminator fails with invalid session.
Impact
Legacy-authenticated SDK clients (authenticated before OIDC was available and still on their legacy api-session) can enter a permanent invalid session retry storm. Reproduced when expanding a single-node controller into a multi-node HA cluster: the cluster churn triggers a session re-create while the old durable record still exists, the dedup fires, and the client's dial/bind traffic stops working. Affects both dial (create.circuit) and bind (create.terminator).
Root cause
controller/internal/routes/session_router.go Create:
MapCreateSessionToModel assigns a fresh cuid as entity.Id.
CreateJwt signs the JWT with jti = entity.Id.
Session.Create runs getExistingSessionEntity; on a dedup hit it overwrites entity.Id with the existing session's id and returns without persisting a record under the new id.
Result: jti (new id) != persisted session id (existing id). loadFromBolt -> Session.Read(jti) -> not found -> invalid session.
Fix
Run Session.Create (which resolves the id via dedup or persists) before CreateJwt, so the token is always signed with the id of the durable record. Preserves delete-as-revoke semantics (still a real durable session; no bypass).
Affected
- main and release-v2.0.x (2.0.x line).
Summary
In the edge client
POST /sessions(legacy) flow, the service-access JWT is signed with the session id before the durable session is created. When the create dedups to a pre-existing session for the same(api-session, type, service), the JWT keeps the freshly generated id while the stored record uses the existing id. The client is left holding a token whosejtimatches no stored session, so every subsequentcreate-circuit/create-terminatorfails withinvalid session.Impact
Legacy-authenticated SDK clients (authenticated before OIDC was available and still on their legacy api-session) can enter a permanent
invalid sessionretry storm. Reproduced when expanding a single-node controller into a multi-node HA cluster: the cluster churn triggers a session re-create while the old durable record still exists, the dedup fires, and the client's dial/bind traffic stops working. Affects both dial (create.circuit) and bind (create.terminator).Root cause
controller/internal/routes/session_router.goCreate:MapCreateSessionToModelassigns a fresh cuid asentity.Id.CreateJwtsigns the JWT withjti = entity.Id.Session.CreaterunsgetExistingSessionEntity; on a dedup hit it overwritesentity.Idwith the existing session's id and returns without persisting a record under the new id.Result:
jti(new id) != persisted session id (existing id).loadFromBolt->Session.Read(jti)-> not found ->invalid session.Fix
Run
Session.Create(which resolves the id via dedup or persists) beforeCreateJwt, so the token is always signed with the id of the durable record. Preserves delete-as-revoke semantics (still a real durable session; no bypass).Affected