Open API token lifecycle
cTrader's Open API allows jeden valid access token per cTrader ID (cID) na raz. Moment nowy token jest issued — scheduled refresh, lub re-authorization gdy user links inny account na same cID — poprzedni access token to invalidated. Copy engine running na remote node holds ten teraz-dead token, więc nowy token musi reach to bez dropping live connection.
Model
OpenApiAuthorizationto aggregate które holds cID's encrypted access + refresh tokens. Unique index na(UserId, CtidUserId)enforces dokładnie jeden authorization per cID per user.TokenVersion— monotonic counter bumped każdy raz token rotates (Refresh(), które także covers re-auth path gdy inny account to linked na same cID). To version marker dla single-valid-token rule i to co running host uses do detect change nawet jeśli dwa token strings happen do collide.- Tokens to encrypted na rest via
ISecretProtector(EncryptionPurposes.OpenApiAccessToken/OpenApiRefreshToken). Nigdy nie logged lub stored w plaintext.
Propagation (graceful in-place swap)
- Token rotates → nowy token + bumped
TokenVersionto persisted. CopyEngineSupervisorna hosting node re-reads plan każdy reconcile cycle i computes token signature (access tokens + versions). Change means rotation.- Zamiast tearing down host i restarting (które by drop master's execution stream), supervisor pushes nowy token do running host.
- Host re-authenticates affected account na existing socket
(
ProtoOAAccountAuthReqznowu) viaSwapAccessTokenAsync, wtedy light reconcile. Old token dies; copy stream nigdy nie stops.
To co makes cross-cID case safe: user adding drugi account z same cID mid-run invalidates old token, i running copy profile keeps going na nowy.
Refresh
OpenApiTokenRefreshService (background) proactively refreshes authorizations przed expiry;
OpenApiAuthorization.IsExpiring(threshold, now) gates to. cTrader rotates refresh token
na każdy refresh, więc nowy refresh token to persisted immediately; read-only cache które nie może
persist by self-invalidate (relevant do in-cluster test Job, które mounts writable copy
z secret).
Failure escalation
Failed refresh to nie silent. OpenApiAuthorization.MarkRefreshFailed(reason, now, criticalWindow)
records RefreshFailedAt, increments ConsecutiveRefreshFailures, i zawsze raises
AccessTokenRefreshFailed (warning). Gdy token to teraz within App:OpenApi:TokenRefreshCriticalWindow
(default 6h) z expiry i refresh to wciąż failing, to escalates once z
AccessTokenRefreshCritical domain event + Critical log więc owner może re-authorize przed
copy/prop-firm operations lose token. Failure counter i escalation latch reset na next
successful Refresh. Service keeps retrying każdy TokenRefreshInterval, więc provider/maintenance
outage self-heals gdy refresh endpoint returns.
Invalidation alert & auto-recovery (M1)
Partial/again-authorization na cID invalidates token running copy host wciąż holds. Gdy
trading call rejects z OpenApiErrorKind.TokenInvalid, host raises distinct
CopyTokenInvalidated alert (log 1078) — nie generic failure — więc notification channel knows
token needs attention. Recovery to automatic: supervisor re-reads authorization każdy cycle i,
gdy refreshed token changes token signature, pushes to do running host dla in-place
swap — copying resumes z no manual re-add. NotLinkable profile (token/auth temporarily
unresolvable) jest likewise re-evaluated każdy supervisor cycle i hosted moment jego plan builds znowu.
Host liveness watchdog (M2)
Supervisor watches każdy hosted profile's run task. Jeśli host exits lub faults gdy jego profile to
wciąż assigned do ten node, watchdog cancels i restarts to next cycle (log
CopyHostRestarted), więc wedged host self-heals zamiast needing manual restart — i jeden profile's
failure nigdy nie stalls others (per-profile isolation).
Testy
- Unit —
TokenVersionbumps naRefresh; host performs in-place swap bez restart; cross-cID invalidation swaps source i destination tokens; invalidated destination token raisesCopyTokenInvalidatedi auto-recovers na next token push (M1); watchdogIsHostDeaddecision restarts completed/faulted host i leaves reassigned profile alone (M2). - Integracja —
TokenVersionpersists + increments przez EF na real Postgres; token signature changes na version bump nawet jeśli string to unchanged.