From 508c3ac6d2f3cd76cdbd540b724e8efb6c74bd0c Mon Sep 17 00:00:00 2001 From: Oliver Jakoubek Date: Tue, 27 Jan 2026 11:01:28 +0100 Subject: [PATCH] fix: remove Basic prefix from custom auth headers Custom headers like X-API-Auth expect raw base64 credentials without the "Basic " prefix. The standard Authorization header continues to use the proper Basic auth format via SetBasicAuth(). Co-Authored-By: Claude Opus 4.5 --- .beads/export-state/fbbdf412d0fd5173.json | 6 +++--- .beads/issues.jsonl | 1 + auth.go | 4 ++-- auth_test.go | 12 ++++++------ 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.beads/export-state/fbbdf412d0fd5173.json b/.beads/export-state/fbbdf412d0fd5173.json index 8c57567..d7ec90b 100644 --- a/.beads/export-state/fbbdf412d0fd5173.json +++ b/.beads/export-state/fbbdf412d0fd5173.json @@ -1,6 +1,6 @@ { "worktree_root": "/home/oli/Dev/kanboard-api", - "last_export_commit": "f8daa20ddd0d8ccae6ff2bbcd619a5f2b9017847", - "last_export_time": "2026-01-27T10:27:23.906334189+01:00", - "jsonl_hash": "76cece249c29be2a46702a269bddea03d105be2c4ad48d22459d3dda9be4aae6" + "last_export_commit": "96601980c3838ba7e38859330dfcc3d8183b35c1", + "last_export_time": "2026-01-27T11:01:28.430072224+01:00", + "jsonl_hash": "3136cc76a92ba36a130a3ba6a13f3bb239b7a54f6d3a345ae64815d3ec602ffb" } \ No newline at end of file diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 2751e36..15cf124 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -28,4 +28,5 @@ {"id":"kanboard-api-xhf","title":"Create example programs","description":"Create example programs demonstrating library usage.\n\n## Examples to create\n```\nexamples/\n├── basic/\n│ └── main.go # Basic client setup and simple operations\n├── fluent/\n│ └── main.go # Fluent API demonstration\n└── search/\n └── main.go # Search functionality demo\n```\n\n## basic/main.go\n- Client creation with API token\n- Get all projects\n- Get tasks from a project\n- Create a simple task\n\n## fluent/main.go\n- Client configuration with all options\n- Task creation with TaskParams\n- Task updates with TaskUpdateParams\n- Tag operations\n\n## search/main.go\n- Project-specific search\n- Global search across all projects\n\n## Files to create\n- examples/basic/main.go\n- examples/fluent/main.go\n- examples/search/main.go\n\n## Acceptance criteria\n- Examples compile and are well-commented\n- Cover main use cases\n- Show both fluent and direct API styles","status":"closed","priority":3,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:53.604889443+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T19:25:02.384085528+01:00","closed_at":"2026-01-15T19:25:02.384085528+01:00","close_reason":"Created basic, fluent, and search example programs","dependencies":[{"issue_id":"kanboard-api-xhf","depends_on_id":"kanboard-api-2ze","type":"blocks","created_at":"2026-01-15T17:46:55.571585285+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-xhf","depends_on_id":"kanboard-api-una","type":"blocks","created_at":"2026-01-15T17:46:55.63515762+01:00","created_by":"Oliver Jakoubek"}]} {"id":"kanboard-api-zfc","title":"Implement TaskScope tag methods with read-modify-write (CRITICAL)","description":"Implement TaskScope tag methods with read-modify-write pattern. CRITICAL feature.\n\n## TaskScope methods to implement\n- GetTags(ctx) (map[int]string, error)\n- SetTags(ctx, tags ...string) error - replaces ALL tags\n- ClearTags(ctx) error - removes ALL tags\n- AddTag(ctx, tag string) error - read-modify-write\n- RemoveTag(ctx, tag string) error - read-modify-write\n- HasTag(ctx, tag string) (bool, error)\n\n## Read-Modify-Write Workflow for AddTag\n1. Get task via getTask (need project_id)\n2. Get current tags via getTaskTags\n3. Check if tag already exists\n4. If not: add tag to list\n5. Call setTaskTags with updated list\n\n## Read-Modify-Write Workflow for RemoveTag\n1. Get task via getTask (need project_id)\n2. Get current tags via getTaskTags\n3. Filter out the tag to remove\n4. Call setTaskTags with filtered list\n5. If tag didn't exist: no error (idempotent)\n\n## Files to modify\n- task_scope.go\n\n## IMPORTANT WARNING\nThis is NOT atomic. Concurrent tag modifications may cause data loss. Document this limitation.\n\n## Acceptance criteria\n- AddTag is idempotent (no error if tag exists)\n- RemoveTag is idempotent (no error if tag doesn't exist)\n- HasTag correctly checks tag existence","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:08.911429864+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:36:32.533937315+01:00","closed_at":"2026-01-15T18:36:32.533937315+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-zfc","depends_on_id":"kanboard-api-16r","type":"blocks","created_at":"2026-01-15T17:43:49.517064988+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-zfc","depends_on_id":"kanboard-api-una","type":"blocks","created_at":"2026-01-15T17:43:49.593313748+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-zfc","depends_on_id":"kanboard-api-91a","type":"blocks","created_at":"2026-01-15T17:43:49.690872321+01:00","created_by":"Oliver Jakoubek"}]} {"id":"kanboard-api-zg2","title":"Implement File API methods","description":"Implement direct API methods for task file operations.\n\n## Methods to implement (Must-have)\n- GetAllTaskFiles(ctx, taskID int) ([]TaskFile, error) - getAllTaskFiles\n- CreateTaskFile(ctx, projectID, taskID int, filename string, content []byte) (int, error) - createTaskFile\n\n## Methods to implement (Nice-to-have)\n- DownloadTaskFile(ctx, fileID int) ([]byte, error) - downloadTaskFile\n- RemoveTaskFile(ctx, fileID int) error - removeTaskFile\n\n## TaskScope methods to add\n- GetFiles(ctx) ([]TaskFile, error)\n- UploadFile(ctx, filename string, content []byte) (*TaskFile, error)\n\n## Files to create\n- files.go\n- task_scope.go (extend)\n\n## Acceptance criteria\n- File content base64 encoded for upload\n- CreateTaskFile returns file ID","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:09.748005313+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:45:05.499871045+01:00","closed_at":"2026-01-15T18:45:05.499871045+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-zg2","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:43:49.984099418+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-zg2","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:43:50.049331328+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"kanboard-iuf","title":"Remove Basic prefix from custom auth headers","description":"## Problem\n\nWhen using a custom header name (e.g., `X-API-Auth` for Kanboard), the library incorrectly prepends `\"Basic \"` to the base64-encoded credentials.\n\n**Current behavior** (`auth.go:27` and `auth.go:43`):\n```go\nreq.Header.Set(a.headerName, \"Basic \"+basicAuthValue(user, a.token))\n```\n\nSends: `X-API-Auth: Basic dXNlcjpwYXNzd29yZA==`\n\n**Expected behavior** per [Kanboard API docs](https://docs.kanboard.org/v1/api/authentication/):\n\n- Standard `Authorization` header: `Basic base64(username:password)` ✓\n- Custom `X-API-Auth` header: `base64(username:password)` (no \"Basic \" prefix!)\n\nShould send: `X-API-Auth: dXNlcjpwYXNzd29yZA==`\n\n## Impact\n\nAuthentication fails when using the custom header option. The server returns HTML (login page) instead of JSON because authentication is rejected.\n\n## Suggested Fix\n\nIn both `apiTokenAuth.Apply()` and `basicAuth.Apply()`, remove the \"Basic \" prefix when using a custom header name:\n\n```go\nif a.headerName != \"\" {\n req.Header.Set(a.headerName, basicAuthValue(user, a.token)) // no \"Basic \" prefix\n} else {\n req.SetBasicAuth(user, a.token) // uses standard Authorization header with \"Basic \"\n}\n```\n\n## Affected Code\n\n- `auth.go:27` - `apiTokenAuth.Apply()`\n- `auth.go:43` - `basicAuth.Apply()`\n\n## Acceptance Criteria\n\n- [ ] Custom header (`X-API-Auth`) sends raw base64 value without \"Basic \" prefix\n- [ ] Standard `Authorization` header still works with \"Basic \" prefix\n- [ ] Tests updated to verify both behaviors\n- [ ] All tests passing","status":"closed","priority":1,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-27T10:43:21.717917986+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-27T11:01:19.467981335+01:00","closed_at":"2026-01-27T11:01:19.467981335+01:00","close_reason":"Closed"} {"id":"kanboard-r3p","title":"Support optional API user in token authentication","description":"## Description\n\nBei der API-Token-Authentifizierung soll neben dem API-Key optional auch ein API-User konfigurierbar sein. Wenn kein User angegeben wird, soll weiterhin der Standard-User \"jsonrpc\" verwendet werden.\n\n## Current Behavior\n\nIn `auth.go`, the `apiTokenAuth` struct hardcodes the username \"jsonrpc\":\n\n```go\nfunc (a *apiTokenAuth) Apply(req *http.Request) {\n req.SetBasicAuth(\"jsonrpc\", a.token)\n}\n```\n\n## Expected Behavior\n\n- Add an optional `user` field to `apiTokenAuth`\n- If user is empty/not provided, default to \"jsonrpc\"\n- If user is provided, use that value for HTTP Basic Auth\n\n## Acceptance Criteria\n\n- [ ] `apiTokenAuth` struct has an optional user field\n- [ ] Default behavior unchanged when no user specified (uses \"jsonrpc\")\n- [ ] Custom user is used when explicitly provided\n- [ ] Client configuration supports setting the API user\n- [ ] Tests cover both default and custom user scenarios","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-23T17:39:37.745294723+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-23T17:55:23.860864584+01:00","closed_at":"2026-01-23T17:55:23.860864584+01:00","close_reason":"Closed"} diff --git a/auth.go b/auth.go index 2bf8593..b8a240b 100644 --- a/auth.go +++ b/auth.go @@ -24,7 +24,7 @@ func (a *apiTokenAuth) Apply(req *http.Request) { user = "jsonrpc" } if a.headerName != "" { - req.Header.Set(a.headerName, "Basic "+basicAuthValue(user, a.token)) + req.Header.Set(a.headerName, basicAuthValue(user, a.token)) } else { req.SetBasicAuth(user, a.token) } @@ -40,7 +40,7 @@ type basicAuth struct { // Apply adds HTTP Basic Auth with username and password. func (a *basicAuth) Apply(req *http.Request) { if a.headerName != "" { - req.Header.Set(a.headerName, "Basic "+basicAuthValue(a.username, a.password)) + req.Header.Set(a.headerName, basicAuthValue(a.username, a.password)) } else { req.SetBasicAuth(a.username, a.password) } diff --git a/auth_test.go b/auth_test.go index 376adf3..18b9d64 100644 --- a/auth_test.go +++ b/auth_test.go @@ -200,8 +200,8 @@ func TestAPITokenAuth_CustomHeader(t *testing.T) { return } - // Verify the custom header has the correct Basic auth value - expected := "Basic " + base64Encode("jsonrpc:my-api-token") + // Verify the custom header has raw base64 value (no "Basic " prefix) + expected := base64Encode("jsonrpc:my-api-token") if customAuth != expected { t.Errorf("expected X-API-Auth=%s, got %s", expected, customAuth) } @@ -244,8 +244,8 @@ func TestBasicAuth_CustomHeader(t *testing.T) { return } - // Verify the custom header has the correct Basic auth value - expected := "Basic " + base64Encode("admin:secret") + // Verify the custom header has raw base64 value (no "Basic " prefix) + expected := base64Encode("admin:secret") if customAuth != expected { t.Errorf("expected X-Custom-Auth=%s, got %s", expected, customAuth) } @@ -281,8 +281,8 @@ func TestCustomHeader_WithCustomUser(t *testing.T) { return } - // Verify the custom header uses the custom username - expected := "Basic " + base64Encode("custom-user:my-token") + // Verify the custom header uses raw base64 value with custom username + expected := base64Encode("custom-user:my-token") if customAuth != expected { t.Errorf("expected X-API-Auth=%s, got %s", expected, customAuth) }