Skip to main content

Minting

A taken-over session could otherwise issue itself a dozen long-lived tokens in seconds, and they would outlive the password change that ends the session. PersonalTokens mints for the signed-in user of a request, under these rules:

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Pushery\SanctumHardening\Tokens\PersonalTokens;

Route::post('/settings/tokens', function (Request $request, PersonalTokens $tokens) {
$minted = $tokens->mint($request, $request->string('name')->toString(), ['orders:read'], 90);

// The plain text exists once, in this answer.
return response()->json(['token' => $minted->plainTextToken], 201);
})->middleware(['auth', 'password.confirm']);

The rules of a mint​

RuleSettingRefusal
a model that holds tokensnot_tokenable
for another tokenable, a Gate ability that lets the signed-in user manage its tokenstokenable_abilitynot_allowed
a password confirmed within the timeoutminting.password_timeout, or auth.password_timeoutpassword_not_confirmed
at most this many tokens that have not expiredminting.max_tokens_per_userlimit_reached
at most this many mints a minuteminting.per_minutetoo_fast
one mint at a time for a usertoo_fast
a lifetime from the list, never longer than the longestminting.lifetimes, minting.max_lifetimelifetime
no token without an expiry unless allowedminting.allow_no_expirylifetime
a name of 1 to 255 characters, here and in mintForSystem()name

Each refusal is a MintRefused exception with its reason, and retryAfter or limit where they apply. A mint that is refused costs the minute nothing. The lifetime is given in days: one of the configured ones, 0 for a token without an expiry where that is allowed, and null for the default: minting.default_lifetime where a token may be given it, otherwise the longest allowed lifetime below it, otherwise the shortest allowed one. PersonalTokens::lifetimes() and PersonalTokens::defaultLifetime() answer both for a screen of your own.

The password is read the way Laravel's password.confirm middleware records it, so the route that mints sits behind that middleware.

A limit of the application's own​

Where a plan decides how many tokens a user may hold, bind a resolver. The lower of its answer and minting.max_tokens_per_user counts. It is asked about the tokenable a token is minted for, the user or a workspace:

use Illuminate\Database\Eloquent\Model;
use Pushery\SanctumHardening\Tokens\ResolvesTokenLimit;

final class PlanTokenLimit implements ResolvesTokenLimit
{
public function limitFor(Model $tokenable): ?int
{
return $tokenable->getAttribute('plan') === 'free' ? 2 : null;
}
}
use App\Tokens\PlanTokenLimit;
use Pushery\SanctumHardening\Tokens\ResolvesTokenLimit;

$this->app->bind(ResolvesTokenLimit::class, PlanTokenLimit::class);

Tokens of a workspace​

A token can belong to another model than the signed-in user, a workspace for instance, so that it outlives the person who minted it. mintFor() mints one under the rules above, counted for that tokenable: its own limit, its own minute and its own lock. The password is the signed-in user's:

use App\Models\Workspace;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Pushery\SanctumHardening\Tokens\PersonalTokens;

Route::post('/workspaces/{workspace}/tokens', function (Request $request, Workspace $workspace, PersonalTokens $tokens) {
$minted = $tokens->mintFor($request, $workspace, $request->string('name')->toString(), ['orders:read'], 90);

return response()->json(['token' => $minted->plainTextToken], 201);
})->middleware(['auth', 'password.confirm']);

Who may do so is the application's call. The package asks the Gate for the ability manage-tokens with the workspace as its argument, and refuses with not_allowed where the application defines none. A policy of the model answers it in manageTokens(), or a definition of its own does:

use App\Models\User;
use App\Models\Workspace;
use Illuminate\Support\Facades\Gate;

Gate::define('manage-tokens', fn (User $user, Workspace $workspace): bool => $workspace->owner_id === $user->id);

Where manage-tokens already means something else in the application, tokenable_ability names another ability. mint($request, …) is mintFor() for the signed-in user, whose own tokens never need the Gate.

Tokens of the application's own code​

mintForSystem() mints without a password and outside the limits of a person, and it may carry a reserved ability:

use Pushery\SanctumHardening\Tokens\PersonalTokens;

$device = app(PersonalTokens::class)->mintForSystem($user, 'Phone', ['device:sync'], now()->addYear());

Listing and revoking​

listFor($request) returns the signed-in user's tokens, the newest first, without a token that carries a reserved ability. revoke($request, $tokenId) deletes one of them and answers whether it did. Both take a workspace as their last argument, listFor($request, $workspace) and revoke($request, $tokenId, $workspace), and ask the Gate the way mintFor() does: a user it does not allow sees no token and revokes none. To tell the tokens apart, TokenPrefixes::of($tokens) gives the first twelve characters of each token's secret, as they stand at the start of the value that was handed out. A token minted outside the package has none.