Skip to content

Latest commit

 

History

History
273 lines (215 loc) · 14 KB

File metadata and controls

273 lines (215 loc) · 14 KB

OAuth KV Storage Schema

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.

Overview

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.

Key Naming Conventions

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

Data Structures

Clients

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 clientSecret is 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 i18n map 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 raw field#<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: true is stored only when the client explicitly selects tokenEndpointAuthMethod, and is not returned by OAuthHelpers. When the marker is absent, confidential clients may use client_secret_basic or client_secret_post after 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; setting tokenEndpointAuthMethod with OAuthHelpers.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.

Authorization Grants

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 resource field 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.

Tokens

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 audience field 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 omits resource.

TTL: Access tokens typically have a 1 hour (3600 seconds) TTL by default

Security Considerations

  1. 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.

  2. 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
  3. 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)
  4. 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
  5. TTL-based Expiration: Access tokens automatically expire using KV's TTL feature, reducing the need for manual cleanup.

  6. 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
  7. 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
  8. Cryptographic Hash Verification: When validating credentials, the system hashes the provided value and compares it with the stored hash, rather than comparing plaintext values.

Example Workflow with Encrypted Props

  1. A client is registered, creating a client:{clientId} entry with a hashed client secret.

  2. A user authorizes the client, creating a grant:{userId}:{grantId} entry that includes:

    • The hashed authorization code in the authCodeId field
    • PKCE code challenge and method (if PKCE is used)
    • A new AES-256 encryption key is generated specifically for this grant
    • The props data 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
  3. 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 authCodeId hash is retained so a replay of the same code can be verified before any action is taken; the authCodeWrappedKey and PKCE fields are removed from the grant. The absence of authCodeWrappedKey (with authCodeId still set) marks the code as already used
    • A refresh token is generated and its hash is stored in the grant's refreshTokenId field
    • 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
  4. 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
  5. Access tokens expire automatically after their TTL.

  6. 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
  7. 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

Implementation Notes

  • 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 via deleteClient(), 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).