API keys

Long-lived tokens for scripts, CI pipelines, and third-party integrations. Distinct from JWT (short-lived session) and from verification tokens (one-use).

Creating a key

use Derafu\IdentityBundle\Contract\Service\ApiKeyManagerInterface;

$generated = $apiKeyManager->create(
    user: $currentUser,
    name: 'CI Pipeline',
    scopes: ['read:invoices', 'write:invoices'],
    expiresAt: new DateTimeImmutable('+1 year')  // null = never
);

// Show this once — it won't be available again:
$plaintext = $generated->plaintext;  // "dik_a1b2c3d4e5..."

The plaintext starts with dik_ (Derafu Identity Key) + 40 random hex chars. The database stores a SHA-256 hash + a short display prefix (dik_a1b2) for the key-management UI.

Authenticating with a key

The bundle ships an authenticator. Enable it on your API firewall:

security:
    firewalls:
        api:
            stateless: true
            custom_authenticators:
                - Derafu\IdentityBundle\Security\Authenticator\ApiKeyAuthenticator

Clients send the key in the Authorization header:

Authorization: Bearer dik_a1b2c3d4e5f6...

The authenticator only fires when the Bearer value starts with dik_ — no conflict with JWT tokens.

Scopes

Scopes are app-defined strings that restrict what a key can do. The bundle stores and checks them but does not define a catalog.

// Scopes are passed at creation:
$apiKeyManager->create($user, 'Read-only bot', ['read:invoices']);
  • Empty scopes ([]) = unrestricted — the key can do everything the user can.
  • Non-empty scopes = the key can only perform operations that match a listed scope.

Protecting endpoints with scopes

Use the #[RequiresScope] attribute:

use Derafu\IdentityBundle\Attribute\RequiresScope;

#[RequiresScope('write:invoices')]
#[Route('/api/invoices', methods: ['POST'])]
public function createInvoice(): Response { ... }

Multiple #[RequiresScope] are AND-ed — the key must hold every listed scope.

Session users bypass scopes. Scopes restrict what a token can do, not what a user can do. If the request is authenticated via regular session (not an API key), the scope check is skipped entirely.

Programmatic scope check

use Derafu\IdentityBundle\Contract\Service\ApiKeyContextInterface;

$apiKey = $apiKeyContext->getCurrent();

if ($apiKey !== null && !$apiKey->hasScope('write:invoices')) {
    throw new AccessDeniedException('Insufficient scope.');
}

Revoking a key

$apiKeyManager->revoke($apiKey);
// Idempotent — revoking an already-revoked key is a no-op.

Key format summary

Part Example Stored in DB
Full plaintext dik_a1b2c3d4e5f6... (44 chars) Never
Display prefix dik_a1b2 (8 chars) prefix column
Hash SHA-256 of plaintext (64 hex) token_hash column
Scopes ['read:invoices'] JSON column

Defining your app’s scope catalog

The bundle is scope-agnostic. Your app defines what scopes exist:

// A simple enum (recommended):
enum ApiScope: string
{
    case READ_INVOICES = 'read:invoices';
    case WRITE_INVOICES = 'write:invoices';
    case MANAGE_CONTRIBUYENTES = 'manage:contribuyentes';
}

// Validate at key-creation time:
$scopes = array_map(fn ($s) => ApiScope::from($s)->value, $requestedScopes);
$apiKeyManager->create($user, $name, $scopes);

The naming convention (namespace:action) is a suggestion, not a requirement. Use whatever makes sense for your API.

On this page

Last updated on 16/04/2026 by Anonymous