#!/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= ./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."