Pup supports OAuth2 authentication with PKCE (Proof Key for Code Exchange) for secure, browser-based authentication with Datadog. This is the recommended authentication method as it provides better security and granular access control compared to API keys.
- PKCE Protection (S256): Prevents authorization code interception attacks
- Dynamic Client Registration (DCR): Each CLI installation gets unique credentials
- CSRF Protection: State parameter validation prevents cross-site request forgery
- Secure Token Storage: Tokens stored in the OS keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service); falls back to a JSON file under
~/.config/pup/with0600permissions when no keychain is available - Automatic Token Refresh: Seamless token refresh before expiration
- No Hardcoded Credentials: No need to manage long-lived API keys
- Granular Revocation: Revoke access for one installation without affecting others
- Scope-Based Permissions: Request only necessary OAuth scopes
- User Context: Actions performed as the authenticated user
- Better Audit Trail: OAuth tokens provide clearer audit logs
pup auth loginThis will:
- Register a new OAuth client with Datadog (if first time)
- Generate PKCE challenge and state parameter
- Open your browser to Datadog's authorization page
- Start a local callback server on
http://127.0.0.1:<random-port>/callback - Wait for you to approve the requested scopes
- Exchange the authorization code for access/refresh tokens
- Store tokens securely (OS keychain, or JSON file under
~/.config/pup/with0600permissions when no keychain is available)
pup auth statusShows your current authentication status including:
- Whether you're authenticated
- Token expiration time
- Site you're authenticated with
pup auth refreshManually refresh your access token using the refresh token. This happens automatically when making API calls, but you can force it with this command.
pup auth logoutClears all stored tokens and client credentials for the current site.
┌─────────┐ ┌──────────┐
│ User │ │ Datadog │
│ CLI │ │ OAuth │
└────┬────┘ └────┬─────┘
│ │
│ 1. Check for existing client credentials │
│─────────────────────────────────────────> │
│ │
│ 2. Register new client (if needed - DCR) │
│─────────────────────────────────────────> │
│ <────────────────────────────────────────│
│ client_id, client_secret │
│ │
│ 3. Generate PKCE challenge & state │
│─────────────────┐ │
│ │ │
│ <───────────────┘ │
│ │
│ 4. Start local callback server │
│─────────────────┐ │
│ │ │
│ <───────────────┘ │
│ │
│ 5. Open browser with authorization URL │
│─────────────────────────────────────────> │
│ │
│ 6. User approves scopes │
│ │
│ 7. Redirect to callback with auth code │
│ <────────────────────────────────────────│
│ │
│ 8. Exchange code for tokens (with PKCE) │
│─────────────────────────────────────────> │
│ <────────────────────────────────────────│
│ access_token, refresh_token │
│ │
│ 9. Store tokens securely │
│─────────────────┐ │
│ │ │
│ <───────────────┘ │
│ │
Based on RFC 7591, each CLI installation registers as a unique OAuth client:
{
"client_name": "Datadog Pup CLI",
"redirect_uris": ["http://127.0.0.1:<port>/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "client_secret_post"
}Response includes:
client_id: Unique client identifierclient_secret: Client secret for token exchange- Stored in
~/.config/pup/client_<site>.json
Proof Key for Code Exchange prevents authorization code interception:
- Generate Code Verifier: 128-character random string
- Generate Code Challenge:
BASE64URL(SHA256(code_verifier)) - Include in Authorization: Send
code_challengeandcode_challenge_method=S256 - Include in Token Exchange: Send
code_verifierto prove possession
Tokens are stored in the OS keychain by default (macOS Keychain, Windows
Credential Manager, Linux Secret Service via the keyring crate). When a
keychain is unavailable, pup falls back to a JSON file at
~/.config/pup/tokens_<site>.json with 0600 permissions. Set
DD_TOKEN_STORAGE=file to force file storage.
The token payload is:
{
"access_token": "<token>",
"refresh_token": "<token>",
"token_type": "Bearer",
"expires_in": 3600,
"expires_at": "2024-02-04T12:00:00Z",
"scope": "dashboards_read dashboards_write ..."
}Pup requests OAuth scopes covering the read/write surface of supported
commands. The list below is illustrative — see
src/auth/ for the canonical, code-driven scope set:
dashboards_read- Read dashboardsdashboards_write- Create/update/delete dashboards
monitors_read- Read monitorsmonitors_write- Create/update monitorsmonitors_downtime- Manage downtimes
apm_read- Read APM data and traces
slos_read- Read SLOsslos_write- Create/update SLOsslos_corrections- Manage SLO corrections
incident_read- Read incidentsincident_write- Create/update incidents
synthetics_read- Read synthetic testssynthetics_write- Create/update/delete synthetic tests
security_monitoring_signals_read- Read security signalssecurity_monitoring_rules_read- Read security rulessecurity_monitoring_findings_read- Read security findings
rum_apps_read- Read RUM applicationsrum_apps_write- Manage RUM applications
hosts_read- Read host information
user_access_read- Read user access informationuser_self_profile_read- Read own user profile
cases_read- Read casescases_write- Create/update cases
events_read- Read events
logs_read_data- Read log datalogs_read_index_data- Read log index data
metrics_read- Read metricstimeseries_query- Query timeseries data
usage_read- Read usage data
Tokens are automatically refreshed when:
- Making an API call with an expired token
- Token is within 5 minutes of expiration
The refresh happens transparently in the background.
Force a token refresh:
pup auth refreshAccess tokens typically expire after 1 hour. The CLI:
- Checks expiration before each API call
- Automatically refreshes if needed
- Uses the refresh token (valid for 30 days)
- Re-prompts for login if refresh token expires
Pup supports all Datadog sites with separate credentials per site:
# US1 (default)
export DD_SITE="datadoghq.com"
pup auth login
# EU1
export DD_SITE="datadoghq.eu"
pup auth login
# US3
export DD_SITE="us3.datadoghq.com"
pup auth login
# US5
export DD_SITE="us5.datadoghq.com"
pup auth login
# AP1
export DD_SITE="ap1.datadoghq.com"
pup auth loginEach site maintains separate:
- Client credentials (
client_<site>.json) - Access/refresh tokens (
tokens_<site>.json)
If the browser doesn't open automatically:
⚠️ Could not open browser automatically
Please open this URL manually: https://datadoghq.com/oauth2/v1/authorize?...
Copy and paste the URL into your browser manually.
If you don't complete authorization within 5 minutes:
Error: timeout waiting for OAuth callback
Run pup auth login again to restart the flow.
If your access token expires and refresh fails:
⚠️ Token expired
Run 'pup auth refresh' to refresh or 'pup auth login' to re-authenticate
Try pup auth refresh first. If that fails, run pup auth login to start a new session.
The callback server scans [8000, 8080, 8888, 9000] and binds the first one that's free. If all four are busy, login fails with the list above.
When pup auth login runs inside an SSH-tunneled remote workspace, the operator typically forwards localhost ports to the laptop browser. To avoid forwarding all four candidate ports, pin one of the four DCR-registered ports with --callback-port (or PUP_OAUTH_CALLBACK_PORT):
ssh -L 8000:127.0.0.1:8000 workspace-host
PUP_OAUTH_CALLBACK_PORT=8000 pup auth login --org acme
# or per-invocation:
pup auth login --org acme --callback-port 8000The pinned value must be one of [8000, 8080, 8888, 9000] — those are the redirect URIs registered with the OAuth server during DCR, so any other port would be rejected at the authorize step regardless. Precedence is --callback-port > PUP_OAUTH_CALLBACK_PORT > the auto-scan default. When pinned, login fails fast if the port is already in use — there is no fallback, since a silent fallback would orphan the OAuth callback when the browser hits a port that isn't forwarded.
If you see a CSRF protection error:
Error: state parameter mismatch (CSRF protection)
This indicates a potential security issue. Run pup auth login again to start a fresh flow.
- Each installation gets unique
client_idandclient_secret - Stored in
~/.config/pup/client_<site>.jsonwith0600permissions - Never committed to version control
- Can be revoked individually without affecting other installations
- Access tokens are short-lived (1 hour)
- Refresh tokens are longer-lived (30 days)
- Stored with restricted file permissions
- Never logged or printed to console
- Automatically refreshed before expiration
- Prevents authorization code interception attacks
- Uses S256 (SHA256) code challenge method
- Code verifier is cryptographically random (128 characters)
- Never transmitted in the authorization request
- State parameter is cryptographically random (32 characters)
- Validated on callback to prevent cross-site request forgery
- New state generated for each authorization flow
| Feature | OAuth2 | API Keys |
|---|---|---|
| Setup | Browser login | Copy/paste keys |
| Security | Short-lived tokens | Long-lived keys |
| Revocation | Per-installation | Organization-wide |
| Scopes | Granular | All or nothing |
| Audit Trail | User-specific | Key-specific |
| Rotation | Automatic (refresh) | Manual |
| PKCE Protection | Yes | N/A |
| Token Storage | Secure local files | Environment variables |
~/.config/pup/
├── client_datadoghq_com.json # DCR client credentials
└── tokens_datadoghq_com.json # OAuth2 tokens
src/auth/
├── mod.rs # Auth module entry point
├── types.rs # Shared auth types
├── dcr.rs # Dynamic Client Registration
├── pkce.rs # PKCE code verifier/challenge generation
├── storage.rs # Token and credential storage (keychain + JSON file fallback)
└── callback.rs # Local callback server
- RFC 6749: OAuth 2.0 Authorization Framework
- RFC 7591: OAuth 2.0 Dynamic Client Registration Protocol
- RFC 7636: Proof Key for Code Exchange (PKCE)
- PR #84: Original TypeScript implementation reference
- Automatic token refresh background service
- Support for custom OAuth scopes
- OAuth2 device flow for headless environments