Compare commits

..

1 Commits

Author SHA1 Message Date
appleboy be5114b976 feat: add CSRF crumb support and session management for Jenkins API
- Add support for Jenkins CSRF protection by managing and adding CSRF crumb to POST requests
- Store and cache CSRF crumb after fetching from Jenkins for session reuse
- Use an HTTP client with CookieJar for session management to support CSRF crumb handling
- Update existing tests to check for presence of CookieJar in the HTTP client
- Improve test servers to only count job trigger POSTs, ignoring GET requests for crumbs

fix #48

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-12-27 10:42:00 +08:00
7 changed files with 28 additions and 230 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
+6 -145
View File
@@ -46,16 +46,10 @@ Whether you're managing a hybrid CI/CD environment or orchestrating complex mult
- [Configuration](#configuration)
- [Jenkins Server Setup](#jenkins-server-setup)
- [Authentication](#authentication)
- [Understanding Jenkins Authentication](#understanding-jenkins-authentication)
- [CSRF Protection Notice](#csrf-protection-notice)
- [Parameters Reference](#parameters-reference)
- [Usage](#usage)
- [Command Line](#command-line)
- [Docker](#docker)
- [Troubleshooting](#troubleshooting)
- [Error: 403 No valid crumb was included in the request](#error-403-no-valid-crumb-was-included-in-the-request)
- [Error: 401 Unauthorized](#error-401-unauthorized)
- [Remote Token Not Working](#remote-token-not-working)
- [Development](#development)
- [Building](#building)
- [Testing](#testing)
@@ -131,20 +125,7 @@ docker run -d -v jenkins_home:/var/jenkins_home -p 8080:8080 -p 50000:50000 --re
### Authentication
#### Understanding Jenkins Authentication
Jenkins supports multiple authentication methods for triggering builds. This tool supports two approaches:
**1. API Token Authentication (Recommended)**
Use Jenkins user credentials with an API token. This method:
- ✅ Works with all Jenkins configurations
- ✅ Supports CSRF protection (enabled by default in modern Jenkins)
- ✅ Supports wait mode to monitor build completion
- ✅ Provides full access to Jenkins API features
To create an API token:
Jenkins API tokens are recommended for authentication. To create an API token:
1. Log into Jenkins
2. Click on your username (top right)
@@ -155,42 +136,7 @@ To create an API token:
![personal token](./images/personal-token.png)
**2. Remote Trigger Token Authentication**
Use a remote trigger token configured in your Jenkins job. **Important limitations**:
- ⚠️ **Does not work** with Jenkins CSRF protection enabled (default in modern Jenkins)
- ⚠️ Requires anonymous users to have read access to the job, OR
- ⚠️ Must be combined with API token authentication (see Combined Authentication below)
**3. Combined Authentication (Recommended for Remote Tokens)**
Use both API token and remote trigger token together:
- ✅ Works with CSRF protection enabled
- ✅ Provides double authentication security
- ✅ Supports all features including wait mode
```bash
drone-jenkins \
--host http://jenkins.example.com/ \
--user YOUR_USERNAME \
--token YOUR_API_TOKEN \
--remote-token YOUR_REMOTE_TOKEN \
--job my-jenkins-job
```
#### CSRF Protection Notice
Modern Jenkins installations have CSRF protection enabled by default. If you encounter errors like:
```txt
Error 403 No valid crumb was included in the request
```
This means your Jenkins has CSRF protection enabled. You **must** use API token authentication (option 1 or 3 above). Remote trigger token alone will not work.
For more information about Jenkins CSRF protection, see the [official Jenkins documentation](https://www.jenkins.io/doc/book/security/csrf-protection/).
Alternatively, you can use a remote trigger token configured in your Jenkins job settings.
### Parameters Reference
@@ -209,19 +155,10 @@ For more information about Jenkins CSRF protection, see the [official Jenkins do
| Timeout | `--timeout` | `PLUGIN_TIMEOUT`, `JENKINS_TIMEOUT` | No | Maximum time to wait for job completion (default: 30m) |
| Debug | `--debug` | `PLUGIN_DEBUG`, `JENKINS_DEBUG` | No | Enable debug mode to show detailed parameter information (default: false) |
**Authentication Requirements**:
**Authentication Requirements**: You must provide either:
For Jenkins with **CSRF protection enabled** (default in modern Jenkins):
- **Required**: `user` + `token` (API token authentication)
- **Optional**: `remote-token` (for additional security)
For Jenkins with **CSRF protection disabled** (not recommended):
- **Option 1**: `user` + `token` (API token authentication)
- **Option 2**: `remote-token` only (requires anonymous read access to job)
**Important**: If you encounter "403 No valid crumb" errors, you must use API token authentication (`user` + `token`).
- `user` + `token` (API token authentication), OR
- `remote-token` (remote trigger token authentication)
**Parameters Format**: The `parameters` field accepts a multi-line string where each line contains one `key=value` pair:
@@ -283,22 +220,9 @@ drone-jenkins \
--job my-jenkins-job
```
**Using combined authentication (API token + remote token - Recommended):**
**Using remote token authentication:**
```bash
drone-jenkins \
--host http://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--remote-token REMOTE_TOKEN_HERE \
--job my-jenkins-job
```
**Using remote token only (only works without CSRF protection):**
```bash
# Note: This will fail if Jenkins has CSRF protection enabled
# You will get "403 No valid crumb" error
drone-jenkins \
--host http://jenkins.example.com/ \
--remote-token REMOTE_TOKEN_HERE \
@@ -385,18 +309,6 @@ docker run --rm \
ghcr.io/appleboy/drone-jenkins
```
**With combined authentication (API token + remote token):**
```bash
docker run --rm \
-e JENKINS_URL=http://jenkins.example.com/ \
-e JENKINS_USER=appleboy \
-e JENKINS_TOKEN=xxxxxxx \
-e JENKINS_REMOTE_TOKEN=your_remote_token \
-e JENKINS_JOB=my-jenkins-job \
ghcr.io/appleboy/drone-jenkins
```
**Wait for job completion:**
```bash
@@ -448,57 +360,6 @@ docker run --rm \
For more detailed examples and advanced configurations, see [DOCS.md](DOCS.md).
## Troubleshooting
### Error: 403 No valid crumb was included in the request
**Cause**: Your Jenkins server has CSRF protection enabled (this is the default in modern Jenkins). Learn more at the [Jenkins CSRF Protection documentation](https://www.jenkins.io/doc/book/security/csrf-protection/).
**Solution**: Use API token authentication instead of remote token only:
```bash
# ❌ This will fail with CSRF protection enabled
drone-jenkins \
--host http://jenkins.example.com/ \
--remote-token YOUR_REMOTE_TOKEN \
--job my-jenkins-job
# ✅ Use this instead
drone-jenkins \
--host http://jenkins.example.com/ \
--user YOUR_USERNAME \
--token YOUR_API_TOKEN \
--job my-jenkins-job
# ✅ Or combine both for additional security
drone-jenkins \
--host http://jenkins.example.com/ \
--user YOUR_USERNAME \
--token YOUR_API_TOKEN \
--remote-token YOUR_REMOTE_TOKEN \
--job my-jenkins-job
```
### Error: 401 Unauthorized
**Cause**: Invalid credentials or incorrect authentication method.
**Solutions**:
1. Verify your username and API token are correct
2. Ensure you're using an API token, not your Jenkins password
3. Check if you have permission to trigger the job
4. Make sure both `--user` and `--token` are provided together
### Remote Token Not Working
**Cause**: Remote trigger tokens alone only work in specific scenarios:
- Jenkins has CSRF protection disabled (not recommended), AND
- Anonymous users have read access to the job
**Solution**: Use combined authentication (API token + remote token) as shown in the examples above.
## Development
### Building
-13
View File
@@ -46,7 +46,6 @@
- [配置](#配置)
- [Jenkins 服务器设置](#jenkins-服务器设置)
- [认证](#认证)
- [CSRF 保护注意事项](#csrf-保护注意事项)
- [参数参考](#参数参考)
- [使用方式](#使用方式)
- [命令行](#命令行)
@@ -139,18 +138,6 @@ docker run -d -v jenkins_home:/var/jenkins_home -p 8080:8080 -p 50000:50000 --re
或者,您可以使用在 Jenkins 任务设置中配置的远程触发令牌。
#### CSRF 保护注意事项
现代 Jenkins 安装默认启用 CSRF 保护。如果您遇到以下错误:
```
Error 403 No valid crumb was included in the request
```
这表示您的 Jenkins 已启用 CSRF 保护。您**必须**使用 API 令牌认证(user + token)。单独使用远程触发令牌将无法工作。
如需更多关于 Jenkins CSRF 保护的信息,请参阅 [Jenkins 官方文档](https://www.jenkins.io/doc/book/security/csrf-protection/)。
### 参数参考
| 参数 | CLI 标志 | 环境变量 | 必需 | 说明 |
-13
View File
@@ -46,7 +46,6 @@
- [設定](#設定)
- [Jenkins 伺服器設定](#jenkins-伺服器設定)
- [認證](#認證)
- [CSRF 保護注意事項](#csrf-保護注意事項)
- [參數參考](#參數參考)
- [使用方式](#使用方式)
- [命令列](#命令列)
@@ -139,18 +138,6 @@ docker run -d -v jenkins_home:/var/jenkins_home -p 8080:8080 -p 50000:50000 --re
或者,您可以使用在 Jenkins 任務設定中配置的遠端觸發令牌。
#### CSRF 保護注意事項
現代 Jenkins 安裝預設啟用 CSRF 保護。如果您遇到以下錯誤:
```
Error 403 No valid crumb was included in the request
```
這表示您的 Jenkins 已啟用 CSRF 保護。您**必須**使用 API 令牌認證(user + token)。單獨使用遠端觸發令牌將無法運作。
如需更多關於 Jenkins CSRF 保護的資訊,請參閱 [Jenkins 官方文件](https://www.jenkins.io/doc/book/security/csrf-protection/)。
### 參數參考
| 參數 | CLI 旗標 | 環境變數 | 必要 | 說明 |
+5 -9
View File
@@ -224,7 +224,7 @@ func (jenkins *Jenkins) sendRequest(
req *http.Request,
crumb *CrumbResponse,
) (*http.Response, error) {
if jenkins.Auth != nil && jenkins.Auth.Username != "" && jenkins.Auth.Token != "" {
if jenkins.Auth != nil {
req.SetBasicAuth(jenkins.Auth.Username, jenkins.Auth.Token)
}
@@ -277,14 +277,10 @@ func (jenkins *Jenkins) postAndGetLocation(
path string,
params url.Values,
) (int, error) {
// Fetch CSRF crumb before POST request (only if authenticated)
var crumb *CrumbResponse
if jenkins.Auth != nil && jenkins.Auth.Username != "" && jenkins.Auth.Token != "" {
var err error
crumb, err = jenkins.getCrumb(ctx)
if err != nil {
return 0, fmt.Errorf("failed to get crumb: %w", err)
}
// Fetch CSRF crumb before POST request
crumb, err := jenkins.getCrumb(ctx)
if err != nil {
return 0, fmt.Errorf("failed to get crumb: %w", err)
}
requestURL := jenkins.buildURL(path, params)
+9 -15
View File
@@ -88,15 +88,12 @@ func (p Plugin) validateConfig() error {
if p.BaseURL == "" {
return errors.New("jenkins base URL is required")
}
// Validate authentication: either (user + token) or remote-token must be provided
hasUserAuth := p.Username != "" && p.Token != ""
hasRemoteToken := p.RemoteToken != ""
if !hasUserAuth && !hasRemoteToken {
return errors.New("authentication required")
if p.Username == "" {
return errors.New("jenkins username is required")
}
if p.Token == "" {
return errors.New("jenkins API token is required")
}
return nil
}
@@ -116,13 +113,10 @@ func (p Plugin) Exec(ctx context.Context) error {
return errors.New("at least one Jenkins job name is required")
}
// Set up authentication (only if username and token are provided)
var auth *Auth
if p.Username != "" && p.Token != "" {
auth = &Auth{
Username: p.Username,
Token: p.Token,
}
// Set up authentication
auth := &Auth{
Username: p.Username,
Token: p.Token,
}
// Initialize Jenkins client
+7 -34
View File
@@ -32,33 +32,24 @@ func TestValidateConfig(t *testing.T) {
errorMsg: "jenkins base URL is required",
},
{
name: "missing authentication",
name: "missing username and token",
plugin: Plugin{
BaseURL: "http://example.com",
},
wantError: true,
errorMsg: "authentication required",
errorMsg: "jenkins username is required",
},
{
name: "missing token (only username)",
name: "missing token",
plugin: Plugin{
BaseURL: "http://example.com",
Username: "foo",
},
wantError: true,
errorMsg: "authentication required",
errorMsg: "jenkins API token is required",
},
{
name: "missing username (only token)",
plugin: Plugin{
BaseURL: "http://example.com",
Token: "bar",
},
wantError: true,
errorMsg: "authentication required",
},
{
name: "user and token auth",
name: "all required config present",
plugin: Plugin{
BaseURL: "http://example.com",
Username: "foo",
@@ -66,24 +57,6 @@ func TestValidateConfig(t *testing.T) {
},
wantError: false,
},
{
name: "remote token auth",
plugin: Plugin{
BaseURL: "http://example.com",
RemoteToken: "remote-token-123",
},
wantError: false,
},
{
name: "both auth methods",
plugin: Plugin{
BaseURL: "http://example.com",
Username: "foo",
Token: "bar",
RemoteToken: "remote-token-123",
},
wantError: false,
},
}
for _, tt := range tests {
@@ -259,7 +232,7 @@ func TestExecMissingJenkinsUsername(t *testing.T) {
assert.Error(t, err)
assert.Contains(t, err.Error(), "configuration error")
assert.Contains(t, err.Error(), "authentication required")
assert.Contains(t, err.Error(), "jenkins username is required")
}
// TestExecMissingJenkinsToken tests Exec with missing token
@@ -273,7 +246,7 @@ func TestExecMissingJenkinsToken(t *testing.T) {
assert.Error(t, err)
assert.Contains(t, err.Error(), "configuration error")
assert.Contains(t, err.Error(), "authentication required")
assert.Contains(t, err.Error(), "jenkins API token is required")
}
// TestExecMissingJenkinsJob tests Exec with missing or empty job list