From 1f2d3b4cd51052d464dd4fd63d5ca594459812f2 Mon Sep 17 00:00:00 2001 From: Ralph Soika Date: Mon, 17 Nov 2025 00:04:40 +0100 Subject: [PATCH] new impl authManager --- .../ksef/api/KSeFAPIService.java | 7 +- .../ksef/api/KSeFAuthManager.java | 223 +++++++++++++----- .../ksef/api/KSeFEncryptionHelper.java | 40 +++- .../ksef/api/KSeFAPIServiceTest.java | 20 +- .../ksef/api/KSeFAuthManagerTest.java | 2 + 5 files changed, 216 insertions(+), 76 deletions(-) diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAPIService.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAPIService.java index 448c6b0..5ceadba 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAPIService.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAPIService.java @@ -67,6 +67,7 @@ public class KSeFAPIService { // First open an interactive session (reuses existing if valid) kseFAuthManager.openSession(); + logger.info("โ”œโ”€โ”€ ๐Ÿ“ค Upload Invoice..."); // Validate session @@ -177,8 +178,10 @@ public class KSeFAPIService { throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API, "Invoice upload failed: " + e.getMessage()); } finally { - kseFAuthManager.closeSession(); - kseFAuthManager.deleteCurrentSession(); + + // finally we close the session + kseFAuthManager.closeInteractiveSession(); + kseFAuthManager.deleteCurrentAuthSession(); } } diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAuthManager.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAuthManager.java index d4eb4f7..2922fd9 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAuthManager.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAuthManager.java @@ -185,12 +185,15 @@ public class KSeFAuthManager { public void openSession() throws PluginException { logger.info("โ”œโ”€โ”€ open Session..."); if (!hasValidSession()) { + + // first we make sure that we do not have any old sessions! + // this.deleteAllAuthSessions(); + logger.info("โ”‚ โ”œโ”€โ”€ open new interactive KSeF Session..."); // open new session this.loadPublicKeyCertificates(); this.authChallenge(); this.authKSeFToken(); - // this.waitSomeTime(3000); // Jetzt Status abwarten this.waitForAuthStatus(); @@ -199,10 +202,8 @@ public class KSeFAuthManager { this.redeemToken(); this.openInteractiveSession(); - // Delete deprecated sessions - this.deleteDeprecatedSessions(); - this.checkTokenStatus(); + this.waitForSessionStatus(); } else { logger.warning("โ”‚ โ”œโ”€โ”€ ! Session already exists - reuse existing KSeF Session..."); @@ -461,7 +462,8 @@ public class KSeFAuthManager { } /** - * Wartet auf den Korrekten Auth Status... + * Waits for the auth status code 200 after a new authentication process was + * initiated * * @throws PluginException */ @@ -473,11 +475,11 @@ public class KSeFAuthManager { long timeout = 10_000; // 10 Sekunden while (System.currentTimeMillis() - start < timeout) { - if (this.checkTokenStatus() == 200) { - break; // Erfolg + if (this.checkAuthStatus() >= 200) { + break; // ok or borken } // kleine Pause, um CPU zu schonen - waitSomeTime(200); + waitSomeTime(500); } @@ -488,6 +490,48 @@ public class KSeFAuthManager { } + /** + * Waits for the session status code 200 after a new interactive session was + * initiated + * + * + * @throws PluginException + */ + public void waitForSessionStatus() throws PluginException { + logger.info("โ”œโ”€โ”€ ๐Ÿ”œ Waiting for Session status 200..."); + + // Now we need to wait until auth/{AuthRef} will return 200 + long start = System.currentTimeMillis(); + long timeout = 120_000; // 120 Sekunden + + while (System.currentTimeMillis() - start < timeout) { + int status = this.checkSessionStatus(); + + // Status 100 = Session opened and ready for uploads! + if (status == 100 || status == 200) { + logger.info("โ”‚ โ””โ”€โ”€ โœ“ Session is ready (status " + status + ")"); + return; // Session is ready! + } + + // Error statuses (400+) + if (status >= 400) { + throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API, + "API Error - invalid session status: " + status); + } + + // Wait and retry + waitSomeTime(500); + + } + + // Optional: prรผfen, ob Timeout erreicht wurde + if (System.currentTimeMillis() - start >= timeout) { + throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API, + "API Error - failed to init new interactive session - โš ๏ธ Timeout reached after 120 seconds!"); + } + + } + /** * waits some time.. */ @@ -738,10 +782,10 @@ public class KSeFAuthManager { * * https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Uzyskiwanie-dostepu/paths/~1api~1v2~1auth~1%7BreferenceNumber%7D/get * - * @return HTTP Response Code + * @return Auth Status Code * @throws PluginException */ - public int checkTokenStatus() throws PluginException { + public int checkAuthStatus() throws PluginException { logger.info("โ”œโ”€โ”€ โ†ฉ๏ธ KSeF API check auth status..."); String uri = baseURI + "/auth/" + authRefNumber; int httpResponse = -1; @@ -808,19 +852,77 @@ public class KSeFAuthManager { } /** - * This method returns all active sessions + * This method verifies the actual status of the status of an interactive + * session + * + * The method returns the aut status code (not the HTTP Response code!) + * + * https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Status-wysylki-i-UPO/paths/~1api~1v2~1sessions~1%7BreferenceNumber%7D/get + * + * @return Session Status Code + * @throws PluginException + */ + public int checkSessionStatus() throws PluginException { + logger.info("โ”œโ”€โ”€ โ†ฉ๏ธ KSeF API check session status..."); + String uri = baseURI + "/sessions/" + this.sessionRefNumber; + int httpResponse = -1; + int statusCode = -1; + logger.info("โ”‚ โ”œโ”€โ”€ GET: " + uri); + HttpResponse response = null; + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(uri)) + .header("Accept", "application/json") + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + this.accessToken) + .GET() + .build(); + response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (IOException | InterruptedException e) { + throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API, + "API Error: Unable to request auth status: " + e.getMessage()); + } + + httpResponse = response.statusCode(); + logger.info("โ”‚ โ”œโ”€โ”€ HTTP Response: " + httpResponse); + + logger.info("โ”‚ โ”œโ”€โ”€ " + response.body()); + + try (Jsonb jsonb = JsonbBuilder.create()) { + JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class); + + // Status Code aus dem status-Objekt extrahieren + if (jsonObject.containsKey("status")) { + JsonObject statusObject = jsonObject.getJsonObject("status"); + if (statusObject.containsKey("code")) { + statusCode = statusObject.getInt("code"); + logger.info("โ”‚ โ”œโ”€โ”€ Status Code: " + statusCode); + logger.info("โ”‚ โ”œโ”€โ”€ Status Description: " + statusObject.getString("description")); + } + } + + } 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()); + } + + return statusCode; + } + + /** + * This method returns all active auth sessions * * https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Aktywne-sesje/paths/~1api~1v2~1auth~1sessions/get * * @throws PluginException */ - public JsonObject getActiveSessions() throws PluginException { + public JsonObject getActiveAuthSessions() throws PluginException { logger.info("โ”œโ”€โ”€ ๐Ÿšธ KSeF API active sessions..."); String uri = baseURI + "/auth/sessions"; - if (debug) { - logger.info("โ”‚ โ”œโ”€โ”€ GET: " + uri); - } + logger.info("โ”‚ โ”œโ”€โ”€ GET: " + uri); + HttpResponse response = null; try { HttpRequest request = HttpRequest.newBuilder() @@ -838,7 +940,7 @@ public class KSeFAuthManager { if (true) { logger.info("โ”‚ โ”œโ”€โ”€ HTTP Response: " + response.statusCode()); - // logger.info("Response: " + response.body()); + logger.info("โ”‚ โ”œโ”€โ”€ Response: " + response.body()); } try (Jsonb jsonb = JsonbBuilder.create()) { JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class); @@ -853,19 +955,52 @@ public class KSeFAuthManager { } /** - * Deletes all old sessions + * Deletes the current auth session (authRefNumber) * - * The method first asks for old sessions and if we have more then 2 sessions we - * delete the deprecated ones. + * The method should be called wenn all operations are completed * * @param sessionData * @throws PluginException */ - public void deleteDeprecatedSessions() throws PluginException { + public void deleteCurrentAuthSession() throws PluginException { + + // delete old sessions.... + logger.info("โ”œโ”€โ”€ ๐Ÿšฎ KSeF API delete current auth sessions..."); + String uri = baseURI + "/auth/sessions/current"; + + HttpResponse response = null; + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(uri)) + .header("Accept", "application/json") + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + accessToken) + .DELETE() + .build(); + response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (IOException | InterruptedException e) { + throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API, + "API Error: Unable to request auth status: " + e.getMessage()); + } + + logger.info("โ”‚ โ”œโ”€โ”€ HTTP Response: " + response.statusCode()); + + } + + /** + * Deletes all auth sessions + * + * The method should be called before openInteractiveSession() to make sure no + * old sessions are still active + * + * @param sessionData + * @throws PluginException + */ + public void deleteAllAuthSessions() throws PluginException { int maxIterations = 10; // Sicherheits-Begrenzung for (int iteration = 0; iteration < maxIterations; iteration++) { - JsonObject currentSessions = this.getActiveSessions(); + JsonObject currentSessions = this.getActiveAuthSessions(); // Get list of refNumbers List referenceNumbers = new ArrayList<>(); @@ -877,21 +1012,15 @@ public class KSeFAuthManager { } // Wenn nur noch eine Session da ist, abbrechen - if (referenceNumbers.size() <= 1) { - if (iteration == 0) { - logger.info("โ”‚ โ”œโ”€โ”€ Only one session found, no deprecated sessions"); - } else { - logger.info("โ”œโ”€โ”€ ๐ŸŽ‰ All deprecated sessions deleted after " + iteration + " iterations"); - } + if (referenceNumbers.size() == 0) { + logger.info("โ”œโ”€โ”€ ๐ŸŽ‰ No deprecated sessions found"); break; } // delete old sessions.... - logger.info("โ”œโ”€โ”€ ๐Ÿšฎ KSeF API delete deprecated sessions (iteration " + (iteration + 1) + ")..."); + logger.info("โ”œโ”€โ”€ ๐Ÿšฎ KSeF API delete deprecated sessions..."); String uri = baseURI + "/auth/sessions/"; - - // Alle Eintrรคge ab Index 1 (also ab dem zweiten Eintrag) durchgehen - for (int i = 1; i < referenceNumbers.size(); i++) { + for (int i = 0; i < referenceNumbers.size(); i++) { String referenceNumber = referenceNumbers.get(i); logger.info("โ”‚ โ”œโ”€โ”€ delete deprecated session: " + referenceNumber); @@ -913,7 +1042,7 @@ public class KSeFAuthManager { logger.info("โ”‚ โ”œโ”€โ”€ HTTP Response: " + response.statusCode()); } - logger.info("โ”‚ โ”œโ”€โ”€ Deleted " + (referenceNumbers.size() - 1) + " sessions in this iteration"); + logger.info("โ”‚ โ”œโ”€โ”€ Deleted " + referenceNumbers.size() + " sessions in this iteration"); // Kurze Pause vor der nรคchsten Iteration if (referenceNumbers.size() > 1 && iteration < maxIterations - 1) { @@ -934,34 +1063,6 @@ public class KSeFAuthManager { } } - /** - * Deletes the current session - * - * @throws PluginException - */ - public void deleteCurrentSession() throws PluginException { - - logger.info("โ”œโ”€โ”€ ๐Ÿšฎ KSeF API delete current session..."); - String uri = baseURI + "/auth/sessions/current"; - - HttpResponse response = null; - try { - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(uri)) - .header("Accept", "application/json") - .header("Content-Type", "application/json") - .header("Authorization", "Bearer " + accessToken) - .DELETE() - .build(); - response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - } catch (IOException | InterruptedException e) { - throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API, - "API Error: Unable to request auth status: " + e.getMessage()); - } - - logger.info("โ”‚ โ”œโ”€โ”€ HTTP Response: " + response.statusCode()); - } - /** * This method closes the current session * @@ -973,7 +1074,7 @@ public class KSeFAuthManager { * * @throws PluginException */ - public void closeSession() throws PluginException { + public void closeInteractiveSession() throws PluginException { logger.info("โ”œโ”€โ”€ ๐Ÿ“ฆ KSeF API close session..."); // https://ksef-test.mf.gov.pl/api/v2/sessions/online/{referenceNumber}/close String uri = baseURI + "/sessions/online/" + sessionRefNumber + "/close"; diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFEncryptionHelper.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFEncryptionHelper.java index 2de6629..29741cd 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFEncryptionHelper.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFEncryptionHelper.java @@ -1,9 +1,11 @@ package com.alexanderlogistics.ksef.api; +import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.SecureRandom; +import java.security.spec.MGF1ParameterSpec; import java.util.Base64; import javax.crypto.BadPaddingException; @@ -12,6 +14,8 @@ import javax.crypto.IllegalBlockSizeException; import javax.crypto.KeyGenerator; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; +import javax.crypto.spec.OAEPParameterSpec; +import javax.crypto.spec.PSource; import javax.crypto.spec.SecretKeySpec; /** @@ -21,7 +25,8 @@ import javax.crypto.spec.SecretKeySpec; public class KSeFEncryptionHelper { private static final String AES_ALGORITHM = "AES"; - private static final String RSA_TRANSFORMATION = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; + private static final String xxxRSA_TRANSFORMATION = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; + private static final String RSA_TRANSFORMATION = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"; private static final int AES_KEY_SIZE = 256; private static final int IV_SIZE = 16; @@ -61,6 +66,26 @@ public class KSeFEncryptionHelper { * @throws BadPaddingException if padding is incorrect */ public static String encryptAesKey(SecretKey aesKey, PublicKey ksefPublicKey) + throws NoSuchAlgorithmException, NoSuchPaddingException, + InvalidKeyException, IllegalBlockSizeException, BadPaddingException, + InvalidAlgorithmParameterException { // <- Exception hinzufรผgen! + + Cipher rsaCipher = Cipher.getInstance(RSA_TRANSFORMATION); + + // WICHTIG: Explizite OAEP-Parameter setzen (wie bei Token-Verschlรผsselung!) + OAEPParameterSpec oaepParams = new OAEPParameterSpec( + "SHA-256", + "MGF1", + MGF1ParameterSpec.SHA256, + PSource.PSpecified.DEFAULT); + + rsaCipher.init(Cipher.ENCRYPT_MODE, ksefPublicKey, oaepParams); + + byte[] encryptedKey = rsaCipher.doFinal(aesKey.getEncoded()); + return Base64.getEncoder().encodeToString(encryptedKey); + } + + private static String OLDencryptAesKey(SecretKey aesKey, PublicKey ksefPublicKey) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { @@ -107,15 +132,16 @@ public class KSeFEncryptionHelper { * * @param ksefPublicKey KSeF's RSA public key * @return SessionEncryption containing all encryption data - * @throws NoSuchAlgorithmException if algorithm is not available - * @throws NoSuchPaddingException if padding scheme is not available - * @throws InvalidKeyException if the public key is invalid - * @throws IllegalBlockSizeException if the key size is invalid - * @throws BadPaddingException if padding is incorrect + * @throws NoSuchAlgorithmException if algorithm is not available + * @throws NoSuchPaddingException if padding scheme is not available + * @throws InvalidKeyException if the public key is invalid + * @throws IllegalBlockSizeException if the key size is invalid + * @throws BadPaddingException if padding is incorrect + * @throws InvalidAlgorithmParameterException */ public static SessionEncryption createSessionEncryption(PublicKey ksefPublicKey) throws NoSuchAlgorithmException, NoSuchPaddingException, - InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + InvalidKeyException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException { // Generate AES key and IV SecretKey aesKey = generateAesKey(); diff --git a/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/ksef/api/KSeFAPIServiceTest.java b/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/ksef/api/KSeFAPIServiceTest.java index 886c59b..364c801 100644 --- a/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/ksef/api/KSeFAPIServiceTest.java +++ b/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/ksef/api/KSeFAPIServiceTest.java @@ -3,6 +3,7 @@ package com.alexanderlogistics.ksef.api; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; import java.io.IOException; import java.nio.file.Files; @@ -12,6 +13,7 @@ import java.util.logging.Logger; import org.imixs.workflow.FileData; import org.imixs.workflow.ItemCollection; +import org.imixs.workflow.exceptions.PluginException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -52,15 +54,20 @@ public class KSeFAPIServiceTest { @Test @DisplayName("Upload Invoice XML (AES-256 + RSA)") - public void testUploadInvoice() throws Exception { + public void testUploadInvoice() { logger.info("==> Test: Upload Invoice XML"); - ItemCollection workitem = createWorkitem(); - - // Execute upload - - String referenceNumber = apiService.uploadInvoice(workitem, "example-invoice-01.xml"); + ItemCollection workitem = null; + String referenceNumber = null; + try { + workitem = createWorkitem(); + // Execute upload + referenceNumber = apiService.uploadInvoice(workitem, "example-invoice-01.xml"); + } catch (IOException | PluginException e) { + logger.info("โš ๏ธ Upload failed: " + e.getMessage()); + fail(e); + } // Verify result assertNotNull(referenceNumber, "Reference number should not be null"); assertFalse(referenceNumber.isEmpty(), "Reference number should not be empty"); @@ -70,6 +77,7 @@ public class KSeFAPIServiceTest { FileData file = workitem.getFileData("example-invoice-01.xml"); ItemCollection fileAttributes = new ItemCollection(file.getAttributes()); assertEquals(referenceNumber, fileAttributes.getItemValueString("ksef.referenceNumber")); + } private ItemCollection createWorkitem() throws IOException { diff --git a/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/ksef/api/KSeFAuthManagerTest.java b/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/ksef/api/KSeFAuthManagerTest.java index 327979d..37b10d7 100644 --- a/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/ksef/api/KSeFAuthManagerTest.java +++ b/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/ksef/api/KSeFAuthManagerTest.java @@ -90,6 +90,8 @@ public class KSeFAuthManagerTest { manager.openInteractiveSession(); + manager.waitForSessionStatus(); + assertNotNull(manager.getAccessToken(), "AccessToken fehlt"); // assertNotNull(manager.getRefNumber(), "ReferenceNumber fehlt"); assertNotNull(manager.getSessionRefNumber(), "SessionReferenceNumber fehlt");