Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 47 additions & 6 deletions rust/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use cli_engine::{
};

use crate::environments::{self, ResolvedEnv};
use crate::pat::{self, PatEntry};

/// Single auth provider that dispatches to env-specific PKCE providers.
///
Expand Down Expand Up @@ -49,6 +50,25 @@ fn build_provider(env: &ResolvedEnv) -> PkceAuthProvider {
.with_redirect_uri(environments::REDIRECT_URI)
}

/// Build a `Credential` from a PAT entry.
///
/// The PAT is opaque to the CLI; the gateway exchanges it for an OAuth access
/// token. `identity` is set to a display name since the token itself carries no
/// readable subject claim.
Comment thread
jpage-godaddy marked this conversation as resolved.
fn pat_credential(env: &str, entry: &PatEntry) -> Credential {
Credential {
token: entry.token.clone(),
provider: pat::PROVIDER.to_owned(),
env: env.to_owned(),
identity: if entry.name.is_empty() {
"PAT".to_owned()
} else {
format!("PAT ({})", entry.name)
},
..Credential::default()
}
}

/// Emit (at debug level) the OAuth parameters that will be used for login and
/// the code→token exchange. cli-engine builds the actual token request, so this
/// is the CLI's single point of visibility into the client id / endpoints that
Expand Down Expand Up @@ -91,23 +111,37 @@ impl AuthProvider for GoDaddyAuthProvider {
}

async fn get_credential(&self, env: &str, command: &str, tier: &str) -> Result<Credential> {
if let Some(entry) = pat::resolve_pat(env).await {
return Ok(pat_credential(env, &entry));
}
let provider = self.provider_for(env)?;
provider.get_credential(env, command, tier).await
}

async fn get_credential_for(&self, req: &CredentialRequest<'_>) -> Result<Credential> {
// Forward to the env's PKCE provider, which performs OAuth scope step-up
// when the cached token lacks the command's required scopes.
// PATs have fixed scopes; the gateway enforces them. Use a PAT when one is
// configured, otherwise fall back to PKCE OAuth scope step-up.
if let Some(entry) = pat::resolve_pat(req.env).await {
return Ok(pat_credential(req.env, &entry));
}
let provider = self.provider_for(req.env)?;
provider.get_credential_for(req).await
}

async fn status(&self, env: &str) -> Result<Credential> {
if let Some(entry) = pat::resolve_pat(env).await {
return Ok(pat_credential(env, &entry));
}
let provider = self.provider_for(env)?;
provider.status(env).await
}

async fn logout(&self, env: &str) -> Result<()> {
// Remove any stored PAT for the environment; ignore errors because the
// OAuth logout that follows is authoritative and PAT may not exist.
if let Err(err) = pat::delete_pat(env).await {
tracing::debug!(env, error = %err, "ignoring PAT delete error during logout");
}
Comment thread
jpage-godaddy marked this conversation as resolved.
let provider = self.provider_for(env)?;
provider.logout(env).await
}
Expand All @@ -119,13 +153,20 @@ impl AuthProvider for GoDaddyAuthProvider {
// warning) on a malformed local config, so this never fails wholesale.
let listable =
environments::listable().map_err(|e| CliCoreError::message(e.to_string()))?;
let mut envs = Vec::new();
let mut envs = std::collections::BTreeSet::new();
match pat::registry_envs().await {
Ok(pats) => envs.extend(pats),
Err(err) => {
tracing::warn!(
error = %err,
"failed to load PAT registry while listing environments; continuing"
);
}
}
for resolved in listable {
Comment thread
jpage-godaddy marked this conversation as resolved.
let provider = build_provider(&resolved);
envs.extend(provider.list_environments().await.unwrap_or_default());
}
envs.sort();
envs.dedup();
Ok(envs)
Ok(envs.into_iter().collect())
}
}
4 changes: 3 additions & 1 deletion rust/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod environments;
mod extension;
mod hosting;
mod output_schema;
mod pat;
mod payments;
mod quote_cache;
mod scopes;
Expand Down Expand Up @@ -45,7 +46,7 @@ async fn main() -> ExitCode {
• hosting — manage Node.js PaaS applications (create, upload, deploy)\n \
• payments — manage the payment methods used for purchases\n\
\n\
Most commands need authentication; run `gddy auth login` first (or just run a\n\
Most commands need authentication; run `gddy auth login` first, or use a PAT via `gddy pat add` / `GDDY_PAT` for non-interactive workflows (or just run a\n\
command and follow the prompt). Use `--env` to target an environment and\n\
`gddy tree` to see every command. New here? Try `gddy domain available <name>`.",
)
Expand Down Expand Up @@ -93,6 +94,7 @@ async fn main() -> ExitCode {
.with_module(domain::module())
.with_module(env::module())
.with_module(hosting::module())
.with_module(pat::module())
.with_module(payments::module())
.with_module(webhook::module()),
);
Expand Down
108 changes: 108 additions & 0 deletions rust/src/pat/guides/auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# GoDaddy CLI authentication

The GoDaddy CLI supports two ways to authenticate:

- **OAuth via `gddy auth login`** — interactive, browser-based login. Best for local development.
- **Personal Access Token (PAT) via `gddy pat add`** — non-interactive, long-lived token. Best for CI/CD, scripts, and headless environments.

Both can be configured at the same time. Per environment, the CLI uses a PAT when one is available; otherwise it falls back to OAuth and opens a browser.

## Interactive OAuth

Run:

```sh
gddy auth login --env prod
```

Your browser opens to the GoDaddy login screen. After you approve the CLI, an access token is stored in your OS keychain (or a protected file fallback) and reused until it expires. If a command needs wider scopes, the CLI may re-prompt through the browser.

## Personal Access Tokens

### Creating a PAT

1. Sign in to the GoDaddy Developer Portal.
2. Open **Personal Access Tokens**.
3. Choose a name, the scopes you need, and an expiration (up to 365 days).
4. Copy the plaintext token immediately — it is shown only once.

The token looks like:

```text
gd_pat_<base62>_<8-hex-crc>
```

### Storing a PAT in the CLI

Read the token from stdin so it does not appear in shell history:

```sh
echo 'gd_pat_...' | gddy pat add --env prod "CI token"
```

Alternatively, pass it explicitly:

```sh
gddy pat add --env prod --token 'gd_pat_...' "CI token"
```

PATs are saved to your platform's `gddy` configuration directory (e.g. `~/.config/gddy/pat.toml` on Linux) with owner-only permissions where the platform supports it (`0600`).

### Listing and removing PATs

List stored PATs (only the last four characters are shown):

```sh
gddy pat list
```

Remove the PAT for an environment:

```sh
gddy pat remove --env prod
```

Removing the PAT from the CLI does **not** revoke it in the Developer Portal.

### Using PATs in CI

Instead of storing a PAT in the registry, supply it through an environment variable:

```sh
export GDDY_PAT=gd_pat_...
gddy domain list --env prod
```

For per-environment tokens:

```sh
export GDDY_PAT_PROD=gd_pat_...
export GDDY_PAT_OTE=gd_pat_...
gddy domain list --env prod
```

Per-environment variables take precedence over `GDDY_PAT`.

### How the CLI chooses a credential

For each command, the CLI resolves credentials in this order:

1. `GDDY_PAT_<ENV>` environment variable
2. `GDDY_PAT` environment variable
3. Stored PAT for the environment in the `gddy` configuration directory (e.g. `~/.config/gddy/pat.toml` on Linux)
4. OAuth token from `gddy auth login` (browser flow)

If a PAT is configured, the CLI sends it as:

```text
Authorization: Bearer gd_pat_...
```

The GoDaddy API gateway exchanges the PAT for a short-lived access token and enforces the PAT's scopes. If a command needs scopes the PAT does not grant, the API returns `403` and the CLI surfaces that error.

## Security notes

- Treat PATs like passwords. Do not commit them to source control.
- Prefer `GDDY_PAT_<ENV>` environment variables in CI over storing PATs in the registry file.
- Regenerate leaked or suspected PATs in the Developer Portal immediately.
- The CLI validates PAT format before storing, but does **not** contact the API to verify a PAT is still active.
Loading