KSeF Impl and docu
This commit is contained in:
parent
26b08eb507
commit
11b28ef9c3
6 changed files with 700 additions and 168 deletions
|
|
@ -1,41 +1,174 @@
|
||||||
# KSeF - Converter
|
# KSeF API Integration
|
||||||
|
|
||||||
Die Idee ist es Rechnungen nach Erhalt aus cargosoft über die KSeF FA(2) API an die Behörde zu senden.
|
Java integration for the Polish **Krajowy System e-Faktur (KSeF)** - the national e-invoicing system for Poland.
|
||||||
|
|
||||||
# API
|
## Overview
|
||||||
|
|
||||||
Dokumentation: https://ksef-test.mf.gov.pl/docs/v2/index.html
|
This integration provides secure communication with the KSeF API to upload electronic invoices. It handles the complex multi-step authentication flow and encryption requirements mandated by the Polish tax authorities.
|
||||||
|
|
||||||
Die Basis url ist: "https://ksef-test.mf.gov.pl/api/v2";
|
## Components
|
||||||
|
|
||||||
Der Ablauf für die Übermittlung ist folgender:
|
### 1. KSeFAuthManager
|
||||||
|
|
||||||
- vom Polnischen Goverment (bzw. AGL/Taxman) haben wir ein Secret bekommen (KSeFToken)
|
Manages authentication and session lifecycle for the KSeF API.
|
||||||
- wir machen den call `/security/public-key-certificates` um Security Zertifikate zu erhalten
|
|
||||||
- wir machen den call `/auth/challenge` um eine Challenge ID zu erhalten
|
|
||||||
- wir machen den call `/auth/ksef-token` um eine ReferenceID und einen AccessToken zu erhalten
|
|
||||||
- wir senden die Rechnung - mit dem öffentlichen Schlüssel verschlüsselt zusammen mit unserer ReferenceID und dem AccessToken an das Goverment.
|
|
||||||
|
|
||||||
## Generieren einer Referenznummer.
|
**Key Responsibilities:**
|
||||||
|
|
||||||
Man benötigt eine Referenznummer (Session ID). Dise kann man wie folgt generieren:
|
- Multi-step authentication flow
|
||||||
|
- RSA certificate management
|
||||||
|
- Session key generation and encryption
|
||||||
|
- Access token lifecycle management
|
||||||
|
- Session reuse and validation
|
||||||
|
|
||||||
1.) "/auth/challenge" aufrufen um einen Challenge ID zu erhalten
|
### 2. KSeFAPIService
|
||||||
|
|
||||||
2.) "/auth/ksef-token" ksef Token + Challenge ID senden - man bekommt die Referenznummer (Gültig 1 Stunde)
|
Provides business-level methods for invoice operations.
|
||||||
|
|
||||||
## Rechnung senden
|
**Key Responsibilities:**
|
||||||
|
|
||||||
API Aufruf: https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Wysylka-interaktywna/paths/~1api~1v2~1sessions~1online~1%7BreferenceNumber%7D~1invoices/post
|
- Invoice encryption (AES-256-CBC)
|
||||||
|
- Invoice upload to KSeF
|
||||||
|
- Hash calculation and validation
|
||||||
|
- Error handling and reporting
|
||||||
|
|
||||||
Details:
|
## Authentication Flow
|
||||||
|
|
||||||
https://github.com/CIRFMF/ksef-docs/blob/main/sesja-interaktywna.md#2-wys%C5%82anie-faktury
|
The KSeF API requires a sophisticated multi-step authentication process:
|
||||||
|
|
||||||
Man muss die XML Datei mit dem öffentlichen Schlüssel der Behörde vor dem Versand verschlüsseln
|
```
|
||||||
|
1. Load Public Keys
|
||||||
|
└─> GET /security/public-key-certificates
|
||||||
|
|
||||||
### Das Rechnungsformat:
|
2. Challenge Request
|
||||||
|
└─> POST /auth/challenge
|
||||||
|
└─> Returns: challenge + timestamp
|
||||||
|
|
||||||
XML Datei
|
3. Token Authentication
|
||||||
|
└─> POST /auth/ksef-token
|
||||||
|
└─> Encrypts: token|timestamp with RSA-OAEP SHA-256
|
||||||
|
└─> Returns: authToken + referenceNumber
|
||||||
|
|
||||||
https://github.com/CIRFMF/ksef-docs/blob/main/faktury/weryfikacja-faktury.md
|
4. Token Redemption
|
||||||
|
└─> POST /auth/token/redeem
|
||||||
|
└─> Returns: accessToken
|
||||||
|
|
||||||
|
5. Open Interactive Session
|
||||||
|
└─> POST /sessions/online
|
||||||
|
└─> Generates AES-256 key + IV
|
||||||
|
└─> Encrypts session key with RSA
|
||||||
|
└─> Returns: sessionRefNumber + validUntil
|
||||||
|
```
|
||||||
|
|
||||||
|
## Invoice Upload Flow
|
||||||
|
|
||||||
|
Once authenticated, invoices can be uploaded:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Reuse or Create Session
|
||||||
|
└─> Validates existing session or creates new one
|
||||||
|
|
||||||
|
2. Encrypt Invoice
|
||||||
|
└─> AES-256-CBC encryption using session key
|
||||||
|
|
||||||
|
3. Calculate Hashes
|
||||||
|
└─> SHA-256 of original XML
|
||||||
|
└─> SHA-256 of encrypted XML
|
||||||
|
|
||||||
|
4. Upload Invoice
|
||||||
|
└─> POST /sessions/online/{sessionRef}/invoices
|
||||||
|
└─> Returns: referenceNumber
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Features
|
||||||
|
|
||||||
|
### Multi-Layer Encryption
|
||||||
|
|
||||||
|
- **RSA-OAEP SHA-256**: Token encryption with timestamp binding
|
||||||
|
- **RSA-OAEP SHA-1**: Session key encryption
|
||||||
|
- **AES-256-CBC**: Invoice content encryption
|
||||||
|
|
||||||
|
### Security Mechanisms
|
||||||
|
|
||||||
|
- **Replay Attack Protection**: Timestamp-bound tokens
|
||||||
|
- **Session Management**: Automatic session reuse and validation
|
||||||
|
- **Certificate Validation**: Dynamic X.509 certificate loading
|
||||||
|
- **UTC Timezone Handling**: Prevents timezone-related vulnerabilities
|
||||||
|
|
||||||
|
## Usage Example
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Initialize Auth Manager
|
||||||
|
KSeFAuthManager authManager = new KSeFAuthManager();
|
||||||
|
authManager.setKsefToken("your-ksef-token");
|
||||||
|
authManager.setKsefNip("1234567890");
|
||||||
|
authManager.setKsefEndpoint("https://ksef-test.mf.gov.pl/api/v2");
|
||||||
|
authManager.init();
|
||||||
|
|
||||||
|
// Initialize API Service
|
||||||
|
KSeFAPIService apiService = new KSeFAPIService();
|
||||||
|
apiService.kseFAuthManager = authManager;
|
||||||
|
|
||||||
|
// Upload Invoice
|
||||||
|
ItemCollection workitem = new ItemCollection();
|
||||||
|
FileData fileData = new FileData("invoice.xml", xmlBytes, null, null);
|
||||||
|
workitem.addFileData(fileData);
|
||||||
|
|
||||||
|
String referenceNumber = apiService.uploadInvoice(workitem, "invoice.xml");
|
||||||
|
System.out.println("Invoice uploaded: " + referenceNumber);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Required environment variables:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
ksef.api.token=your-ksef-authentication-token
|
||||||
|
ksef.api.nip=your-company-nip-number
|
||||||
|
ksef.api.endpoint=https://ksef-test.mf.gov.pl/api/v2
|
||||||
|
ksef.api.debug=false
|
||||||
|
```
|
||||||
|
|
||||||
|
## Session Management
|
||||||
|
|
||||||
|
The `KSeFAuthManager` automatically handles session lifecycle:
|
||||||
|
|
||||||
|
- **Session Reuse**: Validates `sessionValidUntil` timestamp (UTC)
|
||||||
|
- **Auto-Renewal**: Opens new session if current one expired
|
||||||
|
- **Thread-Safe**: Uses `@Lock(LockType.WRITE)` for concurrent access
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Test environment endpoint:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://ksef-test.mf.gov.pl/api/v2
|
||||||
|
```
|
||||||
|
|
||||||
|
Production endpoint:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://ksef.mf.gov.pl/api/v2
|
||||||
|
```
|
||||||
|
|
||||||
|
See `KSeFAPIServiceTest.java` for complete test examples.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The implementation uses `PluginException` for error handling with two error types:
|
||||||
|
|
||||||
|
- `CONFIG_ERROR`: Configuration or setup issues
|
||||||
|
- `API_ERROR`: API communication or response errors
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Jakarta EE (EJB, JSON-B)
|
||||||
|
- Java 11+ (HttpClient, Crypto APIs)
|
||||||
|
- Imixs Workflow (for document management)
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [KSeF Official Documentation](https://www.gov.pl/web/kas/ksef)
|
||||||
|
- [KSeF API Specification](https://ksef-test.mf.gov.pl/docs/v2/index.html)
|
||||||
|
- [Upload Invoice](https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Wysylka-interaktywna/paths/~1api~1v2~1sessions~1online~1%7BreferenceNumber%7D~1invoices/post)
|
||||||
|
- [Facture Details](https://github.com/CIRFMF/ksef-docs/blob/main/sesja-interaktywna.md#2-wys%C5%82anie-faktury)
|
||||||
|
- [XML Invoice Example](https://github.com/CIRFMF/ksef-docs/blob/main/faktury/weryfikacja-faktury.md)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,207 @@
|
||||||
|
package com.alexanderlogistics.ksef;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import javax.crypto.BadPaddingException;
|
||||||
|
import javax.crypto.IllegalBlockSizeException;
|
||||||
|
import javax.crypto.NoSuchPaddingException;
|
||||||
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
import org.imixs.workflow.FileData;
|
||||||
|
import org.imixs.workflow.ItemCollection;
|
||||||
|
import org.imixs.workflow.exceptions.PluginException;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.annotation.security.DeclareRoles;
|
||||||
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
|
import jakarta.annotation.security.RunAs;
|
||||||
|
import jakarta.ejb.Singleton;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.json.JsonObject;
|
||||||
|
import jakarta.json.bind.Jsonb;
|
||||||
|
import jakarta.json.bind.JsonbBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* THe KSeFAPIService provides methods to open an interactive session and upload
|
||||||
|
* an invoice.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@DeclareRoles({ "org.imixs.ACCESSLEVEL.NOACCESS", "org.imixs.ACCESSLEVEL.READERACCESS",
|
||||||
|
"org.imixs.ACCESSLEVEL.AUTHORACCESS", "org.imixs.ACCESSLEVEL.EDITORACCESS",
|
||||||
|
"org.imixs.ACCESSLEVEL.MANAGERACCESS" })
|
||||||
|
@RolesAllowed({ "org.imixs.ACCESSLEVEL.NOACCESS", "org.imixs.ACCESSLEVEL.READERACCESS",
|
||||||
|
"org.imixs.ACCESSLEVEL.AUTHORACCESS", "org.imixs.ACCESSLEVEL.EDITORACCESS",
|
||||||
|
"org.imixs.ACCESSLEVEL.MANAGERACCESS" })
|
||||||
|
@Singleton
|
||||||
|
@RunAs("org.imixs.ACCESSLEVEL.MANAGERACCESS")
|
||||||
|
public class KSeFAPIService {
|
||||||
|
|
||||||
|
private static Logger logger = Logger.getLogger(KSeFAPIService.class.getName());
|
||||||
|
|
||||||
|
public static final String ERROR_API = "API_ERROR";
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
KSeFAuthManager kseFAuthManager;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
void init() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method uploads a KSeF Invoice document (XML).
|
||||||
|
* The response of the upload is a 'referenceNumber' which is stored into the
|
||||||
|
* item ksef.referenceNumber
|
||||||
|
*
|
||||||
|
* @param workitem - the workitem containing the invoice file
|
||||||
|
* @param fileName - the name of the invoice file to upload
|
||||||
|
* @return referenceNumber - the KSeF reference number for the uploaded invoice
|
||||||
|
* @throws PluginException
|
||||||
|
*/
|
||||||
|
public String uploadInvoice(ItemCollection workitem, String fileName)
|
||||||
|
throws PluginException {
|
||||||
|
String referenceNumber = null;
|
||||||
|
logger.info("├── Upload Invoice...");
|
||||||
|
|
||||||
|
// First open an interactive Session. The KSeFAuthManager automatically reuses
|
||||||
|
// an existing session
|
||||||
|
kseFAuthManager.openSession();
|
||||||
|
|
||||||
|
if (kseFAuthManager.getAccessToken() == null) {
|
||||||
|
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||||||
|
"API Error - missing AccessToken!");
|
||||||
|
}
|
||||||
|
if (kseFAuthManager.getSessionRefNumber() == null) {
|
||||||
|
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||||||
|
"API Error - missing SessionRefNumber!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get session encryption keys
|
||||||
|
byte[] aesKeyBytes = kseFAuthManager.getSessionAesKey();
|
||||||
|
byte[] ivBytes = kseFAuthManager.getSessionIv();
|
||||||
|
|
||||||
|
if (aesKeyBytes == null || ivBytes == null) {
|
||||||
|
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||||||
|
"API Error - missing session encryption keys!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load invoice file
|
||||||
|
FileData fileData = workitem.getFileData(fileName);
|
||||||
|
if (fileData == null) {
|
||||||
|
throw new PluginException(KSeFAPIService.class.getSimpleName(), ERROR_API,
|
||||||
|
"File not found: " + fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] invoiceXml = fileData.getContent();
|
||||||
|
logger.info("│ ├── XML Invoice loaded - " + invoiceXml.length + " bytes");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// ---------- 1) Use session AES key and IV ----------
|
||||||
|
logger.info("│ ├── Using session AES Key...");
|
||||||
|
SecretKeySpec aesKey = new SecretKeySpec(aesKeyBytes, "AES");
|
||||||
|
IvParameterSpec iv = new IvParameterSpec(ivBytes);
|
||||||
|
|
||||||
|
// ---------- 2) Encrypt XML with AES-256-CBC ----------
|
||||||
|
logger.info("│ ├── Encrypting invoice with AES...");
|
||||||
|
javax.crypto.Cipher aesCipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||||
|
aesCipher.init(javax.crypto.Cipher.ENCRYPT_MODE, aesKey, iv);
|
||||||
|
byte[] encryptedInvoiceXml = aesCipher.doFinal(invoiceXml);
|
||||||
|
|
||||||
|
// ---------- 3) Calculate SHA-256 hashes ----------
|
||||||
|
logger.info("│ ├── Calculating hashes...");
|
||||||
|
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
|
||||||
|
|
||||||
|
// Hash of original invoice
|
||||||
|
byte[] xmlHashBytes = sha256.digest(invoiceXml);
|
||||||
|
String invoiceHash = Base64.getEncoder().encodeToString(xmlHashBytes);
|
||||||
|
|
||||||
|
// Hash of encrypted invoice
|
||||||
|
xmlHashBytes = sha256.digest(encryptedInvoiceXml);
|
||||||
|
String encryptedInvoiceHash = Base64.getEncoder().encodeToString(xmlHashBytes);
|
||||||
|
|
||||||
|
// ---------- 4) Build request body ----------
|
||||||
|
String jsonBody = String.format("{ " +
|
||||||
|
"\"invoiceHash\": \"%s\", " +
|
||||||
|
"\"invoiceSize\": %d, " +
|
||||||
|
"\"encryptedInvoiceHash\": \"%s\", " +
|
||||||
|
"\"encryptedInvoiceSize\": %d, " +
|
||||||
|
"\"encryptedInvoiceContent\": \"%s\", " +
|
||||||
|
"\"offlineMode\": false " +
|
||||||
|
"}",
|
||||||
|
invoiceHash,
|
||||||
|
invoiceXml.length,
|
||||||
|
encryptedInvoiceHash,
|
||||||
|
encryptedInvoiceXml.length,
|
||||||
|
Base64.getEncoder().encodeToString(encryptedInvoiceXml));
|
||||||
|
|
||||||
|
// ---------- 5) Prepare HTTP POST request ----------
|
||||||
|
String uri = kseFAuthManager.getBaseURI() + "/sessions/online/"
|
||||||
|
+ kseFAuthManager.getSessionRefNumber() + "/invoices";
|
||||||
|
logger.info("│ ├── Endpoint: " + uri);
|
||||||
|
|
||||||
|
if (kseFAuthManager.isDebug()) {
|
||||||
|
logger.info("│ ├── Request body: " + jsonBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(uri))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Authorization", "Bearer " + kseFAuthManager.getAccessToken())
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// ---------- 6) Execute request ----------
|
||||||
|
logger.info("│ ├── Sending request...");
|
||||||
|
var response = kseFAuthManager.getHttpClient().send(request,
|
||||||
|
java.net.http.HttpResponse.BodyHandlers.ofString());
|
||||||
|
|
||||||
|
logger.info("│ ├── Response Code: " + response.statusCode());
|
||||||
|
|
||||||
|
if (kseFAuthManager.isDebug()) {
|
||||||
|
logger.info("│ ├── Response Body: " + response.body());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 7) Validate response status ----------
|
||||||
|
if (response.statusCode() != 202) {
|
||||||
|
throw new PluginException(KSeFAPIService.class.getSimpleName(), ERROR_API,
|
||||||
|
"Invoice upload failed: " + response.statusCode() + " - " + response.body());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 8) Parse response and extract reference number ----------
|
||||||
|
try (Jsonb jsonb = JsonbBuilder.create()) {
|
||||||
|
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
|
||||||
|
referenceNumber = jsonObject.getString("referenceNumber");
|
||||||
|
|
||||||
|
logger.info("│ ├── Reference Number: " + referenceNumber);
|
||||||
|
logger.info("├── ✅ Invoice upload successful");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.severe("├── ⚠️ Error parsing JSON response: " + e.getMessage());
|
||||||
|
throw new PluginException(KSeFAPIService.class.getSimpleName(), ERROR_API,
|
||||||
|
"Error parsing JSON response: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
|
||||||
|
| IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) {
|
||||||
|
logger.severe("├── ⚠️ Encryption error: " + e.getMessage());
|
||||||
|
throw new PluginException(KSeFAPIService.class.getSimpleName(), ERROR_API,
|
||||||
|
"Encryption error during invoice upload: " + e.getMessage());
|
||||||
|
} catch (IOException | InterruptedException e) {
|
||||||
|
logger.severe("├── ⚠️ HTTP request error: " + e.getMessage());
|
||||||
|
throw new PluginException(KSeFAPIService.class.getSimpleName(), ERROR_API,
|
||||||
|
"HTTP error during invoice upload: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return referenceNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,9 @@ import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.security.spec.MGF1ParameterSpec;
|
import java.security.spec.MGF1ParameterSpec;
|
||||||
|
|
@ -16,7 +19,10 @@ import java.util.Base64;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import javax.crypto.BadPaddingException;
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.IllegalBlockSizeException;
|
||||||
|
import javax.crypto.NoSuchPaddingException;
|
||||||
import javax.crypto.spec.OAEPParameterSpec;
|
import javax.crypto.spec.OAEPParameterSpec;
|
||||||
import javax.crypto.spec.PSource;
|
import javax.crypto.spec.PSource;
|
||||||
|
|
||||||
|
|
@ -110,13 +116,16 @@ public class KSeFAuthManager {
|
||||||
.connectTimeout(Duration.ofSeconds(10))
|
.connectTimeout(Duration.ofSeconds(10))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// this.jsonb = JsonbBuilder.create();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpClient getHttpClient() {
|
public HttpClient getHttpClient() {
|
||||||
return httpClient;
|
return httpClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isDebug() {
|
||||||
|
return debug;
|
||||||
|
}
|
||||||
|
|
||||||
public void setKsefToken(String token) {
|
public void setKsefToken(String token) {
|
||||||
this.ksefToken = Optional.ofNullable(token);
|
this.ksefToken = Optional.ofNullable(token);
|
||||||
}
|
}
|
||||||
|
|
@ -141,18 +150,6 @@ public class KSeFAuthManager {
|
||||||
this.debug = 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() {
|
public String getBaseURI() {
|
||||||
return baseURI;
|
return baseURI;
|
||||||
}
|
}
|
||||||
|
|
@ -173,6 +170,77 @@ public class KSeFAuthManager {
|
||||||
return challenge;
|
return challenge;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] getSessionAesKey() {
|
||||||
|
return sessionAesKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getSessionIv() {
|
||||||
|
return sessionIv;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method opens a new interactive session to upload invoices.
|
||||||
|
* you can call hasValidSession() to verify if a session already exists.
|
||||||
|
*
|
||||||
|
* @throws PluginException
|
||||||
|
*/
|
||||||
|
public void openSession() throws PluginException {
|
||||||
|
logger.info("├── open new KSeF Session...");
|
||||||
|
if (!hasValidSession()) {
|
||||||
|
// open new session
|
||||||
|
this.loadPublicKeyCertificates();
|
||||||
|
|
||||||
|
this.authChallenge();
|
||||||
|
this.authKSeFToken();
|
||||||
|
this.redeemToken();
|
||||||
|
this.openInteractiveSession();
|
||||||
|
} else {
|
||||||
|
logger.info("├── reuse existing KSeF Session...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies if sessionValidUntil and sessionRefNumber is valid
|
||||||
|
*
|
||||||
|
* @return true if a valid session exists, false otherwise
|
||||||
|
*/
|
||||||
|
public boolean hasValidSession() {
|
||||||
|
// Check if session reference number exists
|
||||||
|
if (sessionRefNumber == null || sessionRefNumber.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if valid until timestamp exists
|
||||||
|
if (sessionValidUntil == null || sessionValidUntil.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse the validUntil timestamp (ISO 8601 format with UTC)
|
||||||
|
DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
|
||||||
|
Instant validUntilInstant = Instant.from(formatter.parse(sessionValidUntil));
|
||||||
|
|
||||||
|
// Get current time in UTC
|
||||||
|
Instant now = Instant.now();
|
||||||
|
|
||||||
|
// Check if session is still valid
|
||||||
|
boolean isValid = now.isBefore(validUntilInstant);
|
||||||
|
|
||||||
|
if (debug && isValid) {
|
||||||
|
logger.info("├── Session is still valid until: " + sessionValidUntil);
|
||||||
|
} else if (debug) {
|
||||||
|
logger.info("├── Session expired at: " + sessionValidUntil);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
// If parsing fails, assume session is invalid
|
||||||
|
logger.warning("├── ⚠️ Error parsing sessionValidUntil timestamp: " + e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Diese method führt den challenge Request durch
|
* Diese method führt den challenge Request durch
|
||||||
*
|
*
|
||||||
|
|
@ -236,29 +304,36 @@ public class KSeFAuthManager {
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
@Lock(LockType.WRITE)
|
@Lock(LockType.WRITE)
|
||||||
public void authKSeFToken() throws Exception {
|
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";
|
String uri = baseURI + "/auth/ksef-token";
|
||||||
logger.info("│ ├── Endpoint: " + uri);
|
logger.info("│ ├── Endpoint: " + uri);
|
||||||
String jsonPayload = String.format(
|
|
||||||
"{" +
|
|
||||||
"\"challenge\": \"%s\"," +
|
|
||||||
"\"contextIdentifier\": {" +
|
|
||||||
" \"type\": \"Nip\"," +
|
|
||||||
" \"value\": \"%s\"" +
|
|
||||||
"}," +
|
|
||||||
"\"encryptedToken\": \"%s\"" +
|
|
||||||
"}",
|
|
||||||
challenge, ksefNip.get(), getEncryptedToken());
|
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpResponse<String> response = null;
|
||||||
.uri(URI.create(uri))
|
try {
|
||||||
.header("Accept", "application/json")
|
String jsonPayload = String.format(
|
||||||
.header("Content-Type", "application/json")
|
"{" +
|
||||||
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
|
"\"challenge\": \"%s\"," +
|
||||||
.build();
|
"\"contextIdentifier\": {" +
|
||||||
|
" \"type\": \"Nip\"," +
|
||||||
|
" \"value\": \"%s\"" +
|
||||||
|
"}," +
|
||||||
|
"\"encryptedToken\": \"%s\"" +
|
||||||
|
"}",
|
||||||
|
challenge, ksefNip.get(), getEncryptedToken());
|
||||||
|
|
||||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(uri))
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
|
||||||
|
.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 ksef-token");
|
||||||
|
}
|
||||||
logger.info("│ ├── Response Code = " + response.statusCode());
|
logger.info("│ ├── Response Code = " + response.statusCode());
|
||||||
if (debug) {
|
if (debug) {
|
||||||
logger.info("Response: " + response.body());
|
logger.info("Response: " + response.body());
|
||||||
|
|
@ -293,12 +368,13 @@ public class KSeFAuthManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper Method to open an interactive session
|
* Helper Method to open an interactive session. The method fetches a
|
||||||
|
* sessionRefNumber which is mandatory to upload an invoice.
|
||||||
*
|
*
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
@Lock(LockType.WRITE)
|
@Lock(LockType.WRITE)
|
||||||
public void openInteractiveSession() throws Exception {
|
public void openInteractiveSession() throws PluginException {
|
||||||
|
|
||||||
if (accessToken == null) {
|
if (accessToken == null) {
|
||||||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||||||
|
|
@ -308,81 +384,88 @@ public class KSeFAuthManager {
|
||||||
logger.info("├── KSeF API open interactive session...");
|
logger.info("├── KSeF API open interactive session...");
|
||||||
String uri = baseURI + "/sessions/online";
|
String uri = baseURI + "/sessions/online";
|
||||||
logger.info("│ ├── Endpoint: " + uri);
|
logger.info("│ ├── Endpoint: " + uri);
|
||||||
|
HttpResponse<String> response = null;
|
||||||
|
try {
|
||||||
|
// --- AES Schlüssel erzeugen ---
|
||||||
|
SecureRandom rnd = new SecureRandom();
|
||||||
|
byte[] aesKeyBytes = new byte[32]; // 256-bit AES key
|
||||||
|
rnd.nextBytes(aesKeyBytes);
|
||||||
|
byte[] ivBytes = new byte[16]; // 128-bit IV
|
||||||
|
rnd.nextBytes(ivBytes);
|
||||||
|
|
||||||
// --- AES Schlüssel erzeugen ---
|
// --- AES Key mit RSA encrypten ---
|
||||||
SecureRandom rnd = new SecureRandom();
|
Cipher rsa;
|
||||||
byte[] aesKeyBytes = new byte[32]; // 256-bit AES key
|
|
||||||
rnd.nextBytes(aesKeyBytes);
|
|
||||||
byte[] ivBytes = new byte[16]; // 128-bit IV
|
|
||||||
rnd.nextBytes(ivBytes);
|
|
||||||
|
|
||||||
// --- AES Key mit RSA encrypten ---
|
rsa = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
|
||||||
// Cipher rsa = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
|
|
||||||
Cipher rsa = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
|
|
||||||
rsa.init(Cipher.ENCRYPT_MODE, this.symmetricPublicKey);
|
|
||||||
byte[] encryptedAesKey = rsa.doFinal(aesKeyBytes);
|
|
||||||
|
|
||||||
String jsonPayload = String.format(
|
rsa.init(Cipher.ENCRYPT_MODE, this.symmetricPublicKey);
|
||||||
"{" +
|
byte[] encryptedAesKey = rsa.doFinal(aesKeyBytes);
|
||||||
"\"formCode\": {" +
|
|
||||||
" \"systemCode\": \"FA (3)\"," +
|
|
||||||
" \"schemaVersion\": \"1-0E\"," +
|
|
||||||
" \"value\": \"FA\"" +
|
|
||||||
"}," +
|
|
||||||
"\"encryption\": {" +
|
|
||||||
" \"encryptedSymmetricKey\": \"%s\"," +
|
|
||||||
" \"initializationVector\": \"%s\"" +
|
|
||||||
"}" +
|
|
||||||
"}",
|
|
||||||
Base64.getEncoder().encodeToString(encryptedAesKey),
|
|
||||||
Base64.getEncoder().encodeToString(ivBytes));
|
|
||||||
|
|
||||||
if (debug) {
|
String jsonPayload = String.format(
|
||||||
logger.info("│ ├── Payload: " + jsonPayload);
|
"{" +
|
||||||
}
|
"\"formCode\": {" +
|
||||||
|
" \"systemCode\": \"FA (3)\"," +
|
||||||
|
" \"schemaVersion\": \"1-0E\"," +
|
||||||
|
" \"value\": \"FA\"" +
|
||||||
|
"}," +
|
||||||
|
"\"encryption\": {" +
|
||||||
|
" \"encryptedSymmetricKey\": \"%s\"," +
|
||||||
|
" \"initializationVector\": \"%s\"" +
|
||||||
|
"}" +
|
||||||
|
"}",
|
||||||
|
Base64.getEncoder().encodeToString(encryptedAesKey),
|
||||||
|
Base64.getEncoder().encodeToString(ivBytes));
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
|
||||||
.uri(URI.create(uri))
|
|
||||||
// .header("Accept-Charset", "UTF-8")
|
|
||||||
.header("Accept", "application/json")
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.header("Authorization", "Bearer " + accessToken)
|
|
||||||
.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());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.statusCode() != 201) {
|
|
||||||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
|
||||||
"Failed to open interactive session: " + response.statusCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
// JSON parsen
|
|
||||||
// Session RefNumber aus Response extrahieren und speichern
|
|
||||||
|
|
||||||
try (Jsonb jsonb = JsonbBuilder.create()) {
|
|
||||||
// Annahme: response.body() ist ein String mit JSON-Inhalt
|
|
||||||
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
|
|
||||||
sessionRefNumber = jsonObject.getString("referenceNumber");
|
|
||||||
sessionValidUntil = jsonObject.getString("validUntil");
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
logger.info("│ ├── Session ReferenceNumber = " + sessionRefNumber);
|
logger.info("│ ├── Payload: " + jsonPayload);
|
||||||
logger.info("│ └── Session ValidUntil = " + sessionValidUntil);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- WICHTIG: AES Schlüssel speichern, wir brauchen ihn beim Upload ---
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
this.sessionAesKey = aesKeyBytes;
|
.uri(URI.create(uri))
|
||||||
this.sessionIv = ivBytes;
|
// .header("Accept-Charset", "UTF-8")
|
||||||
} catch (Exception e) {
|
.header("Accept", "application/json")
|
||||||
logger.severe("├── ⚠️ Error parsing JSON response" + e.getMessage());
|
.header("Content-Type", "application/json")
|
||||||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
.header("Authorization", "Bearer " + accessToken)
|
||||||
"Error parsing JSON response: " + e.getMessage());
|
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
|
||||||
|
logger.info("│ ├── Response Code = " + response.statusCode());
|
||||||
|
if (debug) {
|
||||||
|
logger.info("Response: " + response.body());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode() != 201) {
|
||||||
|
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||||||
|
"Failed to open interactive session: " + response.statusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse JSON
|
||||||
|
try (Jsonb jsonb = JsonbBuilder.create()) {
|
||||||
|
// Annahme: response.body() ist ein String mit JSON-Inhalt
|
||||||
|
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
|
||||||
|
sessionRefNumber = jsonObject.getString("referenceNumber");
|
||||||
|
sessionValidUntil = jsonObject.getString("validUntil");
|
||||||
|
if (debug) {
|
||||||
|
logger.info("│ ├── Session ReferenceNumber = " + sessionRefNumber);
|
||||||
|
logger.info("│ └── Session ValidUntil = " + sessionValidUntil);
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMPORTANT: Store the AES key - this key is mandatory to upload a invoice
|
||||||
|
this.sessionAesKey = aesKeyBytes;
|
||||||
|
this.sessionIv = ivBytes;
|
||||||
|
} 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 (NoSuchAlgorithmException | NoSuchPaddingException | IOException | InterruptedException
|
||||||
|
| InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
|
||||||
|
logger.severe("├── ⚠️ API Error: " + e.getMessage());
|
||||||
|
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||||||
|
"API Error openInteractiveSession: " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -393,10 +476,14 @@ public class KSeFAuthManager {
|
||||||
* Format: token|timestamp_in_milliseconds
|
* Format: token|timestamp_in_milliseconds
|
||||||
*
|
*
|
||||||
* @return Base64-kodierter verschlüsselter Token
|
* @return Base64-kodierter verschlüsselter Token
|
||||||
|
* @throws NoSuchPaddingException
|
||||||
|
* @throws NoSuchAlgorithmException
|
||||||
|
* @throws InvalidAlgorithmParameterException
|
||||||
|
* @throws InvalidKeyException
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public String getEncryptedToken() throws Exception {
|
public String getEncryptedToken() throws PluginException {
|
||||||
|
String result = null;
|
||||||
if (ksefTokenPublicKey == null) {
|
if (ksefTokenPublicKey == null) {
|
||||||
throw new IllegalStateException("Public Key not loaded! Call loadPublicKeyCertificates() first.");
|
throw new IllegalStateException("Public Key not loaded! Call loadPublicKeyCertificates() first.");
|
||||||
}
|
}
|
||||||
|
|
@ -404,45 +491,54 @@ public class KSeFAuthManager {
|
||||||
if (challengeTimestamp == null || challengeTimestamp.isEmpty()) {
|
if (challengeTimestamp == null || challengeTimestamp.isEmpty()) {
|
||||||
throw new IllegalStateException("Challenge timestamp is missing! Call authChallenge() first.");
|
throw new IllegalStateException("Challenge timestamp is missing! Call authChallenge() first.");
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
// 1. Timestamp parsen - KSeF liefert zu viele Dezimalstellen
|
||||||
|
// Format: "2025-11-14T14:18:35.606507+00:00"
|
||||||
|
// Flexibler Parser der verschiedene Dezimalstellen akzeptiert
|
||||||
|
DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
|
||||||
|
Instant instant = Instant.from(formatter.parse(challengeTimestamp));
|
||||||
|
|
||||||
// 1. Timestamp parsen - KSeF liefert zu viele Dezimalstellen
|
long timestampMillis = instant.toEpochMilli();
|
||||||
// Format: "2025-11-14T14:18:35.606507+00:00"
|
|
||||||
// Flexibler Parser der verschiedene Dezimalstellen akzeptiert
|
|
||||||
DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
|
|
||||||
Instant instant = Instant.from(formatter.parse(challengeTimestamp));
|
|
||||||
|
|
||||||
long timestampMillis = instant.toEpochMilli();
|
logger.info("│ ├── Timestamp (ms): " + timestampMillis);
|
||||||
|
|
||||||
logger.info("│ ├── Timestamp (ms): " + timestampMillis);
|
// 2. Format erstellen: token|timestamp
|
||||||
|
String plaintext = ksefToken.get() + "|" + timestampMillis;
|
||||||
|
|
||||||
// 2. Format erstellen: token|timestamp
|
if (debug) {
|
||||||
String plaintext = ksefToken.get() + "|" + timestampMillis;
|
logger.info("│ ├── Plaintext format: [token]|[" + timestampMillis + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. RSA-OAEP Verschlüsselung mit SHA-256
|
||||||
|
Cipher cipher;
|
||||||
|
|
||||||
|
cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
|
||||||
|
|
||||||
|
OAEPParameterSpec oaepParams = new OAEPParameterSpec(
|
||||||
|
"SHA-256",
|
||||||
|
"MGF1",
|
||||||
|
MGF1ParameterSpec.SHA256,
|
||||||
|
PSource.PSpecified.DEFAULT);
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, ksefTokenPublicKey, oaepParams);
|
||||||
|
|
||||||
|
byte[] encrypted = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
// 4. Base64 kodieren
|
||||||
|
result = Base64.getEncoder().encodeToString(encrypted);
|
||||||
|
|
||||||
|
logger.info("│ ├── ✓ Token encrypted with RSA-OAEP");
|
||||||
|
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
|
||||||
|
| InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
|
||||||
|
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||||||
|
"API Error - unable to getEncryptedToken: " + e.getMessage());
|
||||||
|
|
||||||
if (debug) {
|
|
||||||
logger.info("│ ├── Plaintext format: [token]|[" + timestampMillis + "]");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. RSA-OAEP Verschlüsselung mit SHA-256
|
|
||||||
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
|
|
||||||
OAEPParameterSpec oaepParams = new OAEPParameterSpec(
|
|
||||||
"SHA-256",
|
|
||||||
"MGF1",
|
|
||||||
MGF1ParameterSpec.SHA256,
|
|
||||||
PSource.PSpecified.DEFAULT);
|
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, ksefTokenPublicKey, oaepParams);
|
|
||||||
|
|
||||||
byte[] encrypted = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
|
|
||||||
|
|
||||||
// 4. Base64 kodieren
|
|
||||||
String result = Base64.getEncoder().encodeToString(encrypted);
|
|
||||||
|
|
||||||
logger.info("│ ├── ✓ Token encrypted with RSA-OAEP");
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redeems the KSeF token and returns a session token
|
* Redeems the KSeF token and returns a session access token to be used to open
|
||||||
|
* an Interactive Session
|
||||||
*
|
*
|
||||||
* POST /auth/token/redeem
|
* POST /auth/token/redeem
|
||||||
*
|
*
|
||||||
|
|
@ -450,21 +546,26 @@ public class KSeFAuthManager {
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
@Lock(LockType.WRITE)
|
@Lock(LockType.WRITE)
|
||||||
public JsonObject redeemToken() throws Exception {
|
public JsonObject redeemToken() throws PluginException {
|
||||||
|
|
||||||
logger.info("├── KSeF API redeem token...");
|
logger.info("├── KSeF API redeem token...");
|
||||||
String uri = baseURI + "/auth/token/redeem";
|
String uri = baseURI + "/auth/token/redeem";
|
||||||
logger.info("│ ├── Endpoint: " + uri);
|
logger.info("│ ├── Endpoint: " + uri);
|
||||||
|
HttpResponse<String> response = null;
|
||||||
|
try {
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(uri))
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.header("Authorization", "Bearer " + authToken)
|
||||||
|
.POST(HttpRequest.BodyPublishers.noBody())
|
||||||
|
.build();
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
response = httpClient.send(request,
|
||||||
.uri(URI.create(uri))
|
HttpResponse.BodyHandlers.ofString());
|
||||||
.header("Accept", "application/json")
|
} catch (IOException | InterruptedException e) {
|
||||||
.header("Authorization", "Bearer " + authToken)
|
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||||||
.POST(HttpRequest.BodyPublishers.noBody())
|
"API Error - unable to redeemToken: " + e.getMessage());
|
||||||
.build();
|
}
|
||||||
|
|
||||||
HttpResponse<String> response = httpClient.send(request,
|
|
||||||
HttpResponse.BodyHandlers.ofString());
|
|
||||||
logger.info("│ ├── Response Code = " + response.statusCode());
|
logger.info("│ ├── Response Code = " + response.statusCode());
|
||||||
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
|
|
@ -503,7 +604,7 @@ public class KSeFAuthManager {
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
@Lock(LockType.WRITE)
|
@Lock(LockType.WRITE)
|
||||||
public void loadPublicKeyCertificates() throws Exception {
|
public void loadPublicKeyCertificates() throws PluginException {
|
||||||
|
|
||||||
logger.info("├── KSeF API load public-key-certificates...");
|
logger.info("├── KSeF API load public-key-certificates...");
|
||||||
String uri = baseURI + "/security/public-key-certificates";
|
String uri = baseURI + "/security/public-key-certificates";
|
||||||
|
|
@ -515,7 +616,13 @@ public class KSeFAuthManager {
|
||||||
.GET()
|
.GET()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
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());
|
logger.info("│ ├── Response Code = " + response.statusCode());
|
||||||
if (debug) {
|
if (debug) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
package com.alexanderlogistics.ksef;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import org.imixs.workflow.FileData;
|
||||||
|
import org.imixs.workflow.ItemCollection;
|
||||||
|
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 KSeFAPIServiceTest {
|
||||||
|
|
||||||
|
private static Logger logger = Logger.getLogger(KSeFAPIServiceTest.class.getName());
|
||||||
|
|
||||||
|
private KSeFAuthManager manager;
|
||||||
|
private KSeFAPIService apiService;
|
||||||
|
|
||||||
|
@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(false);
|
||||||
|
|
||||||
|
// init() ausführen
|
||||||
|
manager.init();
|
||||||
|
|
||||||
|
// Open Session
|
||||||
|
manager.openSession();
|
||||||
|
// Create API Service and inject manager
|
||||||
|
apiService = new KSeFAPIService();
|
||||||
|
apiService.kseFAuthManager = manager;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(4)
|
||||||
|
@DisplayName("Upload Invoice XML (AES-256 + RSA)")
|
||||||
|
public void testUploadInvoice() throws Exception {
|
||||||
|
logger.info("==> Test: Upload Invoice XML");
|
||||||
|
|
||||||
|
ItemCollection workitem = createWorkitem();
|
||||||
|
|
||||||
|
// Execute upload
|
||||||
|
String referenceNumber = apiService.uploadInvoice(workitem, "example-invoice-01.xml");
|
||||||
|
|
||||||
|
// Verify result
|
||||||
|
assertNotNull(referenceNumber, "Reference number should not be null");
|
||||||
|
assertFalse(referenceNumber.isEmpty(), "Reference number should not be empty");
|
||||||
|
|
||||||
|
logger.info("✅ Upload successful - Reference Number: " + referenceNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ItemCollection createWorkitem() throws IOException {
|
||||||
|
ItemCollection workitem = new ItemCollection();
|
||||||
|
// ---------- 2) XML laden ----------
|
||||||
|
Path xmlPath = Paths.get("src/test/resources/ksef/example-invoice-01.xml");
|
||||||
|
byte[] invoiceXml = Files.readAllBytes(xmlPath);
|
||||||
|
|
||||||
|
FileData fileData = new FileData("example-invoice-01.xml", invoiceXml, null, null);
|
||||||
|
workitem.addFileData(fileData);
|
||||||
|
|
||||||
|
return workitem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -42,7 +42,7 @@ public class KSeFAuthManagerTest {
|
||||||
manager.setKsefToken(Params.TOKEN);
|
manager.setKsefToken(Params.TOKEN);
|
||||||
manager.setKsefNip(Params.NIP);
|
manager.setKsefNip(Params.NIP);
|
||||||
manager.setKsefEndpoint("https://ksef-test.mf.gov.pl/api/v2");
|
manager.setKsefEndpoint("https://ksef-test.mf.gov.pl/api/v2");
|
||||||
manager.setDebug(true);
|
manager.setDebug(false);
|
||||||
|
|
||||||
// init() ausführen
|
// init() ausführen
|
||||||
manager.init();
|
manager.init();
|
||||||
|
|
@ -152,7 +152,10 @@ public class KSeFAuthManagerTest {
|
||||||
// manager.getRefNumber() + "/invoices";
|
// manager.getRefNumber() + "/invoices";
|
||||||
String uri = manager.getBaseURI() + "/sessions/online/" + manager.getSessionRefNumber() + "/invoices";
|
String uri = manager.getBaseURI() + "/sessions/online/" + manager.getSessionRefNumber() + "/invoices";
|
||||||
logger.info("│ ├── Endpoint: " + uri);
|
logger.info("│ ├── Endpoint: " + uri);
|
||||||
logger.info("Request: " + jsonBody);
|
|
||||||
|
if (manager.isDebug()) {
|
||||||
|
logger.info("Request: " + jsonBody);
|
||||||
|
}
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
.uri(URI.create(uri))
|
.uri(URI.create(uri))
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
</Adres>
|
</Adres>
|
||||||
<DaneKontaktowe>
|
<DaneKontaktowe>
|
||||||
<Email>Lucja17@example.net</Email>
|
<Email>Lucja17@example.net</Email>
|
||||||
<Telefon>86-588-03-53</Telefon>
|
<Telefon>86-588-03-553</Telefon>
|
||||||
</DaneKontaktowe>
|
</DaneKontaktowe>
|
||||||
</Podmiot1>
|
</Podmiot1>
|
||||||
<Podmiot2>
|
<Podmiot2>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue