Download the PHP package idct/single-use-token-manager without Composer

On this page you can find all versions of the php package idct/single-use-token-manager. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package single-use-token-manager

Single Use Token Manager

CI codecov Mutation score PHP PSR-16 Latest version Downloads

Hand a user something they can redeem exactly once: a password reset link, an email confirmation, a one-off download, a payment callback that must not run twice. The library issues a token into a cache and takes it back out again, and the cache handles expiry, so there is no sweeping job to write and nothing to migrate.

Storage is any PSR-16 cache. When the cache also supports tagging, clearing tokens reaches the tokens and nothing else.

Installation

You also need a PSR-16 cache. Two that work well:

Getting started

example.php in the repository walks through the same ground in more detail and runs as it is:

How it works

A token is stored as one cache entry under the key TKN_ plus its identifier. The identifier is a UUID v4, built from random_bytes(). That matters more than it looks: a token is a bearer capability, so whoever holds the identifier can perform the action, and only a cryptographically secure source makes it unguessable. Time ordered versions are deliberately avoided, for reasons set out in Why the identifier is random. Expiry is the cache's job: a lifetime handed to createToken() goes straight through to the PSR-16 write, so an expired token simply is not there any more.

consumeToken() reads the entry and removes it in the same call. It returns null for a token that was never issued, one that has expired, one already redeemed, and one whose cache entry turned out to hold something else, so calling code only needs the single null check. How that behaves when several callers arrive together is covered in Concurrency.

The API

TokenService

Method What it does
createToken(string $type, mixed $payload = null, ?int $ttl = null, ?string $uid = null): TokenInterface Issues a token and stores it. Pass $uid only to make the token addressable; otherwise it gets an unguessable one. Throws InvalidArgumentException for an unusable type or identifier, and TokenStorageException when the cache refuses the write.
consumeToken(string $uid, bool $keepToken = false): ?TokenInterface Redeems a token, removing it unless $keepToken says otherwise. Returns null when there is no live token under that identifier.
clearAllTokens(): bool Drops every token. See Clearing tokens for what that costs on a cache without tagging.

The token type

A type is lowercase letters and digits, one to sixteen characters. That keeps it safe to put in a cache key or a URL without escaping. reset, verify2fa and invite are fine; Password Reset, pass_reset and an empty string are refused with an InvalidArgumentException before anything is written.

The payload

The payload is whatever the code redeeming the token will need. It travels through the cache, so it has to survive that cache's serialisation. Arrays and plain objects are safe; open resources and closures are not.

Checking a token without spending it

Pass keepToken: true to look a token up without redeeming it. This is for read-only checks, such as rendering a page that says the link is still valid:

Do not use it as a gate in front of a second, redeeming call:

The gap between the check and the spend is exactly where a second request gets in, and the work in the middle widens it. Spend the token first, then do the work:

Reporting a refused write

PSR-16 write methods report failure by returning false rather than by throwing. A service that ignored that would hand back a token the cache never stored, and the user would be sent something they could never redeem. createToken() turns the false into a TokenStorageException instead.

consumeToken() treats a refused delete the same way, raising a TokenRemovalException. A token the cache would not remove is still redeemable, so reporting the failure is better than telling the caller it was spent. The redeemed token is not returned in that case: losing one legitimate redemption beats leaving a token open to replay.

Why the identifier is random

A single-use token is a bearer capability: possession of the identifier is the whole authorisation. That puts it in the same category as a session cookie, not the same category as a database primary key, and it has to be unguessable.

Time ordered UUIDs are not. A v6 identifier carries a node that Symfony computes once per process and then repeats on every token that process issues, so tokens from one php-fpm worker all share their last twelve characters:

One leaked token gives that away for all the others, and the rest is mostly a clock. An attacker who can request a token for their own account learns the node and pins the time, leaving far too little to guess. A v7 identifier is no better here: Symfony seeds it once and then increments, so consecutive values sit next to each other.

RFC 9562 makes the same point in its security considerations: do not assume a UUID is hard to guess, and do not use one as a security capability unless it comes from a CSPRNG.

So the identifier is a v4, which Symfony builds from random_bytes(). The cost is the loss of time ordering, which only ever bought cache index locality, and that is worth very little for entries fetched by exact key that expire on their own.

Addressable tokens

Sometimes the request that redeems a token has nowhere to carry a random identifier. A mobile client posting back an account id and a six digit code the user copied out of an e-mail is the usual shape: the fields are fixed, none of them is 36 characters wide, and the client cannot be changed. The token still has to be findable.

Pass $uid to createToken() and the token is stored under it:

This is a deliberate trade, and it moves two duties onto you.

The identifier is no longer the secret. Deriving it from a user id — or an order number, or anything else an attacker can enumerate — means reaching the token proves nothing at all. Whatever actually authorises the action has to travel in the payload and be checked after the token comes back:

Compare that with the default: a random identifier is itself proof, so a non-null return is the whole check. Give the payload check the same care you would give a password comparison — hash_equals, and a rate limit on top, because a short code taken from an e-mail carries far less entropy than a v4.

Uniqueness becomes yours. Two tokens built with the same identifier are one cache entry, and the second silently replaces the first. That is often what you want, since it gives a flow one live token per user and lets a resend overwrite what came before — but it is worth being deliberate about rather than surprised by.

Identifiers are checked for the characters PSR-16 reserves ({}()/\@:) and for being empty, and rejected with InvalidArgumentException before anything reaches the cache. Everything else is yours to choose. The service namespace, if you set one, still applies, so two services sharing a pool cannot collide even on identical identifiers.

Concurrency

Whether a token really is single use depends on what the cache can do.

PSR-16 has no way to take a value. Reading is get() and removing is delete(), two calls with a gap between them, and every caller that arrives inside that gap reads a token that is still there. Eight processes redeeming one token simultaneously against a plain PSR-16 cache all succeed.

When the cache can read and remove in one operation, consumeToken() uses that instead and exactly one caller wins. Declare the capability by implementing IDCT\SingleUseTokenManager\Contract\AtomicCacheInterface:

The service detects the method rather than the interface, so any cache carrying a compatible take() is used as-is. Backing it is usually one command: Redis and Valkey have had GETDEL since 6.2, which phpredis exposes as getDel().

idct/php-rapid-cache-client ships take() from 1.1 onwards, so it gives you both halves at once: tag-scoped clearing and single use that holds under concurrency, with no wiring beyond constructing it.

tests/Concurrency/single-use-under-load.php demonstrates both halves. It lines eight processes up on one token and asserts that an atomic cache yields exactly one winner while a plain one does not, so neither this section nor the guarantee can drift without the build noticing:

If more than one request can present the same token at once, and both succeeding would matter, use a cache that can take atomically.

Clearing tokens

clearAllTokens() behaves differently depending on what the cache can do.

On a plain PSR-16 cache the only bulk operation available is clear(), which empties the entire pool. Anything else sharing that pool goes with the tokens. Give the token service its own cache pool if that matters.

On a cache that supports tagging every token is written under the tag TKN and clearing invalidates just that tag. Nothing else in the pool is touched.

The service works this out on its own, by checking whether the cache exposes setTagged() and clearByTag(). No configuration and no wiring:

To declare the capability on a cache of your own, implement IDCT\SingleUseTokenManager\Contract\TaggedCacheInterface. Its two methods have the same signatures as the ones on IDCT\Cache\CacheServiceInterface, so a cache satisfying one satisfies the other, and this package keeps idct/php-rapid-cache-client an optional dependency rather than a required one.

Sharing a cache between services

By default every token service writes keys as TKN_<uid> and tags them TKN. Two services sharing one pool would therefore share a key space, and clearing one would clear the other. Pass a namespace to keep them apart:

The namespace prefixes both the key and the tag. Leaving it out keeps the keys byte for byte as earlier versions wrote them, so this is safe to adopt on an existing deployment.

PSR-16 reserves the characters {}()/\@:, and a namespace containing any of them is refused at construction rather than left to fail later as an unreadable cache error.

Note that on a cache without tagging clearAllTokens() still empties the whole pool, because clear() is all PSR-16 offers. The namespace separates the two services' keys, not their blast radius. See Clearing tokens.

Validating an identifier from a request

TokenIdentifier is the request object an endpoint hydrates before redeeming. It carries Symfony Validator, Serializer and OpenAPI attributes, so the identifier is validated, named token in JSON, and documented in the API schema without the endpoint hand rolling any of it.

An empty identifier is rejected, and so is one made only of whitespace, which would otherwise slip through as a cache miss further down the line.

The OpenAPI attribute is inert unless you install zircote/swagger-php, which is why this package does not require it. PHP does not load an attribute class until something reflects over it, so the attribute costs nothing when the generator is absent. Install it when you want the schema:

Layout

Requirements

Testing

Every one of these runs on every CI job, across PHP 8.2 to 8.5 and against both the newest and the lowest allowed dependency resolution. Nothing is gated to a single job, so a failure that only shows up under one version or one resolution still fails the build.

The suite holds four lines and the build fails below any of them: 100% line coverage, 100% method coverage, a mutation score index of 100%, and PHPStan at level max over src, tests and example.php. There is no PHPStan baseline and no ignored error, so a new finding fails the build instead of being recorded and forgotten. Coverage is published to Codecov from each job, flagged by PHP version and dependency mode.

What the functional tests cover

The scenarios in features/service run unchanged against three different caches, because the service is meant to need nothing beyond PSR-16:

Suite Cache Tagging Needs a server
model none not applicable no
array_cache Psr16Cache over ArrayAdapter no no
redis_no_tags Psr16Cache over RedisAdapter no Redis on 6379
rapid_cache_tags RapidCacheClient yes Valkey on 6380

Run one at a time with ./vendor/bin/behat --suite=rapid_cache_tags. The containers behind the last two come from docker-compose.yml:

Changelog

CHANGELOG.md records what changed in each release. If you are coming from 1.x under the old GryfOSS namespace, it also carries the upgrade steps.

Licence

BSD 3-Clause. See LICENSE.


All versions of single-use-token-manager with dependencies

PHP Build Version
Package Version
Requires php Version ^8.2
psr/simple-cache Version ^3.0
symfony/serializer Version ^7.4 || ^8.0
symfony/uid Version ^7.4 || ^8.0
symfony/validator Version ^7.4 || ^8.0
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package idct/single-use-token-manager contains the following files

Loading the files please wait ...