Issue, rotate, and revoke keys safely when it matters most
API keys unlock interfaces for partners, marketplaces, and internal services, yet poorly managed keys remain one of the most common entry points for data breaches. This article shows how to generate API keys with sufficient entropy, scope them to minimal permissions, rotate them without downtime, and revoke them immediately on suspected compromise, with practical guidance for Magento backends.
Table of Contents
- 1. API key vs. OAuth token: which approach when
- 2. Secure key generation: sufficient entropy
- 3. Scoping: limiting permissions to the minimum
- 4. Rotation strategy without downtime
- 5. Instant revocation on compromise
- 6. Never expose keys in client code or URLs
- 7. Storage and secrets management
- 8. Rate limiting and monitoring key usage
- 9. Magento-specific implementation: a dedicated API key module
- 10. Summary
- 11. FAQ
1. API key vs. OAuth token: which approach when
An API key is a single, long-lived secret that a client sends with every request, usually without any built-in expiry mechanism or fine-grained permission logic. OAuth 2.0, by contrast, splits identity, authorization, and access into several roles: client credentials, access token, and refresh token, each with a clearly defined lifetime and scope. For server-to-server integrations without a user context, such as syncing inventory data between Magento and an ERP system, an API key is often the more pragmatic choice, since no authorization server, token refresh flow, or additional infrastructure is required.
The downside shows up at scale: without an expiry date, an issued API key stays valid indefinitely until it's manually revoked. OAuth tokens, in contrast, expire automatically and force clients into a refresh cycle that naturally limits the impact of a compromise. Anyone connecting many external partners with different permission levels, or needing delegated access on behalf of a user, can hardly avoid OAuth. For internal, clearly bounded integrations with few systems, the API key remains the right choice for its simplicity, as long as rotation and scoping are applied consistently.
2. Secure key generation: sufficient entropy
The security of an API key depends almost entirely on its entropy, meaning the number of random bits an attacker would have to guess. A key derived from uniqid(), a timestamp, or an incrementing ID is predictable and therefore worthless as a secret, no matter how long it looks. Cryptographically secure random number generators such as PHP's random_bytes() or sodium_crypto_secretbox_keygen() draw true randomness from the operating system's entropy pool and are the only acceptable basis for key generation.
As a rule of thumb, at least 256 bits of entropy, meaning 32 random bytes, should be used, then base64- or hex-encoded so the value can be safely transported in URLs, headers, and configuration files. An additional prefix such as msft_live_ or msft_test_ makes it easier for automated secret scanners in the CI pipeline to detect accidentally committed keys, without reducing the entropy of the actual secret. It's essential to show the generated plaintext key to the user exactly once at creation time and to store only a hash server-side, so a database leak doesn't automatically compromise every active key.
<?php
declare(strict_types=1);
namespace Mironsoft\ApiKeyManager\Service;
/**
* Generates cryptographically secure API keys and their storage hash.
*/
final class ApiKeyGenerator
{
private const PREFIX_LIVE = 'msft_live_';
private const KEY_BYTES = 32; // 256 bit entropy
/**
* Generate a new plaintext API key and its hash for persistence.
*
* @return array{plain: string, hash: string, prefix: string}
*/
public function generate(): array
{
// Cryptographically secure random bytes, never uniqid() or time()-based values
$randomBytes = random_bytes(self::KEY_BYTES);
$secret = rtrim(strtr(base64_encode($randomBytes), '+/', '-_'), '=');
$plain = self::PREFIX_LIVE . $secret;
// Store only the hash, plaintext is shown to the user exactly once
$hash = hash('sha256', $plain);
// Short prefix for support/log identification without revealing the secret
$prefix = substr($plain, 0, strlen(self::PREFIX_LIVE) + 8);
return ['plain' => $plain, 'hash' => $hash, 'prefix' => $prefix];
}
/**
* Verify a supplied plaintext key against a stored hash using constant-time comparison.
*
* @param string $plain
* @param string $storedHash
* @return bool
*/
public function verify(string $plain, string $storedHash): bool
{
return hash_equals($storedHash, hash('sha256', $plain));
}
}
3. Scoping: limiting permissions to the minimum
An API key without a scope is a master key: if it's compromised, an attacker gets immediate full access to every endpoint and every piece of data. Scoping instead binds a key to a minimal set of allowed actions, for example read-only access to product inventory or the ability to create new orders only, but never access to customer data or payment information. The principle of least privilege limits the damage in a compromise to exactly the area the key was issued for.
In practice, scopes are modeled as named permissions, such as catalog:read, orders:write, or customers:none, which are explicitly assigned when a key is issued and checked server-side on every request, never only on the client. Granularity is a trade-off: overly fine-grained scopes create administrative overhead, while overly broad scopes undermine the principle of least privilege. A proven structure is organized around resource and action, combined with a clear separation between read and write access and separate scopes for sensitive areas such as payment data or admin functions.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
<system>
<section id="mironsoft_apikeymanager" translate="label" type="text" sortOrder="200" showInDefault="1" showInWebsite="0" showInStore="0">
<label>API Key Management</label>
<tab>mironsoft</tab>
<resource>Mironsoft_ApiKeyManager::config</resource>
<group id="rotation" translate="label" sortOrder="10" showInDefault="1">
<label>Rotation</label>
<field id="default_ttl_days" translate="label" type="text" sortOrder="10" showInDefault="1">
<label>Default Key Lifetime (days)</label>
<validate>validate-number validate-greater-than-zero</validate>
</field>
<field id="overlap_window_days" translate="label" type="text" sortOrder="20" showInDefault="1">
<label>Rotation Overlap Window (days)</label>
<comment>Old key stays valid this many days after a new key is issued.</comment>
</field>
</group>
<group id="scopes" translate="label" sortOrder="20" showInDefault="1">
<label>Available Scopes</label>
<field id="allowed_scopes" translate="label" type="textarea" sortOrder="10" showInDefault="1">
<label>Allowed Scopes (one per line)</label>
<comment>e.g. catalog:read, orders:write, customers:none</comment>
</field>
</group>
</section>
</system>
</config>
4. Rotation strategy without downtime
Regular rotation limits the window during which a stolen key remains usable, even if the theft goes unnoticed. The biggest practical mistake in rotation is deactivating the old key the moment the new one is issued: any client that hasn't yet picked up the new key instantly loses access, which in distributed systems with multiple deployment cycles almost guarantees an outage.
The robust solution is an overlap window: a new key is generated while the old one remains valid in parallel for a defined period, say 7 to 14 days. Both keys work simultaneously during that time, giving clients room to update their configuration without time pressure. Once the overlap window expires, the old key is deactivated automatically, ideally through a scheduled cron job rather than manual intervention. For highly sensitive integrations, a fixed rotation interval of 90 days is recommended, supplemented by immediate ad-hoc rotation whenever compromise is suspected.
#!/usr/bin/env bash
# rotate-api-key.sh - Issue a new API key with an overlap window, no downtime
set -euo pipefail
CLIENT_ID="${1:?Usage: rotate-api-key.sh <client_id>}"
OVERLAP_DAYS="${OVERLAP_DAYS:-10}"
echo "[INFO] Issuing new key for client ${CLIENT_ID}"
NEW_KEY_JSON=$(bin/magento mironsoft:apikey:issue --client-id="${CLIENT_ID}" --scope="catalog:read,orders:write" --format=json)
NEW_PREFIX=$(echo "${NEW_KEY_JSON}" | jq -r '.prefix')
echo "[INFO] New key issued with prefix ${NEW_PREFIX}"
echo "[INFO] Old key(s) remain valid for ${OVERLAP_DAYS} more days"
# Schedule the old key deactivation instead of an immediate hard cutover
bin/magento mironsoft:apikey:schedule-deactivation --client-id="${CLIENT_ID}" --exclude-prefix="${NEW_PREFIX}" --after-days="${OVERLAP_DAYS}"
echo "[INFO] Notify integration partner about the new key and the overlap deadline"
echo "[OK] Rotation initiated. Old and new keys are active in parallel."
5. Instant revocation on compromise
A revocation has to take effect immediately, without waiting for the next cache refresh, deployment, or restart. That requires the key validity check to not be served exclusively from a long-lived application cache, but instead checked against the current database or a central revocation store on every request, or at least with a very short cache TTL of a few seconds.
A practical pattern is a denylist in Redis keyed by the hashed key with a short TTL, checked alongside the database so it takes effect consistently and instantly even across distributed application servers. Every revocation should be logged, with a timestamp, the triggering user, and a reason, so it's possible afterward to determine whether the compromise had already caused unusual traffic beforehand. For the worst case, it pays off to have a prepared runbook that describes revocation, notifying affected teams, and issuing a replacement key in a single documented process, instead of improvising the response under stress.
#!/usr/bin/env bash
# revoke-api-key.sh - Immediately revoke a compromised API key
set -euo pipefail
KEY_PREFIX="${1:?Usage: revoke-api-key.sh <key_prefix> <reason>}"
REASON="${2:?Usage: revoke-api-key.sh <key_prefix> <reason>}"
echo "[ALERT] Revoking key with prefix ${KEY_PREFIX}"
bin/magento mironsoft:apikey:revoke --prefix="${KEY_PREFIX}" --reason="${REASON}"
# Purge any short-lived validation cache so revocation is effective within seconds
bin/cache-clean mironsoft_apikey_validation
echo "[INFO] Revocation logged. Notifying security channel."
bin/magento mironsoft:apikey:notify-revocation --prefix="${KEY_PREFIX}" --reason="${REASON}"
echo "[OK] Key ${KEY_PREFIX} revoked and integration team notified."
6. Never expose keys in client code or URLs
An API key sitting in client-side JavaScript, a mobile app, or a publicly accessible configuration file stops being a secret the moment a user opens the developer tools or decompiles the app. Secrets belong exclusively in server-side code, environment variables, or a dedicated secrets manager, never in frontend bundles, mobile app binaries, or public Git repositories. If a client genuinely needs authorization, it should be issued short-lived, tightly scoped tokens that a backend proxy requests on the client's behalf, instead of exposing the actual API key.
Transport is just as critical: a key in the URL query string, such as ?api_key=xyz, reliably ends up in server access logs, browser history, proxy logs, and third-party referrer headers, even when the connection is encrypted with TLS. The correct transport is the Authorization header with the Bearer scheme or a custom prefix, since headers aren't logged in access logs by default and aren't passed along via referrers. Access logs should additionally be configured to never log header values by default, ruling out accidental leaks through the logging infrastructure itself.
<?php
declare(strict_types=1);
namespace Mironsoft\ApiKeyManager\Plugin;
use Magento\Framework\Webapi\Rest\Request;
use Magento\Framework\Exception\AuthorizationException;
use Mironsoft\ApiKeyManager\Api\ApiKeyRepositoryInterface;
/**
* Validates the API key and its scope before a REST request reaches the controller.
*/
final class ValidateApiKeyPlugin
{
/**
* @param ApiKeyRepositoryInterface $apiKeyRepository
*/
public function __construct(
private readonly ApiKeyRepositoryInterface $apiKeyRepository
) {
}
/**
* Check key validity, expiry, revocation status and required scope.
*
* @param Request $subject
* @param string $requiredScope
* @throws AuthorizationException
* @return void
*/
public function beforeDispatch(Request $subject, string $requiredScope): void
{
$header = $subject->getHeader('Authorization') ?: '';
if (!str_starts_with($header, 'Bearer ')) {
throw new AuthorizationException(__('Missing or malformed Authorization header.'));
}
$plainKey = substr($header, 7);
$apiKey = $this->apiKeyRepository->getByHash(hash('sha256', $plainKey));
if ($apiKey === null || $apiKey->isRevoked() || $apiKey->isExpired()) {
throw new AuthorizationException(__('API key is invalid, expired or revoked.'));
}
if (!in_array($requiredScope, $apiKey->getScopes(), true)) {
throw new AuthorizationException(__('API key is missing required scope: %1', $requiredScope));
}
}
}
7. Storage and secrets management
The database should never hold the plaintext key, only a cryptographic hash, similar to password storage. Since API keys already carry high entropy on their own, a fast hash like SHA-256 is sufficient; a computationally expensive password hashing scheme like bcrypt or Argon2 isn't needed here and would add unnecessary latency to validating every single request. An additionally stored prefix of the first few characters lets support staff or logs identify a key uniquely without exposing the secret itself.
For the application's own configuration, such as database credentials or keys for outbound integrations, a .env file should never sit in the web root, and no secret should ever live in a Git repository. Dedicated secrets managers like HashiCorp Vault, AWS Secrets Manager, or, for smaller setups, encrypted environment variables handled through the deployment process, provide access control, audit logs, and automatic rotation for your own infrastructure secrets, kept separate from the API keys you issue to third parties.
8. Rate limiting and monitoring key usage
Every issued API key should be subject to an individual rate limit, which, beyond sizing for the customer or use case, also serves as an early warning system for compromised keys: a sudden, atypical spike in requests from a single key is one of the most reliable signals of abuse, well before any manual log analysis would catch it. Rate limits applied per key, not just per IP address, also prevent a single misbehaving or compromised client from degrading the entire system for every other user.
For monitoring, a simple dashboard showing requests per key over time is often enough to spot anomalies like unusual access times, geographically implausible access patterns, or a sudden cluster of 403 responses due to missing permissions. It's also worth setting up an automated alert that fires when a defined threshold is exceeded and proactively notifies the affected team, before a customer reports the abuse themselves. Every use of a key, whether successful or rejected, should be logged with a timestamp, IP address, and the endpoint called, so that the damage can be precisely scoped in the event of a compromise.
9. Magento-specific implementation: a dedicated API key module
In Magento 2, API key management can be implemented cleanly as a standalone module with its own entity, repository, and admin grid, instead of dumping third-party keys unstructured into core_config_data. A dedicated table with fields for the hashed key, prefix, scope, expiry date, and status forms the foundation, managed through a ServiceContract interface that mirrors Magento's own repository patterns. Validation runs through a plugin on the webapi authorization layer or a dedicated controller plugin for REST endpoints outside the standard webapi.
For the admin interface, a UI grid under its own menu item in the configuration area works well, allowing creation, display of the prefix, scope assignment, and revocation of individual keys, with the full plaintext key shown only in a one-time modal right after creation. The ACL configuration in acl.xml ensures only authorized admin users can issue or revoke keys, while system.xml makes global settings like the default rotation interval and maximum validity period configurable instead of hardcoding them.
| Practice | Insecure | Secure | Why it matters |
|---|---|---|---|
| Storage | Plaintext key in the database | SHA-256 hash with prefix | A DB leak doesn't automatically compromise every key |
| Scope | Full access to every endpoint | Scoped to specific resources/actions | Limits the damage in a compromise |
| Rotation | Key is never rotated | Rotated every 90 days with overlap | Minimizes the window for stolen keys |
| Transport | Key as a URL query parameter | Key in the Authorization header | No leaks via access logs, referrers, browser history |
| Validity | No expiry, valid indefinitely | TTL with enforced expiry | Forgotten keys lose validity automatically |
Mironsoft
API security, secrets management, and key lifecycle for Magento integrations
Ready to lock down your API key management?
We build your API key management from secure issuance to emergency revocation, with scoping, zero-downtime rotation, and monitoring, as a clean Magento module instead of an ad-hoc solution.
Security audit
Reviewing existing key issuance, storage, and transport paths
Module development
Scoping, rotation, and revocation as a standalone Magento module
Monitoring setup
Rate limits, logging, and alerts for suspicious key usage
10. Summary
Secure API key management stands or falls on four core principles: sufficient entropy at generation time through cryptographically secure random number generators, scoping to the minimally necessary permissions, a rotation strategy with an overlap window instead of a hard cutover, and instant revocation on any suspicion of compromise. API keys are often the more pragmatic choice over OAuth for server-to-server integrations without a user context, but they demand the same discipline around storage, transport, and monitoring as any other secret.
Storing keys exclusively as hashes, never exposing them in URLs or client-side code, and monitoring every use with rate limits and logging drastically reduces the attack surface without complicating the integration for legitimate partners. In Magento 2, this model can be implemented as a standalone module with clearly separated responsibilities for issuance, validation, and revocation, fully controllable through ACL and system configuration, instead of as an ad-hoc solution bolted onto existing modules.
API Key Management: Issuance, Rotation, Revocation - The Essentials at a Glance
Generation
256 bits of entropy via random_bytes(), never uniqid() or timestamps. Store only a hash, show the plaintext once.
Scoping
Least privilege per key, granular by resource and action, checked server-side on every request.
Rotation & revocation
Overlap window instead of a hard cutover, instant revocation via a central denylist when suspected.
Transport & monitoring
Always the Authorization header, never the URL. Per-key rate limits and logging as an early warning system.