Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions .github/workflows/maven-build-example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ on:
- 'example/**'
- '.github/workflows/*example*'

defaults:
run:
working-directory: ./example

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
# TODO: revert later — currently forcing branch checkout for GitHub build
- name: Checkout current branch
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref || github.ref }}

- uses: actions/setup-java@v4
with:
Expand All @@ -33,9 +33,13 @@ jobs:
key: ${{ runner.os }}-m2-v17-${{ secrets.CACHE_VERSION }}-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2-v17-${{ secrets.CACHE_VERSION }}

- name: Build
run: mvn --batch-mode compile
# TODO: revert later — currently forcing branch checkout for GitHub build
- name: Build and install library
run: mvn -B clean install -DskipTests

- name: Test and package
run: mvn --batch-mode package
# TODO: revert later — currently forcing branch checkout for GitHub build
- name: Build example app
run: |
cd example
mvn -B clean verify

6 changes: 5 additions & 1 deletion .github/workflows/maven-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
# TODO: revert later — currently forcing branch checkout for GitHub build
- name: Checkout current branch
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref || github.ref }}

- uses: actions/setup-java@v4
with:
Expand Down
15 changes: 14 additions & 1 deletion example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ When the application has started, open the _ngrok_ HTTPS URL in your preferred w
- [Using DigiDoc4j in production mode with the `prod` profile](#using-digidoc4j-in-production-mode-with-the-prod-profile)
+ [Stateful and stateless authentication](#stateful-and-stateless-authentication)
+ [Assuring that the signing and authentication certificate subjects match](#assuring-that-the-signing-and-authentication-certificate-subjects-match)
+ [Requesting the signing certificate in a separate step](#requesting-the-signing-certificate-in-a-separate-step)
* [HTTPS support](#https-support)
+ [How to verify that HTTPS is configured properly](#how-to-verify-that-https-is-configured-properly)
* [Deployment](#deployment)
Expand Down Expand Up @@ -119,7 +120,9 @@ The `src/main/java/eu/webeid/example` directory contains the Spring Boot applica
- `WebEidChallengeNonceFilter` for issuing the challenge nonce required by the authentication flow,
- `WebEidMobileAuthInitFilter` for issuing the challenge nonce and generating the deep link with the authentication request, used to initiate the mobile authentication flow,
- `WebEidAjaxLoginProcessingFilter` and `WebEidLoginPageGeneratingFilter` for handling login requests.
- `service`: Web eID signing service implementation that uses DigiDoc4j, and DigiDoc4j runtime configuration,
- `service`: Web eID signing service implementation that uses DigiDoc4j, and DigiDoc4j runtime configuration.
- `SigningService`: prepares ASiC-E containers and finalizes signatures.
- `MobileSigningService`: orchestrates the mobile signing flow (builds mobile signing requests/responses) and supports requesting the signing certificate in a separate step when enabled by configuration.
- `web`: Spring Web MVC controller for the welcome page and Spring Web REST controller that provides a digital signing endpoint.

The `src/resources` directory contains the resources used by the application:
Expand Down Expand Up @@ -177,6 +180,16 @@ A common alternative to stateful authentication is stateless authentication with

It is usually required to verify that the signing certificate subject matches the authentication certificate subject by assuring that both ID codes match. This check is implemented at the beginning of the `SigningService.prepareContainer()` method.

### Requesting the signing certificate in a separate step

In some deployments, the signing certificate is not reused from the authentication flow. Instead, it is retrieved directly from the user’s ID-card during the signing process itself.

This approach is useful when the signing process is performed without a prior authentication step. For example, in a mobile flow, the user may start signing directly without authenticating beforehand. In such cases, the signing certificate must be requested separately from the user’s ID-card before the signature can be created.

When this mode is enabled in the configuration, the backend issues a separate request for the signing certificate using the `MobileSigningService`. The service communicates with the client to obtain the certificate before the signing container is prepared, ensuring that the correct certificate chain is available for the signature.

This behavior is controlled by the `request-signing-cert` flag in the `application.yaml` configuration files (`application-dev.yaml`, `application-prod.yaml`). When the flag is set to **true**, the application explicitly requests the signing certificate during the signing process, demonstrating the separate signing certificate retrieval flow. When set to **false**, the signing uses the signing certificate that was already obtained during authentication, and no additional request is made.

## HTTPS support

There are two ways of adding HTTPS support to a Spring Boot application:
Expand Down
4 changes: 4 additions & 0 deletions example/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<dependency>
<groupId>org.digidoc4j</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import eu.webeid.example.security.WebEidMobileAuthInitFilter;
import eu.webeid.example.security.ui.WebEidLoginPageGeneratingFilter;
import eu.webeid.security.challenge.ChallengeNonceGenerator;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
Expand All @@ -39,9 +40,9 @@
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
import org.thymeleaf.ITemplateEngine;
import org.thymeleaf.web.servlet.JakartaServletWebApplication;

@Configuration
@ConfigurationPropertiesScan
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true)
public class ApplicationConfiguration {
Expand All @@ -53,7 +54,7 @@ public SecurityFilterChain filterChain(
AuthenticationConfiguration authConfig,
ChallengeNonceGenerator challengeNonceGenerator,
ITemplateEngine templateEngine,
JakartaServletWebApplication webApp
WebEidMobileProperties webEidMobileProperties
) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
Expand All @@ -62,9 +63,9 @@ public SecurityFilterChain filterChain(
.anyRequest().authenticated()
)
.authenticationProvider(webEidAuthenticationProvider)
.addFilterBefore(new WebEidMobileAuthInitFilter("/auth/mobile/init", "/auth/mobile/login", challengeNonceGenerator), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new WebEidMobileAuthInitFilter("/auth/mobile/init", "/auth/mobile/login", challengeNonceGenerator, webEidMobileProperties), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new WebEidChallengeNonceFilter("/auth/challenge", challengeNonceGenerator), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new WebEidLoginPageGeneratingFilter("/auth/mobile/login", "/auth/login", templateEngine, webApp), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new WebEidLoginPageGeneratingFilter("/auth/mobile/login", "/auth/login", templateEngine), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new WebEidAjaxLoginProcessingFilter("/auth/login", authConfig.getAuthenticationManager()), UsernamePasswordAuthenticationFilter.class)
.logout(l -> l.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()))
.headers(h -> h.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,24 +79,19 @@ public ChallengeNonceGenerator generator(ChallengeNonceStore challengeNonceStore
}

@Bean
public AuthTokenValidator validator(YAMLConfig yamlConfig) {
public AuthTokenValidator validator(WebEidAuthTokenProperties authTokenProperties) {
try {
return new AuthTokenValidatorBuilder()
.withSiteOrigin(URI.create(yamlConfig.getLocalOrigin()))
.withSiteOrigin(URI.create(authTokenProperties.validation().localOrigin()))
.withTrustedCertificateAuthorities(loadTrustedCACertificatesFromCerFiles())
.withTrustedCertificateAuthorities(loadTrustedCACertificatesFromTrustStore(yamlConfig))
.withOcspRequestTimeout(yamlConfig.getOcspRequestTimeout())
.withTrustedCertificateAuthorities(loadTrustedCACertificatesFromTrustStore(authTokenProperties))
.withOcspRequestTimeout(authTokenProperties.validation().ocspRequestTimeout())
.build();
} catch (JceException e) {
throw new RuntimeException("Error building the Web eID auth token validator.", e);
}
}

@Bean
public YAMLConfig yamlConfig() {
return new YAMLConfig();
}

private X509Certificate[] loadTrustedCACertificatesFromCerFiles() {
List<X509Certificate> caCertificates = new ArrayList<>();

Expand All @@ -118,7 +113,7 @@ private X509Certificate[] loadTrustedCACertificatesFromCerFiles() {
return caCertificates.toArray(new X509Certificate[0]);
}

private X509Certificate[] loadTrustedCACertificatesFromTrustStore(YAMLConfig yamlConfig) {
private X509Certificate[] loadTrustedCACertificatesFromTrustStore(WebEidAuthTokenProperties authTokenProperties) {
List<X509Certificate> caCertificates = new ArrayList<>();

try (InputStream is = ValidationConfiguration.class.getResourceAsStream(CERTS_RESOURCE_PATH + activeProfile + "/" + TRUSTED_CERTIFICATES_JKS)) {
Expand All @@ -127,7 +122,7 @@ private X509Certificate[] loadTrustedCACertificatesFromTrustStore(YAMLConfig yam
return new X509Certificate[0];
}
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
keystore.load(is, yamlConfig.getTrustStorePassword().toCharArray());
keystore.load(is, authTokenProperties.validation().trustStorePassword().toCharArray());
Enumeration<String> aliases = keystore.aliases();
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2020-2025 Estonian Information System Authority
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package eu.webeid.example.config;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.validation.annotation.Validated;

import java.time.Duration;

@Validated
@ConfigurationProperties(prefix = "web-eid-auth-token")
public record WebEidAuthTokenProperties(WebEidAuthTokenValidation validation) {

public record WebEidAuthTokenValidation(
@NotBlank String localOrigin,
String siteCertHash,
@NotBlank String trustStorePassword,
@DefaultValue("5s") Duration ocspRequestTimeout,
@NotNull Boolean useDigiDoc4jProdConfiguration) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,13 @@

package eu.webeid.example.config;

import jakarta.servlet.ServletContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.thymeleaf.web.servlet.JakartaServletWebApplication;
import jakarta.validation.constraints.NotBlank;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

@Configuration
public class ThymeleafWebAppConfiguration {

@Bean
public JakartaServletWebApplication jakartaServletWebApplication(ServletContext servletContext) {
return JakartaServletWebApplication.buildApplication(servletContext);
}
@Validated
@ConfigurationProperties(prefix = "web-eid-mobile")
public record WebEidMobileProperties(
@NotBlank String baseRequestUri,
boolean requestSigningCert) {
}
89 changes: 0 additions & 89 deletions example/src/main/java/eu/webeid/example/config/YAMLConfig.java

This file was deleted.

Loading