117 lines
3.9 KiB
Bash
117 lines
3.9 KiB
Bash
#!/usr/bin/env bash
|
|||
|
|
# device-flow-demo.sh -- showcase Authentik OAuth 2.0 Device Authorization
|
||
|
|
# Grant (RFC 8628) end to end, pure curl + jq.
|
||
|
|
#
|
||
|
|
# Requires: curl, jq
|
||
|
|
#
|
||
|
|
# Usage:
|
||
|
|
# CLIENT_ID=<device-flow provider client_id> ./scripts/device-flow-demo.sh
|
||
|
|
#
|
||
|
|
# Steps:
|
||
|
|
# 1. Requests a device_code + user_code from Authentik.
|
||
|
|
# 2. Prints the verification URL + user_code for approval in a browser.
|
||
|
|
# 3. Polls the token endpoint until approved, then prints the access_token.
|
||
|
|
#
|
||
|
|
# NOTE on quoting: the device_code returned by Authentik can contain raw
|
||
|
|
# quote, backslash, and backtick characters. Always pass it to curl via
|
||
|
|
# --data-urlencode from a shell variable (curl encodes it safely). Never
|
||
|
|
# re-embed it inside another quoted string (a Python literal, a second shell
|
||
|
|
# layer, etc) -- that is what breaks it.
|
||
|
|
|
||
|
|
set -euo pipefail
|
||
|
|
|
||
|
|
AUTHENTIK_URL="${AUTHENTIK_URL:-https://authentik.riotpiao.com}"
|
||
|
|
CLIENT_ID="${CLIENT_ID:?CLIENT_ID is required}"
|
||
|
|
SCOPE="${SCOPE:-openid}"
|
||
|
|
|
||
|
|
JQ_DEVICE_CODE='.device_code'
|
||
|
|
JQ_USER_CODE='.user_code'
|
||
|
|
JQ_VERIFICATION_URI='.verification_uri'
|
||
|
|
JQ_EXPIRES_IN='.expires_in'
|
||
|
|
JQ_INTERVAL='.interval'
|
||
|
|
JQ_ERROR='.error'
|
||
|
|
JQ_ERROR_DESC='.error_description'
|
||
|
|
JQ_ACCESS_TOKEN='.access_token'
|
||
|
|
|
||
|
|
echo "==> Requesting device code from ${AUTHENTIK_URL}..."
|
||
|
|
if ! DEVICE_RESP="$(curl -sS --fail \
|
||
|
|
"${AUTHENTIK_URL}/application/o/device/" \
|
||
|
|
--data-urlencode "client_id=${CLIENT_ID}" \
|
||
|
|
--data-urlencode "scope=${SCOPE}")"; then
|
||
|
|
echo "FAILED: device code request errored (bad CLIENT_ID or unreachable Authentik)." >&2
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
|
||
|
|
DEVICE_CODE="$(echo "$DEVICE_RESP" | jq -r "$JQ_DEVICE_CODE")"
|
||
|
|
if [ -z "$DEVICE_CODE" ] || [ "$DEVICE_CODE" = "null" ]; then
|
||
|
|
echo "FAILED: no device_code in response: $DEVICE_RESP" >&2
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
|
||
|
|
USER_CODE="$(echo "$DEVICE_RESP" | jq -r "$JQ_USER_CODE")"
|
||
|
|
VERIFICATION_URI="$(echo "$DEVICE_RESP" | jq -r "$JQ_VERIFICATION_URI")"
|
||
|
|
EXPIRES_IN="$(echo "$DEVICE_RESP" | jq -r "$JQ_EXPIRES_IN")"
|
||
|
|
INTERVAL="$(echo "$DEVICE_RESP" | jq -r "${JQ_INTERVAL} // 5")"
|
||
|
|
|
||
|
|
echo
|
||
|
|
echo " Go to: ${VERIFICATION_URI}"
|
||
|
|
echo " Enter code: ${USER_CODE}"
|
||
|
|
echo " (expires in ${EXPIRES_IN}s)"
|
||
|
|
echo
|
||
|
|
echo "==> Polling for approval every ${INTERVAL}s..."
|
||
|
|
|
||
|
|
DEADLINE=$(( $(date +%s) + EXPIRES_IN ))
|
||
|
|
ACCESS_TOKEN=""
|
||
|
|
|
||
|
|
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
|
||
|
|
sleep "$INTERVAL"
|
||
|
|
|
||
|
|
TOKEN_RESP="$(curl -sS \
|
||
|
|
"${AUTHENTIK_URL}/application/o/token/" \
|
||
|
|
--data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
|
||
|
|
--data-urlencode "device_code=${DEVICE_CODE}" \
|
||
|
|
--data-urlencode "client_id=${CLIENT_ID}")"
|
||
|
|
|
||
|
|
ERR="$(echo "$TOKEN_RESP" | jq -r "${JQ_ERROR} // empty")"
|
||
|
|
|
||
|
|
if [ -z "$ERR" ]; then
|
||
|
|
ACCESS_TOKEN="$(echo "$TOKEN_RESP" | jq -r "$JQ_ACCESS_TOKEN")"
|
||
|
|
break
|
||
|
|
elif [ "$ERR" = "authorization_pending" ]; then
|
||
|
|
echo " ...still waiting for approval"
|
||
|
|
continue
|
||
|
|
elif [ "$ERR" = "slow_down" ]; then
|
||
|
|
INTERVAL=$((INTERVAL + 5))
|
||
|
|
continue
|
||
|
|
else
|
||
|
|
DESC="$(echo "$TOKEN_RESP" | jq -r "${JQ_ERROR_DESC} // ${JQ_ERROR}")"
|
||
|
|
echo "FAILED: $DESC" >&2
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
|
||
|
|
if [ -z "$ACCESS_TOKEN" ]; then
|
||
|
|
echo "Timed out waiting for approval." >&2
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
|
||
|
|
echo
|
||
|
|
echo "==> Got a token. Decoded payload:"
|
||
|
|
python3 - "$ACCESS_TOKEN" <<'PYEOF'
|
||
|
|
import sys, json, base64
|
||
|
|
tok = sys.argv[1].split(".")[1]
|
||
|
|
tok += "=" * (-len(tok) % 4)
|
||
|
|
print(json.dumps(json.loads(base64.urlsafe_b64decode(tok)), indent=2))
|
||
|
|
PYEOF
|
||
|
|
|
||
|
|
echo
|
||
|
|
echo "==> Example use as a bearer token:"
|
||
|
|
echo "curl https://api.riotpiao.com/v1/reasoning/chat/completions \\"
|
||
|
|
echo " -H \"Authorization: Bearer ${ACCESS_TOKEN}\" \\"
|
||
|
|
echo " -H \"Content-Type: application/json\" \\"
|
||
|
|
echo " -d \"{\\\"model\\\":\\\"reasoning\\\",\\\"messages\\\":[{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"hi\\\"}]}\""
|
||
|
|
echo
|
||
|
|
echo "NOTE: api.riotpiao.com does not yet validate Authentik JWTs -- Kongs"
|
||
|
|
echo "model routes currently enforce a static key-auth credential instead."
|
||
|
|
echo "See k8s/apps/api/DEVICE-GRANT-PLAN.md Phase 2 for the pending work."
|