@@ -20,12 +20,6 @@ AUDIENCES=(
20
20
" apk.cgr.dev"
21
21
" cgr.dev"
22
22
)
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=" "
29
23
# Default headless mode
30
24
HEADLESS=yes
31
25
# Default operation mode
@@ -52,142 +46,54 @@ OPTIONS:
52
46
EOF
53
47
}
54
48
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"
82
53
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"
125
55
fi
126
- echo " $ts "
127
56
}
128
57
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 () {
131
60
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
135
63
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)
140
66
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} "
143
70
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=" "
154
75
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)
177
78
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")) )
181
81
# 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
183
83
fi
184
84
fi
85
+ echo " $ttl "
185
86
}
186
87
88
+ # Get the token and refresh TTLs for a given audience.
89
+ # Outputs two space-separated values: <token_ttl> <refresh_ttl>
187
90
get_current_ttls () {
188
91
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 "
191
97
}
192
98
193
99
logout_audience () {
@@ -201,33 +107,45 @@ logout_audience() {
201
107
202
108
refresh_audience () {
203
109
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."
216
119
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
218
134
fi
219
135
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
225
141
fi
226
142
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"
229
146
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 "
231
149
fi
232
150
}
233
151
@@ -253,9 +171,13 @@ option_error() {
253
171
# Validate and normalize TTL threshold value
254
172
validate_ttl_threshold () {
255
173
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
259
181
echo $(( ttl_minutes * 60 ))
260
182
}
261
183
@@ -269,9 +191,10 @@ while [[ $# -gt 0 ]]; do
269
191
echo " ${VERSION} "
270
192
exit 0;;
271
193
--headless)
194
+ [[ -z " $2 " ]] && option_error " --headless requires an argument: 'yes' or 'no'."
272
195
case " ${2,,} " in
273
196
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'." ;;
275
198
esac ;;
276
199
--ttl-threshold)
277
200
[[ -n " $2 " && " $2 " =~ ^[0-9]+$ ]] || option_error " --ttl-threshold requires a numeric argument."
@@ -285,9 +208,9 @@ while [[ $# -gt 0 ]]; do
285
208
esac
286
209
done
287
210
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
292
215
293
216
process_audiences
0 commit comments