Ir al contenido principal
Laravel, shipping fast.

El recurso anidado les da a los tokens sus propios endpoints, en su propio controlador. Detente ahí un momento, porque esta es la situación sobre la que advirtió el Capítulo 4.

TokenController es un controlador nuevo. Está dentro del grupo auth:sanctum y no tiene ninguna otra protección. scoped() garantiza que un token pertenece al consumidor de la URL. No dice nada sobre si quien llama tiene algo que ver con ese consumidor. Tal como está, cualquier consumidor con cualquier token podría llamar a POST /api/consumers/7/tokens y recibir un token válido para el consumidor 7, o fabricarse un token de operador. Un endpoint que emite credenciales es el más peligroso del sistema, y está abierto por defecto.

Así que la primera línea del controlador lo cierra:

// app/Http/Controllers/TokenController.php
#[Middleware('ability:consumers:manage')]
class TokenController
{
    public function store(
        StoreTokenRequest $request,
        Consumer $consumer,
    ): JsonResponse {
        $token = $consumer->issueToken(
            name: $request->validated('name'),
            abilities: $request->validated('abilities'),
        );

        return response()->json(['data' => [
            'id' => $token->accessToken->id,
            'name' => $token->accessToken->name,
            'token' => $token->plainTextToken,
        ]], Response::HTTP_CREATED);
    }
}

El atributo está sobre la clase, así que cubre todas las acciones, incluidas las que se añadan después. Solo los operadores pasan de ahí.

El texto plano está en la respuesta a esta única petición y en ningún otro sitio. Las otras dos acciones no tienen nada secreto que mostrar:

// app/Http/Controllers/TokenController.php
public function index(
    Consumer $consumer,
): AnonymousResourceCollection {
    return TokenResource::collection($consumer->tokens);
}

public function destroy(
    Consumer $consumer,
    PersonalAccessToken $token,
): Response {
    $token->delete();

    return response()->noContent();
}

TokenResource lista el nombre, las habilidades, last_used_at y expires_at, y no tiene forma de mostrar el token porque la base de datos solo guarda su hash. destroy es la revocación, y como la ruta tiene ámbito, el token que recibe es siempre uno de este consumidor.

issueToken() es un método pequeño del modelo, para que las dos reglas del Capítulo 4 (habilidades explícitas y una caducidad) se apliquen en un solo lugar:

// app/Models/Consumer.php
public function issueToken(
    string $name,
    array $abilities,
    int $days = 365,
): NewAccessToken {
    return $this->createToken(
        name: $name,
        abilities: $abilities,
        expiresAt: now()->addDays($days),
    );
}

El request decide qué habilidades se pueden pedir:

// app/Http/Requests/StoreTokenRequest.php
return [
    'name' => ['required', 'string', 'max:100'],
    'abilities' => ['required', 'array', 'min:1', 'max:10'],
    'abilities.*' => [
        'bail', 'string', 'distinct',
        Rule::enum(Ability::class),
        new HeldByCaller,
    ],
];

Rule::enum significa que una errata como license:write es un 422 en el momento en que se emite el token. La alternativa es un token que no puede hacer lo que su dueño cree que puede, descubierto en producción. También deja fuera a *, porque * no es un caso del enum.

HeldByCaller es un objeto regla como el del Capítulo 2:

// app/Rules/HeldByCaller.php
public function validate(
    string $attribute,
    mixed $value,
    Closure $fail,
): void {
    $held = is_string($value)
        && request()->user()?->tokenCan($value);

    if (! $held) {
        $fail('validation.held_by_caller')->translate();
    }
}

Quien llama solo puede conceder habilidades que él mismo tiene. Sin la regla, un token de operador limitado a unas pocas habilidades podría emitir un token con todas, y el primero de esos tokens sería una fábrica de más.

No se pudo cargar el audio. Inténtalo de nuevo en un momento.