Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
20 changes: 7 additions & 13 deletions rust/src/pat/guides/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,10 @@ Your browser opens to the GoDaddy login screen. After you approve the CLI, an ac

### 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>
```
1. Sign in to the [Personal Access Token page](https://developer.godaddy.com/personal-access-token).
2. Click **+ Generate Token**.
3. In the **Generate personal access token** dialog, fill in a **Name**, an **Expiration** (in days), and the **Scopes** the token needs (see the [PAT scopes reference](https://developer.godaddy.com/en/docs/api-users/auth#pat-scopes) — e.g. `domains.domain:read`, `domains.dns:update`). A write-scoped token also satisfies reads for the same resource; a read-scoped token is refused on writes.
4. Click **Generate Token**. The token is shown once in a "Copy your new token" dialog — copy it immediately. You can't retrieve it again from the Personal Access Token page; if you lose it, revoke it and generate a new one.

### Storing a PAT in the CLI

Expand Down Expand Up @@ -62,7 +56,7 @@ Remove the PAT for an environment:
gddy pat remove --env prod
```

Removing the PAT from the CLI does **not** revoke it in the Developer Portal.
Removing the PAT from the CLI does **not** revoke it in the Developer Portal. To revoke it for real, go to the [Personal Access Token page](https://developer.godaddy.com/personal-access-token), click the trash icon next to the token, and confirm.

### Using PATs in CI

Expand Down Expand Up @@ -104,5 +98,5 @@ The GoDaddy API gateway exchanges the PAT for a short-lived access token and enf

- 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.
- Regenerate leaked PATs on the [Personal Access Token page](https://developer.godaddy.com/personal-access-token) immediately.
- The CLI only validates PAT format before storing, but it does **not** contact the API to verify the PAT is still active or has the scopes you need.
66 changes: 36 additions & 30 deletions rust/src/pat/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,7 @@ output_schema!(PatRemoveResult {
});

/// PAT prefix advertised by GoDaddy. A valid PAT starts with this string.
pub const PAT_PREFIX: &str = "gd_pat_";

/// The base portion of the advertised prefix, used when validating the
/// `gd_pat_<entropy>_<crc>` structure.
const PAT_PREFIX_BODY: &str = "gd_pat";
const PAT_PREFIX: &str = "gd_pat_";

/// Default env-var PAT applied to any environment.
pub const PAT_ENV_VAR: &str = "GDDY_PAT";
Expand Down Expand Up @@ -122,21 +118,17 @@ fn save_registry(path: &std::path::Path, registry: &PatRegistry) -> Result<(), C
registry.save(path)
}

/// Validate that `token` looks like a GoDaddy PAT: `gd_pat_<base62>_<8 hex crc>`.
/// Validate that `token` looks like a GoDaddy PAT. This only checks for the
/// `gd_pat_` prefix plus at least one character of content after it with no
/// embedded whitespace, so that we are not overly tied to the implementation
/// details of the token format while still rejecting untrimmed values (e.g.
/// `gd_pat_abc\n` from an env var or file with a trailing newline) that would
/// otherwise be sent as a garbage Bearer token.
#[must_use]
pub fn is_valid_pat(token: &str) -> bool {
let Some((body, crc)) = token.rsplit_once('_') else {
return false;
};
if crc.len() != 8 || !crc.chars().all(|c| c.is_ascii_hexdigit()) {
return false;
}
let Some((prefix, entropy)) = body.rsplit_once('_') else {
return false;
};
prefix == PAT_PREFIX_BODY
&& !entropy.is_empty()
&& entropy.chars().all(|c| c.is_ascii_alphanumeric())
token
.strip_prefix(PAT_PREFIX)
.is_some_and(|rest| !rest.is_empty() && !rest.chars().any(char::is_whitespace))
Comment thread
jpage-godaddy marked this conversation as resolved.
}
Comment thread
jpage-godaddy marked this conversation as resolved.

/// Returns the last four characters of a PAT. If the token is four characters or
Expand Down Expand Up @@ -350,9 +342,9 @@ fn add_command() -> RuntimeCommandSpec {
let name = string_arg(&ctx.args, "name");
let token = resolve_token_arg(&ctx.args).await?;
if !is_valid_pat(&token) {
return Err(CliCoreError::message(format!(
"token does not look like a GoDaddy PAT (expected `{PAT_PREFIX}...`); refusing to store it"
)));
return Err(CliCoreError::message(
"token doesn't look like a GoDaddy PAT; refusing to store it. Run `gddy guide auth` for details on creating PATs.",
));
}

let entry = PatEntry { token, name };
Expand Down Expand Up @@ -484,23 +476,37 @@ mod tests {
use super::*;

#[test]
fn rejects_malformed_pats() {
fn rejects_tokens_without_the_pat_prefix() {
assert!(!is_valid_pat(""));
assert!(!is_valid_pat("notapat"));
assert!(!is_valid_pat("gd_pat_"));
assert!(!is_valid_pat("gd_pat_abc")); // missing crc
assert!(!is_valid_pat("gd_pat_abc_xyz")); // crc too short
assert!(!is_valid_pat("gd_pat_abc_zzzzzzzz")); // crc not hex
assert!(!is_valid_pat("gd_pat_ab!c_12345678")); // invalid char in entropy
assert!(!is_valid_pat("gd_pat")); // missing trailing underscore
assert!(!is_valid_pat("gd_pat_")); // nothing after the prefix
}

#[test]
fn rejects_tokens_containing_whitespace_after_the_prefix() {
// An untrimmed env var or file (e.g. a trailing newline) should not be
// treated as valid — it would otherwise be sent as a garbage Bearer
// token. This covers both whitespace-only tails and whitespace
// embedded alongside real-looking content.
assert!(!is_valid_pat("gd_pat_ "));
assert!(!is_valid_pat("gd_pat_\n"));
assert!(!is_valid_pat("gd_pat_\t\t"));
assert!(!is_valid_pat("gd_pat_abc123\n"));
assert!(!is_valid_pat("gd_pat_ abc123"));
assert!(!is_valid_pat("gd_pat_abc 123"));
}

#[test]
fn accepts_well_formed_pat() {
fn accepts_any_non_empty_value_after_the_prefix() {
// The CLI does not assume a stable shape beyond the prefix, so these
// (including the exact strings from DEVEX-889's bug report) all pass.
assert!(is_valid_pat("gd_pat_abc123_1234abcd"));
assert!(is_valid_pat("gd_pat_aA0bB1cC2_abcdef12"));
// entropy may be long
assert!(is_valid_pat("gd_pat_1234567890abcdef1234567890abcdef"));
assert!(is_valid_pat("gd_pat_YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo="));
assert!(is_valid_pat(
"gd_pat_abcdefghijklmnopqrstuvwxyz0123456789_abcdef12"
"gd_pat_abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGH"
));
}

Expand Down