Skip to content

Instantly share code, notes, and snippets.

@smoser
Last active September 16, 2024 21:06
Show Gist options
  • Save smoser/568f03b41efe80f57cea4605beec71ac to your computer and use it in GitHub Desktop.
Save smoser/568f03b41efe80f57cea4605beec71ac to your computer and use it in GitHub Desktop.

chainctl token helper

I set out with the goal of just running something 'login-images 30m' and having it force me to log in (in --headless mode) to each of the configured endpoints, and then running it again would refresh the token, or force me to auth again if i had to in able to ensure 30m of peace.

When refresh tokens are correctly working, the end goal would be to run this on ssh login with a value of something like '4h' (4 hours) and then start a timer of some sort that just kept refreshing in the background.

This is what I have right now.

  • login-images -c/--check will show status

    $ login-images -c
    console-api.enforce.dev - 0m
    apk.cgr.dev - 0m
    cgr.dev - 48m
    
  • login-images without args will login to the audience provided or the builtin list

    Below, the was good for 58 minutes so it did not force a new login. The other two sites needed login.

    $ login-images 
    console-api.enforce.dev had 0m wanted 30m
    Authenticating...
    Enter the verification code MZDD-XVMP in your browser at: https://auth.chainguard.dev/activate
    Code will be valid for 900 seconds
    Token received!
    Successfully exchanged token.
    Valid! Id: aa4458f1e77704575053c079c9b3b6db8bb47642
    console-api.enforce.dev - good for 59m
    apk.cgr.dev had 0m wanted 30m
    Authenticating...
    Enter the verification code QCDR-XFWJ in your browser at: https://auth.chainguard.dev/activate
    Code will be valid for 900 seconds
    Token received!
    Successfully exchanged token.
    Valid! Id: aa4458f1e77704575053c079c9b3b6db8bb47642
    apk.cgr.dev - good for 60m
    cgr.dev - good for 58m
    

Notes

  • Refresh tokens basically can't be used here, at least with --headless (mono/#18280) and they don't get listed in status (mono/#18279).
#!/bin/bash
# shellcheck disable=SC2015,SC2039,SC2166,SC3043
VERBOSITY=${VERBOSITY:-1}
HEADLESS="--headless"
rf() {
"$@" || {
stderr "failed [$?]:" "$@"
exit 1;
}
}
stderr() { echo "$@" 1>&2; }
debug() { [ $VERBOSITY -lt 1 ] || stderr "$@"; }
is_coreutils_date() {
if [ -z "$_IS_COREUTILS_DATE" ]; then
date --help 2>&1 | grep -q "coreutils" &&
_IS_COREUTILS_DATE=true || _IS_COREUTILS_DATE=false
fi
case "$_IS_COREUTILS_DATE" in
true) return 0;;
false) return 1;;
esac
}
unix2utc() {
date -R "--date=@$1" +"%Y-%m-%d %H:%M:%S %z %Z"
}
tounix() {
local date="$1"
if is_coreutils_date; then
# wierd, output says has numerical timezone (offset) string (EDT or UTC).
date=${date% [A-Z][A-Z][A-Z]}
rf date --date="$date" "+%s"
else
rf date -d"$date" -D"%Y-%m-%d %H:%M:%S %z %Z" "+%s"
fi
}
get_refresh_expire() {
local audience="$1" audsafe="" cached="" f="" out=""
audsafe=${audience//\//-}
[ -d ~/Library/Caches ] && cached=$(echo ~/Library/Caches) ||
cached=$(echo "$HOME/.cache")
f="$cached/chainguard/$audsafe/refresh-token"
[ -f "$f" ] || { echo 0; return 1; }
out=$(base64 -d < "$f") || {
stderr "get_refresh_expire: failed: base64 -d '$f' failed"
return 1;
}
# this is in unix timestamp
exp=$(echo "$out" | jq .exp) && [ -n "$exp" ] || {
stderr "jq of base64 decoded '$f' returned $? had exp='$exp'"
return 1
}
unix2utc "$exp"
}
getttl() {
local audience="$1" rc="" stout=""
set -- chainctl auth status --quick --output=json --audience="$audience"
stout=$("$@")
rc=$?
if [ $rc -ne 0 -a $rc -ne 1 ]; then
stderr "failed execute[$rc]: $*"
[ -z "$stout" ] || stderr "output: ${stout}"
return $rc
fi
local valid="" reason=""
valid=$(echo "$stout" | jq -r .valid) || {
stderr "fail execute[$rc]: $*"
stderr "produced invalid json output"
return 1
}
[ "$valid" = "true" -o "$valid" = "false" ] ||
{ stderr "unexpected output from $*. valid='$valid'"; return 1; }
[ "$valid" = "false" ] && {
_RET=0
_RET_ref=0
_RET_tok_exp=0
_RET_ref_exp=0
_RET_OUT="$stout"
return 0
}
set -- chainctl auth status --output=json --audience="$audience"
stout=$("$@") || {
rc=$?
stderr "fail execute[$rc] (passed with --quick): $*"
[ -z "$stout" ] || stderr "output: ${stout}"
return 1
}
local expire="" exps="" refexpire="" refexps="" nows=""
expire=$(echo "$stout" | jq -r .expiry) &&
[ -n "$expire" ] || {
stderr "failed to get expiry info from status"
return 1
}
exps=$(tounix "$expire") || {
stderr "failed to convert $expire for audience=$audience"
return 1
}
refexpire=$(get_refresh_expire "$audience") || {
stderr "failed to get refresh expire for $audience"
return 1
}
refexps=$(tounix "$refexpire")
nows=$(date "+%s") || fail "date +%s failed"
email=$(echo "$stout" | jq -r .email)
#stderr "$audience [$email] good for $(((exps-nows)/60))m"
_RET=$((exps-nows))
_RET_ref=$((refexps-nows))
_RET_tok_exp="$expire"
_RET_ref_exp="$refexps"
_RET_OUT="$stout"
}
tok_with_ttl() {
local audience="$1" wantttl="$2"
local rttl="" ttl="" out=""
getttl "$audience" || return 1
rttl="$_RET_ref"
ttl="$_RET"
# if refresh is valid: refresh, it should be free
if [ "$rttl" -gt "$wantttl" ]; then
out=$(chainctl auth login ${HEADLESS:+"$HEADLESS"} --audience="$audience" 2>&1) || return
getttl "$audience" || return 1
rttl="$_RET_ref"
ttl="$_RET"
fi
if [ $ttl -gt $wantttl -a $rttl -gt $wantttl ]; then
echo "$audience - good for $((ttl/60))m refresh=$((rttl/60))m"
return 0
fi
debug "$audience had ttl=$((ttl/60))m rttl=$((rttl/60))m wanted $((wantttl/60))m"
chainctl auth login ${HEADLESS:+"$HEADLESS"} --audience="$audience" || return
getttl "$audience" || return
echo "$audience - good for $((_RET/60))m refresh=$((_RET_ref/60))m"
}
## CHAINGUARD_IDENTITY gets set. hmm... not sure where.
command -v chainctl >/dev/null 2>&1 || { stderr "no chainctl in PATH"; exit 1; }
# this is required to get better headless login flow
if ! chainctl config view | grep -q "device-flow: chainguard" ; then
stderr "Please run: chainctl config set auth.device-flow chainguard"
exit 1
fi
# this you probably just want.
if ! chainctl config view | grep -q "social-login: google-oauth2" ; then
stderr "Probably you want: chainctl config set default.social-login google-oauth2"
exit 1
fi
if [ -f ~/.docker/config.json ]; then
out=$(jq -r '.credHelpers."cgr.dev"' < ~/.docker/config.json)
[ $? -eq 0 ] || exit 1
if [ "$out" != "cgr" ]; then
rf chainctl auth configure-docker ${HEADLESS:+"${HEADLESS}"}
fi
fi
check=false
if [ "$1" = "--check" -o "$1" = "-c" ]; then
check=true
shift
fi
want_ttl=$((30*60))
case "$1" in
--want=*) want_ttl=$((60*${1#--want=})); shift;;
--want|-w) want_ttl=$((60*$2)); shift 2;;
-w[0-9]*) want_ttl=$((60*${1#-w})); shift;;
esac
if [ $# -eq 0 ]; then
# cgr.dev - created by chainctl auth configure-docker
# https://console-api.enforce.dev - created by chainctl --output=json img repos list --parent=""
# apk.cgr.dev - created by enterprise-packages 'make apk-token'
# console-api.enforce.dev - ???
set -- https://console-api.enforce.dev console-api.enforce.dev apk.cgr.dev cgr.dev
fi
if [ "$check" = "true" ]; then
for audience in "$@"; do
rf getttl "$audience"
#id=$(echo "$_RET_OUT" | jq --raw .identity)
extra=$(echo "$_RET_OUT" | jq -r '. | .email + " / " + .identity')
echo "$audience - $((_RET/60))m refresh=$((_RET_ref/60))m${extra:+ ${extra}}"
done
exit
fi
for audience in "$@"; do
rf tok_with_ttl "$audience" "$want_ttl"
done
exit
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment