From cadfdaef1b8789b2334ebf1705bdf5d764db1a52 Mon Sep 17 00:00:00 2001
From: JiriUhlir <149317995+JiriUhlir@users.noreply.github.com>
Date: Mon, 15 Jun 2026 08:42:56 +0200
Subject: [PATCH] applicationId uz neni povinne
---
Client/ClientSecretAuthentication.cs | 79 +++++++++++++++++++
Client/ClientSecretDokladApiBuilder.cs | 21 +++++
Client/DokladApiFactory.cs | 13 ++-
Credentials/IdokladCredentials.cs | 9 ++-
Credentials/RequestCredentialsProvider.cs | 4 +-
.../CredentialHeadersOperationFilter.cs | 2 +-
Program.cs | 9 ++-
7 files changed, 127 insertions(+), 10 deletions(-)
create mode 100644 Client/ClientSecretAuthentication.cs
create mode 100644 Client/ClientSecretDokladApiBuilder.cs
diff --git a/Client/ClientSecretAuthentication.cs b/Client/ClientSecretAuthentication.cs
new file mode 100644
index 0000000..f81ce13
--- /dev/null
+++ b/Client/ClientSecretAuthentication.cs
@@ -0,0 +1,79 @@
+using System.Net.Http;
+using IdokladSdk;
+using IdokladSdk.Authentication;
+using IdokladSdk.Authentication.Models;
+using IdokladSdk.Exceptions;
+using Newtonsoft.Json;
+
+namespace Idoklad.Client;
+
+///
+/// iDoklad client_credentials authentication that sends only client_id and
+/// client_secret (no application_id).
+///
+/// The official SDK's mandates an applicationId and
+/// always posts application_id, but iDoklad's standard client_credentials grant does not
+/// require it. This implementation is used when the caller does not supply an ApplicationId.
+///
+public sealed class ClientSecretAuthentication : IAuthentication
+{
+ private const string Scope = "idoklad_api";
+
+ private readonly string _clientId;
+ private readonly string _clientSecret;
+
+ public ClientSecretAuthentication(string clientId, string clientSecret)
+ {
+ _clientId = clientId;
+ _clientSecret = clientSecret;
+ }
+
+ public DokladConfiguration Configuration { get; set; } = default!;
+
+ public string RefreshToken => null!;
+
+ public bool UseRefreshToken { get => false; set { } }
+
+ public async Task GetTokenAsync(HttpClient httpClient, CancellationToken cancellationToken = default)
+ {
+ var postData = new List>
+ {
+ new("grant_type", "client_credentials"),
+ new("client_id", _clientId),
+ new("client_secret", _clientSecret),
+ new("scope", Scope),
+ };
+
+ // The SDK targets the v2 token endpoint (server/v2/connect/token), which mandates
+ // application_id. Without an applicationId we use the classic endpoint
+ // (server/connect/token), which authenticates with client_id + client_secret only.
+ var tokenUrl = Configuration.IdentityServerTokenUrl.ToString()
+ .Replace("/server/v2/connect/token", "/server/connect/token");
+
+ using var request = new HttpRequestMessage(HttpMethod.Post, tokenUrl)
+ {
+ Content = new FormUrlEncodedContent(postData),
+ };
+
+ using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
+ var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ throw new IdokladAuthenticationException(content);
+ }
+
+ var tokenizer = JsonConvert.DeserializeObject(content);
+ if (tokenizer is null || string.IsNullOrEmpty(tokenizer.AccessToken))
+ {
+ var error = JsonConvert.DeserializeObject(content);
+ throw new IdokladAuthenticationException(error);
+ }
+
+ // GrantType defaults to ClientCredentials (enum value 0), which is correct here.
+ return tokenizer;
+ }
+
+ public Task RefreshAccessTokenAsync(HttpClient httpClient, CancellationToken cancellationToken = default)
+ => GetTokenAsync(httpClient, cancellationToken);
+}
diff --git a/Client/ClientSecretDokladApiBuilder.cs b/Client/ClientSecretDokladApiBuilder.cs
new file mode 100644
index 0000000..07db762
--- /dev/null
+++ b/Client/ClientSecretDokladApiBuilder.cs
@@ -0,0 +1,21 @@
+using IdokladSdk;
+using IdokladSdk.Authentication;
+using IdokladSdk.Builders;
+
+namespace Idoklad.Client;
+
+///
+/// variant that uses a caller-supplied
+/// (here ) instead of the SDK's applicationId-mandatory
+/// client credentials. The rest of the builder pipeline (HttpClient, options, urls, Build) is reused.
+///
+public sealed class ClientSecretDokladApiBuilder : DokladApiBuilder
+{
+ private readonly IAuthentication _authentication;
+
+ public ClientSecretDokladApiBuilder(string appName, string appVersion, IAuthentication authentication)
+ : base(appName, appVersion)
+ => _authentication = authentication;
+
+ protected override IAuthentication GetAuthentication() => _authentication;
+}
diff --git a/Client/DokladApiFactory.cs b/Client/DokladApiFactory.cs
index 8927f01..fcecd8d 100644
--- a/Client/DokladApiFactory.cs
+++ b/Client/DokladApiFactory.cs
@@ -27,8 +27,17 @@ public sealed class DokladApiFactory
{
var httpClient = _httpClientFactory.CreateClient(HttpClientName);
- var builder = new DokladApiBuilder(_settings.AppName, _settings.AppVersion)
- .AddClientCredentialsAuthentication(credentials.ClientId, credentials.ClientSecret, credentials.ApplicationId)
+ // With an ApplicationId, use the SDK's client_credentials (sends application_id).
+ // Without it, authenticate with client_id + client_secret only (iDoklad's default).
+ DokladApiBuilder builder = string.IsNullOrWhiteSpace(credentials.ApplicationId)
+ ? new ClientSecretDokladApiBuilder(
+ _settings.AppName,
+ _settings.AppVersion,
+ new ClientSecretAuthentication(credentials.ClientId, credentials.ClientSecret))
+ : new DokladApiBuilder(_settings.AppName, _settings.AppVersion)
+ .AddClientCredentialsAuthentication(credentials.ClientId, credentials.ClientSecret, credentials.ApplicationId);
+
+ builder = builder
.AddHttpClient(httpClient)
.AddApiContextOptions(options => options.Language = credentials.Language);
diff --git a/Credentials/IdokladCredentials.cs b/Credentials/IdokladCredentials.cs
index eb4ad07..be32f15 100644
--- a/Credentials/IdokladCredentials.cs
+++ b/Credentials/IdokladCredentials.cs
@@ -10,6 +10,13 @@ public sealed record IdokladCredentials
{
public required string ClientId { get; init; }
public required string ClientSecret { get; init; }
- public required string ApplicationId { get; init; }
+
+ ///
+ /// Optional iDoklad ApplicationId (GUID from the developer portal). Only needed for partner
+ /// applications whose client_credentials grant requires application_id. When empty, the
+ /// service authenticates with client_id + client_secret only.
+ ///
+ public string? ApplicationId { get; init; }
+
public required Language Language { get; init; }
}
diff --git a/Credentials/RequestCredentialsProvider.cs b/Credentials/RequestCredentialsProvider.cs
index f56936c..5b2d022 100644
--- a/Credentials/RequestCredentialsProvider.cs
+++ b/Credentials/RequestCredentialsProvider.cs
@@ -28,12 +28,12 @@ public sealed class RequestCredentialsProvider
var clientId = HeaderOrDefault(headers, CredentialConstants.ClientIdHeader, _settings.ClientId);
var clientSecret = HeaderOrDefault(headers, CredentialConstants.ClientSecretHeader, _settings.ClientSecret);
+ // ApplicationId is optional: iDoklad's standard client_credentials grant does not require it.
var applicationId = HeaderOrDefault(headers, CredentialConstants.ApplicationIdHeader, _settings.ApplicationId);
var missing = new List();
if (string.IsNullOrWhiteSpace(clientId)) missing.Add(CredentialConstants.ClientIdHeader);
if (string.IsNullOrWhiteSpace(clientSecret)) missing.Add(CredentialConstants.ClientSecretHeader);
- if (string.IsNullOrWhiteSpace(applicationId)) missing.Add(CredentialConstants.ApplicationIdHeader);
if (missing.Count > 0)
{
throw new MissingCredentialsException(missing);
@@ -43,7 +43,7 @@ public sealed class RequestCredentialsProvider
{
ClientId = clientId!,
ClientSecret = clientSecret!,
- ApplicationId = applicationId!,
+ ApplicationId = string.IsNullOrWhiteSpace(applicationId) ? null : applicationId,
Language = ResolveLanguage(headers),
};
}
diff --git a/Infrastructure/CredentialHeadersOperationFilter.cs b/Infrastructure/CredentialHeadersOperationFilter.cs
index 5b361c6..5290684 100644
--- a/Infrastructure/CredentialHeadersOperationFilter.cs
+++ b/Infrastructure/CredentialHeadersOperationFilter.cs
@@ -30,7 +30,7 @@ public sealed class CredentialHeadersOperationFilter : IOperationFilter
"iDoklad OAuth2 ClientSecret (SENSITIVE). Must be sent in this header over TLS only — never in the URL or body. Overrides the IDOKLAD_CLIENT_SECRET environment default. Required if no environment default is configured.");
AddHeader(operation, CredentialConstants.ApplicationIdHeader,
- "iDoklad ApplicationId from the developer portal. Overrides the IDOKLAD_APPLICATION_ID environment default. Required by the client credentials flow if no environment default is configured.");
+ "OPTIONAL. iDoklad ApplicationId (GUID from the developer portal). Only needed for partner applications whose client_credentials grant requires application_id. Leave empty for a standard app — authentication then uses client_id + client_secret only. Overrides IDOKLAD_APPLICATION_ID.");
AddHeader(operation, CredentialConstants.LanguageHeader,
"Optional response language override for the iDoklad API: Cz, Sk or En.", example: "Cz");
diff --git a/Program.cs b/Program.cs
index 55cb27b..9b2c969 100644
--- a/Program.cs
+++ b/Program.cs
@@ -59,13 +59,14 @@ builder.Services.AddSwaggerGen(options =>
"using the OAuth2 client credentials flow.\n\n" +
"**Credentials.** Every agenda endpoint needs an iDoklad ClientId, ClientSecret and ApplicationId. " +
"Sensitive values are required in request headers and are never accepted in the query string or body:\n\n" +
- "- `X-ClientId` — iDoklad OAuth2 ClientId\n" +
- "- `X-ClientSecret` — iDoklad OAuth2 ClientSecret (sensitive; TLS only)\n" +
- "- `X-ApplicationId` — iDoklad ApplicationId from the developer portal\n" +
+ "- `X-ClientId` — iDoklad OAuth2 ClientId (required)\n" +
+ "- `X-ClientSecret` — iDoklad OAuth2 ClientSecret (required; sensitive; TLS only)\n" +
+ "- `X-ApplicationId` — OPTIONAL ApplicationId (GUID) for partner apps only; leave empty for a standard app\n" +
"- `X-Idoklad-Language` — optional response language (Cz, Sk, En)\n\n" +
"If a header is omitted, the matching environment default " +
"(`IDOKLAD_CLIENT_ID`, `IDOKLAD_CLIENT_SECRET`, `IDOKLAD_APPLICATION_ID`) is used. " +
- "If neither a header nor a default is available, the request is rejected with 401.",
+ "Only ClientId and ClientSecret are required; if either is missing the request is rejected with 401. " +
+ "When no ApplicationId is provided, authentication uses client_id + client_secret only.",
});
options.OperationFilter();