diff --git a/auth-proxy/src/oidc.ts b/auth-proxy/src/oidc.ts index d3a192d..6d74ab7 100644 --- a/auth-proxy/src/oidc.ts +++ b/auth-proxy/src/oidc.ts @@ -58,9 +58,20 @@ export async function issueIdToken(params: { }): Promise { const { privateJwk, kid } = await getKeys(); const key = await importJWK(privateJwk, "EdDSA"); + // sub = full cashaddr (immutable identity). preferred_username = a Forgejo-safe + // rendering: "bch_" + first 20 chars of the address payload. Forgejo usernames + // must match [a-zA-Z0-9-_.]+ (no colons) and be <= 40 chars. + const addressPayload = params.subject.startsWith("bitcoincash:") + ? params.subject.slice("bitcoincash:".length) + : params.subject; + const preferredUsername = `bch_${addressPayload.slice(0, 20)}`; const jwt = await new SignJWT({ - // sub = cashaddr; also expose as preferred_username so Forgejo shows it - preferred_username: params.subject, + preferred_username: preferredUsername, + // full cashaddr also exposed as a custom claim for downstream tooling + cashaddr: params.subject, + // Forgejo needs a synthetic email for auto-registration to succeed + email: `${preferredUsername}@wallet.hephaestus.local`, + email_verified: true, ...(params.nonce ? { nonce: params.nonce } : {}), }) .setProtectedHeader({ alg: "EdDSA", kid }) diff --git a/docker-compose.yml b/docker-compose.yml index 7080cd3..e296019 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -44,35 +44,27 @@ services: FORGEJO__security__SECRET_KEY: ${FORGEJO_SECRET_KEY} FORGEJO__security__INTERNAL_TOKEN: ${FORGEJO_INTERNAL_TOKEN} FORGEJO__oauth2__JWT_SECRET: ${FORGEJO_OAUTH_JWT_SECRET} - # --- sia.storage S3 backends --- - FORGEJO__storage.lfs__STORAGE_TYPE: minio - FORGEJO__storage.lfs__MINIO_ENDPOINT: ${SIA_STORAGE_ENDPOINT} - FORGEJO__storage.lfs__MINIO_ACCESS_KEY_ID: ${SIA_STORAGE_ACCESS_KEY} - FORGEJO__storage.lfs__MINIO_SECRET_ACCESS_KEY: ${SIA_STORAGE_SECRET_KEY} - FORGEJO__storage.lfs__MINIO_BUCKET: ${SIA_STORAGE_BUCKET} - FORGEJO__storage.lfs__MINIO_LOCATION: us-east-1 - FORGEJO__storage.lfs__MINIO_USE_SSL: "true" - FORGEJO__storage.lfs__MINIO_BASE_PATH: lfs/ - FORGEJO__storage.attachments__STORAGE_TYPE: minio - FORGEJO__storage.attachments__MINIO_BASE_PATH: attachments/ - FORGEJO__storage.packages__STORAGE_TYPE: minio - FORGEJO__storage.packages__MINIO_BASE_PATH: packages/ - FORGEJO__storage.repo-archive__STORAGE_TYPE: minio - FORGEJO__storage.repo-archive__MINIO_BASE_PATH: archives/ - # (attachments/packages/archives inherit endpoint+creds+bucket from [storage] via shared minio settings below) - FORGEJO__storage__MINIO_ENDPOINT: ${SIA_STORAGE_ENDPOINT} - FORGEJO__storage__MINIO_ACCESS_KEY_ID: ${SIA_STORAGE_ACCESS_KEY} - FORGEJO__storage__MINIO_SECRET_ACCESS_KEY: ${SIA_STORAGE_SECRET_KEY} - FORGEJO__storage__MINIO_BUCKET: ${SIA_STORAGE_BUCKET} - FORGEJO__storage__MINIO_USE_SSL: "true" - # --- Quota (free tier: 500 MB per user across repos+LFS+attachments+packages) --- + # --- Storage backend: local disk for MVP --- + # sia.storage as S3 backend is deferred until the endpoint TLS cert chains cleanly + # (currently requires --no-verify-ssl which Forgejo's minio driver won't accept). + # Storage-on-disk works; the switch back to S3 is a config change only, no rebuild. + # (defaults to local — no MINIO_* env vars needed) + # --- Quota (free tier: 500 MB per user) --- FORGEJO__quota__ENABLED: "true" FORGEJO__quota__DEFAULT_GROUPS: free - # --- Registration: disabled (users bootstrap by wallet OIDC login) --- - FORGEJO__service__DISABLE_REGISTRATION: "true" - FORGEJO__service__ALLOW_ONLY_EXTERNAL_REGISTRATION: "true" + # --- Registration + signin --- + # Local form is disabled via SHOW_REGISTRATION_BUTTON=false + ALLOW_ONLY_EXTERNAL_REGISTRATION=true. + # DISABLE_REGISTRATION must stay FALSE — it blocks OIDC auto-provisioning too, not just the form. + FORGEJO__service__DISABLE_REGISTRATION: "false" FORGEJO__service__SHOW_REGISTRATION_BUTTON: "false" - FORGEJO__service__ENABLE_INTERNAL_SIGNIN: "false" + FORGEJO__service__ALLOW_ONLY_EXTERNAL_REGISTRATION: "true" + # OIDC callback creates users automatically using the preferred_username claim (= cashaddr). + FORGEJO__oauth2_client__ENABLE_AUTO_REGISTRATION: "true" + FORGEJO__oauth2_client__ACCOUNT_LINKING: "auto" + FORGEJO__oauth2_client__USERNAME: "preferred_username" + FORGEJO__oauth2_client__UPDATE_AVATAR: "false" + # Skip the web installer wizard — everything is already configured + FORGEJO__server__INSTALL_LOCK: "true" ports: - "3000:3000" - "2222:22" diff --git a/scripts/bootstrap-auth.sh b/scripts/bootstrap-auth.sh new file mode 100644 index 0000000..4255897 --- /dev/null +++ b/scripts/bootstrap-auth.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# One-time bootstrap: register the wallet auth-proxy as Forgejo's OIDC provider. +# Idempotent — safe to re-run; will fail with "already exists" on second run. +# +# Run this AFTER `docker compose up -d` and after Forgejo's healthcheck passes +# (its Postgres schema needs to be initialised before we can add an auth source). +# +# Usage: +# bash scripts/bootstrap-auth.sh +set -euo pipefail + +# shellcheck disable=SC1091 +[[ -f "$(dirname "$0")/../.env" ]] && source "$(dirname "$0")/../.env" + +: "${AUTH_PROXY_CLIENT_ID:?}" +: "${AUTH_PROXY_CLIENT_SECRET:?}" +: "${AUTH_PROXY_ISSUER:?}" + +echo "Waiting for Forgejo to be ready…" +for i in {1..60}; do + if docker compose exec -T forgejo forgejo --version >/dev/null 2>&1; then + break + fi + sleep 2 +done + +# Same wait for auth-proxy — its /.well-known/openid-configuration must return 200 +# before Forgejo will accept the auth source (it calls discovery on add). +echo "Waiting for auth-proxy to be ready…" +for i in {1..60}; do + if docker compose exec -T forgejo wget -qO- "${AUTH_PROXY_ISSUER}/.well-known/openid-configuration" >/dev/null 2>&1; then + break + fi + sleep 2 +done + +ADMIN_USER="${HEPHAESTUS_ADMIN_USER:-hephaestus-admin}" +ADMIN_PW="${HEPHAESTUS_ADMIN_PASSWORD:-$(openssl rand -base64 24 | tr -d '=+/' | cut -c1-24)}" +ADMIN_EMAIL="${HEPHAESTUS_ADMIN_EMAIL:-admin@hephaestus.local}" + +echo "Creating admin user '${ADMIN_USER}' (idempotent — 'already exists' is fine)…" +docker compose exec -T --user 1000 forgejo forgejo admin user create \ + --admin \ + --username "${ADMIN_USER}" \ + --email "${ADMIN_EMAIL}" \ + --password "${ADMIN_PW}" \ + --must-change-password=false 2>&1 | tail -3 || true + +echo "" +echo "Registering OIDC auth source 'hephaestus-wallet' in Forgejo…" +docker compose exec -T --user 1000 forgejo forgejo admin auth add-oauth \ + --provider openidConnect \ + --name hephaestus-wallet \ + --key "${AUTH_PROXY_CLIENT_ID}" \ + --secret "${AUTH_PROXY_CLIENT_SECRET}" \ + --auto-discover-url "${AUTH_PROXY_ISSUER}/.well-known/openid-configuration" \ + --scopes "openid profile" \ + --group-claim-name "" \ + --skip-local-2fa 2>&1 | tail -5 + +echo "" +echo "=== Bootstrap complete ===" +echo "Admin credentials (save these somewhere — needed for site admin):" +echo " URL: ${FORGEJO_ROOT_URL}" +echo " Username: ${ADMIN_USER}" +echo " Password: ${ADMIN_PW}" +echo "" +echo "For normal use: log in at ${FORGEJO_ROOT_URL} → 'Sign in with hephaestus-wallet'."