Skip to content

Commit 1ceeb94

Browse files
NFC-82 NFC signing support for web-eid example
Signed-off-by: Sander Kondratjev <[email protected]>
1 parent b0f90da commit 1ceeb94

28 files changed

+1309
-874
lines changed

.github/workflows/maven-build-example.yml

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,6 @@ on:
1010
- 'example/**'
1111
- '.github/workflows/*example*'
1212

13-
defaults:
14-
run:
15-
working-directory: ./example
16-
1713
jobs:
1814
build:
1915
runs-on: ubuntu-latest
@@ -33,9 +29,9 @@ jobs:
3329
key: ${{ runner.os }}-m2-v17-${{ secrets.CACHE_VERSION }}-${{ hashFiles('**/pom.xml') }}
3430
restore-keys: ${{ runner.os }}-m2-v17-${{ secrets.CACHE_VERSION }}
3531

36-
- name: Build
37-
run: mvn --batch-mode compile
38-
39-
- name: Test and package
40-
run: mvn --batch-mode package
32+
- name: Install library
33+
run: mvn -B -ntp install
4134

35+
- name: Build example project
36+
working-directory: ./example
37+
run: mvn -B -ntp package

example/README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ When the application has started, open the _ngrok_ HTTPS URL in your preferred w
8282
- [Using DigiDoc4j in production mode with the `prod` profile](#using-digidoc4j-in-production-mode-with-the-prod-profile)
8383
+ [Stateful and stateless authentication](#stateful-and-stateless-authentication)
8484
+ [Assuring that the signing and authentication certificate subjects match](#assuring-that-the-signing-and-authentication-certificate-subjects-match)
85+
+ [Requesting the signing certificate in a separate step](#requesting-the-signing-certificate-in-a-separate-step)
8586
* [HTTPS support](#https-support)
8687
+ [How to verify that HTTPS is configured properly](#how-to-verify-that-https-is-configured-properly)
8788
* [Deployment](#deployment)
@@ -119,7 +120,9 @@ The `src/main/java/eu/webeid/example` directory contains the Spring Boot applica
119120
- `WebEidChallengeNonceFilter` for issuing the challenge nonce required by the authentication flow,
120121
- `WebEidMobileAuthInitFilter` for issuing the challenge nonce and generating the deep link with the authentication request, used to initiate the mobile authentication flow,
121122
- `WebEidAjaxLoginProcessingFilter` and `WebEidLoginPageGeneratingFilter` for handling login requests.
122-
- `service`: Web eID signing service implementation that uses DigiDoc4j, and DigiDoc4j runtime configuration,
123+
- `service`: Web eID signing service implementation that uses DigiDoc4j, and DigiDoc4j runtime configuration.
124+
- `SigningService`: prepares ASiC-E containers and finalizes signatures.
125+
- `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.
123126
- `web`: Spring Web MVC controller for the welcome page and Spring Web REST controller that provides a digital signing endpoint.
124127

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

178181
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.
179182

183+
### Requesting the signing certificate in a separate step
184+
185+
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.
186+
187+
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.
188+
189+
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.
190+
191+
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 **false**, the application explicitly requests the signing certificate during the signing process, demonstrating the separate signing certificate retrieval flow. When set to **true**, the signing uses the signing certificate that was already obtained during authentication, and no additional request is made.
192+
180193
## HTTPS support
181194

182195
There are two ways of adding HTTPS support to a Spring Boot application:

example/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@
3838
<groupId>org.springframework.boot</groupId>
3939
<artifactId>spring-boot-starter-thymeleaf</artifactId>
4040
</dependency>
41+
<dependency>
42+
<groupId>org.springframework.boot</groupId>
43+
<artifactId>spring-boot-starter-validation</artifactId>
44+
</dependency>
4145

4246
<dependency>
4347
<groupId>org.digidoc4j</groupId>

example/src/main/java/eu/webeid/example/config/ApplicationConfiguration.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import eu.webeid.example.security.WebEidMobileAuthInitFilter;
2929
import eu.webeid.example.security.ui.WebEidLoginPageGeneratingFilter;
3030
import eu.webeid.security.challenge.ChallengeNonceGenerator;
31+
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
3132
import org.springframework.context.annotation.Bean;
3233
import org.springframework.context.annotation.Configuration;
3334
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
@@ -39,9 +40,9 @@
3940
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
4041
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
4142
import org.thymeleaf.ITemplateEngine;
42-
import org.thymeleaf.web.servlet.JakartaServletWebApplication;
4343

4444
@Configuration
45+
@ConfigurationPropertiesScan
4546
@EnableWebSecurity
4647
@EnableMethodSecurity(securedEnabled = true)
4748
public class ApplicationConfiguration {
@@ -53,7 +54,7 @@ public SecurityFilterChain filterChain(
5354
AuthenticationConfiguration authConfig,
5455
ChallengeNonceGenerator challengeNonceGenerator,
5556
ITemplateEngine templateEngine,
56-
JakartaServletWebApplication webApp
57+
WebEidMobileProperties webEidMobileProperties
5758
) throws Exception {
5859
return http
5960
.authorizeHttpRequests(auth -> auth
@@ -62,9 +63,9 @@ public SecurityFilterChain filterChain(
6263
.anyRequest().authenticated()
6364
)
6465
.authenticationProvider(webEidAuthenticationProvider)
65-
.addFilterBefore(new WebEidMobileAuthInitFilter("/auth/mobile/init", "/auth/mobile/login", challengeNonceGenerator), UsernamePasswordAuthenticationFilter.class)
66+
.addFilterBefore(new WebEidMobileAuthInitFilter("/auth/mobile/init", "/auth/mobile/login", challengeNonceGenerator, webEidMobileProperties), UsernamePasswordAuthenticationFilter.class)
6667
.addFilterBefore(new WebEidChallengeNonceFilter("/auth/challenge", challengeNonceGenerator), UsernamePasswordAuthenticationFilter.class)
67-
.addFilterBefore(new WebEidLoginPageGeneratingFilter("/auth/mobile/login", "/auth/login", templateEngine, webApp), UsernamePasswordAuthenticationFilter.class)
68+
.addFilterBefore(new WebEidLoginPageGeneratingFilter("/auth/mobile/login", "/auth/login", templateEngine), UsernamePasswordAuthenticationFilter.class)
6869
.addFilterBefore(new WebEidAjaxLoginProcessingFilter("/auth/login", authConfig.getAuthenticationManager()), UsernamePasswordAuthenticationFilter.class)
6970
.logout(l -> l.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()))
7071
.headers(h -> h.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))

example/src/main/java/eu/webeid/example/config/ValidationConfiguration.java

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -79,24 +79,19 @@ public ChallengeNonceGenerator generator(ChallengeNonceStore challengeNonceStore
7979
}
8080

8181
@Bean
82-
public AuthTokenValidator validator(YAMLConfig yamlConfig) {
82+
public AuthTokenValidator validator(WebEidAuthTokenProperties authTokenProperties) {
8383
try {
8484
return new AuthTokenValidatorBuilder()
85-
.withSiteOrigin(URI.create(yamlConfig.getLocalOrigin()))
85+
.withSiteOrigin(URI.create(authTokenProperties.validation().localOrigin()))
8686
.withTrustedCertificateAuthorities(loadTrustedCACertificatesFromCerFiles())
87-
.withTrustedCertificateAuthorities(loadTrustedCACertificatesFromTrustStore(yamlConfig))
88-
.withOcspRequestTimeout(yamlConfig.getOcspRequestTimeout())
87+
.withTrustedCertificateAuthorities(loadTrustedCACertificatesFromTrustStore(authTokenProperties))
88+
.withOcspRequestTimeout(authTokenProperties.validation().ocspRequestTimeout())
8989
.build();
9090
} catch (JceException e) {
9191
throw new RuntimeException("Error building the Web eID auth token validator.", e);
9292
}
9393
}
9494

95-
@Bean
96-
public YAMLConfig yamlConfig() {
97-
return new YAMLConfig();
98-
}
99-
10095
private X509Certificate[] loadTrustedCACertificatesFromCerFiles() {
10196
List<X509Certificate> caCertificates = new ArrayList<>();
10297

@@ -117,17 +112,22 @@ private X509Certificate[] loadTrustedCACertificatesFromCerFiles() {
117112

118113
return caCertificates.toArray(new X509Certificate[0]);
119114
}
120-
121-
private X509Certificate[] loadTrustedCACertificatesFromTrustStore(YAMLConfig yamlConfig) {
115+
private X509Certificate[] loadTrustedCACertificatesFromTrustStore(WebEidAuthTokenProperties authTokenProperties) {
122116
List<X509Certificate> caCertificates = new ArrayList<>();
123117

124118
try (InputStream is = ValidationConfiguration.class.getResourceAsStream(CERTS_RESOURCE_PATH + activeProfile + "/" + TRUSTED_CERTIFICATES_JKS)) {
125119
if (is == null) {
126120
LOG.info("Truststore file {} not found for {} profile", TRUSTED_CERTIFICATES_JKS, activeProfile);
127121
return new X509Certificate[0];
128122
}
123+
124+
String trustStorePassword = authTokenProperties.validation().trustStorePassword();
125+
if (trustStorePassword == null || trustStorePassword.isBlank()) {
126+
throw new IllegalStateException("Truststore password must be configured because truststore exists for profile '" + activeProfile + "'.");
127+
}
128+
129129
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
130-
keystore.load(is, yamlConfig.getTrustStorePassword().toCharArray());
130+
keystore.load(is, trustStorePassword.toCharArray());
131131
Enumeration<String> aliases = keystore.aliases();
132132
while (aliases.hasMoreElements()) {
133133
String alias = aliases.nextElement();
@@ -140,7 +140,4 @@ private X509Certificate[] loadTrustedCACertificatesFromTrustStore(YAMLConfig yam
140140

141141
return caCertificates.toArray(new X509Certificate[0]);
142142
}
143-
144-
145-
146143
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright (c) 2020-2025 Estonian Information System Authority
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in all
12+
* copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
* SOFTWARE.
21+
*/
22+
23+
package eu.webeid.example.config;
24+
25+
import jakarta.validation.constraints.NotBlank;
26+
import jakarta.validation.constraints.NotNull;
27+
import jakarta.validation.constraints.Pattern;
28+
import org.springframework.boot.context.properties.ConfigurationProperties;
29+
import org.springframework.boot.context.properties.bind.DefaultValue;
30+
import org.springframework.validation.annotation.Validated;
31+
32+
import java.time.Duration;
33+
34+
@Validated
35+
@ConfigurationProperties(prefix = "web-eid-auth-token")
36+
public record WebEidAuthTokenProperties(WebEidAuthTokenValidation validation) {
37+
38+
public record WebEidAuthTokenValidation(
39+
@NotBlank
40+
@Pattern(
41+
regexp = "^https://[A-Za-z0-9.-]+(:\\d{1,5})?$",
42+
message = "Origin URL must be in the form of 'https://' <hostname> [ ':' <port> ] and not end with a trailing slash"
43+
)
44+
String localOrigin,
45+
String siteCertHash,
46+
String trustStorePassword,
47+
@DefaultValue("5s") Duration ocspRequestTimeout,
48+
@NotNull Boolean useDigiDoc4jProdConfiguration) {
49+
}
50+
}

example/src/main/java/eu/webeid/example/config/ThymeleafWebAppConfiguration.java renamed to example/src/main/java/eu/webeid/example/config/WebEidMobileProperties.java

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,14 @@
2222

2323
package eu.webeid.example.config;
2424

25-
import jakarta.servlet.ServletContext;
26-
import org.springframework.context.annotation.Bean;
27-
import org.springframework.context.annotation.Configuration;
28-
import org.thymeleaf.web.servlet.JakartaServletWebApplication;
25+
import jakarta.validation.constraints.NotBlank;
26+
import jakarta.validation.constraints.Pattern;
27+
import org.springframework.boot.context.properties.ConfigurationProperties;
28+
import org.springframework.validation.annotation.Validated;
2929

30-
@Configuration
31-
public class ThymeleafWebAppConfiguration {
32-
33-
@Bean
34-
public JakartaServletWebApplication jakartaServletWebApplication(ServletContext servletContext) {
35-
return JakartaServletWebApplication.buildApplication(servletContext);
36-
}
30+
@Validated
31+
@ConfigurationProperties(prefix = "web-eid-mobile")
32+
public record WebEidMobileProperties(
33+
@NotBlank @Pattern(regexp = "^.*(?:[^/]|://)$", message = "Base URI must not have a trailing slash") String baseRequestUri,
34+
boolean requestSigningCert) {
3735
}

example/src/main/java/eu/webeid/example/config/YAMLConfig.java

Lines changed: 0 additions & 89 deletions
This file was deleted.

0 commit comments

Comments
 (0)