Skip to content

Commit c49bdf2

Browse files
committed
refactor(token): Read TTLs directly from cache files
This commit refactors the token time-to-live (TTL) checking logic for improved performance, clarity, and robustness. - The monolithic TTL checking logic has been split into two distinct functions: `get_token_ttl` and `get_refresh_ttl`, each with a single responsibility. - Both functions now read expiration timestamps directly from the `oidc-token` and `refresh-token` files in the `chainctl` cache. This avoids executing the `chainctl` binary, which is significantly faster and prevents the side-effect of triggering an interactive login if a token has expired. - A new helper function, `get_audience_cache_dir`, has been introduced to centralize the logic for constructing cache paths, reducing code duplication. As a result of these changes, the complex and brittle date-parsing functions (`tounix`, `detect_date_type`, `try_date_parse`) are no longer needed and have been removed, simplifying the script considerably.
1 parent 87cc288 commit c49bdf2

File tree

1 file changed

+77
-154
lines changed

1 file changed

+77
-154
lines changed

home-manager/_mixins/features/development/cg-tokens.sh

Lines changed: 77 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,6 @@ AUDIENCES=(
2020
"apk.cgr.dev"
2121
"cgr.dev"
2222
)
23-
# Global variables to store TTLs, populated by get_current_ttls
24-
CURRENT_TOKEN_TTL_SEC=0
25-
CURRENT_REFRESH_TTL_SEC=0
26-
# Global variable to track date command type and label, populated by detect_date_type
27-
DATE_TYPE=""
28-
DATE_LABEL=""
2923
# Default headless mode
3024
HEADLESS=yes
3125
# Default operation mode
@@ -52,142 +46,54 @@ OPTIONS:
5246
EOF
5347
}
5448

55-
# Detect date command type and set global DATE_TYPE variable
56-
detect_date_type() {
57-
if [ -z "$DATE_TYPE" ]; then
58-
if date --help 2>&1 | grep -q "BusyBox v"; then
59-
DATE_LABEL="BusyBox"
60-
DATE_TYPE="busybox"
61-
elif date --help 2>&1 | grep -q "coreutils"; then
62-
DATE_LABEL="GNU Core Utilities"
63-
DATE_TYPE="coreutils"
64-
elif [[ "$(uname)" == "Darwin" ]]; then
65-
DATE_LABEL="BSDCoreUtils (macOS)"
66-
DATE_TYPE="bsd"
67-
else
68-
DATE_LABEL="Unknown"
69-
DATE_TYPE="unknown"
70-
fi
71-
fi
72-
}
73-
74-
# Helper function to try date parsing
75-
try_date_parse() {
76-
local cmd_args=("$@")
77-
local ts=""
78-
79-
ts=$(LC_ALL=C "${cmd_args[@]}" 2>/dev/null)
80-
if [[ "$ts" =~ ^[0-9]+$ ]]; then
81-
echo "$ts"
49+
# Get the OS-specific base directory for the Chainguard cache.
50+
_get_cache_base_dir() {
51+
if [[ "$(uname)" == "Darwin" ]]; then
52+
echo "${HOME}/Library/Caches/chainguard"
8253
else
83-
echo ""
84-
fi
85-
}
86-
87-
# Convert a date string to a Unix timestamp
88-
# Input date string format e.g., "2025-06-30 12:30:05 -0700 PDT"
89-
tounix() {
90-
local date_str="$1"
91-
local ts=""
92-
detect_date_type
93-
94-
# Strip timezone abbreviation (e.g., BST, PDT) as it can sometimes confuse date parsers
95-
local cleaned_date_str="${date_str% [A-Z][A-Z][A-Z]}"
96-
97-
# Treat unknown date type like coreutils. Works for uutils.
98-
if [[ "$DATE_TYPE" == "coreutils" ]] || [[ "$DATE_TYPE" == "unknown" ]]; then
99-
ts=$(try_date_parse date --date="$cleaned_date_str" "+%s")
100-
if [[ -z "$ts" ]]; then
101-
# If stripping failed, try with the original string.
102-
ts=$(try_date_parse date --date="$date_str" "+%s")
103-
fi
104-
elif [[ "$DATE_TYPE" == "busybox" ]]; then
105-
ts=$(try_date_parse date -d "$cleaned_date_str" "+%s")
106-
if [[ -z "$ts" ]]; then
107-
# If stripping failed, try with the original string.
108-
ts=$(try_date_parse date -d "$date_str" "+%s")
109-
fi
110-
elif [[ "$DATE_TYPE" == "bsd" ]]; then
111-
# BSD date (macOS specific): requires -j and -f for parsing arbitrary date strings.
112-
# Try original string, with full format, BSD needs exact format match
113-
ts=$(try_date_parse date -jf "%Y-%m-%d %H:%M:%S %z %Z" "$date_str" "+%s")
114-
if [[ -z "$ts" ]]; then
115-
# Try cleaned string with simpler format (fallback for missing timezone)
116-
ts=$(try_date_parse date -jf "%Y-%m-%d %H:%M:%S %z" "$cleaned_date_str" "+%s")
117-
fi
118-
fi
119-
120-
# If 'ts' is empty, catch and report an error.
121-
if [[ -z "$ts" ]]; then
122-
echo "✗ ERROR! Failed to parse date string '$date_str' to Unix timestamp." >&2
123-
echo "⚑ DEBUG! Date parsing failed using: ${DATE_LABEL}" >&2
124-
return 1
54+
echo "${HOME}/.cache/chainguard"
12555
fi
126-
echo "$ts"
12756
}
12857

129-
# Get the token TTL and populate CURRENT_TOKEN_TTL_SEC
130-
get_token_ttl() {
58+
# Get TTL from a token file.
59+
_get_ttl_from_file() {
13160
local audience="$1"
132-
CURRENT_TOKEN_TTL_SEC=0
133-
local now_ts
134-
now_ts=$(date "+%s")
61+
local token_filename="$2"
62+
local ttl=0
13563

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

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

144-
if [[ -n "$expiry_date_str" && "$expiry_date_str" != "null" ]]; then
145-
local expiry_ts
146-
expiry_ts=$(tounix "$expiry_date_str")
147-
if [[ $? -eq 0 && -n "$expiry_ts" ]]; then
148-
CURRENT_TOKEN_TTL_SEC=$((expiry_ts - now_ts))
149-
# Ensure TTL is not negative
150-
if [[ $CURRENT_TOKEN_TTL_SEC -lt 0 ]]; then CURRENT_TOKEN_TTL_SEC=0; fi
151-
fi
152-
fi
153-
}
71+
if [[ -f "$token_file" ]]; then
72+
local token_data
73+
token_data=$(cat "$token_file")
74+
local expiry_ts_str=""
15475

155-
# Get the refresh TTL and populate CURRENT_REFRESH_TTL_SEC
156-
get_refresh_ttl() {
157-
local audience="$1"
158-
CURRENT_REFRESH_TTL_SEC=0
159-
local now_ts
160-
now_ts=$(date "+%s")
161-
162-
# Cache directory for chainctl refresh tokens
163-
local CHAINCTL_CACHE_DIR="${HOME}/.cache/chainguard"
164-
if [[ "$(uname)" == "Darwin" ]]; then
165-
CHAINCTL_CACHE_DIR="${HOME}/Library/Caches/chainguard"
166-
fi
167-
# Get refresh token expiry from cache file
168-
# Sanitize audience for use in file path (replace / with -)
169-
local audsafe="${audience//\//-}"
170-
local refresh_token_file="${CHAINCTL_CACHE_DIR}/${audsafe}/refresh-token"
171-
if [[ -f "$refresh_token_file" ]]; then
172-
local refresh_data_b64
173-
refresh_data_b64=$(cat "$refresh_token_file")
174-
local refresh_exp_ts_str
175-
# Suppress stderr from base64/jq if file is malformed
176-
refresh_exp_ts_str=$(echo "$refresh_data_b64" | base64 -d 2>/dev/null | jq -r .exp 2>/dev/null)
76+
# Decode the JWT payload and extract the 'exp' (expiration time) claim.
77+
expiry_ts_str=$(echo "$token_data" | cut -d'.' -f2 | base64 -d 2>/dev/null | jq -r .exp 2>/dev/null)
17778

178-
if [[ -n "$refresh_exp_ts_str" && "$refresh_exp_ts_str" != "null" && "$refresh_exp_ts_str" =~ ^[0-9]+$ ]]; then
179-
# The .exp field in refresh token is already a Unix timestamp
180-
CURRENT_REFRESH_TTL_SEC=$((refresh_exp_ts_str - now_ts))
79+
if [[ -n "$expiry_ts_str" && "$expiry_ts_str" != "null" && "$expiry_ts_str" =~ ^[0-9]+$ ]]; then
80+
ttl=$((expiry_ts_str - $(date "+%s")))
18181
# Ensure TTL is not negative
182-
if [[ $CURRENT_REFRESH_TTL_SEC -lt 0 ]]; then CURRENT_REFRESH_TTL_SEC=0; fi
82+
if [[ $ttl -lt 0 ]]; then ttl=0; fi
18383
fi
18484
fi
85+
echo "$ttl"
18586
}
18687

88+
# Get the token and refresh TTLs for a given audience.
89+
# Outputs two space-separated values: <token_ttl> <refresh_ttl>
18790
get_current_ttls() {
18891
local audience="$1"
189-
get_token_ttl "$audience"
190-
get_refresh_ttl "$audience"
92+
local token_ttl
93+
token_ttl=$(_get_ttl_from_file "$audience" "oidc-token")
94+
local refresh_ttl
95+
refresh_ttl=$(_get_ttl_from_file "$audience" "refresh-token")
96+
echo "$token_ttl $refresh_ttl"
19197
}
19298

19399
logout_audience() {
@@ -201,33 +107,45 @@ logout_audience() {
201107

202108
refresh_audience() {
203109
local audience="$1"
204-
get_current_ttls "$audience"
205-
# Determine if any token refresh is needed
206-
if [[ $CURRENT_TOKEN_TTL_SEC -lt $TTL_THRESHOLD_SEC || $CURRENT_REFRESH_TTL_SEC -lt $TTL_THRESHOLD_SEC ]]; then
207-
# If the refresh token's remaining life is less than the refresh threshold,
208-
# logout first and login to obtain a new, potentially longer-lived refresh token.
209-
if [[ $CURRENT_REFRESH_TTL_SEC -lt $TTL_THRESHOLD_SEC ]]; then
210-
echo "$audience: Refresh token TTL ($((CURRENT_REFRESH_TTL_SEC / 60)) mins) is less than desired new token TTL ($((TTL_THRESHOLD_SEC / 60)) mins)."
211-
echo "↻ Logging out and logging in to refresh tokens."
212-
# Suppress error if logout fails, as it's not critical.
213-
chainctl auth logout --audience="$audience" >/dev/null 2>&1 || true
214-
if chainctl auth login --audience="$audience" >/dev/null 2>&1; then
215-
echo "✔ Authenticated."
110+
local token_ttl_sec refresh_ttl_sec
111+
read -r token_ttl_sec refresh_ttl_sec < <(get_current_ttls "$audience")
112+
113+
# Determine if any token refresh is needed.
114+
if [[ $token_ttl_sec -lt $TTL_THRESHOLD_SEC || $refresh_ttl_sec -lt $TTL_THRESHOLD_SEC ]]; then
115+
# If the refresh token's TTL is below the threshold, we need a full re-authentication.
116+
if [[ $refresh_ttl_sec -lt $TTL_THRESHOLD_SEC ]]; then
117+
if [[ $refresh_ttl_sec -eq 0 ]]; then
118+
echo "$audience not logged in or refresh token expired."
216119
else
217-
echo "✗ ERROR! Failed to reauthenticate."
120+
echo "$audience refresh token TTL ($((refresh_ttl_sec / 60)) mins) is low."
121+
fi
122+
# Logout first to ensure a clean state. Suppress error as it's not critical.
123+
chainctl auth logout --audience="$audience" >/dev/null 2>&1 || true
124+
echo "$audience performing full re-authentication..."
125+
126+
# Build the login command. In interactive mode, redirect output to hide browser messages.
127+
local login_cmd="chainctl auth login --audience=\"$audience\""
128+
if [[ "${HEADLESS}" != "yes" ]]; then
129+
login_cmd+=" >/dev/null 2>&1"
130+
fi
131+
if ! eval "$login_cmd"; then
132+
echo "✗ ERROR! Failed to reauthenticate $audience" >&2
133+
return 1
218134
fi
219135
else
220-
echo -n "♽ Refreshing token "
221-
if chainctl auth login --audience="$audience" >/dev/null 2>&1; then
222-
echo -n ""
223-
else
224-
echo "✗ ERROR! Failed to refresh token."
136+
# The access token needs a simple, non-interactive refresh.
137+
echo "$audience refreshing token... "
138+
if ! chainctl auth login --audience="$audience" >/dev/null 2>&1; then
139+
echo "✗ ERROR! Failed to refresh token for $audience."
140+
return 1
225141
fi
226142
fi
227-
get_current_ttls "$audience"
228-
echo "TTL is $((CURRENT_TOKEN_TTL_SEC / 60)) mins with a refresh TTL of $((CURRENT_REFRESH_TTL_SEC / 60)) mins: $audience"
143+
# Re-fetch TTLs to report the new values.
144+
read -r token_ttl_sec refresh_ttl_sec < <(get_current_ttls "$audience")
145+
echo "$audience refreshed. New TTL is $((token_ttl_sec / 60)) mins. Refresh TTL is $((refresh_ttl_sec / 60)) mins"
229146
else
230-
echo "✔ TTL is $((CURRENT_TOKEN_TTL_SEC / 60)) mins. Refresh threshold is $((TTL_THRESHOLD_SEC / 60)) mins: $audience"
147+
# Tokens are fine, no action needed.
148+
echo "✔ TTL is $((token_ttl_sec / 60)) mins. Refresh TTL is $((refresh_ttl_sec / 60)) mins: $audience"
231149
fi
232150
}
233151

@@ -253,9 +171,13 @@ option_error() {
253171
# Validate and normalize TTL threshold value
254172
validate_ttl_threshold() {
255173
local ttl_minutes="$1"
256-
local note_prefix="⚑ NOTE! TTL threshold of ${ttl_minutes}m is"
257-
[[ $ttl_minutes -lt 5 ]] && echo "${note_prefix} too low, capping to 5 mins." >&2 && ttl_minutes=5
258-
[[ $ttl_minutes -gt 60 ]] && echo "${note_prefix} too high, capping to 60 mins." >&2 && ttl_minutes=60
174+
if [[ $ttl_minutes -lt 5 ]]; then
175+
echo "⚑ NOTE! TTL threshold of ${ttl_minutes}m is too low, capping to 5 mins." >&2
176+
ttl_minutes=5
177+
elif [[ $ttl_minutes -gt 60 ]]; then
178+
echo "⚑ NOTE! TTL threshold of ${ttl_minutes}m is too high, capping to 60 mins." >&2
179+
ttl_minutes=60
180+
fi
259181
echo $((ttl_minutes * 60))
260182
}
261183

@@ -269,9 +191,10 @@ while [[ $# -gt 0 ]]; do
269191
echo "${VERSION}"
270192
exit 0;;
271193
--headless)
194+
[[ -z "$2" ]] && option_error "--headless requires an argument: 'yes' or 'no'."
272195
case "${2,,}" in
273196
yes|no) HEADLESS="${2,,}"; shift 2;;
274-
*) option_error "--headless requires an argument: 'yes' or 'no'.";;
197+
*) option_error "Invalid argument for --headless: '$2'. Use 'yes' or 'no'.";;
275198
esac;;
276199
--ttl-threshold)
277200
[[ -n "$2" && "$2" =~ ^[0-9]+$ ]] || option_error "--ttl-threshold requires a numeric argument."
@@ -285,9 +208,9 @@ while [[ $# -gt 0 ]]; do
285208
esac
286209
done
287210

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

293216
process_audiences

0 commit comments

Comments
 (0)