Skip to content

Commit 9e6deca

Browse files
Jabenclaude
andcommitted
Add basic authentication support for Gotenberg API
Implements HTTP Basic Authentication to support Gotenberg instances configured with the --api-enable-basic-auth flag (Gotenberg v8+). Features: - Added BasicAuthUsername and BasicAuthPassword to GotenbergSharpClientOptions - Automatic Authorization header injection when credentials are configured - Docker Compose setup for local testing with basic auth enabled - Comprehensive test suite using AwesomeAssertions/NUnit Changes: - lib/Domain/Settings/GotenbergSharpClientOptions.cs: Added auth properties - lib/Extensions/TypedClientServiceCollectionExtensions.cs: Added auth header logic - docker/docker-compose-basic-auth.yml: Docker setup with test credentials - test/GotenbergSharpClient.Tests/BasicAuthTests.cs: Integration tests - README.md: Updated with basic auth documentation and docker-compose usage Closes #54 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent e033565 commit 9e6deca

8 files changed

Lines changed: 247 additions & 0 deletions

File tree

GotenbergSharpApiClient.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
1010
.editorconfig = .editorconfig
1111
EndProjectSection
1212
EndProject
13+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GotenbergSharpClient.Tests", "test\GotenbergSharpClient.Tests\GotenbergSharpClient.Tests.csproj", "{EEAF9CA2-7962-176A-E851-BF81D8DE31F0}"
14+
EndProject
1315
Global
1416
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1517
Debug|Any CPU = Debug|Any CPU
@@ -32,6 +34,18 @@ Global
3234
{75F783A4-9392-412F-9DFE-00EE89527C10}.Release|x64.Build.0 = Release|Any CPU
3335
{75F783A4-9392-412F-9DFE-00EE89527C10}.Release|x86.ActiveCfg = Release|Any CPU
3436
{75F783A4-9392-412F-9DFE-00EE89527C10}.Release|x86.Build.0 = Release|Any CPU
37+
{EEAF9CA2-7962-176A-E851-BF81D8DE31F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
38+
{EEAF9CA2-7962-176A-E851-BF81D8DE31F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
39+
{EEAF9CA2-7962-176A-E851-BF81D8DE31F0}.Debug|x64.ActiveCfg = Debug|Any CPU
40+
{EEAF9CA2-7962-176A-E851-BF81D8DE31F0}.Debug|x64.Build.0 = Debug|Any CPU
41+
{EEAF9CA2-7962-176A-E851-BF81D8DE31F0}.Debug|x86.ActiveCfg = Debug|Any CPU
42+
{EEAF9CA2-7962-176A-E851-BF81D8DE31F0}.Debug|x86.Build.0 = Debug|Any CPU
43+
{EEAF9CA2-7962-176A-E851-BF81D8DE31F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
44+
{EEAF9CA2-7962-176A-E851-BF81D8DE31F0}.Release|Any CPU.Build.0 = Release|Any CPU
45+
{EEAF9CA2-7962-176A-E851-BF81D8DE31F0}.Release|x64.ActiveCfg = Release|Any CPU
46+
{EEAF9CA2-7962-176A-E851-BF81D8DE31F0}.Release|x64.Build.0 = Release|Any CPU
47+
{EEAF9CA2-7962-176A-E851-BF81D8DE31F0}.Release|x86.ActiveCfg = Release|Any CPU
48+
{EEAF9CA2-7962-176A-E851-BF81D8DE31F0}.Release|x86.Build.0 = Release|Any CPU
3549
EndGlobalSection
3650
GlobalSection(SolutionProperties) = preSolution
3751
HideSolutionNode = FALSE

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
.NET C# Client for interacting with the [Gotenberg](https://gotenberg.dev/) v7 & v8 micro-service's API. [Gotenberg](https://github.com/gotenberg/gotenberg) is a [Docker-powered stateless API](https://hub.docker.com/r/gotenberg/gotenberg/) for converting & merging HTML, Markdown and Office documents to PDF. The client supports a configurable [Polly](http://www.thepollyproject.org/) **retry policy** with exponential backoff for handling transient exceptions.
1010

1111
# Getting Started
12+
13+
## Using Docker Run
1214
*Pull the image from dockerhub.com*
1315
```powershell
1416
> docker pull gotenberg/gotenberg:latest
@@ -18,6 +20,17 @@
1820
docker run --name gotenbee8x --rm -p 3000:3000 gotenberg/gotenberg:latest gotenberg --api-timeout=1800s --log-level=debug
1921
```
2022

23+
## Using Docker Compose (with Basic Auth)
24+
For local development with basic authentication enabled, use the provided docker-compose file:
25+
26+
```powershell
27+
docker-compose -f docker/docker-compose-basic-auth.yml up -d
28+
```
29+
30+
Pre-configured with test credentials:
31+
- **Username:** `testuser`
32+
- **Password:** `testpass`
33+
2134
# .NET Core Project Setup
2235
*Install nuget package into your project*
2336
```powershell
@@ -40,6 +53,24 @@ PM> Install-Package Gotenberg.Sharp.Api.Client
4053
}
4154
```
4255

56+
### Optional: Basic Authentication
57+
**Gotenberg v8+** - If your Gotenberg instance is configured with basic authentication (using `--api-enable-basic-auth`), you can provide credentials in the settings:
58+
59+
```json
60+
"GotenbergSharpClient": {
61+
"ServiceUrl": "http://localhost:3000",
62+
"HealthCheckUrl": "http://localhost:3000/health",
63+
"BasicAuthUsername": "your-username",
64+
"BasicAuthPassword": "your-password",
65+
"RetryPolicy": {
66+
"Enabled": true,
67+
"RetryCount": 4,
68+
"BackoffPower": 1.5,
69+
"LoggingEnabled": true
70+
}
71+
}
72+
```
73+
4374
## Configure Services In Startup.cs
4475
```csharp
4576
public void ConfigureServices(IServiceCollection services)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
version: '3.8'
2+
3+
services:
4+
gotenberg:
5+
image: gotenberg/gotenberg:8
6+
ports:
7+
- "3000:3000"
8+
environment:
9+
- GOTENBERG_API_BASIC_AUTH_USERNAME=testuser
10+
- GOTENBERG_API_BASIC_AUTH_PASSWORD=testpass
11+
command:
12+
- "gotenberg"
13+
- "--api-enable-basic-auth"
14+
- "--api-timeout=1800s"
15+
- "--log-level=debug"
16+
restart: unless-stopped

lib/Domain/Settings/GotenbergSharpClientOptions.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,18 @@ public class GotenbergSharpClientOptions
2828
public Uri HealthCheckUrl { get; set; } = new Uri("http://localhost:3000/health");
2929

3030
public RetryOptions RetryPolicy { get; set; } = new RetryOptions();
31+
32+
/// <summary>
33+
/// Optional username for HTTP Basic Authentication.
34+
/// When set along with <see cref="BasicAuthPassword"/>, the client will include
35+
/// an Authorization header with basic auth credentials in all requests.
36+
/// </summary>
37+
public string? BasicAuthUsername { get; set; }
38+
39+
/// <summary>
40+
/// Optional password for HTTP Basic Authentication.
41+
/// When set along with <see cref="BasicAuthUsername"/>, the client will include
42+
/// an Authorization header with basic auth credentials in all requests.
43+
/// </summary>
44+
public string? BasicAuthPassword { get; set; }
3145
}

lib/Extensions/TypedClientServiceCollectionExtensions.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ public static IHttpClientBuilder AddGotenbergSharpClient(
3838
var ops = GetOptions(sp);
3939
client.Timeout = ops.TimeOut;
4040
client.BaseAddress = ops.ServiceUrl;
41+
42+
// Add basic auth header if credentials are provided
43+
if (!string.IsNullOrWhiteSpace(ops.BasicAuthUsername) &&
44+
!string.IsNullOrWhiteSpace(ops.BasicAuthPassword))
45+
{
46+
var credentials = Convert.ToBase64String(
47+
System.Text.Encoding.ASCII.GetBytes($"{ops.BasicAuthUsername}:{ops.BasicAuthPassword}"));
48+
client.DefaultRequestHeaders.Authorization =
49+
new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", credentials);
50+
}
4151
});
4252
}
4353

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
using AwesomeAssertions;
2+
using Gotenberg.Sharp.API.Client.Domain.Builders;
3+
using Gotenberg.Sharp.API.Client.Domain.Settings;
4+
using Gotenberg.Sharp.API.Client.Extensions;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using Microsoft.Extensions.Options;
7+
using NUnit.Framework;
8+
9+
namespace GotenbergSharpClient.Tests;
10+
11+
[TestFixture]
12+
public class BasicAuthTests
13+
{
14+
private const string GotenbergUrl = "http://localhost:3000";
15+
private const string TestUsername = "testuser";
16+
private const string TestPassword = "testpass";
17+
18+
[Test]
19+
public async Task Client_WithBasicAuth_ShouldIncludeAuthorizationHeader()
20+
{
21+
// Arrange
22+
var services = new ServiceCollection();
23+
24+
services.AddOptions<GotenbergSharpClientOptions>()
25+
.Configure(options =>
26+
{
27+
options.ServiceUrl = new Uri(GotenbergUrl);
28+
options.BasicAuthUsername = TestUsername;
29+
options.BasicAuthPassword = TestPassword;
30+
});
31+
32+
services.AddGotenbergSharpClient();
33+
34+
var serviceProvider = services.BuildServiceProvider();
35+
var client = serviceProvider.GetRequiredService<Gotenberg.Sharp.API.Client.GotenbergSharpClient>();
36+
37+
// Act - Create a simple HTML to PDF request
38+
var builder = new HtmlRequestBuilder()
39+
.AddDocument(doc => doc.SetBody("<html><body><h1>Basic Auth Test</h1></body></html>"));
40+
41+
// Assert - This will fail if auth is not properly configured or Gotenberg rejects the request
42+
var result = await client.HtmlToPdfAsync(builder);
43+
44+
result.Should().NotBeNull("Basic auth should be properly configured and accepted by Gotenberg");
45+
result.Length.Should().BeGreaterThan(0);
46+
}
47+
48+
[Test]
49+
public async Task Client_WithoutBasicAuth_ShouldFailWhenGotenbergRequiresAuth()
50+
{
51+
// Arrange
52+
var services = new ServiceCollection();
53+
54+
services.AddOptions<GotenbergSharpClientOptions>()
55+
.Configure(options =>
56+
{
57+
options.ServiceUrl = new Uri(GotenbergUrl);
58+
// Deliberately not setting BasicAuthUsername or BasicAuthPassword
59+
});
60+
61+
services.AddGotenbergSharpClient();
62+
63+
var serviceProvider = services.BuildServiceProvider();
64+
var client = serviceProvider.GetRequiredService<Gotenberg.Sharp.API.Client.GotenbergSharpClient>();
65+
66+
// Act
67+
var builder = new HtmlRequestBuilder()
68+
.AddDocument(doc => doc.SetBody("<html><body><h1>No Auth Test</h1></body></html>"));
69+
70+
// Assert - Should fail with 401 Unauthorized when Gotenberg requires auth
71+
var act = () => client.HtmlToPdfAsync(builder);
72+
73+
await act.Should().ThrowAsync<Exception>("Gotenberg should reject requests without proper authentication");
74+
}
75+
76+
[Test]
77+
public async Task Client_WithInvalidCredentials_ShouldFailWithUnauthorized()
78+
{
79+
// Arrange
80+
var services = new ServiceCollection();
81+
82+
services.AddOptions<GotenbergSharpClientOptions>()
83+
.Configure(options =>
84+
{
85+
options.ServiceUrl = new Uri(GotenbergUrl);
86+
options.BasicAuthUsername = "wronguser";
87+
options.BasicAuthPassword = "wrongpassword";
88+
});
89+
90+
services.AddGotenbergSharpClient();
91+
92+
var serviceProvider = services.BuildServiceProvider();
93+
var client = serviceProvider.GetRequiredService<Gotenberg.Sharp.API.Client.GotenbergSharpClient>();
94+
95+
// Act
96+
var builder = new HtmlRequestBuilder()
97+
.AddDocument(doc => doc.SetBody("<html><body><h1>Invalid Auth Test</h1></body></html>"));
98+
99+
// Assert - Should fail with 401 Unauthorized
100+
var act = () => client.HtmlToPdfAsync(builder);
101+
102+
await act.Should().ThrowAsync<Exception>("Invalid credentials should be rejected");
103+
}
104+
105+
[Test]
106+
public void GotenbergSharpClientOptions_BasicAuthProperties_ShouldBeNullableAndOptional()
107+
{
108+
// Arrange & Act
109+
var options = new GotenbergSharpClientOptions();
110+
111+
// Assert
112+
options.BasicAuthUsername.Should().BeNull("BasicAuthUsername should be nullable and default to null");
113+
options.BasicAuthPassword.Should().BeNull("BasicAuthPassword should be nullable and default to null");
114+
}
115+
116+
[Test]
117+
public void GotenbergSharpClientOptions_WithBasicAuthSet_ShouldRetainValues()
118+
{
119+
// Arrange
120+
var options = new GotenbergSharpClientOptions
121+
{
122+
BasicAuthUsername = TestUsername,
123+
BasicAuthPassword = TestPassword
124+
};
125+
126+
// Assert
127+
options.BasicAuthUsername.Should().Be(TestUsername);
128+
options.BasicAuthPassword.Should().Be(TestPassword);
129+
}
130+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Global using directives
2+
3+
global using System.Diagnostics;
4+
5+
global using AwesomeAssertions;
6+
7+
global using Microsoft.Extensions.Configuration;
8+
9+
global using NUnit.Framework;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<LangVersion>latest</LangVersion>
6+
<Nullable>enable</Nullable>
7+
<ImplicitUsings>enable</ImplicitUsings>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="AwesomeAssertions" Version="9.2.0" />
12+
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.9" />
13+
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.9" />
14+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
15+
<PackageReference Include="NUnit" Version="4.4.0" />
16+
<PackageReference Include="NUnit3TestAdapter" Version="5.1.0" />
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<ProjectReference Include="..\..\lib\Gotenberg.Sharp.Api.Client.csproj" />
21+
</ItemGroup>
22+
23+
</Project>

0 commit comments

Comments
 (0)