This commit is contained in:
Ralph Soika 2025-11-15 15:28:31 +01:00
parent bb058306f0
commit 1b31712ac7
7 changed files with 260 additions and 250 deletions

View file

@ -398,20 +398,23 @@
</dependency>
<!-- KSeF 2.0 Client -->
<!-- KSeF 2.0 Client
<dependency>
<groupId>pl.akmf.ksef-sdk</groupId>
<artifactId>ksef-client</artifactId>
<version>3.0.4</version>
<scope>test</scope>
</dependency>
-->
</dependencies>
<!--
<repositories>
<repository>
<id>github-cirfmf</id>
<url>https://maven.pkg.github.com/CIRFMF/ksef-client-java</url>
</repository>
</repositories>
-->
</project>

View file

@ -80,12 +80,12 @@ public class KSeFAPIService {
public String uploadInvoice(ItemCollection workitem, String fileName)
throws PluginException {
String referenceNumber = null;
logger.info("├── Upload Invoice...");
logger.info("├── 📤 Upload Invoice...");
// First open an interactive Session. The KSeFAuthManager automatically reuses
// an existing session
kseFAuthManager.openSession();
// kseFAuthManager.openSession();
if (kseFAuthManager.getAccessToken() == null) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,

View file

@ -78,18 +78,23 @@ public class KSeFAuthManager {
private HttpClient httpClient;
private String baseURI = "";
private String challenge = "";
private String refNumber = null;
private String authToken = null;
private String accessToken = null;
private String challengeTimestamp = null;
// public keys
private String symmetricCertBase64;
private String ksefTokenCertBase64;
private PublicKey symmetricPublicKey;
private PublicKey ksefTokenPublicKey;
// Auth information
private String challenge = "";
private String authRefNumber = null;
private String authToken = null;
private String challengeTimestamp = null;
// Session information
private String accessToken = null;
private String sessionRefNumber;
private String sessionValidUntil;
private byte[] sessionAesKey;
private byte[] sessionIv;
@ -154,8 +159,8 @@ public class KSeFAuthManager {
return baseURI;
}
public String getRefNumber() {
return refNumber;
public String getAuthRefNumber() {
return authRefNumber;
}
public String getAuthToken() {
@ -193,6 +198,9 @@ public class KSeFAuthManager {
this.authChallenge();
this.authKSeFToken();
this.checkTokenStatus();
this.redeemToken();
this.openInteractiveSession();
} else {
@ -246,6 +254,85 @@ public class KSeFAuthManager {
// }
}
/**
* Helper Method to read the certificates
*
* @throws Exception
*/
@Lock(LockType.WRITE)
public void loadPublicKeyCertificates() throws PluginException {
logger.info("├── ♻️ KSeF API load public-key-certificates...");
String uri = baseURI + "/security/public-key-certificates";
if (debug) {
logger.info("│ ├── Endpoint: " + uri);
}
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.header("Accept", "application/json")
.GET()
.build();
HttpResponse<String> response = null;
try {
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
} catch (IOException | InterruptedException e) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Unable to send request for KSeF certificates endpoint: " + e.getMessage());
}
if (debug) {
logger.info("│ ├── Response Code = " + response.statusCode());
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("KsefTokenEncryption"))) {
ksefTokenCertBase64 = entry.getString("certificate");
if (debug) {
logger.info("│ ├── ✓ KsefTokenEncryption certificate found.");
} // Public Key extrahieren
ksefTokenPublicKey = extractPublicKeyFromCertificate(ksefTokenCertBase64);
logger.info("│ ├── ✓ RSA PublicKey successfully extracted.");
}
if (usages.stream().anyMatch(v -> v.toString().contains("SymmetricKeyEncryption"))) {
symmetricCertBase64 = entry.getString("certificate");
if (debug) {
logger.info("│ ├── ✓ SymmetricKeyEncryption certificate found.");
}
// Public Key extrahieren
symmetricPublicKey = extractPublicKeyFromCertificate(symmetricCertBase64);
logger.info("│ ├── ✓ Symetric PublicKey successfully extracted.");
}
}
if (ksefTokenPublicKey == null) {
logger.warning("│ ├── ⚠️ No certificate with usage 'ksefTokenPublicKey' found!");
}
if (symmetricPublicKey == null) {
logger.warning("│ ├── ⚠️ No certificate with usage 'symmetricPublicKey' found!");
}
} catch (Exception e) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Error parsing certificates JSON: " + e.getMessage());
}
}
/**
* Diese method führt den challenge Request durch
*
@ -254,9 +341,12 @@ public class KSeFAuthManager {
// @Lock(LockType.READ)
@Lock(LockType.WRITE)
public void authChallenge() throws PluginException {
logger.info("├── KSeF API Challenge...");
logger.info("├── 🛅 KSeF API Challenge...");
String uri = baseURI + "/auth/challenge";
if (debug) {
logger.info("│ ├── Endpoint: " + uri);
}
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri)) // nur Endpoint checken
.header("Accept", "application/json")
@ -266,8 +356,8 @@ public class KSeFAuthManager {
HttpResponse<String> response;
try {
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
logger.info("│ ├── Response Code = " + response.statusCode());
if (debug) {
logger.info("│ ├── Response Code = " + response.statusCode());
logger.info("Response: " + response.body());
}
// Prüfen, dass wir eine API-Antwort bekommen, kein HTML
@ -283,10 +373,9 @@ public class KSeFAuthManager {
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
challenge = jsonObject.getString("challenge");
challengeTimestamp = jsonObject.getString("timestamp");
if (debug) {
logger.info("│ ├── challenge: " + challenge);
logger.info("│ ├── timestamp: " + challengeTimestamp);
}
logger.info("│ ├── ✓ challenge: " + challenge);
logger.info("│ ├── ✓ timestamp: " + challengeTimestamp);
} catch (Exception e) {
logger.severe("├── ⚠️ Error parsing JSON response" + e.getMessage());
@ -310,10 +399,12 @@ public class KSeFAuthManager {
*/
@Lock(LockType.WRITE)
public void authKSeFToken() throws PluginException {
logger.info("├── KSeF API auth ksef-token...");
logger.info("├── 🛃 KSeF API auth ksef-token...");
String uri = baseURI + "/auth/ksef-token";
logger.info("│ ├── Endpoint: " + uri);
if (debug) {
logger.info("│ ├── Endpoint: " + uri);
}
HttpResponse<String> response = null;
try {
String jsonPayload = String.format(
@ -339,8 +430,9 @@ public class KSeFAuthManager {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"API Error: Unable to request ksef-token");
}
logger.info("│ ├── Response Code = " + response.statusCode());
if (debug) {
logger.info("│ ├── Response Code = " + response.statusCode());
logger.info("Response: " + response.body());
}
@ -356,14 +448,15 @@ public class KSeFAuthManager {
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
// Reference Number speichern
refNumber = jsonObject.getString("referenceNumber");
logger.info("Extracted referenceNumber: " + refNumber);
authRefNumber = jsonObject.getString("referenceNumber");
logger.info("│ ├── ✓ referenceNumber: " + authRefNumber);
// Authentication Token speichern
JsonObject authTokenObject = jsonObject.getJsonObject("authenticationToken");
authToken = authTokenObject.getString("token");
logger.info("Extracted authenticationToken: " + authToken);
if (debug) {
logger.info("│ ├── ✓ authenticationToken: " + authToken);
}
} catch (Exception e) {
logger.severe("├── ⚠️ Error parsing JSON response" + e.getMessage());
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
@ -386,9 +479,11 @@ public class KSeFAuthManager {
"Error - missing access token!");
}
logger.info("├── KSeF API open interactive session...");
logger.info("├── 🔐 KSeF API open interactive session...");
String uri = baseURI + "/sessions/online";
if (debug) {
logger.info("│ ├── Endpoint: " + uri);
}
HttpResponse<String> response = null;
try {
// --- AES Schlüssel erzeugen ---
@ -436,8 +531,8 @@ public class KSeFAuthManager {
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
logger.info("│ ├── Response Code = " + response.statusCode());
if (debug) {
logger.info("│ ├── Response Code = " + response.statusCode());
logger.info("Response: " + response.body());
}
@ -553,9 +648,12 @@ public class KSeFAuthManager {
@Lock(LockType.WRITE)
public JsonObject redeemToken() throws PluginException {
logger.info("├── KSeF API redeem token...");
logger.info("├── 🛂 KSeF API redeem token...");
String uri = baseURI + "/auth/token/redeem";
if (debug) {
logger.info("│ ├── Endpoint: " + uri);
}
HttpResponse<String> response = null;
try {
HttpRequest request = HttpRequest.newBuilder()
@ -571,10 +669,10 @@ public class KSeFAuthManager {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"API Error - unable to redeemToken: " + e.getMessage());
}
logger.info("│ ├── Response Code = " + response.statusCode());
if (debug) {
logger.info("│ └── Response: " + response.body());
logger.info("│ ├── Response Code = " + response.statusCode());
logger.info("│ ├── Response: " + response.body());
}
if (response.statusCode() >= 400) {
@ -591,7 +689,7 @@ public class KSeFAuthManager {
JsonObject accessTokenObj = jsonObject.getJsonObject("accessToken");
this.accessToken = accessTokenObj.getString("token");
logger.info("│ └── Access Token received");
logger.info("│ └── ☑️ Access Token received");
return jsonObject;
@ -603,80 +701,6 @@ public class KSeFAuthManager {
}
}
/**
* Helper Method to read the certificates
*
* @throws Exception
*/
@Lock(LockType.WRITE)
public void loadPublicKeyCertificates() throws PluginException {
logger.info("├── KSeF API load 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")
.GET()
.build();
HttpResponse<String> response = null;
try {
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
} catch (IOException | InterruptedException e) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Unable to send request for KSeF certificates endpoint: " + e.getMessage());
}
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("KsefTokenEncryption"))) {
ksefTokenCertBase64 = entry.getString("certificate");
logger.info("│ ├── KsefTokenEncryption certificate found.");
// Public Key extrahieren
ksefTokenPublicKey = extractPublicKeyFromCertificate(ksefTokenCertBase64);
logger.info("│ ├── ✓ RSA PublicKey successfully extracted.");
}
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.");
}
}
if (ksefTokenPublicKey == null) {
logger.warning("│ ├── ⚠️ No certificate with usage 'ksefTokenPublicKey' found!");
}
if (symmetricPublicKey == null) {
logger.warning("│ ├── ⚠️ No certificate with usage 'symmetricPublicKey' 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");
@ -686,45 +710,60 @@ public class KSeFAuthManager {
}
/**
* Helper Method to analyze the auth status
* This method verifies the actual status of the authentication process for the
* current auth token (this is the token received from the start of the auth
* procedure)
*
* @return
* @throws Exception
* https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Uzyskiwanie-dostepu/paths/~1api~1v2~1auth~1%7BreferenceNumber%7D/get
*
* @throws PluginException
*/
public JsonObject getAuthStatus() throws Exception {
if (refNumber == null) {
throw new IllegalStateException("Reference number is missing. Call generateAuthToken() first.");
}
public void checkTokenStatus() throws PluginException {
logger.info("├── 🌡️ KSeF API check auth status...");
String uri = baseURI + "/auth/" + authRefNumber;
logger.info("├── KSeF API check authentication status...");
String uri = baseURI + "/auth/" + refNumber;
if (debug) {
logger.info("│ ├── Endpoint: " + 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 " + 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());
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());
}
if (response.statusCode() >= 500) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_CONFIG,
"Unable to access KSeF API: response=" + response.statusCode());
if (true) {
logger.info("│ ├── Response Code = " + response.statusCode());
logger.info("Response: " + response.body());
}
try (Jsonb jsonb = JsonbBuilder.create()) {
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
Boolean isRedeemed = jsonObject.containsKey("isTokenRedeemed") ? jsonObject.getBoolean("isTokenRedeemed")
: null;
logger.info("│ ├── isTokenRedeemed: " + isRedeemed);
return jsonObject;
String lastTokenRefreshDate = jsonObject.containsKey("lastTokenRefreshDate")
? jsonObject.getString("lastTokenRefreshDate")
: null;
String refreshTokenValidUntil = jsonObject.containsKey("refreshTokenValidUntil")
? jsonObject.getString("refreshTokenValidUntil")
: null;
logger.info("│ ├── lastTokenRefreshDate: " + lastTokenRefreshDate);
if (isRedeemed) {
logger.info("│ ├── ✓ isTokenRedeemed: " + isRedeemed);
logger.info("│ ├── ✓ refreshTokenValidUntil: " + refreshTokenValidUntil);
} else {
logger.info("│ ├── 𐄂 isTokenRedeemed: " + isRedeemed);
}
} catch (Exception e) {
logger.severe("├── ⚠️ Error parsing JSON response: " + e.getMessage());
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,

View file

@ -0,0 +1,26 @@
package com.alexanderlogistics;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
public class TestLoggerConfig {
public static void setupTestLogger() {
Logger logger = Logger.getLogger("");
Handler[] handlers = logger.getHandlers();
for (Handler handler : handlers) {
if (handler instanceof ConsoleHandler) {
handler.setFormatter(new SimpleFormatter() {
@Override
public String format(LogRecord record) {
return String.format("%s%n",
record.getMessage());
}
});
}
}
}
}

View file

@ -21,6 +21,8 @@ import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import com.alexanderlogistics.TestLoggerConfig;
@ExtendWith(MockitoExtension.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class KSeFAPIServiceTest {
@ -32,13 +34,16 @@ public class KSeFAPIServiceTest {
@BeforeEach
void setup() throws Exception {
TestLoggerConfig.setupTestLogger();
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);
manager.setDebug(false);
// init() ausführen
manager.init();
@ -46,6 +51,7 @@ public class KSeFAPIServiceTest {
// Open Session
manager.openSession();
manager.checkTokenStatus();
// manager.loadPublicKeyCertificates();
// manager.authChallenge();
// manager.authKSeFToken();

View file

@ -62,7 +62,7 @@ public class KSeFAuthManagerTest {
manager.redeemToken();
assertNotNull(
manager.getRefNumber(),
manager.getAuthRefNumber(),
"RefNumber darf nicht NULL sein");
assertNotNull(

View file

@ -1,64 +0,0 @@
package com.alexanderlogistics.ksef;
import java.util.logging.Logger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import pl.akmf.ksef.sdk.api.services.DefaultCryptographyService;
import pl.akmf.ksef.sdk.client.interfaces.KSeFClient;
/**
* Dieser Test testet den offiziellen KSeF Client
*
* <dependency>
* <groupId>pl.akmf.ksef-sdk</groupId>
* <artifactId>ksef-client</artifactId>
* <version>3.0.4</version>
* <scope>test</scope>
* </dependency>
*/
@ExtendWith(MockitoExtension.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class KSeFClientTest {
private static Logger logger = Logger.getLogger(KSeFClientTest.class.getName());
private DefaultCryptographyService cryptographyService;
private KSeFClient ksefClient;
private String accessToken;
@BeforeEach
void setup() throws Exception {
// Simply instantiate as POJOs
// cryptographyService = new DefaultCryptographyService();
}
// ------------------------------------------------------------
// 1) Challenge abholen
// ------------------------------------------------------------
// @Test
// @Order(1)
// @DisplayName("POST /api/v2/auth/token/redeem")
// void testRedeemToken() throws Exception {
// EncryptionData encryptionData = cryptographyService.getEncryptionData();
// OpenOnlineSessionRequest request = new OpenOnlineSessionRequestBuilder()
// .withFormCode(new FormCode(SystemCode.FA_3, SchemaVersion.VERSION_1_0E,
// SessionValue.FA))
// .withEncryptionInfo(encryptionData.encryptionInfo())
// .build();
// OpenOnlineSessionResponse openOnlineSessionResponse =
// ksefClient.openOnlineSession(request,
// accessToken);
// logger.info("OK");
// }
}