Pending Security Tokens
This page documents the pending asset security token subsystem: what it is, why it exists, available strategies, configuration options, and usage examples.
What is a pending security token?
A pending security token is a short-lived ownership proof associated with a pending asset. It helps protect access to pending files and ensures that only the actor that created the pending asset can access, confirm, or convert the pending asset into a permanent asset.
Tokens are intentionally short-lived (configurable TTL) and are compared using a timing-safe comparison to avoid leaking information via timing attacks. The built-in session, cookie, and owner strategies store an HMAC digest on the pending asset, not the raw owner secret.
Why use security tokens?
- Prevents accidental or malicious access to pending files by ID alone.
- Makes it safe to expose pending IDs to clients, since ownership is validated before the pending asset can be read or converted.
- Supports multiple storage strategies (session, cookie, headers, signed URLs, database) depending on your application's architecture.
Configuration (Asset config)
The Asset configuration exposes a pendingSecurityToken option where you can set the concrete class used to manage tokens:
\Maniaba\AssetConnect\Pending\PendingSecurityToken\SessionPendingSecurityToken(default) — stores a per-pending secret in the active session and stores only its HMAC digest in pending metadata.CookiePendingSecurityToken— stores a per-pending secret in an HTTP-only SameSite cookie and stores only its HMAC digest in pending metadata.OwnerPendingSecurityToken— derives the HMAC digest from the current owner resolved byPendingOwnerResolverInterface; use this for API/JWT flows.null— disables security token validation; pending assets will not be protected by tokens.
Example (default config):
// src/Config/Asset.php
public ?string $pendingSecurityToken = \Maniaba\AssetConnect\Pending\PendingSecurityToken\SessionPendingSecurityToken::class;
The HMAC strategies use Config\Asset::$pendingSecurityKey. If it is null, they use Config\Encryption::$key. In production, configure a stable secret key.
public ?string $pendingSecurityKey = null;
Use .env to override it in a normal CodeIgniter 4 application:
asset.pendingSecurityKey = your-random-secret
Set pendingSecurityToken to null only when pending IDs are never exposed to untrusted clients.
Available interfaces and base class
Maniaba\AssetConnect\Pending\Interfaces\PendingSecurityTokenInterface— interface that any token strategy must implement. Public methods:generateToken(string $pendingId): string— generate the pending asset security value for the given pending ID. Built-in HMAC strategies return the digest stored in pending metadata.retrieveToken(string $pendingId): ?string— retrieve the token for the given pending ID from the chosen strategy (session, cookie, header, etc.).validateToken(PendingAsset $pendingAsset, ?string $tokenProvided = null): bool— validate the pending asset against the provider's current security context.-
deleteToken(string $pendingId): void— remove stored token data for cleanup. -
Maniaba\AssetConnect\Pending\Interfaces\PendingOwnerResolverInterface— resolves the current owner for stateless API/JWT flows. Return a stable value such as the JWTsub, or a stronger value likesub:jtiorsub:device_idwhen pending assets must be tied to a specific token/device. -
Maniaba\AssetConnect\Pending\PendingSecurityToken\AbstractPendingSecurityToken— provides common behavior: - Validates constructor parameters: positive TTL and token length between 1 and 64 bytes.
randomStringToken()— usesrandom_bytes()andbin2hex()to build a cryptographically random token. May throw randomness-related exceptions.validateToken()— default validation usesretrieveToken()when the token is not passed explicitly and compares usinghash_equals().-
Requires concrete classes to implement
initialize()where service wiring (for example, session) is performed. -
Maniaba\AssetConnect\Pending\PendingSecurityToken\AbstractHmacPendingSecurityToken— base for built-in ownership-bound strategies: - Builds a SHA-256 HMAC from
pendingId + owner proof. - Ignores explicit client-provided token values during validation.
- Requires the current session/cookie/owner resolver to produce the owner proof again.
Default implementation: SessionPendingSecurityToken
Namespace: Maniaba\AssetConnect\Pending\PendingSecurityToken\SessionPendingSecurityToken
Behavior and notes:
- Stores a random per-pending secret in CodeIgniter session tempdata with a key derived from the pending ID.
- Constructor parameters: token TTL (seconds) and token byte length.
generateToken($pendingId)— generates a session secret, stores it in tempdata (with TTL), and returns the HMAC digest that is stored in pending metadata.retrieveToken($pendingId)— reads the per-pending session secret and returns it ornull.deleteToken($pendingId)— removes tempdata for cleanup.initialize()loads thesessionservice and throwsInvalidArgumentExceptionif the session service is not available.
This strategy is appropriate when you control the user's browser session (typical web app). The client only needs to submit the pending ID; validation succeeds only when the same server-side session still contains the per-pending secret.
API/JWT implementation: OwnerPendingSecurityToken
Namespace: Maniaba\AssetConnect\Pending\PendingSecurityToken\OwnerPendingSecurityToken
Use this strategy when there is no browser session and the current actor is authenticated through an API token such as JWT.
use Maniaba\AssetConnect\Pending\Interfaces\PendingOwnerResolverInterface;
final class JwtPendingOwnerResolver implements PendingOwnerResolverInterface
{
public function resolveOwnerId(): ?string
{
$claims = service('auth')->jwtClaims();
return $claims->sub; // Or "{$claims->sub}:{$claims->jti}" for token/device binding.
}
}
public ?string $pendingSecurityToken = \Maniaba\AssetConnect\Pending\PendingSecurityToken\OwnerPendingSecurityToken::class;
public string|\Maniaba\AssetConnect\Pending\Interfaces\PendingOwnerResolverInterface|null $pendingOwnerResolver = JwtPendingOwnerResolver::class;
For a normal API flow, the client still sends only pending_id on form submit. OwnerPendingSecurityToken recomputes the HMAC from the current owner and rejects the pending asset if another owner attempts to consume the same ID.
Other strategies
- Cookie-backed tokens: store the token in an HTTP-only cookie scoped for the upload/confirm routes. This is useful for stateless endpoints when you still want the browser to carry the token.
- Header-based tokens: the client stores the token (for example in a JS variable) and includes it in a custom header when confirming the pending asset.
- Signed URLs / DB storage: for advanced scenarios you can implement a strategy that persists tokens in a database table or issues short-lived signed URLs.
To use a different strategy, implement PendingSecurityTokenInterface and change Config\Asset::$pendingSecurityToken to your class.
Usage examples
Important: tokens are generated automatically when you persist a pending asset through PendingAsset::store() (which delegates to PendingAssetManager::store()). The manager will call the configured token provider's generateToken($pendingId) and set the returned token on the PendingAsset instance ($pending->security_token). You typically do not need to call generateToken() yourself.
1) Upload flow — store pending asset and return only the pending ID:
use Maniaba\AssetConnect\Pending\PendingAsset;
$result = PendingAsset::createFromRequest('file');
$pending = $result['file'][0];
// Persist pending asset. PendingAssetManager::store() will generate a token
$pending->store();
return $this->response->setJSON([
'pending_id' => $pending->id,
]);
2) Token validation when confirming/adding a pending asset
Token validation is performed by PendingAssetManager::fetchById(string $id, ?string $token = null): ?PendingAsset.
If a token provider is configured, fetchById() will call the provider's validateToken() internally. When validation fails (or asset is missing/expired) fetchById() returns null.
For built-in HMAC strategies, validation normally uses the current session/cookie/owner resolver. The client does not need a separate token:
use Maniaba\AssetConnect\Pending\PendingAssetManager;
$manager = PendingAssetManager::make();
$pending = $manager->fetchById($pendingId);
if ($pending === null) {
// Asset not found, expired, or token invalid
throw new \RuntimeException('Pending asset not found or invalid security token.');
}
// proceed to add asset from pending
Notes:
- If
Config\\Asset::$pendingSecurityTokenisnull, token generation and validation are disabled andfetchById()behaves as a normal read (subject to expiry checks). SessionPendingSecurityToken,CookiePendingSecurityToken, andOwnerPendingSecurityTokenignore explicit client-provided token values and validate against the current server-side owner context.addAssetFromPending($pendingId)consumes the pending ID, deletes the pending file and provider token, then stores the real asset from a temporary source file.
3) Cleaning up tokens
After you have confirmed and consumed a pending asset, you may call deleteToken($pendingId) to remove any stored token material from the provider (session, cookie, DB, ...):
$tokener->deleteToken($pending->id);
Notes:
- If
Config\Asset::$pendingSecurityTokenisnull, the manager will not generate a token and token validation will be skipped. - If you need to manually generate tokens for special flows, you can call the provider's
generateToken()directly — but be aware you must also persist/return the token to the client in your flow.
Constructor options and robustness
AbstractPendingSecurityToken constructor accepts two parameters:
$tokenTTLSeconds(int) — how long the token persists in the chosen strategy. Must be > 0.$tokenLength(int) — number of random bytes used to generate the token (converted to hex), must be between 1 and 64.
The constructor will throw an InvalidArgumentException if parameters are invalid.
randomStringToken() may throw randomness-related exceptions if the system cannot generate secure random bytes.
Security considerations
- Always use
hash_equals()(the abstract implementation does) when comparing tokens to avoid timing attacks. - Keep TTL small for sensitive workflows.
- If storing tokens in cookies, use HttpOnly, Secure, SameSite attributes and consider rotating tokens.
- Avoid exposing tokens in URLs unless they are single-use and short-lived.