From 12d4e6925b0d57738b2f7b0b407c0adb57d7c61c Mon Sep 17 00:00:00 2001 From: johba Date: Sat, 28 Mar 2026 08:37:14 +0000 Subject: [PATCH] fix: disinto init OAuth2 + WP v3 compatibility (#812, #814) - Rewrite URL-encoded Docker-internal hostnames in OAuth2 redirect - Submit all Forgejo grant form fields (client_id, state, redirect_uri, granted) - Add WOODPECKER_OPEN to compose template for first user OAuth registration - Add WOODPECKER_GRPC_ADDR to compose template - Fix WP repo activation: use query param with numeric Forgejo repo ID - WP v3 PAT creation via session cookie + CSRF header Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/disinto | 54 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/bin/disinto b/bin/disinto index ef6924d..cc3cdb3 100755 --- a/bin/disinto +++ b/bin/disinto @@ -190,7 +190,9 @@ services: WOODPECKER_FORGEJO_URL: http://forgejo:3000 WOODPECKER_FORGEJO_CLIENT: ${WP_FORGEJO_CLIENT:-} WOODPECKER_FORGEJO_SECRET: ${WP_FORGEJO_SECRET:-} - WOODPECKER_HOST: http://woodpecker:8000 + WOODPECKER_HOST: http://localhost:8000 + WOODPECKER_OPEN: "true" + WOODPECKER_GRPC_ADDR: ":9000" WOODPECKER_AGENT_SECRET: ${WOODPECKER_AGENT_SECRET:-} WOODPECKER_DATABASE_DRIVER: sqlite3 WOODPECKER_DATABASE_DATASOURCE: /var/lib/woodpecker/woodpecker.sqlite @@ -1444,9 +1446,16 @@ generate_woodpecker_token() { return 1 fi - # Rewrite internal Docker network URL to host-accessible URL - # (compose uses http://forgejo:3000 internally) - wp_redir=$(printf '%s' "$wp_redir" | sed "s|http://forgejo:3000|${forge_url}|g") + # Rewrite internal Docker network URLs to host-accessible URLs. + # Handle both plain and URL-encoded forms of the internal hostnames. + local forge_url_enc wp_server_enc + forge_url_enc=$(printf '%s' "$forge_url" | sed 's|:|%3A|g; s|/|%2F|g') + wp_server_enc=$(printf '%s' "$wp_server" | sed 's|:|%3A|g; s|/|%2F|g') + wp_redir=$(printf '%s' "$wp_redir" \ + | sed "s|http://forgejo:3000|${forge_url}|g" \ + | sed "s|http%3A%2F%2Fforgejo%3A3000|${forge_url_enc}|g" \ + | sed "s|http://woodpecker:8000|${wp_server}|g" \ + | sed "s|http%3A%2F%2Fwoodpecker%3A8000|${wp_server_enc}|g") # Step 3: Hit Forgejo OAuth authorize endpoint with session # First time: shows consent page. Already approved: redirects with code. @@ -1462,11 +1471,17 @@ generate_woodpecker_token() { # Auto-approved: extract code from redirect auth_code=$(printf '%s' "$redirect_loc" | sed 's/.*code=\([^&]*\).*/\1/') else - # Consent page: extract CSRF and POST grant approval - local consent_csrf + # Consent page: extract CSRF and all form fields, POST grant approval + local consent_csrf form_client_id form_state form_redirect_uri consent_csrf=$(grep -o 'name="_csrf"[^>]*' "$auth_body_file" 2>/dev/null \ | head -1 | grep -oE '(content|value)="[^"]*"' | head -1 \ | cut -d'"' -f2) || consent_csrf="" + form_client_id=$(grep 'name="client_id"' "$auth_body_file" 2>/dev/null \ + | grep -oE 'value="[^"]*"' | cut -d'"' -f2) || form_client_id="" + form_state=$(grep 'name="state"' "$auth_body_file" 2>/dev/null \ + | grep -oE 'value="[^"]*"' | cut -d'"' -f2) || form_state="" + form_redirect_uri=$(grep 'name="redirect_uri"' "$auth_body_file" 2>/dev/null \ + | grep -oE 'value="[^"]*"' | cut -d'"' -f2) || form_redirect_uri="" if [ -n "$consent_csrf" ]; then local grant_headers @@ -1474,6 +1489,12 @@ generate_woodpecker_token() { -D - -o /dev/null -X POST \ "${forge_url}/login/oauth/grant" \ --data-urlencode "_csrf=${consent_csrf}" \ + --data-urlencode "client_id=${form_client_id}" \ + --data-urlencode "state=${form_state}" \ + --data-urlencode "scope=" \ + --data-urlencode "nonce=" \ + --data-urlencode "redirect_uri=${form_redirect_uri}" \ + --data-urlencode "granted=true" \ 2>/dev/null) || grant_headers="" redirect_loc=$(printf '%s' "$grant_headers" \ @@ -1525,9 +1546,16 @@ generate_woodpecker_token() { fi # Step 5: Create persistent personal access token via Woodpecker API + # WP v3 requires CSRF header for POST operations with session tokens. + local wp_csrf + wp_csrf=$(curl -sf -b "user_sess=${wp_token}" \ + "${wp_server}/web-config.js" 2>/dev/null \ + | sed -n 's/.*WOODPECKER_CSRF = "\([^"]*\)".*/\1/p') || wp_csrf="" + local pat_resp final_token pat_resp=$(curl -sf -X POST \ - -H "Authorization: Bearer ${wp_token}" \ + -b "user_sess=${wp_token}" \ + ${wp_csrf:+-H "X-CSRF-Token: ${wp_csrf}"} \ "${wp_server}/api/user/token" \ 2>/dev/null) || pat_resp="" @@ -1589,12 +1617,18 @@ activate_woodpecker_repo() { if [ -n "$wp_repo_id" ] && [ "$wp_repo_id" != "0" ]; then echo "Repo: ${forge_repo} already active in Woodpecker (id=${wp_repo_id})" else + # Get Forgejo repo numeric ID for WP activation + local forge_repo_id + forge_repo_id=$(curl -sf \ + -H "Authorization: token ${FORGE_TOKEN}" \ + "${FORGE_URL:-http://localhost:3000}/api/v1/repos/${forge_repo}" 2>/dev/null \ + | jq -r '.id // empty' 2>/dev/null) || forge_repo_id="" + local activate_resp activate_resp=$(curl -sf -X POST \ -H "Authorization: Bearer ${wp_token}" \ - -H "Content-Type: application/json" \ - "${wp_server}/api/repos" \ - -d "{\"forge_remote_id\":\"${forge_repo}\"}" 2>/dev/null) || activate_resp="" + "${wp_server}/api/repos?forge_remote_id=${forge_repo_id:-0}" \ + 2>/dev/null) || activate_resp="" wp_repo_id=$(printf '%s' "$activate_resp" | jq -r '.id // empty' 2>/dev/null) || true