# Personal Access Tokens (PAT)

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

Personal Access Tokens are the primary authentication mechanism for programmatic API access in Forgejo. They are scoped, revocable credentials that act on behalf of a user.

---

## Key Concepts

- **Token management endpoints require basic auth** — you cannot use a PAT to list, create, or delete PATs. This is a deliberate security boundary.
- **Token values are shown only once** — the `sha1` field in the creation response contains the token. It is never retrievable again after creation.
- **Tokens are scoped** — each token has a set of permission scopes that limit what it can access.
- **Tokens can have expiration** — optionally set an expiry date when creating a token.

---

## Endpoints

All token management endpoints are under `/users/{username}/tokens` and **require basic authentication**.

### List Tokens

```
GET /api/v1/users/{username}/tokens
```

Returns a list of all tokens for the authenticated user. Note: the token values (`sha1`) are **not** included in list responses — only metadata (id, name, scopes, etc.).

**Authentication**: Basic auth required.

```bash
curl -s "${FORGEJO_URL}/api/v1/users/${FORGEJO_USERNAME}/tokens" \
  -u "${FORGEJO_USERNAME}:${FORGEJO_PASSWORD}" | jq .
```

**Response** (array of AccessToken objects):

```json
[
  {
    "id": 1,
    "name": "{token_name}",
    "sha1": "",
    "token_last_eight": "a1b2c3d4",
    "scopes": ["all"]
  }
]
```

---

### Create Token

```
POST /api/v1/users/{username}/tokens
```

Creates a new personal access token. The token value is returned **only in this response**.

**Authentication**: Basic auth required.

**Request Body** (`CreateAccessTokenOption`):

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | Yes | Unique name for the token. Used to identify and delete the token. |
| `scopes` | array of strings | No | Permission scopes. Defaults to no scopes (very limited). Use `["all"]` for full access. |

```bash
# Create a token with full access
curl -s -X POST "${FORGEJO_URL}/api/v1/users/${FORGEJO_USERNAME}/tokens" \
  -u "${FORGEJO_USERNAME}:${FORGEJO_PASSWORD}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "{token_name}",
    "scopes": ["all"]
  }' | jq .
```

**Response** (AccessToken object with `sha1` populated):

```json
{
  "id": 42,
  "name": "{token_name}",
  "sha1": "{full_token}",
  "token_last_eight": "ef456789",
  "scopes": ["all"]
}
```

**IMPORTANT**: Save the `sha1` value immediately. It will never be shown again.

```bash
# Create and capture the token value in one step
NEW_TOKEN=$(curl -s -X POST "${FORGEJO_URL}/api/v1/users/${FORGEJO_USERNAME}/tokens" \
  -u "${FORGEJO_USERNAME}:${FORGEJO_PASSWORD}" \
  -H "Content-Type: application/json" \
  -d '{"name": "{token_name}", "scopes": ["all"]}' | jq -r '.sha1')

echo "Token: ${NEW_TOKEN}"
```

#### Create a Token with Specific Scopes

```bash
# Create a read-only token for repository access
curl -s -X POST "${FORGEJO_URL}/api/v1/users/${FORGEJO_USERNAME}/tokens" \
  -u "${FORGEJO_USERNAME}:${FORGEJO_PASSWORD}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "{token_name}",
    "scopes": ["read:repository", "read:user"]
  }' | jq .
```

---

### Delete Token

```
DELETE /api/v1/users/{username}/tokens/{token}
```

Deletes a token by its name or ID. The `{token}` path parameter can be either the token's **name** (string) or its **numeric ID**.

**Authentication**: Basic auth required.

```bash
# Delete by token name
curl -s -X DELETE "${FORGEJO_URL}/api/v1/users/${FORGEJO_USERNAME}/tokens/{token_name}" \
  -u "${FORGEJO_USERNAME}:${FORGEJO_PASSWORD}"

# Delete by token ID
curl -s -X DELETE "${FORGEJO_URL}/api/v1/users/${FORGEJO_USERNAME}/tokens/{token_id}" \
  -u "${FORGEJO_USERNAME}:${FORGEJO_PASSWORD}"
```

**Response**: `204 No Content` on success (empty body).

```bash
# Delete and verify (check HTTP status code)
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
  -X DELETE "${FORGEJO_URL}/api/v1/users/${FORGEJO_USERNAME}/tokens/{token_name}" \
  -u "${FORGEJO_USERNAME}:${FORGEJO_PASSWORD}")

if [ "$HTTP_STATUS" = "204" ]; then
  echo "Token deleted successfully"
elif [ "$HTTP_STATUS" = "404" ]; then
  echo "Token not found"
elif [ "$HTTP_STATUS" = "422" ]; then
  echo "Validation error"
else
  echo "Unexpected status: $HTTP_STATUS"
fi
```

---

## Using a Token for API Access

Once you have a token, use it with the `Authorization: token` header for all standard API calls:

```bash
# Get authenticated user info
curl -s "${FORGEJO_URL}/api/v1/user" \
  -H "Authorization: token ${FORGEJO_PAT}" | jq .

# List your repositories
curl -s "${FORGEJO_URL}/api/v1/user/repos" \
  -H "Authorization: token ${FORGEJO_PAT}" | jq '.[].full_name'

# Create an issue
curl -s -X POST "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues" \
  -H "Authorization: token ${FORGEJO_PAT}" \
  -H "Content-Type: application/json" \
  -d '{"title": "Bug report", "body": "Description here"}'
```

The token can also be passed as a query parameter, though this is **less secure** since URLs may appear in server logs:

```bash
# Query parameter method (NOT recommended for production)
curl -s "${FORGEJO_URL}/api/v1/user?token=${FORGEJO_PAT}"
```

---

## Token Scopes

Scopes control what a token can access. Use the principle of least privilege — grant only the scopes your use case requires.

### Available Scopes

| Scope | Description |
|---|---|
| `all` | Full access to all API endpoints. Equivalent to granting every scope. |
| `read:activitypub` | Read ActivityPub-related data |
| `write:activitypub` | Write ActivityPub-related data |
| `read:admin` | Read admin-level data (users, orgs, settings) |
| `write:admin` | Write admin-level data |
| `read:issue` | Read issues and comments |
| `write:issue` | Create/edit issues and comments |
| `read:misc` | Read miscellaneous data (signing keys, markdown rendering, etc.) |
| `write:misc` | Write miscellaneous data |
| `read:notification` | Read notifications |
| `write:notification` | Mark notifications as read |
| `read:organization` | Read organization data |
| `write:organization` | Create/edit organizations and teams |
| `read:package` | Read package data |
| `write:package` | Upload/delete packages |
| `read:repository` | Read repository data, branches, commits, files |
| `write:repository` | Create/edit repos, branches, push, manage hooks |
| `read:user` | Read user profile data, followers, stars |
| `write:user` | Edit user settings, manage SSH keys, manage OAuth2 apps |

### Scope Format

- `read:*` grants read-only access to that category.
- `write:*` grants read **and** write access (write implies read).
- `all` grants everything.

### Common Scope Combinations

```bash
# CI/CD automation: needs repo and issue access
'{"name": "{token_name}", "scopes": ["write:repository", "write:issue"]}'

# Read-only monitoring: check repo status and issues
'{"name": "{token_name}", "scopes": ["read:repository", "read:issue"]}'

# Organization management
'{"name": "{token_name}", "scopes": ["write:organization", "read:user"]}'

# Full access (use sparingly)
'{"name": "{token_name}", "scopes": ["all"]}'
```

---

## Token Expiration

Tokens can optionally be created with an expiration date. Once expired, the token stops working.

```bash
# Create a token that expires in 90 days (if supported by server version)
# Note: The expiration field may vary by Forgejo version
curl -s -X POST "${FORGEJO_URL}/api/v1/users/${FORGEJO_USERNAME}/tokens" \
  -u "${FORGEJO_USERNAME}:${FORGEJO_PASSWORD}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "{token_name}",
    "scopes": ["all"]
  }' | jq .
```

If your Forgejo version supports the `expiration` field, you can set it as an ISO 8601 timestamp. Check the Swagger docs at `${FORGEJO_URL}/api/swagger` for your specific version's supported fields.

---

## Best Practices

1. **Use descriptive token names** — name tokens after their purpose (e.g., `ci-deploy`, `backup-script`, `monitoring-readonly`).

2. **Apply minimum scopes** — never use `["all"]` when a narrower scope suffices.

3. **One token per use case** — create separate tokens for separate applications so you can revoke individually.

4. **Store tokens securely** — use environment variables, secret managers, or encrypted configuration. Never hardcode tokens in source code.

5. **Rotate regularly** — periodically delete old tokens and create new ones.

6. **Audit periodically** — list your tokens and remove any that are no longer in use:
   ```bash
   curl -s "${FORGEJO_URL}/api/v1/users/${FORGEJO_USERNAME}/tokens" \
     -u "${FORGEJO_USERNAME}:${FORGEJO_PASSWORD}" | jq '.[].name'
   ```

7. **Never log tokens** — be cautious with shell commands that might echo or log the token value.

---

## Troubleshooting

### "401 Unauthorized" when managing tokens

Token endpoints require **basic auth**, not token auth:

```bash
# WRONG — will return 401
curl -s "${FORGEJO_URL}/api/v1/users/${FORGEJO_USERNAME}/tokens" \
  -H "Authorization: token ${FORGEJO_PAT}"

# CORRECT — use basic auth
curl -s "${FORGEJO_URL}/api/v1/users/${FORGEJO_USERNAME}/tokens" \
  -u "${FORGEJO_USERNAME}:${FORGEJO_PASSWORD}"
```

### "422 Unprocessable Entity" when creating a token

The token name is likely already taken. Token names must be unique per user.

```bash
# Check existing token names
curl -s "${FORGEJO_URL}/api/v1/users/${FORGEJO_USERNAME}/tokens" \
  -u "${FORGEJO_USERNAME}:${FORGEJO_PASSWORD}" | jq '.[].name'
```

### Token stopped working

- The token may have been deleted or expired.
- The token scopes may not cover the endpoint you are calling.
- Verify the token is still valid:
  ```bash
  curl -s -o /dev/null -w "%{http_code}" \
    "${FORGEJO_URL}/api/v1/user" \
    -H "Authorization: token ${FORGEJO_PAT}"
  # 200 = valid, 401 = invalid/expired
  ```
