Skip to main content
Deprecated: Please move to the corresponding endpoints under OIDC Service v2. This service will be removed in the next major version of ZITADEL.

Overview

The BetaWebKeyServiceApi provides methods for managing web key pairs used to sign and verify OIDC tokens (ID tokens, access tokens).

Initialization

require 'zitadel/client'

client = Zitadel::Client::ApiClient.new
client.config.access_token = 'your_access_token'

webkey_service = Zitadel::Client::Api::BetaWebKeyServiceApi.new(client)
Web key management typically requires instance administrator permissions.

Key Concepts

  • Web Key: A public/private key pair for signing OIDC tokens
  • Active Key: The key currently used to sign new tokens
  • Initial Key: Newly created key, published but not yet active
  • Inactive Key: Previously active key, can still verify old tokens

Key Methods

Key Creation

beta_web_key_service_create_web_key_request
object
required
Key generation parameters
# Create RSA key (default: 2048 bits, SHA256)
request = Zitadel::Client::Models::BetaWebKeyServiceCreateWebKeyRequest.new(
  config: {
    rsa: {
      bits: 'RSA_BITS_2048',
      hasher: 'RSA_HASHER_SHA256'
    }
  }
)

response = webkey_service.create_web_key(request)

puts "Key ID: #{response.details.key_id}"
puts "State: #{response.details.state}" # STATE_INITIAL
puts "Algorithm: #{response.details.algorithm}"
Required Permission: iam.web_key.writeReturns: BetaWebKeyServiceCreateWebKeyResponse
The JWKs endpoint (/.well-known/openid-configuration) is cached. Wait for cache expiration (default 5min) before activating new keys.

Key Activation

beta_web_key_service_activate_web_key_request
object
required
Key activation request
request = Zitadel::Client::Models::BetaWebKeyServiceActivateWebKeyRequest.new(
  key_id: 'key_123456789'
)

response = webkey_service.activate_web_key(request)

puts "Key activated: #{response.details.key_id}"
puts "New state: #{response.details.state}" # STATE_ACTIVE
  • Only one key can be active at a time
  • Previously active key becomes inactive
  • Do not activate keys created within cache duration (5min default)
  • Ensure public key has propagated to clients before activation
Required Permission: iam.web_key.writeReturns: BetaWebKeyServiceActivateWebKeyResponse

Key Listing

Returns all web keys and their current states.
request = {}

response = webkey_service.list_web_keys(request)

response.web_keys.each do |key|
  puts "Key ID: #{key.key_id}"
  puts "  Algorithm: #{key.algorithm}"
  puts "  State: #{key.state}"
  puts "  Created: #{key.creation_date}"
  puts "  Activated: #{key.activation_date}"
end
Required Permission: iam.web_key.readReturns: BetaWebKeyServiceListWebKeysResponse

Key Deletion

beta_web_key_service_delete_web_key_request
object
required
Key deletion request
request = Zitadel::Client::Models::BetaWebKeyServiceDeleteWebKeyRequest.new(
  key_id: 'key_123456789'
)

response = webkey_service.delete_web_key(request)

puts "Key deleted at: #{response.details.change_date}"
  • Only inactive keys can be deleted
  • Active keys must be deactivated first (by activating another key)
  • Tokens signed with deleted keys will be invalid
  • JWKs endpoint is cached - tokens may validate until cache expires
Required Permission: iam.web_key.deleteReturns: BetaWebKeyServiceDeleteWebKeyResponse

Key Algorithms

RSA Keys

config: {
  rsa: {
    bits: 'RSA_BITS_2048', # or RSA_BITS_3072, RSA_BITS_4096
    hasher: 'RSA_HASHER_SHA256' # or RSA_HASHER_SHA384, RSA_HASHER_SHA512
  }
}
Recommended: RSA 2048 with SHA256 for most use cases

ECDSA Keys

config: {
  ecdsa: {
    curve: 'ECDSA_CURVE_P256' # or P384, P521
  }
}
Best for: Performance-sensitive applications, smaller key size

EdDSA Keys

config: {
  ed25519: {} # No additional config needed
}
Best for: Modern applications, excellent performance and security

Web Key Lifecycle

1. Key Creation

# Generate new key
request = Zitadel::Client::Models::BetaWebKeyServiceCreateWebKeyRequest.new(
  config: {
    rsa: {
      bits: 'RSA_BITS_2048',
      hasher: 'RSA_HASHER_SHA256'
    }
  }
)

key = webkey_service.create_web_key(request)
puts "Created key: #{key.details.key_id} (state: INITIAL)"

2. Wait for Cache

# Wait for JWKs cache to expire (default 5 minutes)
sleep(300)

# Or check cache-control headers from JWKs endpoint
# GET https://your-instance.zitadel.cloud/.well-known/openid-configuration/jwks

3. Activate New Key

# Activate the new key
request = Zitadel::Client::Models::BetaWebKeyServiceActivateWebKeyRequest.new(
  key_id: key.details.key_id
)

webkey_service.activate_web_key(request)
puts "Key activated - now signing new tokens"

4. Monitor Old Key Usage

# Old key is now inactive but still validates existing tokens
# Monitor token expiration times
# Delete old key after all tokens have expired

5. Delete Old Key

# After token lifetime has passed
request = Zitadel::Client::Models::BetaWebKeyServiceDeleteWebKeyRequest.new(
  key_id: old_key_id
)

webkey_service.delete_web_key(request)
puts "Old key deleted"

Use Cases

Key Rotation

def rotate_signing_key
  # 1. Create new key
  puts "Creating new signing key..."
  create_req = Zitadel::Client::Models::BetaWebKeyServiceCreateWebKeyRequest.new(
    config: {
      rsa: {
        bits: 'RSA_BITS_2048',
        hasher: 'RSA_HASHER_SHA256'
      }
    }
  )
  
  new_key = webkey_service.create_web_key(create_req)
  new_key_id = new_key.details.key_id
  
  puts "New key created: #{new_key_id}"
  puts "Waiting for JWKs cache to expire (5 minutes)..."
  
  # 2. Wait for cache expiration
  sleep(300)
  
  # 3. Get current active key
  list_req = {}
  keys = webkey_service.list_web_keys(list_req)
  active_key = keys.web_keys.find { |k| k.state == 'STATE_ACTIVE' }
  
  # 4. Activate new key (deactivates old one)
  puts "Activating new key..."
  activate_req = Zitadel::Client::Models::BetaWebKeyServiceActivateWebKeyRequest.new(
    key_id: new_key_id
  )
  
  webkey_service.activate_web_key(activate_req)
  
  puts "Key rotation complete!"
  puts "Old key (#{active_key&.key_id}) is now inactive"
  puts "New key (#{new_key_id}) is now active"
  
  # 5. Schedule old key deletion
  # Delete after token lifetime (e.g., 24 hours)
  puts "Schedule deletion of old key after token expiration"
  
  new_key_id
end

List Active and Inactive Keys

def list_keys_by_state
  request = {}
  response = webkey_service.list_web_keys(request)
  
  keys = {
    active: [],
    inactive: [],
    initial: []
  }
  
  response.web_keys.each do |key|
    info = {
      key_id: key.key_id,
      algorithm: key.algorithm,
      created: key.creation_date
    }
    
    case key.state
    when 'STATE_ACTIVE'
      keys[:active] << info
    when 'STATE_INACTIVE'
      keys[:inactive] << info
    when 'STATE_INITIAL'
      keys[:initial] << info
    end
  end
  
  keys
end

# Usage
keys = list_keys_by_state
puts "Active keys: #{keys[:active].size}"
puts "Inactive keys: #{keys[:inactive].size}"
puts "Initial keys: #{keys[:initial].size}"

Cleanup Old Inactive Keys

def cleanup_old_keys(max_age_days: 30)
  request = {}
  response = webkey_service.list_web_keys(request)
  
  cutoff_date = Time.now - (max_age_days * 24 * 60 * 60)
  
  response.web_keys.each do |key|
    # Only delete inactive keys older than cutoff
    if key.state == 'STATE_INACTIVE'
      key_date = Time.parse(key.deactivation_date || key.creation_date)
      
      if key_date < cutoff_date
        delete_req = Zitadel::Client::Models::BetaWebKeyServiceDeleteWebKeyRequest.new(
          key_id: key.key_id
        )
        
        webkey_service.delete_web_key(delete_req)
        puts "Deleted old key: #{key.key_id} (deactivated: #{key_date})"
      end
    end
  end
end

Verify Key State Before Token Generation

def verify_active_key_exists
  request = {}
  response = webkey_service.list_web_keys(request)
  
  active_key = response.web_keys.find { |k| k.state == 'STATE_ACTIVE' }
  
  if active_key
    puts "Active signing key: #{active_key.key_id}"
    puts "  Algorithm: #{active_key.algorithm}"
    puts "  Activated: #{active_key.activation_date}"
    true
  else
    puts "ERROR: No active signing key found!"
    puts "Tokens cannot be issued without an active key."
    false
  end
end

Automated Key Rotation

def schedule_key_rotation(rotation_days: 90)
  request = {}
  response = webkey_service.list_web_keys(request)
  
  active_key = response.web_keys.find { |k| k.state == 'STATE_ACTIVE' }
  
  if active_key
    activation_date = Time.parse(active_key.activation_date)
    rotation_date = activation_date + (rotation_days * 24 * 60 * 60)
    
    if Time.now >= rotation_date
      puts "Active key is #{((Time.now - activation_date) / 86400).to_i} days old"
      puts "Rotation needed! Creating new key..."
      
      rotate_signing_key
    else
      days_until_rotation = ((rotation_date - Time.now) / 86400).to_i
      puts "Next rotation in #{days_until_rotation} days"
    end
  else
    puts "No active key - creating initial key..."
    create_and_activate_initial_key
  end
end

Key States

STATE_INITIAL

  • Newly created key
  • Published in JWKs endpoint
  • Not yet used for signing
  • Can be activated

STATE_ACTIVE

  • Currently signing all new tokens
  • Only one key can be active
  • Published in JWKs endpoint
  • Cannot be deleted

STATE_INACTIVE

  • Previously active key
  • Can still verify old tokens
  • Published in JWKs endpoint (for verification)
  • Can be deleted

Public Key Distribution

Public keys are automatically published to:
https://your-instance.zitadel.cloud/.well-known/openid-configuration/jwks
Applications use this endpoint to verify token signatures.

Cache Behavior

  • Response is cached (default: 5 minutes)
  • Cache-Control header indicates duration
  • Keys may not be immediately available after creation
  • Wait for cache expiration before activating new keys

Key Rotation Best Practices

  • Rotate keys every 90 days minimum
  • More frequent for high-security environments
  • Coordinate with token lifetime
  • Keep old keys for at least the token lifetime
  • Example: If tokens expire in 24 hours, keep old keys for 48+ hours
  • This allows existing tokens to remain valid
  • Create new key at least 5 minutes before activation
  • Allows public key to propagate to caches
  • Prevents validation failures
  • Monitor key age
  • Alert when rotation is due
  • Track token validation failures
  • Audit key creation/deletion

Algorithm Selection

config: {
  rsa: {
    bits: 'RSA_BITS_2048',
    hasher: 'RSA_HASHER_SHA256'
  }
}
Pros: Widely supported, industry standard
Cons: Larger key and signature size
Best for: General use, maximum compatibility

ECDSA

config: {
  ecdsa: {
    curve: 'ECDSA_CURVE_P256'
  }
}
Pros: Smaller keys, good performance
Cons: Less universal support
Best for: Modern applications, mobile apps

EdDSA (Ed25519)

config: {
  ed25519: {}
}
Pros: Excellent performance, strong security, small signatures
Cons: Requires modern JWT libraries
Best for: New applications, high-performance requirements

Security Considerations

Important Security Notes:
  • Never export or share private keys
  • Rotate keys regularly (every 90 days minimum)
  • Delete old keys after grace period
  • Monitor for unauthorized key operations
  • Use strongest algorithm your clients support
  • Maintain audit trail of key operations

Troubleshooting

Tokens Not Validating

# Check if active key exists
keys = webkey_service.list_web_keys({})
active = keys.web_keys.find { |k| k.state == 'STATE_ACTIVE' }

if active.nil?
  puts "No active key! Create and activate a key."
else
  puts "Active key: #{active.key_id}"
  puts "Check if clients have cached old JWKs"
end

Cannot Delete Key

# Error: Key is active
# Solution: Activate another key first

# Create new key
new_key = webkey_service.create_web_key(...)

# Wait for cache
sleep(300)

# Activate new key (deactivates old one)
webkey_service.activate_web_key(
  key_id: new_key.details.key_id
)

# Now delete old key
webkey_service.delete_web_key(
  key_id: old_key_id
)

Required Permissions

  • iam.web_key.read - List and view web keys
  • iam.web_key.write - Create and activate web keys
  • iam.web_key.delete - Delete web keys

Migration Guide

To migrate to OIDC Service v2:
  1. Replace BetaWebKeyServiceApi with appropriate v2 endpoint
  2. Review key management changes
  3. Test token signing and validation
  4. Update key rotation scripts

See Also

Build docs developers (and LLMs) love