This document describes the schema used in the OAUTH_KV storage for the OAuth 2.0 provider library. The library uses Cloudflare Workers KV to store all OAuth-related data, including client registrations, authorization grants, and tokens.
The OAUTH_KV namespace stores several types of objects, each with a distinct key prefix to identify the type of data. The storage leverages KV's built-in TTL (Time-To-Live) functionality for automatic expiration of short-lived data like tokens and authorization codes.
The system implements end-to-end encryption for sensitive application-specific properties (props) to ensure that only holders of valid tokens can access this data.
All keys in the KV namespace follow a consistent pattern to make them easily identifiable:
| Prefix | Purpose | Example |
|---|---|---|
client: |
Client registration data | client:abc123 |
grant:{userId}: |
Authorization grant data | grant:user123:xyz789 |
token: |
Access and refresh tokens | token:ghi789 |
Client records store OAuth client application information.
Key format: client:{clientId}
Content Example:
{
"clientId": "abc123",
"clientSecret": "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8",
"redirectUris": ["https://app.example.com/callback"],
"clientName": "Example App",
"logoUri": "https://app.example.com/logo.png",
"clientUri": "https://app.example.com",
"policyUri": "https://app.example.com/privacy",
"tosUri": "https://app.example.com/terms",
"jwksUri": null,
"i18n": {
"client_name#ja": "サンプルアプリ",
"tos_uri#ja": "https://app.example.com/ja/terms"
},
"contacts": ["dev@example.com"],
"grantTypes": ["authorization_code", "refresh_token"],
"responseTypes": ["code"],
"registrationDate": 1644256123,
"tokenEndpointAuthMethod": "client_secret_basic",
"authMethodExplicit": true
}Note: The
clientSecretis stored as a SHA-256 hash, not in plaintext. The actual secret is only returned to the client when initially created or updated, and never stored.
Note: The optional
i18nmap holds RFC 7591 §2.2 internationalized variants of the human-readable metadata fields (client_name,client_uri,logo_uri,tos_uri,policy_uri), keyed by the rawfield#<BCP 47 language tag>member name. Canonical (un-tagged) values remain in their own fields. URI variants are validated as absolute http(s) URLs, the same as their canonical counterparts.
Note:
authMethodExplicit: trueis stored only when the client explicitly selectstokenEndpointAuthMethod, and is not returned byOAuthHelpers. When the marker is absent, confidential clients may useclient_secret_basicorclient_secret_postafter the stored secret validates. Public (none), CIMD, missing or unknown methods, and secretless records do not receive this compatibility. Authentication never rewrites the client record or its TTL; settingtokenEndpointAuthMethodwithOAuthHelpers.updateClient()adds the marker.
TTL: Dynamically registered clients (DCR) default to 90 days. Clients created via OAuthHelpers.createClient() have no expiration. Configurable via the clientRegistrationTTL option.
Grant records store information about permissions a user has granted to an application, along with the authorization code (initially) and refresh token (after code exchange).
Key format: grant:{userId}:{grantId}
Content Example (during authorization):
{
"id": "xyz789",
"clientId": "abc123",
"userId": "user123",
"scope": ["document.read", "document.write"],
"metadata": {
"label": "My Files Access",
"deviceInfo": "Chrome on Windows"
},
"encryptedProps": "AES-GCM encrypted base64-encoded string",
"createdAt": 1644256123,
"resource": "https://example.com/mcp",
"authCodeId": "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8",
"authCodeWrappedKey": "base64-encoded wrapped encryption key",
"codeChallenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
"codeChallengeMethod": "S256"
}Content Example (after code exchange):
{
"id": "xyz789",
"clientId": "abc123",
"userId": "user123",
"scope": ["document.read", "document.write"],
"metadata": {
"label": "My Files Access",
"deviceInfo": "Chrome on Windows"
},
"encryptedProps": "AES-GCM encrypted base64-encoded string",
"createdAt": 1644256123,
"resource": "https://example.com/mcp",
"authCodeId": "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8",
"refreshTokenId": "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8",
"refreshTokenWrappedKey": "base64-encoded wrapped encryption key"
}Content Example (after refresh token rotation):
{
"id": "xyz789",
"clientId": "abc123",
"userId": "user123",
"scope": ["document.read", "document.write"],
"metadata": {
"label": "My Files Access",
"deviceInfo": "Chrome on Windows"
},
"encryptedProps": "AES-GCM encrypted base64-encoded string",
"createdAt": 1644256123,
"resource": "https://example.com/mcp",
"refreshTokenId": "7f2ab876c546a9e9f988ba7645af78239cfe980a4231ab38fcb895cb244a0a12",
"refreshTokenWrappedKey": "base64-encoded wrapped encryption key",
"previousRefreshTokenId": "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8",
"previousRefreshTokenWrappedKey": "base64-encoded wrapped encryption key for previous token"
}TTL:
- Initially 10 minutes (during authorization process)
- After code exchange, TTL matches the refresh token expiration (defaults to 30 days, configurable via
refreshTokenTTL)
Note: The grant record includes the hash of the authorization code initially, which is replaced by the hash of the refresh token after the code is exchanged. The record has a 10-minute TTL during authorization, which is replaced by the refresh token TTL when the code is exchanged.
Note: The optional
resourcefield stores the RFC 8707 resource authorized for the grant. Grants created by older releases may not contain it. When neither the grant nor provider configuration supplies a resource, the grant remains unbound.
Token records store metadata about issued access tokens, including denormalized grant information for faster access.
Key format: token:{userId}:{grantId}:{tokenId}
Content Example:
{
"id": "ghi789",
"grantId": "xyz789",
"userId": "user123",
"createdAt": 1644256123,
"expiresAt": 1644259723,
"audience": "https://example.com/mcp",
"wrappedEncryptionKey": "base64-encoded wrapped encryption key",
"grant": {
"clientId": "abc123",
"scope": ["document.read", "document.write"],
"encryptedProps": "AES-GCM encrypted base64-encoded string"
}
}Note: The token format is
{userId}:{grantId}:{random-secret}which embeds the identifiers needed for efficient lookups. The token key format includes the user ID and grant ID to enable efficient revocation of all tokens for a specific grant. The token record contains denormalized grant information to eliminate the need for a separate grant lookup during token validation. The token also carries a wrapped encryption key that can only be unwrapped using the actual token string, allowing decryption of the encrypted props.
Note: The optional
audiencefield records the resource restriction for an access token. It may be absent when a legacy grant has no stored resource, no canonical resource is configured, and the token request omitsresource.
TTL: Access tokens typically have a 1 hour (3600 seconds) TTL by default
-
Sensitive Value Storage: No sensitive values are stored in plaintext in KV storage:
- Access tokens and refresh tokens are stored as SHA-256 hashes
- Client secrets are stored as SHA-256 hashes
- Authorization codes are stored as SHA-256 hashes
- For PKCE, only the code challenge is stored, never the code verifier
- Application-specific properties (
props) are encrypted using AES-GCM
This ensures that even if the KV data is compromised, the actual sensitive values cannot be retrieved.
-
End-to-End Encryption for Props:
- Each grant has its own unique AES-256 key for encrypting props
- A constant all-zero initialization vector (IV) is used with AES-GCM encryption
- This is cryptographically secure because each key is only used exactly once
- Using unique keys eliminates the IV randomization requirement of AES-GCM
- The encryption key is wrapped (encrypted) using each token as key material
- The wrapped key can only be unwrapped by someone with the actual token
- No backup of the encryption key is stored anywhere
- Even system administrators cannot decrypt the props without a valid token
-
Key Wrapping Security:
- Token wrapping keys are derived using HMAC-SHA256 with a static key
- The derivation method is different from token ID generation for security separation
- Each token type (authorization code, refresh token, access token) has its own wrapped key
- The wrapping algorithm used is AES-KW (AES Key Wrap)
-
Token Format: Tokens use the format
{userId}:{grantId}:{random-secret}which allows:- Direct access to token records without needing to look up grants separately
- Verification that the token was issued for the specific grant and user
- Enhanced security through proper validation checks
-
TTL-based Expiration: Access tokens automatically expire using KV's TTL feature, reducing the need for manual cleanup.
-
Efficient Storage:
- Refresh tokens are stored within the grant records, eliminating redundant storage
- Grant data is denormalized into token records for faster validation
- Token keys include user ID and grant ID to enable efficient revocation
-
Structured Key Design: The key format
token:{userId}:{grantId}:{tokenId}enables:- Efficient revocation of all tokens for a specific grant
- Easy lookup of all tokens issued to a specific user
- Clean organization of the key-value namespace
-
Cryptographic Hash Verification: When validating credentials, the system hashes the provided value and compares it with the stored hash, rather than comparing plaintext values.
-
A client is registered, creating a
client:{clientId}entry with a hashed client secret. -
A user authorizes the client, creating a
grant:{userId}:{grantId}entry that includes:- The hashed authorization code in the
authCodeIdfield - PKCE code challenge and method (if PKCE is used)
- A new AES-256 encryption key is generated specifically for this grant
- The
propsdata is encrypted using this key with AES-GCM and a constant zero IV - The encryption key is wrapped using the authorization code
- The wrapped key is stored in
authCodeWrappedKey - A 10-minute TTL on the grant record
- The hashed authorization code in the
-
The client exchanges the authorization code for tokens:
- The code is validated by comparing its hash to the one stored in the grant
- The requesting client is validated against the grant's
clientId - If PKCE was used, the code_verifier is validated against the stored code_challenge
- The encryption key is unwrapped using the authorization code
- The key is re-wrapped for both the access token and refresh token
- The
authCodeIdhash is retained so a replay of the same code can be verified before any action is taken; theauthCodeWrappedKeyand PKCE fields are removed from the grant. The absence ofauthCodeWrappedKey(withauthCodeIdstill set) marks the code as already used - A refresh token is generated and its hash is stored in the grant's
refreshTokenIdfield - The wrapped key for the refresh token is stored in
refreshTokenWrappedKey - The grant's TTL is removed, making it permanent
- A new access token is generated and stored as
token:{userId}:{grantId}:{accessTokenId} - The access token record includes the encrypted props, IV, and wrapped key
- Both tokens are returned to the client
-
When the client makes API requests with the access token:
- The system looks up the token directly using the structured key format
- The wrapped encryption key is unwrapped using the access token
- The props are decrypted using the unwrapped key
- The decrypted props are made available to the API handler
-
Access tokens expire automatically after their TTL.
-
Refresh tokens expire based on the configured
refreshTokenTTL(default: 30 days) and are stored directly in the grant.- For security, the provider issues a new refresh token with each refresh operation
- It keeps track of both the current and previous tokens, along with their wrapped keys
- When the new token is used, the previous token is invalidated, but can still be used until replaced
-
When a grant is revoked (or when
deleteClient()is called, which cascades to all grants for that client):- All associated access tokens are found using the key prefix
token:{userId}:{grantId}:and deleted - The grant record is deleted, which also effectively revokes the refresh token and all encrypted data
- All associated access tokens are found using the key prefix
- For high-traffic applications, consider using a caching layer in front of KV to reduce read operations on frequently accessed data.
- Monitor KV usage metrics to ensure you stay within Cloudflare's limits for your plan.
- The design uses KV's
list()capability with key prefixes to efficiently query related data like all grants for a user, eliminating the need for separate list indexes. - When a grant is revoked via
revokeGrant(), associated tokens are immediately deleted. When a client is deleted viadeleteClient(), all grants and tokens for that client are also deleted. - The
purgeExpiredData()method provides defense-in-depth garbage collection for orphaned grants and tokens. It is designed to be called from a scheduled handler (Cron Trigger).