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.