Skip to content

refactor(token): Read TTLs directly from cache files #542

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 18, 2025
Merged
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
231 changes: 77 additions & 154 deletions home-manager/_mixins/features/development/cg-tokens.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,6 @@ AUDIENCES=(
"apk.cgr.dev"
"cgr.dev"
)
# Global variables to store TTLs, populated by get_current_ttls
CURRENT_TOKEN_TTL_SEC=0
CURRENT_REFRESH_TTL_SEC=0
# Global variable to track date command type and label, populated by detect_date_type
DATE_TYPE=""
DATE_LABEL=""
# Default headless mode
HEADLESS=yes
# Default operation mode
Expand All @@ -52,142 +46,54 @@ OPTIONS:
EOF
}

# Detect date command type and set global DATE_TYPE variable
detect_date_type() {
if [ -z "$DATE_TYPE" ]; then
if date --help 2>&1 | grep -q "BusyBox v"; then
DATE_LABEL="BusyBox"
DATE_TYPE="busybox"
elif date --help 2>&1 | grep -q "coreutils"; then
DATE_LABEL="GNU Core Utilities"
DATE_TYPE="coreutils"
elif [[ "$(uname)" == "Darwin" ]]; then
DATE_LABEL="BSDCoreUtils (macOS)"
DATE_TYPE="bsd"
else
DATE_LABEL="Unknown"
DATE_TYPE="unknown"
fi
fi
}

# Helper function to try date parsing
try_date_parse() {
local cmd_args=("$@")
local ts=""

ts=$(LC_ALL=C "${cmd_args[@]}" 2>/dev/null)
if [[ "$ts" =~ ^[0-9]+$ ]]; then
echo "$ts"
# Get the OS-specific base directory for the Chainguard cache.
_get_cache_base_dir() {
if [[ "$(uname)" == "Darwin" ]]; then
echo "${HOME}/Library/Caches/chainguard"
else
echo ""
fi
}

# Convert a date string to a Unix timestamp
# Input date string format e.g., "2025-06-30 12:30:05 -0700 PDT"
tounix() {
local date_str="$1"
local ts=""
detect_date_type

# Strip timezone abbreviation (e.g., BST, PDT) as it can sometimes confuse date parsers
local cleaned_date_str="${date_str% [A-Z][A-Z][A-Z]}"

# Treat unknown date type like coreutils. Works for uutils.
if [[ "$DATE_TYPE" == "coreutils" ]] || [[ "$DATE_TYPE" == "unknown" ]]; then
ts=$(try_date_parse date --date="$cleaned_date_str" "+%s")
if [[ -z "$ts" ]]; then
# If stripping failed, try with the original string.
ts=$(try_date_parse date --date="$date_str" "+%s")
fi
elif [[ "$DATE_TYPE" == "busybox" ]]; then
ts=$(try_date_parse date -d "$cleaned_date_str" "+%s")
if [[ -z "$ts" ]]; then
# If stripping failed, try with the original string.
ts=$(try_date_parse date -d "$date_str" "+%s")
fi
elif [[ "$DATE_TYPE" == "bsd" ]]; then
# BSD date (macOS specific): requires -j and -f for parsing arbitrary date strings.
# Try original string, with full format, BSD needs exact format match
ts=$(try_date_parse date -jf "%Y-%m-%d %H:%M:%S %z %Z" "$date_str" "+%s")
if [[ -z "$ts" ]]; then
# Try cleaned string with simpler format (fallback for missing timezone)
ts=$(try_date_parse date -jf "%Y-%m-%d %H:%M:%S %z" "$cleaned_date_str" "+%s")
fi
fi

# If 'ts' is empty, catch and report an error.
if [[ -z "$ts" ]]; then
echo "✗ ERROR! Failed to parse date string '$date_str' to Unix timestamp." >&2
echo "⚑ DEBUG! Date parsing failed using: ${DATE_LABEL}" >&2
return 1
echo "${HOME}/.cache/chainguard"
fi
echo "$ts"
}

# Get the token TTL and populate CURRENT_TOKEN_TTL_SEC
get_token_ttl() {
# Get TTL from a token file.
_get_ttl_from_file() {
local audience="$1"
CURRENT_TOKEN_TTL_SEC=0
local now_ts
now_ts=$(date "+%s")
local token_filename="$2"
local ttl=0

local status_json
# Get token expiry from chainctl auth status. If the token has expired, this
# will trigger a re-authentication so the output can not be suppressed.
status_json=$(chainctl auth status --output=json --audience="$audience" || echo "{}")
local cache_base_dir
cache_base_dir=$(_get_cache_base_dir)

local expiry_date_str
expiry_date_str=$(echo "$status_json" | jq -r .expiry 2>/dev/null)
# Sanitize the audience to create a safe directory name, just as 'chainctl' does.
local safe_audience="${audience//\//-}"
local token_file="${cache_base_dir}/${safe_audience}/${token_filename}"

if [[ -n "$expiry_date_str" && "$expiry_date_str" != "null" ]]; then
local expiry_ts
expiry_ts=$(tounix "$expiry_date_str")
if [[ $? -eq 0 && -n "$expiry_ts" ]]; then
CURRENT_TOKEN_TTL_SEC=$((expiry_ts - now_ts))
# Ensure TTL is not negative
if [[ $CURRENT_TOKEN_TTL_SEC -lt 0 ]]; then CURRENT_TOKEN_TTL_SEC=0; fi
fi
fi
}
if [[ -f "$token_file" ]]; then
local token_data
token_data=$(cat "$token_file")
local expiry_ts_str=""

# Get the refresh TTL and populate CURRENT_REFRESH_TTL_SEC
get_refresh_ttl() {
local audience="$1"
CURRENT_REFRESH_TTL_SEC=0
local now_ts
now_ts=$(date "+%s")

# Cache directory for chainctl refresh tokens
local CHAINCTL_CACHE_DIR="${HOME}/.cache/chainguard"
if [[ "$(uname)" == "Darwin" ]]; then
CHAINCTL_CACHE_DIR="${HOME}/Library/Caches/chainguard"
fi
# Get refresh token expiry from cache file
# Sanitize audience for use in file path (replace / with -)
local audsafe="${audience//\//-}"
local refresh_token_file="${CHAINCTL_CACHE_DIR}/${audsafe}/refresh-token"
if [[ -f "$refresh_token_file" ]]; then
local refresh_data_b64
refresh_data_b64=$(cat "$refresh_token_file")
local refresh_exp_ts_str
# Suppress stderr from base64/jq if file is malformed
refresh_exp_ts_str=$(echo "$refresh_data_b64" | base64 -d 2>/dev/null | jq -r .exp 2>/dev/null)
# Decode the JWT payload and extract the 'exp' (expiration time) claim.
expiry_ts_str=$(echo "$token_data" | cut -d'.' -f2 | base64 -d 2>/dev/null | jq -r .exp 2>/dev/null)

if [[ -n "$refresh_exp_ts_str" && "$refresh_exp_ts_str" != "null" && "$refresh_exp_ts_str" =~ ^[0-9]+$ ]]; then
# The .exp field in refresh token is already a Unix timestamp
CURRENT_REFRESH_TTL_SEC=$((refresh_exp_ts_str - now_ts))
if [[ -n "$expiry_ts_str" && "$expiry_ts_str" != "null" && "$expiry_ts_str" =~ ^[0-9]+$ ]]; then
ttl=$((expiry_ts_str - $(date "+%s")))
# Ensure TTL is not negative
if [[ $CURRENT_REFRESH_TTL_SEC -lt 0 ]]; then CURRENT_REFRESH_TTL_SEC=0; fi
if [[ $ttl -lt 0 ]]; then ttl=0; fi
fi
fi
echo "$ttl"
}

# Get the token and refresh TTLs for a given audience.
# Outputs two space-separated values: <token_ttl> <refresh_ttl>
get_current_ttls() {
local audience="$1"
get_token_ttl "$audience"
get_refresh_ttl "$audience"
local token_ttl
token_ttl=$(_get_ttl_from_file "$audience" "oidc-token")
local refresh_ttl
refresh_ttl=$(_get_ttl_from_file "$audience" "refresh-token")
echo "$token_ttl $refresh_ttl"
}

logout_audience() {
Expand All @@ -201,33 +107,45 @@ logout_audience() {

refresh_audience() {
local audience="$1"
get_current_ttls "$audience"
# Determine if any token refresh is needed
if [[ $CURRENT_TOKEN_TTL_SEC -lt $TTL_THRESHOLD_SEC || $CURRENT_REFRESH_TTL_SEC -lt $TTL_THRESHOLD_SEC ]]; then
# If the refresh token's remaining life is less than the refresh threshold,
# logout first and login to obtain a new, potentially longer-lived refresh token.
if [[ $CURRENT_REFRESH_TTL_SEC -lt $TTL_THRESHOLD_SEC ]]; then
echo "◍ $audience: Refresh token TTL ($((CURRENT_REFRESH_TTL_SEC / 60)) mins) is less than desired new token TTL ($((TTL_THRESHOLD_SEC / 60)) mins)."
echo "↻ Logging out and logging in to refresh tokens."
# Suppress error if logout fails, as it's not critical.
chainctl auth logout --audience="$audience" >/dev/null 2>&1 || true
if chainctl auth login --audience="$audience" >/dev/null 2>&1; then
echo "✔ Authenticated."
local token_ttl_sec refresh_ttl_sec
read -r token_ttl_sec refresh_ttl_sec < <(get_current_ttls "$audience")

# Determine if any token refresh is needed.
if [[ $token_ttl_sec -lt $TTL_THRESHOLD_SEC || $refresh_ttl_sec -lt $TTL_THRESHOLD_SEC ]]; then
# If the refresh token's TTL is below the threshold, we need a full re-authentication.
if [[ $refresh_ttl_sec -lt $TTL_THRESHOLD_SEC ]]; then
if [[ $refresh_ttl_sec -eq 0 ]]; then
echo "◍ $audience not logged in or refresh token expired."
else
echo "✗ ERROR! Failed to reauthenticate."
echo "◍ $audience refresh token TTL ($((refresh_ttl_sec / 60)) mins) is low."
fi
# Logout first to ensure a clean state. Suppress error as it's not critical.
chainctl auth logout --audience="$audience" >/dev/null 2>&1 || true
echo "↻ $audience performing full re-authentication..."

# Build the login command. In interactive mode, redirect output to hide browser messages.
local login_cmd="chainctl auth login --audience=\"$audience\""
if [[ "${HEADLESS}" != "yes" ]]; then
login_cmd+=" >/dev/null 2>&1"
fi
if ! eval "$login_cmd"; then
echo "✗ ERROR! Failed to reauthenticate $audience" >&2
return 1
fi
else
echo -n "♽ Refreshing token "
if chainctl auth login --audience="$audience" >/dev/null 2>&1; then
echo -n "✔ "
else
echo "✗ ERROR! Failed to refresh token."
# The access token needs a simple, non-interactive refresh.
echo "♽ $audience refreshing token... "
if ! chainctl auth login --audience="$audience" >/dev/null 2>&1; then
echo "✗ ERROR! Failed to refresh token for $audience."
return 1
fi
fi
get_current_ttls "$audience"
echo "TTL is $((CURRENT_TOKEN_TTL_SEC / 60)) mins with a refresh TTL of $((CURRENT_REFRESH_TTL_SEC / 60)) mins: $audience"
# Re-fetch TTLs to report the new values.
read -r token_ttl_sec refresh_ttl_sec < <(get_current_ttls "$audience")
echo "✔ $audience refreshed. New TTL is $((token_ttl_sec / 60)) mins. Refresh TTL is $((refresh_ttl_sec / 60)) mins"
else
echo "✔ TTL is $((CURRENT_TOKEN_TTL_SEC / 60)) mins. Refresh threshold is $((TTL_THRESHOLD_SEC / 60)) mins: $audience"
# Tokens are fine, no action needed.
echo "✔ TTL is $((token_ttl_sec / 60)) mins. Refresh TTL is $((refresh_ttl_sec / 60)) mins: $audience"
fi
}

Expand All @@ -253,9 +171,13 @@ option_error() {
# Validate and normalize TTL threshold value
validate_ttl_threshold() {
local ttl_minutes="$1"
local note_prefix="⚑ NOTE! TTL threshold of ${ttl_minutes}m is"
[[ $ttl_minutes -lt 5 ]] && echo "${note_prefix} too low, capping to 5 mins." >&2 && ttl_minutes=5
[[ $ttl_minutes -gt 60 ]] && echo "${note_prefix} too high, capping to 60 mins." >&2 && ttl_minutes=60
if [[ $ttl_minutes -lt 5 ]]; then
echo "⚑ NOTE! TTL threshold of ${ttl_minutes}m is too low, capping to 5 mins." >&2
ttl_minutes=5
elif [[ $ttl_minutes -gt 60 ]]; then
echo "⚑ NOTE! TTL threshold of ${ttl_minutes}m is too high, capping to 60 mins." >&2
ttl_minutes=60
fi
echo $((ttl_minutes * 60))
}

Expand All @@ -269,9 +191,10 @@ while [[ $# -gt 0 ]]; do
echo "${VERSION}"
exit 0;;
--headless)
[[ -z "$2" ]] && option_error "--headless requires an argument: 'yes' or 'no'."
case "${2,,}" in
yes|no) HEADLESS="${2,,}"; shift 2;;
*) option_error "--headless requires an argument: 'yes' or 'no'.";;
*) option_error "Invalid argument for --headless: '$2'. Use 'yes' or 'no'.";;
esac;;
--ttl-threshold)
[[ -n "$2" && "$2" =~ ^[0-9]+$ ]] || option_error "--ttl-threshold requires a numeric argument."
Expand All @@ -285,9 +208,9 @@ while [[ $# -gt 0 ]]; do
esac
done

case "${HEADLESS}" in
no) chainctl config unset auth.mode >/dev/null 2>&1;;
*) chainctl config set auth.mode headless >/dev/null 2>&1
esac
# Set auth mode via an environment variable to avoid changing global config.
if [[ "${HEADLESS}" == "yes" ]]; then
export CHAINGUARD_AUTH_MODE=headless
fi

process_audiences