applicationId uz neni povinne
This commit is contained in:
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// iDoklad <c>client_credentials</c> authentication that sends only <c>client_id</c> and
|
||||
/// <c>client_secret</c> (no <c>application_id</c>).
|
||||
///
|
||||
/// The official SDK's <see cref="ClientCredentialsAuthentication"/> mandates an applicationId and
|
||||
/// always posts <c>application_id</c>, but iDoklad's standard client_credentials grant does not
|
||||
/// require it. This implementation is used when the caller does not supply an ApplicationId.
|
||||
/// </summary>
|
||||
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<Tokenizer> GetTokenAsync(HttpClient httpClient, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var postData = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
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<Tokenizer>(content);
|
||||
if (tokenizer is null || string.IsNullOrEmpty(tokenizer.AccessToken))
|
||||
{
|
||||
var error = JsonConvert.DeserializeObject<AuthenticationError>(content);
|
||||
throw new IdokladAuthenticationException(error);
|
||||
}
|
||||
|
||||
// GrantType defaults to ClientCredentials (enum value 0), which is correct here.
|
||||
return tokenizer;
|
||||
}
|
||||
|
||||
public Task<Tokenizer> RefreshAccessTokenAsync(HttpClient httpClient, CancellationToken cancellationToken = default)
|
||||
=> GetTokenAsync(httpClient, cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using IdokladSdk;
|
||||
using IdokladSdk.Authentication;
|
||||
using IdokladSdk.Builders;
|
||||
|
||||
namespace Idoklad.Client;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="DokladApiBuilder"/> variant that uses a caller-supplied <see cref="IAuthentication"/>
|
||||
/// (here <see cref="ClientSecretAuthentication"/>) instead of the SDK's applicationId-mandatory
|
||||
/// client credentials. The rest of the builder pipeline (HttpClient, options, urls, Build) is reused.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public string? ApplicationId { get; init; }
|
||||
|
||||
public required Language Language { get; init; }
|
||||
}
|
||||
|
||||
@@ -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<string>();
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
+5
-4
@@ -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<CredentialHeadersOperationFilter>();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user