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
| Rule | Setting | Refusal |
|---|---|---|
| a model that holds tokens | not_tokenable | |
| for another tokenable, a Gate ability that lets the signed-in user manage its tokens | tokenable_ability | not_allowed |
| a password confirmed within the timeout | minting.password_timeout, or auth.password_timeout | password_not_confirmed |
| at most this many tokens that have not expired | minting.max_tokens_per_user | limit_reached |
| at most this many mints a minute | minting.per_minute | too_fast |
| one mint at a time for a user | too_fast | |
| a lifetime from the list, never longer than the longest | minting.lifetimes, minting.max_lifetime | lifetime |
| no token without an expiry unless allowed | minting.allow_no_expiry | lifetime |
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.