Implemented KSeFAuthManager
This commit is contained in:
parent
e76a8dc668
commit
7fe009c23c
3 changed files with 427 additions and 75 deletions
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue