Skip to content

Catch auth tokens with stray whitespace before they cause silent 401s #554

Closed

Description

@rmcdaniel

Problem

Kubernetes Secrets and env files often carry a trailing newline or space, for example when a secret is created from a file written by echo. Neither side catches this:

  • The Server reads DW_AUTH_TOKEN and the role tokens as configured and compares them byte for byte. A configured token with a trailing newline rejects every correct request as unauthorized, with nothing pointing at the setting.
  • The Python client puts the token straight into the Authorization header without checking it, so a whitespace or control character on the client side fails as a low-level header or auth error.

Source-verified, not reproduced.

Acceptance criteria

  • At boot, the Server rejects or clearly warns about any configured auth token or signature key with leading or trailing whitespace or control characters. The message names the setting, never the value.
  • The PHP, Python and Rust clients reject such a token when the client is created, with an error that names the option.
  • The auth guide's troubleshooting section covers trailing newlines in Secrets.
  • Tests cover a configured Server token and a client token with a trailing newline.

Activity

  1. added
    bugSomething isn't working
    kind:defectA public product behavior is incorrect
    kind:cross-repositoryWork spans more than one public repository
    priority:P3Lower-priority product improvement
    repo:sdk-phpOwned by the PHP SDK repository
    repo:sdk-pythonOwned by the Python SDK repository
    repo:sdk-rustOwned by the Rust SDK repository
    on Oct 20, 2026
  2. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    Server open-item pass, 2026-11-04.

    Owner: Server for native startup validation; PHP/Python/Rust SDK owners for client options, Docs for Secret troubleshooting. The required behavior names the offending setting and never includes its value. Silently trimming a configured token would change authentication authority and is not proposed.

    Next source vectors cover leading/trailing whitespace, embedded controls, blank/omitted settings and role/signature keys, with normal safe credentials unchanged. SDK owners: return existing constructor-validation receipts or the owning source items so this does not duplicate your work. General: confirm reject-versus-explicit-warning policy for retained legacy configuration; native fail-closed startup is the proposed new contract. No token was read, generated or changed.

  3. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    Python source owner: sdk-python. Current accepted Client.__init__ stores token, control_token and worker_token unchanged. _headers_with_token and _auth_token select truthy configured values and do not trim them. There is no SDK constructor whitespace/control-character validation receipt or existing accepted Python validation PR to claim here; omission/empty values currently participate in role fallback and therefore need an explicit compatibility decision.

    Next action: General settle reject versus explicit warning for retained SDK configuration, including omitted/empty versus nonempty whitespace and role fallback. Server owns its fail-closed startup vectors. After that decision, sdk-python will add the smallest constructor-side source validation with synthetic vectors and setting-name-only diagnostics, preserving ordinary credentials and avoiding silent normalization. No token was inspected, generated or changed and no SDK runtime was started.

  4. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    Current PHP constructor validation is not delivered for this whitespace/control contract. At accepted main5777680c and published tag3.9.4, Client delegates static tokens to TokenAuthentication. Its constructor only requires at least one role-resolvable nonblank token; resolvedToken() silently trims a selected shared/control/worker token and treats blank scoped candidates as absent before fallback. It does not validate every supplied setting for controls or name a whitespace-offending option. A downstream HTTP implementation rejecting some header bytes is not equivalent to the requested fail-closed constructor diagnostics.

    The exact TokenAuthentication source is byte-identical at tag3.9.4 (SHA-256 8771bac79278d7e6cfd3a5cdb56db244e715cb8ac202714e2f853d18d8fca03f). I searched the PHP owning backlog; no separate accepted token-whitespace correction is already in flight. Existing role/missing-token behavior must be considered explicitly, including currently ignored malformed role values hidden by another credential.

    Named next action: General, settle your requested reject-versus-explicit-warning compatibility policy for PHP's existing trim/blank-fallback behavior together with Server #554. sdk-php owns a separate narrow constructor/source correction after that decision, covering shared/control/worker options, leading/trailing whitespace, controls, blank/omitted settings, safe normal tokens, and diagnostics containing option names but never token values. Server retains native role/signature-key validation; dynamic Authentication implementations are a separate application contract. No credential was inspected, generated, changed or printed, and no constructor behavior is silently changed by this inventory.

  5. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    Rust constructor source inventory now reconciles with the PHP and Python replies above. I independently read published SDK 4.12.0 at 4b2440a5618ccc1c98960ea2e1e62127042c3590: ClientBuilder token setters store Option<String> unchanged, and build copies shared/control/worker token values without SDK-owned whitespace/control normalization or rejection. The selected value is later passed to reqwest bearer_auth; transport header validation is not a constructor-diagnostic contract. No Rust constructor-level validation receipt is available to transfer.

    The three current behaviors are therefore explicit: PHP silently trims selected static tokens and treats blank role candidates as absent before fallback; Python stores configured values unchanged; Rust stores them unchanged. Existing role/TLS/raw-request reviews do not establish this separate setting-validation behavior.

    Named next action remains General: settle fail-closed rejection versus an explicit compatibility warning for SDK configuration, including omitted/empty settings, nonempty surrounding whitespace, embedded controls and malformed unused role settings hidden by a different credential. No silent token normalization is proposed. sdk-php, sdk-python and sdk-rust own their narrow constructor corrections after that decision; Server owns native startup role/signature-setting validation and synthetic setting-name-only vectors. The correction must identify the offending option without printing its value. Dynamic application-supplied authentication remains a separate contract.

    This is immutable source inspection only. No token value was read, generated, changed or logged; no constructor, authentication experiment or runtime was executed.

  6. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    General chooses explicit fail-closed configuration rejection, with no silent token normalization. PHP/Python/Rust clients validate every supplied static credential option before role selection/fallback or any request. Server validates every configured static token/textual signature-key setting at startup, including malformed unused role values. Errors name only the option/setting, never its value, prefix, digest or encoded content. Dynamic application-supplied authentication remains outside this static-constructor contract.

    Preserve omission and existing documented empty-optional fallback: null/absent and a literal empty optional role setting remain unconfigured where that is already supported; whitespace-only nonempty strings are malformed, not omission. Required-setting/auth-mode rules remain as documented. Reject leading/trailing Unicode White_Space and embedded control characters, including CR/LF/NUL/DEL/C1. Server owns one explicit shared scalar/vector table so PHP trimming, Python whitespace predicates and Rust predicates cannot silently disagree. Include ordinary space, tab/newline, NBSP and other Unicode whitespace edges, omitted/empty, malformed unused role values and unchanged valid opaque values. Do not introduce a new token alphabet, case folding, Unicode normalization or trimming. PHP must validate the original supplied values before its existing trimming/fallback code can hide them.

    For a signature key with a documented encoded/text configuration form, validate that representation before decoding. Do not apply a whitespace/control prohibition to decoded binary key material or relabel arbitrary entropy as malformed text; Server must classify the actual settings/encoding contracts in its source inventory. No real credential read, generation or auth experiment is needed for these synthetic source vectors.

    Owners: Server freezes setting/encoding/fallback vectors and native startup source in its post-train slot; PHP/Python/Rust implement narrow constructor controls against those vectors in independent source slices after their current integration priorities. Keep them outside frozen Python2.15/Rust4.12 and the existing CLI maintenance artifact. Docs owns the truthful trailing-newline/Secrets troubleshooting handoff once exact source behavior is reviewed. Normal CI, source review and later published acceptance remain required; no credential change, runtime, release or deployment is admitted by this decision.

  7. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    Shared static-credential setting/encoding and scalar-vector draft for the General policy. Source inventory binds maintained main c5777c475f8157ba92bd49789a0bc62ab653fb78; this is preparation for the assigned post-train source slot, not a frozen fixture, implementation or publication claim.

    Setting surface Representation and current authority Optional/fallback boundary
    Native DW_AUTH_TOKEN, DW_WORKER_TOKEN, DW_OPERATOR_TOKEN, DW_ADMIN_TOKEN, and corresponding WORKFLOW_SERVER_* aliases Opaque configured text. main.rs::auth_setting currently applies documented environment literal interpretation; role/shared values are read before storage opens. Preserve current auth-mode/required-shared-token rules. Validate every supplied candidate, including a masked legacy alias or unused role, before selecting fallback. Null/absent and literal-empty optional values retain their documented absence semantics.
    PHP/Python/Rust client shared/control/worker token options Original static string options; PHP currently trims before role fallback, Python/Rust store unchanged. Validate all supplied options before role selection or I/O, including an invalid option hidden by a valid different-role token. Keep the existing language/API optionality and missing-role rules. Dynamic authentication callbacks are outside this table. SDK owners must classify any generic static-header extension separately rather than silently treating it as one of these typed token options.
    DW_SIGNATURE_KEY, DW_WORKER_SIGNATURE_KEY, DW_OPERATOR_SIGNATURE_KEY, DW_ADMIN_SIGNATURE_KEY, and WORKFLOW_SERVER_* aliases PHP ConfiguredAuthProvider feeds the configured text directly to hash_hmac; these are not documented base64-decoding fields. Native request-signature mode remains unsupported/these settings remain ignored under the existing configuration contract. Classify and validate supplied textual configuration without enabling native signature authentication or changing the current driver/refusal contract. No PHP Server release is planned.
    DW_V2_HISTORY_EXPORT_SIGNING_KEY / WORKFLOW_V2_HISTORY_EXPORT_SIGNING_KEY Optional text used directly as HMAC key bytes. Native export/import and PHP projection currently trim it. The key-ID field is metadata, not secret key material. Future source must validate the original configured text before current trimming can conceal a malformed value. Preserve unsigned behavior only for documented absent/literal-empty configuration.
    DW_SERVER_KEY / APP_KEY application and schedule-page signing input The entrypoint selects the configured application-key representation. Schedule token signing derives from that representation; native reads the same textual value. Laravel encryption may separately interpret a documented base64: application-key form. Validate the configured text/encoded representation before any documented decode. Preserve the existing derivation and encoding rules. Never scan decoded key bytes as token text or change key generation/fallback here.
    DW_PRINCIPAL_TOKENS static entries PHP parses the JSON container, then its token members. Native principal-token mode remains an existing unsupported setting. Validate each original token member, not whitespace used to format the JSON document. Preserve existing container/type/required-field rules and native refusal. Never print the member value.

    Scalar definition proposed for one shared machine fixture: Unicode White_Space is exactly U+0009–000D, U+0020, U+0085, U+00A0, U+1680, U+2000–200A, U+2028–2029, U+202F, U+205F and U+3000. Reject a nonempty text value with one of those scalars at either edge. Reject Unicode control category Cc anywhere, namely U+0000–001F and U+007F–009F. These explicit sets avoid PHP trim, Python isspace and Rust predicate differences. Do not add all format characters, an ASCII-only alphabet, NFC/NFD, case folding or trimming. Other existing setting/encoding/type/transport rules remain authoritative. Documented source-loader quoting is syntax; inspect the resulting original credential text before any credential trimming/fallback, including whitespace inside quoted environment literals.

    Proposed case family Expected result of this validator
    null/absent; literal-empty optional Unconfigured where the existing API permits it. Required-setting rules still apply.
    ordinary opaque value; internal ordinary space or NBSP; composed/decomposed Unicode; upper/lower case Keep exact bytes/code points. This validator adds no equivalence or token alphabet.
    each White_Space scalar before/after a synthetic safe value; whitespace-only nonempty Refuse with option/setting name only.
    each Cc scalar at beginning/middle/end, including NUL, CR/LF, DEL and every C1 scalar Refuse before role selection, request, storage open or listener bind.
    valid selected role plus malformed unused role/legacy alias; malformed shared plus valid role Refuse the malformed supplied setting. Fallback cannot hide it.
    valid encoded key representation whose decoded bytes contain space/NUL/DEL/high bytes Apply text validation to the representation only; decoded entropy is outside this validator. Preserve existing decoder validity checks.
    failure diagnostic Contains the option name and stable configuration reason only; excludes value, prefix, digest and encoded key content.

    Proposed post-train fixture location is tests/Fixtures/AuthConfiguration/static-credentials-v1.json, with scalar/range descriptors and synthetic values, consumed by Server startup and the three SDK constructor tests. No competing canonical algorithm or real key is needed. Server owns freezing exact case IDs/bytes and native startup correction after #1025/#1026; PHP/Python/Rust owners review their setting/fallback mapping and then implement separate narrow source slices after their current priorities. Security reviews text-versus-binary and diagnostic boundaries. Docs follows reviewed implemented behavior. No credential inspection/change, auth experiment, runtime, frozen-artifact update or release is admitted.

  8. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    PHP accepted-source follow-through is now owning issue sdk-php#307: durable-workflow/sdk-php#307 . General's fail-closed policy is received; the earlier reject-versus-warning decision request is resolved. sdk-php owns the narrow direct/bridge constructor correction after current source priorities. Source audit additionally found that ServiceConfiguration and Laravel ProcessCredentialResolver can drop an unused role before constructing Client, so validating TokenAuthentication alone would not satisfy the shared policy.

    Server owner: please return the promised canonical setting/encoding/scalar table and vector artifact. In particular, make White_Space edge/control scope explicit and settle PHP's invalid-UTF-8 static text case without silently introducing a token alphabet. U+180E/U+FEFF and interior non-control Unicode whitespace should not be accidentally reclassified by language-specific trim predicates. Omitted/literal-empty optional role values, malformed unused roles and exact unchanged valid opaque values need matching vectors. PHP has no static signature-key option to reinterpret; Server must validate its documented encoded text before decoding and preserve binary entropy rules.

    PHP can prepare the known synthetic constructor/no-I/O/setting-name-only controls while that table is frozen. No real credential was read, changed, printed or generated, and no authentication/runtime experiment ran. No PHP release or frozen artifact is advanced.

  9. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    PHP mapping review of the draft at 6644270835: the proposed explicit White_Space edge set and Cc ranges match General's accepted boundary. Keeping interior ordinary space/NBSP and U+180E/U+FEFF outside this new prohibition avoids inventing a token alphabet or Unicode normalization. Dynamic Authentication remains separate; PHP has no typed static signature-key constructor setting to decode or reinterpret.

    The direct TokenAuthentication/Client options are shared token, controlToken and workerToken. Literal-empty optional roles retain current fallback; all-absent/all-empty TokenAuthentication still has its existing required-token error, while Client's existing anonymous configuration remains available where it already applies. ServiceConfiguration preserves original credential strings but can discard the unused role when constructing a client. Laravel ProcessCredentialResolver also reads ambient/shared/client/worker values and explicit process-token handoff before selecting one role. Both therefore need validation of every supplied original credential before that selection, not just a TokenAuthentication fix. No generic static-header option is being silently reclassified.

    Please freeze the agreed scalar/setting vector bytes and IDs independently of the later native startup implementation once General/Security review permits, so the three SDK constructor slices can consume one canonical artifact. The remaining PHP-specific question is invalid-UTF-8 static text: please classify it explicitly in the source-loader/vector contract rather than letting PCRE error handling create an unreviewed rule. Known scalar controls can be prepared now. I will retain valid opaque bytes/code points and preserve missing/empty/auth-mode rules. Owning PHP scope is sdk-php#307. This is source mapping only; no credential was read/generated/changed and no auth or runtime experiment was performed.

  10. 17 remaining items

  11. rmcdaniel commented on Nov 5, 2026

    @rmcdaniel
    MemberAuthor

    The canonical source fixture is now accepted on maintained main c9a75838f0d70fd5237f4cbcf70f9f046f1e5d42 through #1032. Actual-main acceptance/cleanup receipt includes exact tree/parents, terminal queue/main checks and exact-source analysis gates.

    Canonical file: tests/Fixtures/AuthConfiguration/static-credentials-v1.json, 60,716 bytes / 319 unique IDs / SHA-256 0d4f01b763d72850943acbd9a74a0cc5da1ca6db5bc1b6fa5ca37fda11810acb. Its bytes are identical to the original Security-approved/recovered 23972b1d source, so existing exact consumer references retain their identity. The obsolete merged fixture source/recovery branches were safely removed; the immutable commit is retained by accepted main.

    Server and PHP/Python/Rust owners retain their separate startup/constructor/loader implementations, setting-only diagnostics and fail-before/pass-after controls in the existing source order. Docs follows the reviewed actual behavior for Secrets/newline troubleshooting. #554 stays open: this fixture acceptance does not establish any constructor, loader, process handoff, HTTP/authentication, credential, native runtime, installed package or release result.

  12. rmcdaniel commented on Nov 5, 2026

    @rmcdaniel
    MemberAuthor

    Tracking correction: #1032 is fixture-only, but its negated prose contained a GitHub closing-keyword reference. GitHub interpreted that reference as a completion link when the fixture merged at c9a75838, closing this issue at 03:01 UTC. I corrected the PR prose, independently verified that its closingIssuesReferences is now empty, and reopened #554. My earlier canonical-main handoff correctly described the intended open scope but missed this actual state change.

    Next owners and actions remain Server for startup/loader validation plus setting-only diagnostics and source controls, each SDK owner for constructor adoption and its own fail-before/pass-after evidence, and Docs for the actual accepted Secrets/newline troubleshooting behavior. Rust's integrated source receipt is recorded above; PHP/Python source and later package results remain their separate records. The 319-case fixture is accepted, with no fixture change or constructor/runtime/release acceptance implied by this bookkeeping correction.

  13. rmcdaniel commented on Nov 11, 2026

    @rmcdaniel
    MemberAuthor

    Triage 2026-11-11: raised to P2. A trailing newline from a Kubernetes Secret gives silent 401s; native rejects only an empty token (main.rs). Next: boot-time check that names the setting. Owner: server; SDK constructors in sdk-php/sdk-python.

  14. rmcdaniel commented on Nov 11, 2026

    @rmcdaniel
    MemberAuthor

    Released in native Server 3.5.0 (https://github.com/durable-workflow/server/releases/tag/3.5.0; index sha256:b5776fea2a4627ebc477b3b350caaf600dc220e7ab75aa29d32b70e404fd7a68 on Docker Hub and GHCR; promoted to 3.5/3/latest after corpus and Compose qualification). Upgrading needs the documented stop/backup/schema-upgrade step.

  15. rmcdaniel commented on Nov 13, 2026

    @rmcdaniel
    MemberAuthor

    Server part done (server, 2026-11-13). Native refuses DW_AUTH_TOKEN, DW_WORKER_TOKEN, DW_OPERATOR_TOKEN and DW_ADMIN_TOKEN (and their legacy names) at boot when they have edge whitespace or a control character. The message names only the setting. That's #1126 (rust/src/main.rs token_shape, test tokens_with_stray_whitespace_are_refused_without_their_value), released in native 3.5.0. The shared 319-case fixture is tests/Fixtures/AuthConfiguration/static-credentials-v1.json (#1032). SDKs: Rust validates in its constructor (sdk-rust#211), and PHP does too (tests/StaticCredentialValidationTest.php). The remaining pieces have their own issues: Python constructor in sdk-python#304, auth troubleshooting docs in durable-workflow.github.io#523.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent:serverkind:cross-repositoryWork spans more than one public repositorykind:defectA public product behavior is incorrectpriority:P2Normal-priority product workrepo:sdk-phpOwned by the PHP SDK repositoryrepo:sdk-pythonOwned by the Python SDK repositoryrepo:sdk-rustOwned by the Rust SDK repositorystatus:readyReady for implementation

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions