skip to content

With Laravel Sanctum, what does $user->createToken() store, what does it return, and how are token abilities attached and checked?

level: middleimportance: should knowfreq 45%

answer

  1. shown once, stored hashed
  2. NewAccessToken->plainTextToken
  3. 'id|secret' string format
  4. abilities default ['*']
  5. tokenCan() exact-string match

basics

~20 s

createToken($name, $abilities = ['*'], $expiresAt = null) saves a personal_access_tokens row with a SHA-256 hash of the secret and JSON abilities, and returns a NewAccessToken whose plainTextToken is shown once. tokenCan() checks the current token's abilities.

solid answer

~40 s

`createToken()` comes from the `HasApiTokens` trait. It generates a secret (`token_prefix` + 40 random characters + a CRC32b checksum), stores `hash('sha256', $secret)` in the `token` column of `personal_access_tokens` together with the name, the abilities array (JSON, default `['*']`) and an optional `expires_at`, and returns a `NewAccessToken`. Its `plainTextToken` is `"{id}|{secret}"`; the database never holds it, so you show it once. On a request, the guard splits the Bearer value on `|`, loads the row by id and compares hashes with `hash_equals`. `$user->tokenCan('bookings:accept')` is true when the current token's abilities contain that exact string or `'*'`; there is no prefix wildcard. Revoke with `currentAccessToken()->delete()` or `tokens()->delete()`.

code

php · 21 lines
php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::post('/sitter/devices', function (Request $request) {
    $new = $request->user()->createToken(
        $request->input('device_name'),
        ['bookings:read', 'bookings:accept'],
        now()->addDays(90),
    );

    // e.g. "42|p8Qz..."; only the SHA-256 of the part after | is stored
    return ['token' => $new->plainTextToken];
})->middleware('auth:sanctum');

Route::delete('/sitter/devices/current', function (Request $request) {
    $request->user()->currentAccessToken()->delete();

    return response()->noContent();
})->middleware('auth:sanctum');

go deeper

for a junior

Know that createToken() returns a NewAccessToken, that plainTextToken is shown once, and that abilities are the second argument.

for a middle

Explain the id|secret format, the SHA-256 hash in the token column, the ['*'] default and tokenCan's exact-string match.

for a senior

Design per-device tokens with narrow abilities and expiry, and wire revocation into logout and password changes.

for a principal

Decide how abilities map to your authorization model so a token can only narrow, never widen, what its user may do.

## Where tokens live Sanctum keeps personal access tokens in one table, `personal_access_tokens`, published from its migration. The owning model is polymorphic (`tokenable_type`, `tokenable_id`), so a `Sitter` model can own tokens as easily as `User`, as long as it uses `Laravel\Sanctum\HasApiTokens`. The trait adds: - `tokens()` — a `morphMany` relation to the token model; - `createToken(string $name, array $abilities = ['*'], ?DateTimeInterface $expiresAt = null)`; - `tokenCan()` / `tokenCant()`; - `currentAccessToken()` / `withAccessToken()`. ## What `createToken()` does, step by step 1. **Generate the secret.** `config('sanctum.token_prefix')` (empty by default, set through `SANCTUM_TOKEN_PREFIX`) + `Str::random(40)` + a CRC32b of those 40 characters. A prefix helps secret-scanning tools spot leaked tokens. 2. **Store a hash, not the secret.** The row gets `token = hash('sha256', $secret)`, plus `name`, `abilities` (cast to JSON) and `expires_at`. 3. **Return a `NewAccessToken`.** It exposes the saved model as `accessToken` and the string `plainTextToken` = `"{row id}|{secret}"`. Because only the hash is stored, the plain text cannot be recovered later: display it once and let the user copy it. The model also marks `token` as hidden from serialization. ## How a Bearer token is verified On a stateless request the `sanctum` guard reads `Authorization: Bearer ...` and calls `PersonalAccessToken::findToken()`: - if the value contains `|`, it splits off the id, loads that row with `find()`, and compares the stored hash with `hash('sha256', $secret)` using `hash_equals`; - without an id, it falls back to a lookup by the hash itself. The guard then checks expiry, attaches the token to the user with `withAccessToken()`, fires `TokenAuthenticated` and updates `last_used_at`. ## Abilities Abilities are Sanctum's analogue of OAuth scopes: free-form strings chosen by you. | Call | Result | |---|---| | `createToken('sitter-app')` | abilities `['*']` — every `tokenCan()` is true | | `createToken('sitter-app', ['bookings:read'])` | only `tokenCan('bookings:read')` is true | | `createToken('x', ['bookings:*'])` then `tokenCan('bookings:read')` | **false** — only the whole string `'*'` is a wildcard | | `tokenCan()` on a user loaded outside a token-authenticated request | false — no current token | `tokenCan()` returns `$this->accessToken && $this->accessToken->can($ability)`, and `can()` is a strict `in_array` against the abilities array or `'*'`. ## Revoking - `$request->user()->currentAccessToken()->delete()` — sign out the device making the request; - `$user->tokens()->where('id', $id)->delete()` — revoke one device from a settings page; - `$user->tokens()->delete()` — revoke everything, for example after a password change. Deletion is immediate: the next request with that token finds no row and fails authentication. ## Design habits that interviewers look for - **One token per device or integration.** Name it after the device (`device_name` from the login request) so a settings page can list and revoke each one. - **Narrow abilities by default.** Give the sitter app `['bookings:read', 'bookings:accept']`, not the `['*']` default, and reserve broader abilities for tokens a user creates deliberately. - **Set an expiry.** The third argument writes `expires_at`; without it, and with the default `null` global expiration, the token lives until deleted. - **Never log `plainTextToken`.** Return it in the response body once and let the client store it in the platform's secure storage. - **Remember `last_used_at`.** By default the guard writes it on every token-authenticated request, which is handy for a "last seen" column on the devices page. ## Customising the token model `Sanctum::usePersonalAccessTokenModel(MyToken::class)` in `AppServiceProvider::boot()` swaps in a subclass of `Laravel\Sanctum\PersonalAccessToken`, for example to add a relation or a column. The guard, `findToken()` and `tokens()` then use your class. Keep the `token` column's hashing intact: the guard's lookup depends on it.

  • A support engineer asks you to look up a sitter's lost token in the database. What can you give them?
    Nothing usable. The `token` column holds only a SHA-256 hash of the secret, and the plain text existed only in the `NewAccessToken` returned at creation. The right move is to revoke the old row and have the sitter create a new token.
  • Why does the plain-text token start with the row id and a pipe?
    It lets `findToken()` load the row by primary key and then compare hashes with `hash_equals`, instead of searching by hash. The guard also rejects a value with a non-numeric id when the token model uses integer keys.

saying these in an interview costs you the question

  • Sanctum encrypts tokens with APP_KEY so they can be shown again
  • An ability like bookings:* matches bookings:read
  • createToken() with no abilities creates a token that can do nothing
  • The plain-text token is stored so the user can copy it later
  • tokenCan() checks the user's roles rather than the token