# Web Session Authentication (Cookie + CSRF)

> **Server**: Forgejo 14.0.4 (Gitea 1.22.0 compatible) at `https://git.cleverthis.com`
> **API Base**: `https://git.cleverthis.com/api/v1`

Web session authentication replicates a browser login flow via curl. This is required for accessing features that are only available through the web UI and have no REST API equivalent.

---

## When Do You Need Web Login?

Most Forgejo interactions are available through the REST API using PAT authentication. However, some features are **web-only** and require session cookies:

| Feature | Available via REST API? | Requires Web Login? |
|---|---|---|
| Repository CRUD | Yes | No |
| Issues, PRs | Yes | No |
| User/Org management | Yes | No |
| Webhooks | Yes | No |
| **CI/Actions run logs** | **No** | **Yes** |
| **CI/Actions run details (web view)** | **No** | **Yes** |
| **Actions workflow re-run** | Partial | Sometimes |
| **Repo settings pages** | Partial | Sometimes |
| **Admin dashboard** | Partial | Sometimes |

The most common reason for web session auth is **accessing CI/Actions run logs**, which are served as HTML pages with no corresponding REST API endpoint for fetching full log output.

---

## The CSRF Login Flow

Forgejo protects its web forms with CSRF (Cross-Site Request Forgery) tokens. To authenticate via curl, you must:

1. Fetch the login page to get a CSRF token
2. Submit the login form with credentials and the CSRF token
3. Use the resulting session cookie for subsequent requests

### Complete Flow

```bash
#!/usr/bin/env bash
# Web session login for Forgejo
# Requires: FORGEJO_URL, FORGEJO_USERNAME, FORGEJO_PASSWORD

set -euo pipefail

# Step 1: Fetch the login page and extract the CSRF token
# Use $$ (current PID) to create a unique cookie jar per process
COOKIE_JAR="/tmp/forgejo-cookies-$$.txt"

LOGIN_PAGE=$(curl -sS -c "$COOKIE_JAR" "${FORGEJO_URL}/user/login")

# Extract CSRF token from the meta tag in the HTML head
CSRF=$(echo "$LOGIN_PAGE" | grep -oP 'name="_csrf"\s+content="\K[^"]+')

if [ -z "$CSRF" ]; then
  echo "ERROR: Failed to extract CSRF token from login page" >&2
  rm -f "$COOKIE_JAR"
  exit 1
fi

echo "CSRF token obtained: ${CSRF:0:10}..."

# Step 2: Submit login form with credentials and CSRF token
# -L follows redirects (Forgejo redirects on successful login)
# -c writes cookies, -b reads cookies (both needed for the full flow)
LOGIN_RESPONSE=$(curl -sS -L \
  -c "$COOKIE_JAR" -b "$COOKIE_JAR" \
  -o /dev/null -w "%{http_code}" \
  --data-urlencode "_csrf=${CSRF}" \
  --data-urlencode "user_name=${FORGEJO_USERNAME}" \
  --data-urlencode "password=${FORGEJO_PASSWORD}" \
  "${FORGEJO_URL}/user/login")

if [ "$LOGIN_RESPONSE" != "200" ]; then
  echo "ERROR: Login failed with HTTP status $LOGIN_RESPONSE" >&2
  rm -f "$COOKIE_JAR"
  exit 1
fi

echo "Login successful"

# Step 3: Use the session cookie for web-only requests
# Example: Fetch a CI/Actions run page
curl -sS -b "$COOKIE_JAR" \
  "${FORGEJO_URL}/{owner}/{repo}/actions/runs/{run_id}" > /tmp/run-page.html

# Step 4: Clean up — always remove the cookie jar when done
rm -f "$COOKIE_JAR"
```

---

## Step-by-Step Breakdown

### Step 1: Fetch Login Page and Extract CSRF Token

```bash
COOKIE_JAR="/tmp/forgejo-cookies-$$.txt"
LOGIN_PAGE=$(curl -sS -c "$COOKIE_JAR" "${FORGEJO_URL}/user/login")
CSRF=$(echo "$LOGIN_PAGE" | grep -oP 'name="_csrf"\s+content="\K[^"]+')
```

**What happens**:
- `curl -c "$COOKIE_JAR"` saves any cookies set by the server (the initial session cookie).
- The login page HTML contains a meta tag like: `<meta name="_csrf" content="abc123...">`
- The `grep -oP` extracts the CSRF token value using a Perl-compatible regex with lookbehind.

**Alternative CSRF extraction** (if `grep -P` is not available):

```bash
# Using sed
CSRF=$(echo "$LOGIN_PAGE" | sed -n 's/.*name="_csrf"\s\+content="\([^"]*\)".*/\1/p' | head -1)

# Using awk
CSRF=$(echo "$LOGIN_PAGE" | awk -F'"' '/name="_csrf"/{for(i=1;i<=NF;i++)if($i~/ content=/)print $(i+1)}' | head -1)
```

### Step 2: Submit Login Credentials

```bash
curl -sS -L -c "$COOKIE_JAR" -b "$COOKIE_JAR" \
  --data-urlencode "_csrf=${CSRF}" \
  --data-urlencode "user_name=${FORGEJO_USERNAME}" \
  --data-urlencode "password=${FORGEJO_PASSWORD}" \
  "${FORGEJO_URL}/user/login"
```

**What happens**:
- `-b "$COOKIE_JAR"` sends the session cookie from Step 1.
- `-c "$COOKIE_JAR"` saves the updated session cookie (now authenticated).
- `--data-urlencode` safely encodes the form fields, including special characters in passwords.
- `-L` follows the redirect chain — Forgejo redirects to the dashboard on success.
- The CSRF token binds the login request to the session, preventing cross-site attacks.

**Form fields**:

| Field | Description |
|---|---|
| `_csrf` | CSRF token from Step 1 |
| `user_name` | Forgejo username |
| `password` | Forgejo password |

### Step 3: Make Authenticated Web Requests

```bash
# Access any web-only resource using the session cookie
curl -sS -b "$COOKIE_JAR" "${FORGEJO_URL}/{owner}/{repo}/actions/runs/{run_id}"
```

The session cookie in the jar authenticates the request as the logged-in user.

### Step 4: Clean Up

```bash
rm -f "$COOKIE_JAR"
```

Always remove the cookie jar when done. The session cookie grants full access to the account.

---

## Practical Example: Fetching CI/Actions Run Logs

This is the most common use case for web session authentication.

```bash
#!/usr/bin/env bash
# Fetch CI/Actions run logs from Forgejo
set -euo pipefail

OWNER="{owner}"
REPO="{repo}"
RUN_ID="{run_id}"

# --- Login ---
COOKIE_JAR="/tmp/forgejo-cookies-$$.txt"
trap "rm -f $COOKIE_JAR" EXIT  # Auto-cleanup on exit

LOGIN_PAGE=$(curl -sS -c "$COOKIE_JAR" "${FORGEJO_URL}/user/login")
CSRF=$(echo "$LOGIN_PAGE" | grep -oP 'name="_csrf"\s+content="\K[^"]+')

curl -sS -L -c "$COOKIE_JAR" -b "$COOKIE_JAR" \
  -o /dev/null \
  --data-urlencode "_csrf=${CSRF}" \
  --data-urlencode "user_name=${FORGEJO_USERNAME}" \
  --data-urlencode "password=${FORGEJO_PASSWORD}" \
  "${FORGEJO_URL}/user/login"

# --- Fetch the run page ---
RUN_PAGE=$(curl -sS -b "$COOKIE_JAR" \
  "${FORGEJO_URL}/${OWNER}/${REPO}/actions/runs/${RUN_ID}")

echo "$RUN_PAGE" > /tmp/run-page.html
echo "Run page saved to /tmp/run-page.html"

# --- Extract log content (example: look for log output in the HTML) ---
# The exact HTML structure depends on the Forgejo version.
# Logs may be in <pre> tags, data attributes, or fetched via XHR endpoints.

# Option A: If logs are fetched via an API-like endpoint under the web session
curl -sS -b "$COOKIE_JAR" \
  "${FORGEJO_URL}/${OWNER}/${REPO}/actions/runs/${RUN_ID}/jobs/0/logs" \
  > /tmp/run-logs.txt 2>/dev/null || true

echo "Log fetch attempted — check /tmp/run-logs.txt"
```

---

## Practical Example: Reusable Login Function

For scripts that make multiple web requests, wrap login in a function:

```bash
#!/usr/bin/env bash
set -euo pipefail

COOKIE_JAR=""

forgejo_web_login() {
  # Create unique cookie jar
  COOKIE_JAR="/tmp/forgejo-cookies-$$.txt"

  # Fetch CSRF token
  local login_page
  login_page=$(curl -sS -c "$COOKIE_JAR" "${FORGEJO_URL}/user/login")

  local csrf
  csrf=$(echo "$login_page" | grep -oP 'name="_csrf"\s+content="\K[^"]+')

  if [ -z "$csrf" ]; then
    echo "ERROR: Failed to extract CSRF token" >&2
    return 1
  fi

  # Login
  local status
  status=$(curl -sS -L \
    -c "$COOKIE_JAR" -b "$COOKIE_JAR" \
    -o /dev/null -w "%{http_code}" \
    --data-urlencode "_csrf=${csrf}" \
    --data-urlencode "user_name=${FORGEJO_USERNAME}" \
    --data-urlencode "password=${FORGEJO_PASSWORD}" \
    "${FORGEJO_URL}/user/login")

  if [ "$status" != "200" ]; then
    echo "ERROR: Login failed (HTTP $status)" >&2
    return 1
  fi

  echo "Web session established"
}

forgejo_web_fetch() {
  # Fetch a URL using the authenticated session
  local url="$1"
  if [ -z "$COOKIE_JAR" ] || [ ! -f "$COOKIE_JAR" ]; then
    echo "ERROR: Not logged in. Call forgejo_web_login first." >&2
    return 1
  fi
  curl -sS -b "$COOKIE_JAR" "$url"
}

forgejo_web_logout() {
  # Clean up cookie jar
  if [ -n "$COOKIE_JAR" ] && [ -f "$COOKIE_JAR" ]; then
    rm -f "$COOKIE_JAR"
    echo "Session cleaned up"
  fi
  COOKIE_JAR=""
}

# Usage:
# forgejo_web_login
# forgejo_web_fetch "${FORGEJO_URL}/owner/repo/actions/runs/1"
# forgejo_web_logout
```

---

## Cookie Management Details

### Cookie Jar Format

The cookie jar is a Netscape-format text file managed by curl:

```
# Netscape HTTP Cookie File
.git.cleverthis.com   TRUE   /   TRUE   0   i_like_gitea   ...session_value...
.git.cleverthis.com   TRUE   /   FALSE  0   _csrf          ...csrf_value...
```

### Key Cookies

| Cookie | Purpose |
|---|---|
| `i_like_gitea` | Session identifier (yes, Forgejo inherits this cookie name from Gitea) |
| `_csrf` | CSRF protection cookie (distinct from the form CSRF token) |
| `lang` | Language preference |

### Session Lifetime

- Sessions have a server-configured timeout (default varies by installation).
- If a session expires mid-script, you will get HTML login pages instead of the expected content.
- For long-running scripts, check for session validity and re-login if needed:

```bash
# Check if session is still valid
check_session() {
  local status
  status=$(curl -sS -b "$COOKIE_JAR" -o /dev/null -w "%{http_code}" \
    "${FORGEJO_URL}/user/settings")

  # 200 = logged in, 302 = redirected to login page
  if [ "$status" = "200" ]; then
    return 0  # Session valid
  else
    return 1  # Session expired
  fi
}
```

---

## CSRF Token Details

Forgejo uses a double-submit CSRF protection pattern:

1. **Meta tag token**: Embedded in the HTML `<meta name="_csrf" content="...">` — used for form submissions.
2. **Cookie token**: Set in the `_csrf` cookie — automatically sent by the browser (or curl with `-b`).

Both must be present and match for a form submission to succeed. The curl flow handles this automatically when you:
- Use `-c` to save cookies (captures the `_csrf` cookie)
- Use `-b` to send cookies (sends the `_csrf` cookie back)
- Include `_csrf` as a form field (from the meta tag extraction)

### CSRF for Non-Login Forms

If you need to submit other web forms (not login) after authenticating, you'll need to:

1. Fetch the page containing the form
2. Extract the CSRF token from that page's meta tag
3. Submit the form with the new CSRF token

```bash
# Example: Access a settings page and extract its CSRF token
SETTINGS_PAGE=$(curl -sS -b "$COOKIE_JAR" "${FORGEJO_URL}/user/settings")
NEW_CSRF=$(echo "$SETTINGS_PAGE" | grep -oP 'name="_csrf"\s+content="\K[^"]+')

# Submit a form on that page
curl -sS -L -b "$COOKIE_JAR" -c "$COOKIE_JAR" \
  --data-urlencode "_csrf=${NEW_CSRF}" \
  --data-urlencode "some_field=some_value" \
  "${FORGEJO_URL}/user/settings"
```

---

## Security Considerations

1. **Cookie jar files contain active sessions** — they grant full account access. Protect them like passwords.

2. **Always clean up cookie jars** — use `trap` to ensure cleanup even on script errors:
   ```bash
   COOKIE_JAR="/tmp/forgejo-cookies-$$.txt"
   trap "rm -f $COOKIE_JAR" EXIT
   ```

3. **Use unique cookie jar paths** — include `$$` (PID) or `$(mktemp)` to avoid collisions between concurrent processes:
   ```bash
   COOKIE_JAR=$(mktemp /tmp/forgejo-cookies-XXXXXX.txt)
   ```

4. **Restrict file permissions** — set the cookie jar to be readable only by the current user:
   ```bash
   COOKIE_JAR=$(mktemp /tmp/forgejo-cookies-XXXXXX.txt)
   chmod 600 "$COOKIE_JAR"
   ```

5. **Never log cookie values** — session cookies in logs allow session hijacking.

6. **Use HTTPS only** — session cookies transmitted over HTTP can be intercepted.

7. **This is not a stable API** — web pages and form structures can change between Forgejo versions. Scripts relying on web session auth are inherently more fragile than REST API scripts. Always test after Forgejo upgrades.

---

## Troubleshooting

### CSRF token extraction fails

The HTML structure may differ. Inspect the login page manually:

```bash
curl -sS "${FORGEJO_URL}/user/login" | grep -i csrf
```

Look for patterns like:
- `<meta name="_csrf" content="...">`
- `<input type="hidden" name="_csrf" value="...">`

Adjust your extraction regex accordingly.

### Login returns 200 but session is not authenticated

- Check that you are sending the cookie jar with both `-c` and `-b` flags.
- Verify the CSRF token is being sent correctly (use `--data-urlencode` to handle special characters).
- Check for 2FA — if the account has two-factor authentication enabled, the login flow requires an additional TOTP step that is difficult to automate.

### "422 Unprocessable Entity" on login

- The CSRF token may have expired. CSRF tokens are tied to the session and have a limited lifetime.
- Fetch a new login page and extract a fresh CSRF token.

### Session expires during long scripts

Re-login when you detect a session expiry (302 redirect to `/user/login`):

```bash
# Wrapper that auto-retries with re-login
forgejo_web_fetch_with_retry() {
  local url="$1"
  local response
  local status

  status=$(curl -sS -b "$COOKIE_JAR" -o /tmp/forgejo-response.html \
    -w "%{http_code}" -L "$url")

  # If redirected to login page, re-authenticate
  if [ "$status" = "200" ] && grep -q '/user/login' /tmp/forgejo-response.html; then
    echo "Session expired, re-authenticating..." >&2
    forgejo_web_login
    curl -sS -b "$COOKIE_JAR" "$url"
  else
    cat /tmp/forgejo-response.html
  fi
}
```

### 2FA (Two-Factor Authentication)

If the account has 2FA enabled, the login flow requires an additional step after the initial login POST. The server returns a 2FA prompt page. Automating this requires:

1. Parsing the 2FA page for a new CSRF token
2. Submitting the TOTP code

```bash
# After initial login, if 2FA is required:
TWOFACTOR_PAGE=$(curl -sS -b "$COOKIE_JAR" -c "$COOKIE_JAR" \
  "${FORGEJO_URL}/user/two_factor")
CSRF_2FA=$(echo "$TWOFACTOR_PAGE" | grep -oP 'name="_csrf"\s+content="\K[^"]+')

curl -sS -L -b "$COOKIE_JAR" -c "$COOKIE_JAR" \
  --data-urlencode "_csrf=${CSRF_2FA}" \
  --data-urlencode "passcode=${TOTP_CODE}" \
  "${FORGEJO_URL}/user/two_factor"
```

Generating `TOTP_CODE` programmatically requires the TOTP secret and a tool like `oathtool`:

```bash
TOTP_CODE=$(oathtool --totp -b "$TOTP_SECRET")
```

This is fragile and not recommended for routine automation. If you need automated access to a 2FA-protected account, use PAT authentication for REST API calls instead.
