Implemented KSeFAuthManager

This commit is contained in:
Ralph Soika 2025-11-13 22:35:20 +01:00
parent e76a8dc668
commit 7fe009c23c
3 changed files with 427 additions and 75 deletions

View file

@ -0,0 +1,332 @@
package com.alexanderlogistics.ksef;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.security.PublicKey;
import java.time.Duration;
import java.util.Optional;
import java.util.logging.Logger;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.imixs.workflow.exceptions.InvalidAccessException;
import org.imixs.workflow.exceptions.PluginException;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.security.RunAs;
import jakarta.ejb.Lock;
import jakarta.ejb.LockType;
import jakarta.ejb.Singleton;
import jakarta.inject.Inject;
import jakarta.json.JsonObject;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
/**
* Der KSeFAuthManager stellt Methoden bereit um einen Access Token für das KSeF
* System in Polen zu erstellen.
*
*
*/
@Singleton
@RunAs("org.imixs.ACCESSLEVEL.MANAGERACCESS")
public class KSeFAuthManager {
private static Logger logger = Logger.getLogger(KSeFAuthManager.class.getName());
public static final String ENV_KSEF_API_TOKEN = "ksef.api.token";
public static final String ENV_KSEF_API_NIP = "ksef.api.nip";
public static final String ENV_KSEF_API_ENDPOINT = "ksef.api.endpoint";
public static final String ENV_KSEF_API_DEBUG = "ksef.api.debug";
public static final String ERROR_CONFIG = "CONFIG_ERROR";
public static final String ERROR_API = "API_ERROR";
@Inject
@ConfigProperty(name = ENV_KSEF_API_TOKEN)
Optional<String> ksefToken;
@Inject
@ConfigProperty(name = ENV_KSEF_API_NIP)
Optional<String> ksefNip;
@Inject
@ConfigProperty(name = ENV_KSEF_API_ENDPOINT)
Optional<String> ksefEndpoint;
@Inject
@ConfigProperty(name = ENV_KSEF_API_DEBUG, defaultValue = "false")
boolean debug;
private HttpClient httpClient;
private String baseURI = "";
private String challenge = "";
private String refNumber = null;
private String authToken = null;
private String symmetricCertBase64; // der Zertifikat-String aus der API
private PublicKey symmetricPublicKey; // der extrahierte RSA Public Key
@PostConstruct
void init() {
if (!ksefToken.isPresent()) {
throw new InvalidAccessException("Missing environment parameter ksef.api.token!");
}
if (!ksefEndpoint.isPresent()) {
throw new InvalidAccessException("Missing environment parameter ksef.api.endpoint!");
}
if (!ksefNip.isPresent()) {
throw new InvalidAccessException("Missing environment parameter ksef.api.nip!");
}
baseURI = ksefEndpoint.get();
if (baseURI.endsWith("/")) {
baseURI = baseURI.substring(0, baseURI.length() - 1);
}
httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.connectTimeout(Duration.ofSeconds(10))
.build();
// this.jsonb = JsonbBuilder.create();
}
public void setKsefToken(String token) {
this.ksefToken = Optional.ofNullable(token);
}
public void setKsefNip(String nip) {
this.ksefNip = Optional.ofNullable(nip);
}
public void setKsefEndpoint(String endpoint) {
this.ksefEndpoint = Optional.ofNullable(endpoint);
}
public void setDebug(boolean debug) {
this.debug = debug;
}
/**
* Encode the access token
*
* @return
*/
public String getEncodedToken() {
String result = java.util.Base64.getEncoder().encodeToString(ksefToken.orElse("").getBytes());
return result;
}
public String getBaseURI() {
return baseURI;
}
public String getRefNumber() {
return refNumber;
}
public String getAuthToken() {
return authToken;
}
public PublicKey getSymmetricPublicKey() {
return symmetricPublicKey;
}
public String getSymmetricCertBase64() {
return symmetricCertBase64;
}
public String getChallengeValue() {
return challenge;
}
/**
* Diese method führt den challenge Request durch
*
* @throws Exception
*/
// @Lock(LockType.READ)
@Lock(LockType.WRITE)
public void getChallenge() throws PluginException {
logger.info("├── KSeF API Challenge...");
String uri = baseURI + "/auth/challenge";
logger.info("│ ├── Endpoint: " + uri);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri)) // nur Endpoint checken
.header("Accept", "application/json")
.POST(HttpRequest.BodyPublishers.noBody()) //
.build();
HttpResponse<String> response;
try {
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
logger.info("│ ├── Response Code = " + response.statusCode());
if (debug) {
logger.info("Response: " + response.body());
}
// Prüfen, dass wir eine API-Antwort bekommen, kein HTML
if (response.statusCode() >= 500) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_CONFIG,
"Unable to access KSeF API: response=" + response.statusCode());
}
// Challenge aus Response extrahieren und speichern
if (response.statusCode() == 200) {
try (Jsonb jsonb = JsonbBuilder.create()) {
// Annahme: response.body() ist ein String mit JSON-Inhalt
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
challenge = jsonObject.getString("challenge");
if (debug) {
logger.info("│ ├── challenge: " + challenge);
}
} catch (Exception e) {
logger.severe("├── ⚠️ Error parsing JSON response" + e.getMessage());
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Error parsing JSON response: " + e.getMessage());
}
}
} catch (IOException | InterruptedException e) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"API Error: Unable to parse KSeF API response");
}
}
/**
* Helper method to request the KSeF Auth Token and referenceNumber
*
* @throws Exception
*/
@Lock(LockType.WRITE)
public void generateAuthToken() throws Exception {
logger.info("├── KSeF API auth ksef-token...");
String uri = baseURI + "/auth/ksef-token";
logger.info("│ ├── Endpoint: " + uri);
String jsonPayload = String.format(
"{" +
"\"challenge\": \"%s\"," +
"\"contextIdentifier\": {" +
" \"type\": \"Nip\"," +
" \"value\": \"%s\"" +
"}," +
"\"encryptedToken\": \"%s\"" +
"}",
challenge, ksefNip.get(), getEncodedToken());
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
logger.info("│ ├── Response Code = " + response.statusCode());
if (debug) {
logger.info("Response: " + response.body());
}
// Prüfen, dass wir eine API-Antwort bekommen
if (response.statusCode() >= 500) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_CONFIG,
"Unable to access KSeF API: response=" + response.statusCode());
}
// JSON parsen und Werte speichern
if (response.statusCode() == 200 || response.statusCode() == 202) {
try (Jsonb jsonb = JsonbBuilder.create()) {
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
// Reference Number speichern
refNumber = jsonObject.getString("referenceNumber");
logger.info("Extracted referenceNumber: " + refNumber);
// Authentication Token speichern
JsonObject authTokenObject = jsonObject.getJsonObject("authenticationToken");
authToken = authTokenObject.getString("token");
logger.info("Extracted authenticationToken: " + authToken);
} catch (Exception e) {
logger.severe("├── ⚠️ Error parsing JSON response" + e.getMessage());
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Error parsing JSON response: " + e.getMessage());
}
}
}
/**
* Helper Method to read the certificates
*
* @throws Exception
*/
@Lock(LockType.WRITE)
public void getKsefPublicKeys() throws Exception {
logger.info("├── KSeF API public-key-certificates...");
String uri = baseURI + "/security/public-key-certificates";
logger.info("│ ├── Endpoint: " + uri);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.header("Accept", "application/json")
.header("Authorization", "Bearer " + authToken)
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
logger.info("│ ├── Response Code = " + response.statusCode());
if (debug) {
logger.info("Response: " + response.body());
}
if (response.statusCode() != 200) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Unexpected response from KSeF certificates endpoint: " + response.statusCode());
}
// JSON parsen
try (Jsonb jsonb = JsonbBuilder.create()) {
var certArray = jsonb.fromJson(response.body(), JsonObject[].class);
for (JsonObject entry : certArray) {
// usage = ["KsefTokenEncryption"] oder ["SymmetricKeyEncryption"]
var usages = entry.getJsonArray("usage");
if (usages.stream().anyMatch(v -> v.toString().contains("SymmetricKeyEncryption"))) {
symmetricCertBase64 = entry.getString("certificate");
logger.info("│ ├── SymmetricKeyEncryption certificate found.");
// Public Key extrahieren
symmetricPublicKey = extractPublicKeyFromCertificate(symmetricCertBase64);
logger.info("│ ├── ✓ RSA PublicKey successfully extracted.");
return; // gefunden wir sind fertig
}
}
logger.warning("│ ├── ⚠️ No certificate with usage 'SymmetricKeyEncryption' found!");
} catch (Exception e) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Error parsing certificates JSON: " + e.getMessage());
}
}
private PublicKey extractPublicKeyFromCertificate(String base64Cert) throws Exception {
byte[] der = java.util.Base64.getDecoder().decode(base64Cert);
java.security.cert.CertificateFactory factory = java.security.cert.CertificateFactory.getInstance("X.509");
var certificate = (java.security.cert.X509Certificate) factory
.generateCertificate(new java.io.ByteArrayInputStream(der));
return certificate.getPublicKey();
}
}

View file

@ -0,0 +1,95 @@
package com.alexanderlogistics.ksef;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class KSeFAuthManagerTest {
private KSeFAuthManager manager;
@BeforeEach
void setup() throws Exception {
manager = new KSeFAuthManager();
// Test config
manager.setKsefToken(Params.TOKEN);
manager.setKsefNip(Params.NIP);
manager.setKsefEndpoint("https://ksef-test.mf.gov.pl/api/v2");
manager.setDebug(true);
// init() ausführen
manager.init();
}
// ------------------------------------------------------------
// 1) Challenge abholen
// ------------------------------------------------------------
@Test
@Order(1)
@DisplayName("GET /auth/challenge should return a challenge")
void testChallenge() throws Exception {
manager.getChallenge();
assertNotNull(
manager.getChallengeValue(),
"Challenge darf nicht NULL sein");
assertFalse(
manager.getChallengeValue().isBlank(),
"Challenge darf nicht leer sein");
System.out.println("Challenge = " + manager.getChallengeValue());
}
// ------------------------------------------------------------
// 2) Zertifikate holen (nur Public Keys)
// ------------------------------------------------------------
@Test
@Order(2)
@DisplayName("GET /security/public-key-certificates should return symmetric key certificate")
void testPublicKeys() throws Exception {
manager.getKsefPublicKeys();
assertNotNull(manager.getSymmetricPublicKey(), "Symmetric RSA PublicKey muss vorhanden sein");
assertNotNull(manager.getSymmetricCertBase64(), "Base64 Zertifikat muss vorhanden sein");
System.out.println("Symmetric PublicKey OK");
}
// ------------------------------------------------------------
// 3) KSeF Token Request
// ------------------------------------------------------------
@Test
@Order(3)
@DisplayName("POST /auth/ksef-token should return a token")
void testKsefToken() throws Exception {
// Challenge muss vorher geholt worden sein
manager.getChallenge();
// Token verschlüsseln
manager.generateAuthToken();
// Token-Call durchführen
String authToken = manager.getAuthToken();
assertNotNull(authToken, "auth token darf nicht NULL sein");
String refNumber = manager.getRefNumber();
assertNotNull(refNumber, "refNumber darf nicht NULL sein");
System.out.println("Received KSeF Token OK");
}
}

View file

@ -9,19 +9,8 @@ import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.logging.Logger;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import org.imixs.workflow.exceptions.ModelException;
import org.imixs.workflow.exceptions.PluginException;
import org.junit.jupiter.api.BeforeAll;
@ -173,68 +162,4 @@ public class TestKSeF {
assertTrue(response.body().contains("certificate"), "Response sollte ein Feld 'certificate' enthalten");
}
@Test
@Order(4)
@DisplayName("Rechnung übermitteln an KSeF 2.0 API")
public void testSendInvoice() throws Exception {
assertNotNull(refNumber, "ReferenceNumber nicht gesetzt");
// 1. Rechnung laden
Path invoicePath = Paths.get("test/resources/test-invoice.xml");
byte[] invoiceBytes = Files.readAllBytes(invoicePath);
int invoiceSize = invoiceBytes.length;
// 2. SHA-256 Hash der Originalrechnung
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
byte[] invoiceHashBytes = sha256.digest(invoiceBytes);
String invoiceHash = Base64.getEncoder().encodeToString(invoiceHashBytes);
// 3. AES-256 Schlüssel + IV generieren
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256);
SecretKey aesKey = keyGen.generateKey();
byte[] ivBytes = new byte[16];
new SecureRandom().nextBytes(ivBytes);
IvParameterSpec iv = new IvParameterSpec(ivBytes);
// 4. Rechnung verschlüsseln (AES-256-CBC + PKCS7Padding)
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, aesKey, iv);
byte[] encryptedInvoiceBytes = cipher.doFinal(invoiceBytes);
int encryptedInvoiceSize = encryptedInvoiceBytes.length;
byte[] encryptedInvoiceHashBytes = sha256.digest(encryptedInvoiceBytes);
String encryptedInvoiceHash = Base64.getEncoder().encodeToString(encryptedInvoiceHashBytes);
// 5. Verschlüsselten Inhalt Base64 codieren
String encryptedInvoiceContent = Base64.getEncoder().encodeToString(encryptedInvoiceBytes);
// 6. JSON-Payload erstellen
String jsonPayload = "{"
+ "\"invoiceHash\":\"" + invoiceHash + "\","
+ "\"invoiceSize\":" + invoiceSize + ","
+ "\"encryptedInvoiceHash\":\"" + encryptedInvoiceHash + "\","
+ "\"encryptedInvoiceSize\":" + encryptedInvoiceSize + ","
+ "\"encryptedInvoiceContent\":\"" + encryptedInvoiceContent + "\","
+ "\"offlineMode\":false"
+ "}";
// 7. HTTP POST Request an die API
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(KSEF_TEST_URL + "/sessions/online/" + refNumber + "/invoices"))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("Authorization", "Bearer " + authToken) // Token aus Authentifizierung
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("Status: " + response.statusCode());
System.out.println("Response: " + response.body());
assertEquals(200, response.statusCode(), "Invoice Upload sollte Status 200 zurückgeben");
assertTrue(response.body().contains("invoiceId"), "Antwort sollte field invoiceId enthalten");
}
}