Skip to content

Commit c3057d1

Browse files
committed
feat(format): rename default prefix from to for versioned key format
1 parent ba6cddd commit c3057d1

File tree

11 files changed

+31
-21
lines changed

11 files changed

+31
-21
lines changed

README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ This library try to follow best practices and relevant RFCs for API key manageme
4444
pattern (`^[a-z][a-z0-9:_\-]*$`) at the schema level. Note: [RFC 6749 §3.3](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3)
4545
permits a broader character set; this restriction is a deliberate keyshield design choice to
4646
prevent injection of arbitrary characters (HTML, SQL fragments, etc.) in scope values.
47+
- **Versioned key format** (industry best practice, aligned with Stripe/GitHub 2023+): the default
48+
prefix embeds a format version (`ak_v1`, `ak_v2`, …) so a future algorithm or structure migration
49+
can be detected at parse time — old keys keep their prefix and old keys always fail with `401`
50+
rather than silently producing wrong hashes. Custom prefixes are still fully supported.
4751

4852
## Installation
4953

@@ -158,10 +162,10 @@ This is a classic API key if you don't modify the service behavior:
158162

159163
**Example:**
160164

161-
`ak-7a74caa323a5410d-mAfP3l6yAxqFz0FV2LOhu2tPCqL66lQnj3Ubd08w9RyE4rV4skUcpiUVIfsKEbzw`
165+
`ak_v1-7a74caa323a5410d-mAfP3l6yAxqFz0FV2LOhu2tPCqL66lQnj3Ubd08w9RyE4rV4skUcpiUVIfsKEbzw`
162166

163167
- "-" separators so that systems can easily split
164-
- Prefix `ak` (for "Api Key"), to identify the key type (useful to indicate that it is an API key).
168+
- Prefix `ak_v1` (for "Api Key v1"), to identify both the key type and the format version — allowing future algorithm migrations without breaking existing keys (e.g. `ak_v2-…` for a future format).
165169
- 16 first characters are the identifier (UUIDv4 without dashes)
166170
- 64 last characters are the secret (random alphanumeric string)
167171

@@ -189,7 +193,7 @@ flowchart LR
189193
190194
%% ── Entry ───────────────────────────────────────────────
191195
INPUT(["`**Api Key**
192-
_ak-7a74…10d-mAfP…bzw_`"]):::startNode
196+
_ak_v1-7a74…10d-mAfP…bzw_`"]):::startNode
193197
194198
%% ── Main flow ───────────────────────────────────────────
195199
CACHED{"`**Is cached key?**
@@ -256,7 +260,7 @@ flowchart LR
256260
{separator}: str
257261
{key_secret}: UUID
258262
259-
_global_prefix = 'ak'_
263+
_global_prefix = 'ak_v1'_
260264
_separator = '-'_`"]:::noteStyle
261265
262266
NOTE_CACHE["`**Cache rules**

docs/index.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ This library try to follow best practices and relevant RFCs for API key manageme
4040
- **[NIST SP 800-132](https://csrc.nist.gov/publications/detail/sp/800-132/final)**: The Bcrypt hasher
4141
pre-hashes the secret via `HMAC-SHA256(pepper, secret)` before passing it to bcrypt, producing a fixed
4242
32-byte digest that eliminates bcrypt's silent 72-byte input truncation.
43+
- **Versioned key format** (industry best practice, aligned with Stripe/GitHub 2023+): the default
44+
prefix embeds a format version (`ak_v1`, `ak_v2`, …) so a future algorithm or structure migration
45+
can be detected at parse time — old keys keep their prefix and always fail with `401`
46+
rather than silently producing wrong hashes. Custom prefixes are still fully supported.
4347

4448
## How API Keys Work
4549

@@ -53,10 +57,10 @@ This is a classic API key if you don't modify the service behavior:
5357

5458
**Example:**
5559

56-
`ak-7a74caa323a5410d-mAfP3l6yAxqFz0FV2LOhu2tPCqL66lQnj3Ubd08w9RyE4rV4skUcpiUVIfsKEbzw`
60+
`ak_v1-7a74caa323a5410d-mAfP3l6yAxqFz0FV2LOhu2tPCqL66lQnj3Ubd08w9RyE4rV4skUcpiUVIfsKEbzw`
5761

5862
- "-" separators so that systems can easily split
59-
- Prefix `ak` (for "Api Key"), to identify the key type (useful to indicate that it is an API key).
63+
- Prefix `ak_v1` (for "Api Key v1"), to identify both the key type and the format version — allowing future algorithm migrations without breaking existing keys (e.g. `ak_v2-…` for a future format).
6064
- 16 first characters are the identifier (UUIDv4 without dashes)
6165
- 64 last characters are the secret (random alphanumeric string)
6266

@@ -84,7 +88,7 @@ flowchart LR
8488
8589
%% ── Entry ───────────────────────────────────────────────
8690
INPUT(["`**Api Key**
87-
_ak-7a74…10d-mAfP…bzw_`"]):::startNode
91+
_ak_v1-7a74…10d-mAfP…bzw_`"]):::startNode
8892
8993
%% ── Main flow ───────────────────────────────────────────
9094
CACHED{"`**Is cached key?**
@@ -151,7 +155,7 @@ flowchart LR
151155
{separator}: str
152156
{key_secret}: UUID
153157
154-
_global_prefix = 'ak'_
158+
_global_prefix = 'ak_v1'_
155159
_separator = '-'_`"]:::noteStyle
156160
157161
NOTE_CACHE["`**Cache rules**

docs/usage/django.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ async def get_service() -> CachedApiKeyService:
125125

126126
```python
127127
import os
128-
os.environ["API_KEY_DEV"] = "ak-mydevkeyid-mysecret64chars"
128+
os.environ["API_KEY_DEV"] = "ak_v1-mydevkeyid-mysecret64chars"
129129

130130
async def get_service() -> ApiKeyService:
131131
svc = ApiKeyService(repo=DjangoApiKeyRepository(), hasher=Argon2ApiKeyHasher())

docs/usage/dotenv.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ If you don't need to have complex system (add, remove, update API keys) manageme
55
You can generate API keys using the CLI `create` command or programmatically, then store them in your `.env` file:
66

77
```bash
8-
API_KEY_DEV=ak-dcde9fa8eec44aa2-n8JK2HPXoosH6UXPL5h2YeO3OdW55WESb97CKc7mbVUzFpWFQYLuDD7Xs8fbco5d
8+
API_KEY_DEV=ak_v1-dcde9fa8eec44aa2-n8JK2HPXoosH6UXPL5h2YeO3OdW55WESb97CKc7mbVUzFpWFQYLuDD7Xs8fbco5d
99
```
1010

1111
## Example

docs/usage/litestar.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ async def provide_svc() -> CachedApiKeyService:
125125

126126
```python
127127
import os
128-
os.environ["API_KEY_DEV"] = "ak-mydevkeyid-mysecret64chars"
128+
os.environ["API_KEY_DEV"] = "ak_v1-mydevkeyid-mysecret64chars"
129129

130130
async def provide_svc() -> ApiKeyService:
131131
svc = ApiKeyService(repo=InMemoryApiKeyRepository(), hasher=Argon2ApiKeyHasher())

docs/usage/quart.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ async def get_service() -> CachedApiKeyService:
109109

110110
```python
111111
import os
112-
os.environ["API_KEY_DEV"] = "ak-mydevkeyid-mysecret64chars"
112+
os.environ["API_KEY_DEV"] = "ak_v1-mydevkeyid-mysecret64chars"
113113

114114
async def get_service() -> ApiKeyService:
115115
svc = ApiKeyService(repo=InMemoryApiKeyRepository(), hasher=Argon2ApiKeyHasher())

examples/example_inmemory_env.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
# load_dotenv()
2323

2424
# Ensure that you respect the format of service
25-
os.environ["API_KEY_DEV"] = "ak-92f5326fb9b44ab7-fSvBMig0r2vY3WR2SmGoZwM949loPU7Yy1JkjIz3RzfCEkQrprQWqQuToLbM2FzN"
25+
os.environ["API_KEY_DEV"] = "ak_v1-92f5326fb9b44ab7-fSvBMig0r2vY3WR2SmGoZwM949loPU7Yy1JkjIz3RzfCEkQrprQWqQuToLbM2FzN"
2626

2727

2828
async def main():

src/keyshield/_schemas.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,9 @@ class ApiKeySearchIn(BaseModel):
125125
last_used_after: Optional[datetime] = Field(None, description="Keys last used after this date")
126126
never_used: Optional[bool] = Field(None, description="True = never used keys, False = used keys")
127127
scopes_contain_all: Optional[List[_ScopeStr]] = Field(None, description="Keys must have ALL these scopes")
128-
scopes_contain_any: Optional[List[_ScopeStr]] = Field(None, description="Keys must have at least ONE of these scopes")
128+
scopes_contain_any: Optional[List[_ScopeStr]] = Field(
129+
None, description="Keys must have at least ONE of these scopes"
130+
)
129131
name_contains: Optional[str] = Field(None, description="Name contains this substring (case-insensitive)")
130132
name_exact: Optional[str] = Field(None, description="Exact name match")
131133
order_by: SortableColumn = Field(SortableColumn.CREATED_AT, description="Field to sort by")

src/keyshield/services/base.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@
1818
Default separator between key_type, key_id, key_secret in the API key string.
1919
Must be not in `token_urlsafe` alphabet. (like '.', ':', '~", '|')
2020
"""
21-
DEFAULT_GLOBAL_PREFIX = "ak"
21+
DEFAULT_GLOBAL_PREFIX = "ak_v1"
2222

2323

2424
@dataclass
2525
class ParsedApiKey:
2626
"""Result of parsing an API key string.
2727
2828
Attributes:
29-
global_prefix: The prefix identifying the key type (e.g., "ak").
29+
global_prefix: The prefix identifying the key type (e.g., "ak_v1").
3030
key_id: The public identifier part of the API key.
3131
key_secret: The secret part of the API key.
3232
raw: The original full API key string.
@@ -45,7 +45,7 @@ class AbstractApiKeyService(ABC):
4545
repo: Repository for persisting API key entities.
4646
hasher: Hasher for hashing secrets. Defaults to Argon2ApiKeyHasher.
4747
separator: Separator in API key format. Defaults to "-".
48-
global_prefix: Prefix for API keys. Defaults to "ak".
48+
global_prefix: Prefix for API keys. Defaults to "ak_v1".
4949
rrd: Deprecated random response delay. Ignored if provided.
5050
min_delay: Minimum delay (seconds) applied to all verify responses.
5151
max_delay: Maximum delay (seconds) applied to all verify responses.

src/keyshield/services/cached.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from keyshield.domain.entities import ApiKey
1414
from keyshield.hasher.base import ApiKeyHasher
1515
from keyshield.repositories.base import AbstractApiKeyRepository
16-
from keyshield.services.base import DEFAULT_SEPARATOR
16+
from keyshield.services.base import DEFAULT_SEPARATOR, DEFAULT_GLOBAL_PREFIX
1717

1818
INDEX_PREFIX = "idx"
1919
"""Prefix for the secondary index mapping key_id to cache_key."""
@@ -56,7 +56,7 @@ def __init__(
5656
cache_prefix: str = "api_key",
5757
cache_ttl: int = 300,
5858
separator: str = DEFAULT_SEPARATOR,
59-
global_prefix: str = "ak",
59+
global_prefix: str = DEFAULT_GLOBAL_PREFIX,
6060
rrd: Optional[float] = None,
6161
min_delay: float = 0.1,
6262
max_delay: float = 0.3,

0 commit comments

Comments
 (0)