Client Credentials authentication implements the standard OAuth2 client credentials grant flow. It’s a straightforward method that uses a client ID and secret to obtain access tokens.
This method is ideal for server-to-server communication in trusted environments where both client and server are under your control.
How It Works
The Client Credentials flow follows this process:
Generate Client Credentials
Create a service user in Zitadel and generate a client secret, which gives you a client ID and client secret.
SDK Initialization
Initialize the SDK with your client ID and secret.
Token Request
The SDK makes a POST request to Zitadel’s token endpoint with:
Grant type: client_credentials
Client ID and secret
Requested scopes
Token Response
Zitadel validates the credentials and returns an access token with expiration time.
Automatic Refresh
The SDK automatically requests a new token when the current one expires.
Setup
1. Create Service User and Generate Secret
In your Zitadel instance:
Navigate to your project
Create a new service user or select an existing one
Generate a client secret
Save the client ID and secret securely
The client secret is only shown once. Save it immediately in a secure location.
2. Store Credentials Securely
Never hardcode credentials in your source code. Use environment variables:
ZITADEL_HOST = https://api.zitadel.example.com
ZITADEL_CLIENT_ID = 234567890123456789@myproject
ZITADEL_CLIENT_SECRET = dEnT7H7UfG1yHUzMGxPDnXvALn0fJE2WgQRjxHC...
Add .env to your .gitignore:
3. Initialize the SDK
Basic Usage
Using Environment Variables
With Debug Logging
require 'zitadel-client'
client = Zitadel :: Client :: Zitadel . with_client_credentials (
"https://api.zitadel.example.com" ,
"234567890123456789@myproject" ,
"dEnT7H7UfG1yHUzMGxPDnXvALn0fJE2WgQRjxHC..."
)
# Make API calls
response = client. users . add_human_user (
Zitadel :: Client :: UserServiceAddHumanUserRequest . new (
username: "john.doe" ,
profile: Zitadel :: Client :: UserServiceSetHumanProfile . new (
given_name: 'John' ,
family_name: 'Doe'
),
email: Zitadel :: Client :: UserServiceSetHumanEmail . new (
email: "[email protected] "
)
)
)
Advanced Configuration
For advanced scenarios, use the builder pattern to customize OAuth parameters:
require 'zitadel-client'
# Build authenticator with custom scopes
authenticator = Zitadel :: Client :: Auth :: ClientCredentialsAuthenticator
. builder (
"https://api.zitadel.example.com" ,
ENV [ 'ZITADEL_CLIENT_ID' ],
ENV [ 'ZITADEL_CLIENT_SECRET' ]
)
. scopes (
'openid' ,
'urn:zitadel:iam:org:project:id:zitadel:aud' ,
'urn:zitadel:iam:org:projects:roles'
)
. build
client = Zitadel :: Client :: Zitadel . new (authenticator)
Builder Methods
The ClientCredentialsAuthenticatorBuilder provides these configuration methods:
Method Description Default scopes(*scopes)OAuth scopes to request ['openid', 'urn:zitadel:iam:org:project:id:zitadel:aud']
Implementation Details
Token Request
The SDK makes a token request like this:
POST /oauth/v2/token HTTP / 1.1
Host : api.zitadel.example.com
Content-Type : application/x-www-form-urlencoded
grant_type=client_credentials&
client_id=234567890123456789@myproject&
client_secret=dEnT7H7UfG1yHUzMGxPDnXvALn0fJE2WgQRjxHC...&
scope=openid+urn:zitadel:iam:org:project:id:zitadel:aud
Token Response
Zitadel responds with:
{
"access_token" : "MtjHodGy4zxKylDOhbRNvlBBewLOYvXz5..." ,
"token_type" : "Bearer" ,
"expires_in" : 43200
}
The SDK stores this token and automatically refreshes it before expiration.
Thread Safety
The ClientCredentialsAuthenticator is thread-safe:
Token refresh is protected by a mutex
Multiple threads can safely share the same client instance
Automatic refresh happens when the token expires
require 'zitadel-client'
# Safe to share across threads
client = Zitadel :: Client :: Zitadel . with_client_credentials (
ENV [ 'ZITADEL_HOST' ],
ENV [ 'ZITADEL_CLIENT_ID' ],
ENV [ 'ZITADEL_CLIENT_SECRET' ]
)
threads = 10 . times . map do
Thread . new do
# All threads safely share the same client
client. settings . get_general_settings
end
end
threads. each ( & :join )
Security Best Practices
Secret Management
Never commit client secrets to version control!
Use environment variables:
# ✅ Good - Uses environment variables
client = Zitadel :: Client :: Zitadel . with_client_credentials (
ENV [ 'ZITADEL_HOST' ],
ENV [ 'ZITADEL_CLIENT_ID' ],
ENV [ 'ZITADEL_CLIENT_SECRET' ]
)
# ❌ Bad - Hardcoded credentials
client = Zitadel :: Client :: Zitadel . with_client_credentials (
"https://api.zitadel.example.com" ,
"234567890123456789@myproject" ,
"dEnT7H7UfG1yHUzMGxPDnXvALn0fJE2WgQRjxHC..."
)
Secret Rotation
Regularly rotate your client secrets:
Generate New Secret
Create a new client secret in Zitadel console.
Update Configuration
Update environment variables with the new secret.
Deploy Changes
Deploy updated configuration to your applications.
Deactivate Old Secret
After confirming the new secret works, deactivate the old one in Zitadel.
Scope Restriction
Request only the scopes your application needs:
# ✅ Good - Minimal scopes
authenticator = Zitadel :: Client :: Auth :: ClientCredentialsAuthenticator
. builder (host, client_id, client_secret)
. scopes ( 'openid' , 'urn:zitadel:iam:org:project:id:zitadel:aud' )
. build
# ⚠️ Avoid - Unnecessary scopes increase risk
authenticator = Zitadel :: Client :: Auth :: ClientCredentialsAuthenticator
. builder (host, client_id, client_secret)
. scopes ( 'openid' , 'profile' , 'email' , 'offline_access' )
. build
Multiple Environments
Use different credentials for each environment:
require 'zitadel-client'
case ENV [ 'RAILS_ENV' ]
when 'production'
client = Zitadel :: Client :: Zitadel . with_client_credentials (
ENV [ 'ZITADEL_PROD_HOST' ],
ENV [ 'ZITADEL_PROD_CLIENT_ID' ],
ENV [ 'ZITADEL_PROD_CLIENT_SECRET' ]
)
when 'staging'
client = Zitadel :: Client :: Zitadel . with_client_credentials (
ENV [ 'ZITADEL_STAGING_HOST' ],
ENV [ 'ZITADEL_STAGING_CLIENT_ID' ],
ENV [ 'ZITADEL_STAGING_CLIENT_SECRET' ]
)
else
client = Zitadel :: Client :: Zitadel . with_client_credentials (
ENV [ 'ZITADEL_DEV_HOST' ],
ENV [ 'ZITADEL_DEV_CLIENT_ID' ],
ENV [ 'ZITADEL_DEV_CLIENT_SECRET' ]
)
end
Using Secret Management Services
For production deployments, use dedicated secret management:
AWS Secrets Manager
HashiCorp Vault
require 'aws-sdk-secretsmanager'
require 'zitadel-client'
require 'json'
# Fetch from AWS Secrets Manager
secrets_client = Aws :: SecretsManager :: Client . new
response = secrets_client. get_secret_value ( secret_id: 'zitadel/credentials' )
creds = JSON . parse (response. secret_string )
client = Zitadel :: Client :: Zitadel . with_client_credentials (
creds[ 'host' ],
creds[ 'client_id' ],
creds[ 'client_secret' ]
)
Troubleshooting
Invalid Client Credentials
Error:
ZitadelError: Failed to refresh token: invalid_client
Solutions:
Verify the client ID is correct
Verify the client secret is correct and hasn’t expired
Check that the service user exists in Zitadel
Ensure the client secret hasn’t been deactivated
Unauthorized Client
Error:
ZitadelError: Failed to refresh token: unauthorized_client
Solutions:
Verify the service user has the required permissions
Check that the client credentials grant is enabled for the service user
Ensure the service user belongs to the correct project
Invalid Scope
Error:
ZitadelError: Failed to refresh token: invalid_scope
Solutions:
Verify the requested scopes are valid
Check that the service user has access to the requested scopes
Remove any custom scopes that aren’t configured in Zitadel
Connection Issues
Error:
ZitadelError: Failed to refresh token: Connection refused
Solutions:
Verify the Zitadel host URL is correct
Check network connectivity to Zitadel
Ensure HTTPS is used (not HTTP)
Verify firewall rules allow outbound HTTPS
Debug Token Issues
Enable debug logging to see detailed token exchange:
client = Zitadel :: Client :: Zitadel . with_client_credentials (
ENV [ 'ZITADEL_HOST' ],
ENV [ 'ZITADEL_CLIENT_ID' ],
ENV [ 'ZITADEL_CLIENT_SECRET' ]
) do | config |
config. debug = true
end
# This will log:
# - Token requests and responses
# - Token refresh events
# - HTTP details
Comparison with Other Methods
Feature Client Credentials Private Key JWT PAT Setup Complexity Low Medium Very Low Security High Highest Medium Secret Management Client secret Private key file Token string Token Refresh Automatic Automatic Manual Production Ready Yes Yes No Rotation Generate new secret Generate new key Generate new token
Source Code Reference
The implementation can be found in:
Main entry point: lib/zitadel/client/zitadel.rb:106-112
Authenticator: lib/zitadel/client/auth/client_credentials_authenticator.rb
Builder: lib/zitadel/client/auth/client_credentials_authenticator.rb:42-61
Base OAuth: lib/zitadel/client/auth/o_auth_authenticator.rb
Next Steps
Private Key JWT Higher security for production
Personal Access Tokens Quick development setup
Additional Resources