PHP SDK guide

Authentication

Authentication is applied at the transport boundary. Prefer separate control-plane and worker principals, rotate them through your secret manager, and never place tokens in workflow history.

One development token

$client = new Client('http://localhost:8080', token: 'dev-token');

This is convenient for a local self-hosted Server. It gives control and worker requests the same bearer token and should not become the default production role model.

Separate client and worker credentials

$controlClient = new Client(
    getenv('DURABLE_WORKFLOW_RUNTIME_URL'),
    namespace: getenv('DURABLE_WORKFLOW_NAMESPACE') ?: 'default',
    controlToken: getenv('DURABLE_WORKFLOW_CLIENT_TOKEN') ?: null,
);

$workerClient = new Client(
    getenv('DURABLE_WORKFLOW_RUNTIME_URL'),
    namespace: getenv('DURABLE_WORKFLOW_NAMESPACE') ?: 'default',
    workerToken: getenv('DURABLE_WORKFLOW_WORKER_TOKEN') ?: null,
);

Control-plane operations use the control token; worker registration, heartbeats, polls, and task completion use the worker token. The fallback rules keep a deliberately shared token usable, but explicit role tokens make least privilege review clearer.

Static credential validation

PHP SDK 3.18.0 validates each supplied static credential in its original form, before choosing a role or falling back to a shared token. This applies to Client and TokenAuthentication constructor options token, controlToken and workerToken, framework ServiceConfiguration credential settings, and Laravel's process-environment and explicit process-token handoff. An invalid unused role also fails validation; supplying a valid token for another role does not hide it.

Static credential strings refuse:

This deliberately rejects malformed UTF-8 that older releases could accept. Valid text is preserved byte for byte: the SDK does not trim, case-fold, normalize Unicode or rewrite internal spaces. Text validation alone does not establish that a runtime or HTTP intermediary accepts an arbitrary opaque credential. Obtain a correctly formed replacement from the credential issuer instead of trimming an invalid credential to make it pass.

null and the literal empty string retain their existing optional fallback behavior. TokenAuthentication still requires at least one nonempty token; an anonymous Client remains available where it was already supported. An empty scoped token can fall back to a valid shared token, but a malformed nonempty scoped token is an error.

The bridge's existing shared-versus-scoped mode rule still applies. ServiceConfiguration::fromArray() treats literal-empty optional settings as absence. Its direct constructor retains the exact supplied properties, including empty strings, and does not normalize them before its mode check. Use null or omission for an absent constructor argument; an empty shared argument combined with scoped arguments still conflicts with that mode rule.

Errors identify only the setting, for example Invalid static credential setting DURABLE_WORKFLOW_WORKER_TOKEN. They do not include the rejected value. Keep that boundary in application error handling and diagnostics too.

For Laravel, keep credentials out of cached configuration and inject the required role at process startup. The resolver validates shared, client, worker and explicit process-handoff text before selection, including values belonging to an unused role. For Symfony, the shared configuration path applies the same policy before constructing either role's client. Review all supplied settings before deploying, rather than checking only the selected token.

Custom dynamic Authentication implementations retain their existing contract. The static-token policy does not change their signing or refresh behavior.

Static headers for an authenticated gateway

StaticHeadersAuthentication adds the same fixed header set to both roles. Use it only when a trusted gateway owns authentication and the fixed headers contain no per-request signature:

use DurableWorkflow\Auth\StaticHeadersAuthentication;

$authentication = new StaticHeadersAuthentication([
    'X-Internal-Service' => 'orders-worker',
]);

For rotating or request-signed authentication, implement Authentication::headers(bool $workerRequest) or provide a transport that owns the signing protocol. Do not construct signatures from inside workflow code.

Keep secrets out of durable state

Workflow input, activity input/result, signal arguments, update arguments, memo, and search attributes may be retained or exposed to operators. Pass a secret reference to an activity and resolve it at execution time instead of recording the credential itself.

Rotation checklist

Browser boundary

Do not expose a Server or Cloud bearer token to browser JavaScript. Call the SDK from a trusted PHP backend and authorize the end user there.