Thanks to visit codestin.com
Credit goes to Github.com

Skip to content

alapierre/go-ksef-client

Sonarcloud Status Renovate enabled Go

GO KSeF 2.0 API client library

API structure generated by ogen-go with AuthFacade, EncryptionService, Client and in memory TokenProvider.

Very early project status - not ready for production use.

Ready futures

  • Sending invoices — interactively and in batch

Design assumptions

Context-bound NIP

  • The client reads the taxpayer identifier (NIP) from context.Context.
  • Rationale:
    • Consistency: a single, implicit source of truth for the current processing scope.
    • Propagation: NIP travels with the request lifetime across layers without changing function signatures.
    • Safety: avoids accidental mix-ups when multiple NIPs may be processed concurrently.

How it works

Set NIP into context once, near the request boundary:

ctx := ksef.Context(ctx, nipString)

Components that require NIP retrieve it from context:

nip, ok := ksef.NipFromContext(ctx)

Authorization flows (e.g., AuthWithToken) expect NIP to be present in the provided context. If missing, an error is returned.

Guidelines

  • Always derive child contexts from the NIP-bearing parent (use the same ctx for subsequent calls).
  • Do not pass NIP as a separate function parameter; rely on context for clarity and consistency.
  • Validate NIP before injecting it into context if your application requires strict input checks.
  • When spawning goroutines or timeouts, carry the same context forward (e.g., context.WithTimeout(ctx, ...)) so NIP remains available.

Example

At startup or per request:

ctx := context.Background()
ctx = ksef.Context(ctx, "")

Use ctx for all API calls that require NIP

TokenProvider Multi-NIP token cache

TokenProvider is a simple in-memory cache for KSeF tokens. It is thread-safe and can be used concurrently from multiple goroutines. In the next implementation, locks will be contextual — currently, the mutex is locked regardless of the NIP

EncryptionService

EncryptionService is responsible for:

  • fetching and caching KSeF public certificates,
  • encryption for two distinct usages:
    • KsefTokenEncryption (auth token + timestamp),
    • SymmetricKeyEncryption (encrypting the AES key used for invoices),
  • optional initialization without contacting the API (when you already have certificates/keys).

Keys are cached separately for each usage and automatically refreshed before expiration using a safety margin (refreshSkew).

Key points:

  • Two independent caches: tokenPub (KsefTokenEncryption) and symKeyPub (SymmetricKeyEncryption).
  • Automatic on-demand fetch from the API only when a key is missing or close to expiration.
  • ForceRefresh() refreshes both caches (does not return keys).
  • Optional preload of certificates/keys in the constructor to avoid API calls.

Initialization

Standard (keys fetched on demand from the API):

// Go
env := ksef.Test
httpClient := &http.Client{ Timeout: 15 * time.Second }

enc, err := cipher.NewEncryptionService(env, httpClient)
if err != nil {
    // handle error
}

No API calls — preload with existing certs or keys

// Go
enc, err := cipher.NewEncryptionService(
    env,
    httpClient,
    cipher.WithPreloadedKeys(cipher.PreloadedKeys{
        // Option A: certificates in DER Base64 form
        TokenCertBase64:     "<base64-der-token>",
        SymmetricCertBase64: "<base64-der-symmetric>",

        // Option B: ready *rsa.PublicKey instances
        // TokenRSAPub:     tokenPub,
        // SymmetricRSAPub: symPub,

        // Optional validity dates (if omitted, they’ll be read from certs)
        // TokenValidTo:     time.Time{},
        // SymmetricValidTo: time.Time{},
    }),
)
if err != nil {
    // handle error
}

Encryption

Encrypt KSeF token (token + timestamp in ms), RSA-OAEP(SHA-256)

encryptedTokenBytes, err := enc.EncryptKsefToken(ctx, token, challenge.Timestamp)
if err != nil {
    // handle error
}

Encrypt the invoice symmetric key (Usage=SymmetricKeyEncryption)

encryptedSymKey, err := enc.EncryptSymmetricKey(ctx, aesKey)

If a required key is missing or expired, EncryptionService will fetch/refresh the proper certificate and update its cache automatically.

if err := enc.ForceRefresh(ctx); err != nil {
    // handle error
}

After ForceRefresh, use the regular methods (EncryptKsefToken, EncryptSymmetricKey, or GetPublicKeyFor). ForceRefresh itself does not return keys.

Implementation notes

  • RSA-OAEP with SHA-256 is used for all RSA encryptions.
  • Each key usage has its own key and validity tracked independently.
  • refreshSkew is a safety margin checked on-demand: when a key is requested and its ValidTo − now ≤ refreshSkew (default 2 minutes), the service refreshes it; there is no periodic timer.
  • GetPublicKeyFor(ctx, usage) returns the current key for the given usage and will fetch/cache it if needed.

Authentication with KSeF Token

package main

import (
	"context"
	"fmt"
	"net/http"
	"time"

	"github.com/alapierre/go-ksef-client/ksef"
	"github.com/alapierre/go-ksef-client/ksef/util"
	"github.com/sirupsen/logrus"
)

func main() {

	logrus.SetLevel(logrus.DebugLevel)

	nip := util.GetEnvOrFailed("KSEF_NIP")
	token := util.GetEnvOrFailed("KSEF_TOKEN")

	httpClient := &http.Client{
		Timeout: 15 * time.Second,
	}

	env := ksef.Test
	authFacade, err := ksef.NewAuthFacade(env, httpClient)
	if err != nil {
		panic(err)
	}

	encryptor, err := ksef.NewEncryptionService(env, httpClient)
	if err != nil {
		panic(err)
	}

	ctx := context.Background()
	ctx = ksef.Context(ctx, nip)
	tokens, err := ksef.WithKsefToken(ctx, authFacade, encryptor, token)

	if err != nil {
		panic(err)
	}

	fmt.Println(tokens.AccessToken.Token)
	fmt.Println(tokens.RefreshToken.Token)

	refreshToken, err := authFacade.RefreshToken(ctx, tokens.RefreshToken.Token)
	if err != nil {
		panic(err)
	}

	fmt.Println(refreshToken.GetToken())
	fmt.Println("Refreshed")
}

Opening an interactive session and sending invoices

package main

import (
    "context"
    "fmt"
    "net/http"
    "time"
    
    "github.com/alapierre/go-ksef-client/ksef"
    "github.com/alapierre/go-ksef-client/ksef/api"
    "github.com/alapierre/go-ksef-client/ksef/util"
    "github.com/sirupsen/logrus"
)

func openSession() {

	logrus.SetLevel(logrus.DebugLevel)

	nip := util.GetEnvOrFailed("KSEF_NIP")
	token := util.GetEnvOrFailed("KSEF_TOKEN")
	buer := util.GetEnvOrFailed("KSEF_BUYER_NIP")

	httpClient := &http.Client{
		Timeout: 15 * time.Second,
	}

	env := ksef.Test

	authFacade, err := ksef.NewAuthFacade(env, httpClient)

	if err != nil {
		panic(err)
	}

	encryptor, err := ksef.NewEncryptionService(env, httpClient)
	if err != nil {
		panic(err)
	}

	ctx := context.Background()
	ctx = ksef.Context(ctx, nip)

	provider := ksef.NewTokenProvider(authFacade, func(ctx context.Context) (*api.AuthenticationTokensResponse, error) {
		return ksef.WithKsefToken(ctx, authFacade, encryptor, token)
	})

	client, err := ksef.NewClient(env, httpClient, provider)

	form := api.FormCode{
		SystemCode:    "FA (3)",
		SchemaVersion: "1-0E",
		Value:         "FA",
	}

	key, err := ksef.GenerateRandom256BitsKey()
	iv, err := ksef.GenerateRandom16BytesIv()
	encryptedKey, err := encryptor.EncryptSymmetricKey(ctx, key)

	enc := api.EncryptionInfo{
		EncryptedSymmetricKey: encryptedKey,
		InitializationVector:  iv,
	}

	session, err := client.OpenInteractiveSession(ctx, form, enc)
	if err != nil {
		panic(err)
	}

	fmt.Println(session)
	
	// send invoices	
	
	invoice, err := util.ReplacePlaceholdersInXML("../invoice_fa_3_type.xml", map[string]any{
		"NIP":        nip,
		"ISSUE_DATE": time.Now(),
		"BUYER_NIP":  buer,
	})
	if err != nil {
		panic(err)
	}

	ir, err := client.SendInvoice(ctx, string(session.ReferenceNumber), api.OptBool{}, invoice, key, iv)
	if err != nil {
		panic(err)
	}

	fmt.Println(ir)
}

Client validation

Cause by ogen issue: ogen-go/ogen#1570 validation of SHA-256 Base64 string is not working corectly. So in the generated code, the validation is commented out. Alternative is to change the following definition in the openapi.json:

from:

      "Sha256HashBase64": {
        "maxLength": 44,
        "minLength": 44,
        "type": "string",
        "description": "SHA-256 w Base64.",
        "format": "byte"
      },

to:

      "Sha256HashBase64": {
        "maxLength": 32,
        "minLength": 32,
        "type": "string",
        "description": "SHA-256 w Base64.",
        "format": "byte"
      },

Too many requests support

Currently KseF OpenAPI does not define Too Many Requests (429) responses for any endpoints. So the client does not handle them — ogen does not generate the code to get Retry-After response header. Issue is tracked here: CIRFMF/ksef-docs#347

Workaround: manually add Too Many Requests 429 responses in OpenAPI spec and regenerate the client.

{
  "responses": {
    "429": {
      "description": "Przekroczono limit żądań",
      "headers": {
        "Retry-After": {
          "description": "ilość sekund na które założona jest blokada",
          "schema": {
            "type": "integer"
          }
        }
      }
    }
  }
}

TokenProvider design and benchmarks

The TokenProvider component is responsible for providing and transparently refreshing KSeF access tokens. It implements api.SecuritySource and is used by the generated client to attach Authorization: Bearer headers to all protected requests.

Design rationale

The provider is designed with the following goals:

  • Correctness under concurrency
    Multiple goroutines may request tokens for the same or different NIPs at the same time. TokenProvider uses an internal cache protected by a mutex to ensure:

    • exactly one full authentication per NIP when tokens are missing or expired,
    • serialized refresh of access tokens per NIP,
    • race-free reads from the cache (verified with go test -race).
  • Steady‑state performance
    In typical usage, most calls should reuse a cached access token that is still valid. This “fast path” avoids network calls and does not allocate memory on the heap.

  • Simplicity vs. complexity
    The current implementation uses a single mutex and a per‑NIP cache. Given the expected load (usually 1–2, up to ~40 NIPs per application instance), more complex patterns (like per‑NIP locks or singleflight groups) were evaluated and considered unnecessary for now. The benchmarks below confirm that the cost of the provider itself is negligible compared to KSeF network latency.

What we benchmark and why

Benchmarks focus on the Bearer method of TokenProvider in several scenarios:

  • Sequential, warm cache
    Multiple calls for the same NIP with tokens already cached and valid.
    This measures the absolute overhead of the provider in the happy‑path case.

  • Parallel, same NIP, warm cache
    Many goroutines requesting a token for the same NIP simultaneously, with tokens already cached.
    This stresses the locking strategy and measures contention on the mutex.

  • Parallel, many NIPs, cold cache
    Many goroutines requesting tokens for many different NIPs when the cache is empty.
    This simulates the worst case (initial warm‑up), where each NIP requires a full authentication. The benchmark is dominated by the simulated authentication delay; it is useful mainly to estimate upper bounds and allocations during warm‑up, not to compare micro‑optimizations.

  • Parallel, many NIPs, warm cache
    Many goroutines requesting tokens for multiple NIPs after the cache has been pre‑filled.
    This is close to real‑world steady‑state with multiple tenants and concurrent traffic.

Sample benchmark results

Go 1.25

go test ./ksef -bench 'BenchmarkTokenProvider_' -benchmem
goos: linux
goarch: amd64
pkg: github.com/alapierre/go-ksef-client/ksef
cpu: Intel(R) Core(TM) i9-9940X CPU @ 3.30GHz
BenchmarkTokenProvider_Sequential-28                    13828598                73.83 ns/op            0 B/op          0 allocs/op
BenchmarkTokenProvider_ParallelSameNip-28                2263152               516.3 ns/op             0 B/op          0 allocs/op
BenchmarkTokenProvider_ParallelManyNips-28                507884              2033 ns/op              64 B/op          2 allocs/op
BenchmarkTokenProvider_ParallelManyNipsWarmCache-28      1261300               970.5 ns/op            64 B/op          2 allocs/op
PASS
ok      github.com/alapierre/go-ksef-client/ksef        21.349s

About

Go KSeF 2.0 API Client library

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •