Standalone Spring Boot application for digital document signing via NemLog-In. Acts as a stateless adapter: receives PDF references over a simple HTTP API, presents the NemLog-In signing iframe, and redirects back to the caller after signing.
Calling application → ITKDev Signing Server (this project) → NemLog-In
Any application that can make HTTP requests can integrate with the signing server. For an example, see the OS2Forms digital_signature module.
- Docker and Docker Compose
- Task (task runner)
- curl and unzip (to download the NemLog-In SDK)
- An OCES3 certificate (
.p12) registered with NemLog-In
NOTE: Java and Maven are not required locally. All build and run commands
execute inside Docker containers (maven:3-eclipse-temurin-21). A named Docker
volume (os2forms-signing-m2) is used to cache Maven dependencies between
builds.
git clone <REPOSITORY_URL>
cd itkdev-signing-serverRun the setup task to download the SDK, initialize configuration, and build everything:
task setupThis performs three steps:
- Downloads and extracts the NemLog-In Signing SDK into
Signing-Server/ - Copies
config/application.yaml.exampletoconfig/application.yaml - Builds the SDK libraries and the webapp
Edit config/application.yaml with your NemLog-In credentials and application
settings:
$EDITOR config/application.yamlPlace your OCES3 certificate at config/certificate.p12.
task devThe application starts on port 8088 by default.
Configuration is split into two namespaces in config/application.yaml:
| Variable | Description |
|---|---|
signing-client-url |
NemLog-In signing client URL |
validation-service-url |
NemLog-In validation service URL |
entity-id |
Your entity ID registered with NemLog-In |
keystore-path |
Path to your OCES3 certificate (.p12) |
key-pair-alias |
Key pair alias in the keystore |
keystore-password |
Keystore password |
private-key-password |
Private key password |
NemLog-In environments:
| Environment | Signing Client URL | Validation URL |
|---|---|---|
| Test | https://underskrift.test-nemlog-in.dk/ |
https://validering.test-nemlog-in.dk/api/validate |
| Production | https://underskrift.nemlog-in.dk/ |
https://validering.nemlog-in.dk/api/validate |
| Variable | Description | Default |
|---|---|---|
hash-salt |
Salt for SHA-1 hash validation of forward URLs. Must match the calling application. | (required) |
allowed-domains |
List of allowed domains for PDF URLs and forward URLs. | [] |
signed-documents-dir |
Directory for storing signed PDFs. | ./signed-documents/ |
source-documents-dir |
Directory for storing fetched source PDFs. | ./signers-documents/ |
debug |
Enable debug logging. | false |
test-page-enabled |
Enable the /test page for manual signing verification. Do not enable in production. |
false |
NOTE: If allowed-domains is empty, all domains are allowed. This is not
recommended for production.
Run task --list to see all available tasks:
| Task | Description |
|---|---|
task setup |
Download SDK, init config, and build everything |
task sdk:download |
Download and extract the NemLog-In Signing SDK |
task build:sdk |
Build SDK libraries (install to local Maven repo) |
task build:sdk:force |
Force rebuild SDK libraries (ignores cache) |
task build |
Build the webapp |
task build:all |
Build SDK + webapp |
task dev |
Run in development mode (mvn spring-boot:run) |
task run:jar |
Run the built JAR directly |
task clean |
Maven clean |
task config:init |
Copy example config to application.yaml |
task docker:build |
Build the Docker image |
task docker:push VERSION=x.y.z |
Build and push Docker image to GHCR |
The application serves a built-in API reference at the landing page (/). Open
the running service in a browser to see full endpoint documentation, parameter
descriptions, and the signing flow diagram.
All actions are served from a single endpoint GET /sign with an action
query parameter:
| Action | Description |
|---|---|
getcid |
Health check — returns a correlation ID |
sign |
Initiate signing (requires uri, forward_url, hash) |
result |
Redirect to forward_url after successful signing |
cancel |
Redirect to forward_url after cancellation |
download |
Download the signed PDF (one-time; file deleted after download) |
The signing result callback from the NemLog-In iframe is handled at
POST /signing-result.
sequenceDiagram
participant Client
participant Server
participant Browser
participant NemLog-In
Client->>Server: GET /sign?action=getcid
Server-->>Client: {"cid": "uuid"}
Client->>Server: GET /sign?action=sign&uri={b64}&forward_url={b64}&hash={sha1}
Note right of Server: Validate hash & domain,<br/>fetch PDF, generate payload
Server-->>Browser: sign.html (signing page)
Browser->>NemLog-In: Load iframe
NemLog-In->>Browser: postMessage("SendParameters")
Browser->>NemLog-In: postMessage(signingPayload)
Note right of NemLog-In: User signs with MitID
NemLog-In->>Browser: postMessage("signedDocument")
Browser->>Server: POST /signing-result
Note right of Server: Save signed PDF,<br/>read forward_url from session
Server-->>Browser: 302 redirect to forward_url
Client->>Server: GET /sign?action=download&file={name}
Server-->>Client: Binary PDF stream
Note right of Server: File deleted after download<br/>(one-time retrieval)
The Docker image is published to GitHub Container Registry:
ghcr.io/itk-dev/signing-server
Pull and run the image directly without building from source:
docker pull ghcr.io/itk-dev/signing-server:latest
docker run --rm -p 8088:8088 \
-e PUID=$(id -u) -e PGID=$(id -g) \
-v ./config/application.yaml:/app/config/application.yaml:ro \
-v ./config/certificate.p12:/app/config/certificate.p12:ro \
-v ./signed-documents:/app/signed-documents \
-v ./signers-documents:/app/signers-documents \
-v ./temp-documents:/app/temp-documents \
ghcr.io/itk-dev/signing-server:latestBuild and optionally push using Task:
task docker:build # Build image locally
task docker:push VERSION=1.0.0 # Build, tag, and push to GHCROr build directly with Docker:
docker build -t ghcr.io/itk-dev/signing-server:latest .NOTE: The SDK source (Signing-Server/) must be present before building the
image. Run task sdk:download first if it hasn't been downloaded yet.
The Dockerfile uses a two-stage build:
- build — Builds the SDK libraries and the webapp (
maven:3-eclipse-temurin-21) - final — Runtime with minimal JRE image (
eclipse-temurin:21-jre-jammy), runs on port 8088. The container starts as root and uses an entrypoint script withgosuto drop privileges toappuserafter adjusting UID/GID at runtime (seePUID/PGIDbelow)
Build and run with Docker Compose for local development:
docker compose build
docker compose up -dThe service is accessible through the nginx reverse proxy on port 8080.
Required volumes:
| Host Path | Container Path | Description |
|---|---|---|
config/application.yaml |
/app/config/application.yaml |
Application configuration |
config/certificate.p12 |
/app/config/certificate.p12 |
OCES3 certificate |
Optional volumes (document storage):
| Host Path | Container Path | Description |
|---|---|---|
signed-documents/ |
/app/signed-documents |
Signed PDF output |
signers-documents/ |
/app/signers-documents |
Fetched source PDFs |
temp-documents/ |
/app/temp-documents |
Temporary files during signing |
Environment variables (.env):
| Variable | Description | Default |
|---|---|---|
COMPOSE_PROJECT_NAME |
Docker Compose project name | sign-server |
COMPOSE_DOMAIN |
Domain for Traefik routing (local dev) | sign.local.itkdev.dk |
PUID |
UID for the container process (appuser) |
1000 (dev) / 1042 (prod) |
PGID |
GID for the container process (appuser) |
same as PUID |
MAX_UPLOAD_SIZE |
Max upload size for nginx and Spring | 56M |
MAX_UPLOAD_SIZE_BYTES |
Max upload size in bytes for Tomcat | 58720256 |
For production with Traefik and HTTPS, set the required environment variables and use the server compose file:
export COMPOSE_PROJECT_NAME=os2forms-signing
export COMPOSE_SERVER_DOMAIN=signing.example.dk
docker compose -f docker-compose.server.yml build
docker compose -f docker-compose.server.yml up -dNginx sits in front of the signing server as a reverse proxy on port 8080.
The application includes a self-contained test page at /test for verifying
the NemLog-In integration without an external system. The test page allows you
to:
- Upload a PDF document
- Sign it via the NemLog-In iframe (MitID authentication)
- Validate the signature against the NemLog-In validation API
- Download the signed PDF
The test page is disabled by default. Enable it by setting itkdev.test-page-enabled: true
in config/application.yaml.
WARNING: The test page bypasses hash validation and domain whitelisting. Do not enable it in production.
After starting the application, verify it works:
-
Built-in documentation: Open
http://localhost:8088/in a browser. You should see the API reference page. -
Health check:
curl http://localhost:8088/sign?action=getcidExpected:
{"cid":"<UUID>"} -
Error handling:
curl "http://localhost:8088/sign?action=sign&uri=dGVzdA==&forward_url=dGVzdA==&hash=invalid"Expected:
{"error":true,"message":"Incorrect hash value","code":0} -
Test page (if enabled): Open
http://localhost:8088/test, upload a PDF, and complete the signing flow.
You also need to configure the nemlogin side of things. This is done in the administration panel: https://administration.nemlog-in.dk/
- Certificate (OCES3 Systemcertifikat)
- metadata file
In the administration panel you select the system you want to manage. You need to add an entityID (same form as an URL). Metadata example file can be found here: https://cms.nemlog-in.dk/media/tcujxmgw/sp-metadata-sample.xml. Then you need to:
- change EntityID (to the one you have used in the administration)
- change URLs to match your system
- have the metadata match the system certificate you are using.
Find more information here: https://www.nemlog-in.dk/integrer-og-administrer-it-systemet/login/dokumentation-og-vejledninger/tekniske-oplysninger-til-brug-for-integrationstest/
Upload the metadata and the certificate in the administration panel.
When this is done you are ready to start testing. To do this press "Provisioner til integrationstest", this will deploy to the integration test environment.
Test away.
- NemLog-In Signing SDK — SignSDK v2.0.2 (build dependency)
- OS2Forms digital_signature — Example client integration (Drupal module)
This project is licensed under the Mozilla Public License 2.0.