new impl authManager

This commit is contained in:
Ralph Soika 2025-11-17 00:04:40 +01:00
parent 6c4e72fe82
commit 1f2d3b4cd5
5 changed files with 216 additions and 76 deletions

View file

@ -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();
}
}

View file

@ -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<String> 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<String> 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<String> 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<String> 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<String> 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";

View file

@ -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();

View file

@ -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 {

View file

@ -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");