|
| 1 | +#!/usr/bin/env bash |
| 2 | +# oidc-exchange.sh — Exchange a GitHub Actions OIDC token for a dovecote token. |
| 3 | +# |
| 4 | +# Usage (called by .github/actions/setup-dovecote/action.yml): |
| 5 | +# |
| 6 | +# OIDC mode (default when MODE != legacy and INPUT_TOKEN is empty): |
| 7 | +# Required env: |
| 8 | +# ACTIONS_ID_TOKEN_REQUEST_URL — GitHub-provided OIDC token endpoint |
| 9 | +# ACTIONS_ID_TOKEN_REQUEST_TOKEN — Bearer token for that endpoint |
| 10 | +# DOVECOTE_SERVER_URL — base URL of the dovecote Worker |
| 11 | +# OIDC_AUDIENCE — audience claim expected by the Worker |
| 12 | +# Optional env: |
| 13 | +# GITHUB_ENV — path to file to append DOVECOTE_TOKEN= to |
| 14 | +# |
| 15 | +# Legacy mode (activated when MODE=legacy OR INPUT_TOKEN is non-empty): |
| 16 | +# Required env: |
| 17 | +# INPUT_TOKEN — the pre-issued dovecote token (dvct_...) |
| 18 | +# No HTTP requests are made. |
| 19 | +# |
| 20 | +# Constraint-4 note (doc): run this step ONLY when the token is about to be |
| 21 | +# used, not at the start of the job, to minimise the window a token is live. |
| 22 | +# |
| 23 | +set -euo pipefail |
| 24 | + |
| 25 | +# ---------- mode detection -------------------------------------------------- |
| 26 | +MODE="${MODE:-}" |
| 27 | +INPUT_TOKEN="${INPUT_TOKEN:-}" |
| 28 | + |
| 29 | +if [[ "$MODE" == "legacy" || -n "$INPUT_TOKEN" ]]; then |
| 30 | + # ---- legacy path: write INPUT_TOKEN directly, zero HTTP ---- |
| 31 | + echo "::add-mask::${INPUT_TOKEN}" |
| 32 | + echo "DOVECOTE_TOKEN=${INPUT_TOKEN}" >> "${GITHUB_ENV}" |
| 33 | + exit 0 |
| 34 | +fi |
| 35 | + |
| 36 | +# ---- OIDC path (default) ---- |
| 37 | + |
| 38 | +# Validate required variables before any network I/O |
| 39 | +if [[ -z "${DOVECOTE_SERVER_URL:-}" ]]; then |
| 40 | + echo "oidc-exchange: DOVECOTE_SERVER_URL is required in OIDC mode (set server-url input or DOVECOTE_SERVER_URL env)" >&2 |
| 41 | + exit 1 |
| 42 | +fi |
| 43 | + |
| 44 | +if [[ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]]; then |
| 45 | + echo "oidc-exchange: ACTIONS_ID_TOKEN_REQUEST_URL is not set — ensure the job has id-token: write permission" >&2 |
| 46 | + exit 1 |
| 47 | +fi |
| 48 | + |
| 49 | +if [[ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ]]; then |
| 50 | + echo "oidc-exchange: ACTIONS_ID_TOKEN_REQUEST_TOKEN is not set" >&2 |
| 51 | + exit 1 |
| 52 | +fi |
| 53 | + |
| 54 | +OIDC_AUDIENCE="${OIDC_AUDIENCE:-}" |
| 55 | +if [[ -z "$OIDC_AUDIENCE" ]]; then |
| 56 | + echo "oidc-exchange: OIDC_AUDIENCE is not set" >&2 |
| 57 | + exit 1 |
| 58 | +fi |
| 59 | + |
| 60 | +# ---------- 1. Fetch GitHub OIDC id_token ----------------------------------- |
| 61 | +OIDC_TMPFILE="$(mktemp)" |
| 62 | +trap 'rm -f "$OIDC_TMPFILE"' EXIT |
| 63 | + |
| 64 | +# Build URL: append audience param (existing query string handled with &) |
| 65 | +if [[ "$ACTIONS_ID_TOKEN_REQUEST_URL" == *"?"* ]]; then |
| 66 | + TOKEN_URL="${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=${OIDC_AUDIENCE}" |
| 67 | +else |
| 68 | + TOKEN_URL="${ACTIONS_ID_TOKEN_REQUEST_URL}?audience=${OIDC_AUDIENCE}" |
| 69 | +fi |
| 70 | + |
| 71 | +OIDC_HTTP_CODE="$( |
| 72 | + curl -sSL \ |
| 73 | + -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ |
| 74 | + -H "Accept: application/json" \ |
| 75 | + -w "%{http_code}" \ |
| 76 | + -o "$OIDC_TMPFILE" \ |
| 77 | + "$TOKEN_URL" |
| 78 | +)" |
| 79 | + |
| 80 | +if [[ "$OIDC_HTTP_CODE" != 2* ]]; then |
| 81 | + echo "oidc-exchange: GitHub OIDC token fetch failed (HTTP ${OIDC_HTTP_CODE})" >&2 |
| 82 | + exit 1 |
| 83 | +fi |
| 84 | + |
| 85 | +# Parse {"value":"<jwt>"} without jq — extract value of "value" key |
| 86 | +ID_TOKEN="$(grep -o '"value":"[^"]*"' "$OIDC_TMPFILE" | grep -o '"[^"]*"$' | tr -d '"')" |
| 87 | +if [[ -z "$ID_TOKEN" ]]; then |
| 88 | + echo "oidc-exchange: could not parse id_token from OIDC response" >&2 |
| 89 | + exit 1 |
| 90 | +fi |
| 91 | + |
| 92 | +# ---------- 2. Exchange id_token for dovecote token ------------------------- |
| 93 | +DVCT_TMPFILE="$(mktemp)" |
| 94 | +trap 'rm -f "$OIDC_TMPFILE" "$DVCT_TMPFILE"' EXIT |
| 95 | + |
| 96 | +DVCT_HTTP_CODE="$( |
| 97 | + curl -sSL \ |
| 98 | + -X POST \ |
| 99 | + -H "Content-Type: application/json" \ |
| 100 | + -H "Accept: application/json" \ |
| 101 | + -d "{\"id_token\":\"${ID_TOKEN}\"}" \ |
| 102 | + -w "%{http_code}" \ |
| 103 | + -o "$DVCT_TMPFILE" \ |
| 104 | + "${DOVECOTE_SERVER_URL}/v1/auth/github-oidc" |
| 105 | +)" |
| 106 | + |
| 107 | +if [[ "$DVCT_HTTP_CODE" != 2* ]]; then |
| 108 | + echo "oidc-exchange: dovecote token exchange failed (HTTP ${DVCT_HTTP_CODE})" >&2 |
| 109 | + exit 1 |
| 110 | +fi |
| 111 | + |
| 112 | +# Parse {"token":"dvct_...",...} without jq |
| 113 | +DVCT_TOKEN="$(grep -o '"token":"[^"]*"' "$DVCT_TMPFILE" | grep -o '"[^"]*"$' | tr -d '"')" |
| 114 | +if [[ -z "$DVCT_TOKEN" ]]; then |
| 115 | + echo "oidc-exchange: could not parse token from dovecote response" >&2 |
| 116 | + exit 1 |
| 117 | +fi |
| 118 | + |
| 119 | +# ---------- 3. Mask BEFORE writing to GITHUB_ENV ---------------------------- |
| 120 | +echo "::add-mask::${DVCT_TOKEN}" |
| 121 | +echo "DOVECOTE_TOKEN=${DVCT_TOKEN}" >> "${GITHUB_ENV}" |
0 commit comments