Skip to content

Commit f7e26a3

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

18 files changed

+458
-170
lines changed

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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@
2222

2323
package eu.webeid.example.config;
2424

25-
import eu.webeid.example.security.AuthTokenDTOAuthenticationProvider;
2625
import eu.webeid.example.security.WebEidAjaxLoginProcessingFilter;
2726
import eu.webeid.example.security.WebEidAuthenticationProvider;
2827
import eu.webeid.example.security.WebEidChallengeNonceFilter;
2928
import eu.webeid.example.security.WebEidMobileAuthInitFilter;
3029
import eu.webeid.example.security.ui.WebEidLoginPageGeneratingFilter;
3130
import eu.webeid.security.challenge.ChallengeNonceGenerator;
31+
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
3232
import org.springframework.context.annotation.Bean;
3333
import org.springframework.context.annotation.Configuration;
3434
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
@@ -43,6 +43,7 @@
4343
import org.thymeleaf.web.servlet.JakartaServletWebApplication;
4444

4545
@Configuration
46+
@ConfigurationPropertiesScan
4647
@EnableWebSecurity
4748
@EnableMethodSecurity(securedEnabled = true)
4849
public class ApplicationConfiguration {

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

Lines changed: 6 additions & 11 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

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

121-
private X509Certificate[] loadTrustedCACertificatesFromTrustStore(YAMLConfig yamlConfig) {
116+
private X509Certificate[] loadTrustedCACertificatesFromTrustStore(WebEidAuthTokenProperties authTokenProperties) {
122117
List<X509Certificate> caCertificates = new ArrayList<>();
123118

124119
try (InputStream is = ValidationConfiguration.class.getResourceAsStream(CERTS_RESOURCE_PATH + activeProfile + "/" + TRUSTED_CERTIFICATES_JKS)) {
@@ -127,7 +122,7 @@ private X509Certificate[] loadTrustedCACertificatesFromTrustStore(YAMLConfig yam
127122
return new X509Certificate[0];
128123
}
129124
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
130-
keystore.load(is, yamlConfig.getTrustStorePassword().toCharArray());
125+
keystore.load(is, authTokenProperties.validation().trustStorePassword().toCharArray());
131126
Enumeration<String> aliases = keystore.aliases();
132127
while (aliases.hasMoreElements()) {
133128
String alias = aliases.nextElement();
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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 org.springframework.boot.context.properties.ConfigurationProperties;
28+
import org.springframework.boot.context.properties.bind.DefaultValue;
29+
import org.springframework.validation.annotation.Validated;
30+
31+
import java.time.Duration;
32+
33+
@Validated
34+
@ConfigurationProperties(prefix = "web-eid-auth-token")
35+
public record WebEidAuthTokenProperties(WebEidAuthTokenValidation validation) {
36+
37+
public record WebEidAuthTokenValidation(
38+
@NotBlank String localOrigin,
39+
String siteCertHash,
40+
@NotBlank String trustStorePassword,
41+
@DefaultValue("5s") Duration ocspRequestTimeout,
42+
@NotNull Boolean useDigiDoc4jProdConfiguration) {
43+
}
44+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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 org.springframework.boot.context.properties.ConfigurationProperties;
27+
import org.springframework.validation.annotation.Validated;
28+
29+
@Validated
30+
@ConfigurationProperties(prefix = "web-eid-mobile")
31+
public record WebEidMobileProperties(
32+
@NotBlank String baseRequestUri,
33+
boolean requestSigningCert) {
34+
}

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

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

example/src/main/java/eu/webeid/example/security/WebEidAuthentication.java

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222

2323
package eu.webeid.example.security;
2424

25+
import eu.webeid.security.authtoken.SupportedSignatureAlgorithm;
2526
import eu.webeid.security.certificate.CertificateData;
27+
import org.springframework.lang.Nullable;
2628
import org.springframework.security.core.Authentication;
2729
import org.springframework.security.core.GrantedAuthority;
2830
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
@@ -36,21 +38,21 @@
3638
public class WebEidAuthentication extends PreAuthenticatedAuthenticationToken implements Authentication {
3739

3840
private final String idCode;
41+
private final transient String signingCertificate;
42+
private final transient List<SupportedSignatureAlgorithm> supportedSignatureAlgorithms;
3943

40-
public static Authentication fromCertificate(X509Certificate userCertificate, List<GrantedAuthority> authorities) throws CertificateEncodingException {
41-
final String principalName = getPrincipalNameFromCertificate(userCertificate);
42-
final String idCode = CertificateData.getSubjectIdCode(userCertificate)
43-
.orElseThrow(() -> new CertificateEncodingException("Certificate does not contain subject ID code"));
44-
return new WebEidAuthentication(principalName, idCode, authorities);
45-
}
46-
47-
public String getIdCode() {
48-
return idCode;
49-
}
50-
51-
private WebEidAuthentication(String principalName, String idCode, List<GrantedAuthority> authorities) {
44+
private WebEidAuthentication(String principalName, String idCode, String signingCertificate, List<SupportedSignatureAlgorithm> supportedSignatureAlgorithms, List<GrantedAuthority> authorities) {
5245
super(principalName, idCode, authorities);
5346
this.idCode = idCode;
47+
this.signingCertificate = signingCertificate;
48+
this.supportedSignatureAlgorithms = supportedSignatureAlgorithms;
49+
}
50+
51+
public static Authentication fromCertificate(X509Certificate userCertificate, @Nullable String signingCertificate, @Nullable List<SupportedSignatureAlgorithm> supportedSignatureAlgorithms, List<GrantedAuthority> authorities) throws CertificateEncodingException {
52+
final String principalName = getPrincipalNameFromCertificate(userCertificate);
53+
final String idCode = CertificateData.getSubjectIdCode(userCertificate)
54+
.orElseThrow(() -> new CertificateEncodingException("Certificate does not contain subject ID code"));
55+
return new WebEidAuthentication(principalName, idCode, signingCertificate, supportedSignatureAlgorithms, authorities);
5456
}
5557

5658
private static String getPrincipalNameFromCertificate(X509Certificate userCertificate) throws CertificateEncodingException {
@@ -62,10 +64,22 @@ private static String getPrincipalNameFromCertificate(X509Certificate userCertif
6264
} else {
6365
// Organization certificates do not have given name and surname fields.
6466
return CertificateData.getSubjectCN(userCertificate)
65-
.orElseThrow(() -> new CertificateEncodingException("Certificate does not contain subject CN"));
67+
.orElseThrow(() -> new CertificateEncodingException("Certificate does not contain subject CN"));
6668
}
6769
}
6870

71+
public String getIdCode() {
72+
return idCode;
73+
}
74+
75+
public String getSigningCertificate() {
76+
return signingCertificate;
77+
}
78+
79+
public List<SupportedSignatureAlgorithm> getSupportedSignatureAlgorithms() {
80+
return supportedSignatureAlgorithms;
81+
}
82+
6983
@Override
7084
public boolean equals(Object o) {
7185
if (!super.equals(o)) return false;

example/src/main/java/eu/webeid/example/security/WebEidAuthenticationProvider.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
package eu.webeid.example.security;
2424

25+
import eu.webeid.example.config.WebEidMobileProperties;
26+
import eu.webeid.security.authtoken.SupportedSignatureAlgorithm;
2527
import eu.webeid.security.authtoken.WebEidAuthToken;
2628
import eu.webeid.security.challenge.ChallengeNonceStore;
2729
import eu.webeid.security.exceptions.AuthTokenException;
@@ -41,6 +43,7 @@
4143
import java.security.cert.X509Certificate;
4244
import java.util.Collections;
4345
import java.util.List;
46+
import java.util.Optional;
4447

4548
/**
4649
* Parses JWT from token string inside AuthTokenDTO and attempts authentication.
@@ -54,10 +57,12 @@ public class WebEidAuthenticationProvider implements AuthenticationProvider {
5457

5558
private final AuthTokenValidator tokenValidator;
5659
private final ChallengeNonceStore challengeNonceStore;
60+
private final WebEidMobileProperties webEidMobileProperties;
5761

58-
public WebEidAuthenticationProvider(AuthTokenValidator tokenValidator, ChallengeNonceStore challengeNonceStore) {
62+
public WebEidAuthenticationProvider(AuthTokenValidator tokenValidator, ChallengeNonceStore challengeNonceStore, WebEidMobileProperties webEidMobileProperties) {
5963
this.tokenValidator = tokenValidator;
6064
this.challengeNonceStore = challengeNonceStore;
65+
this.webEidMobileProperties = webEidMobileProperties;
6166
}
6267

6368
@Override
@@ -72,7 +77,19 @@ public Authentication authenticate(Authentication auth) throws AuthenticationExc
7277
try {
7378
final String nonce = challengeNonceStore.getAndRemove().getBase64EncodedNonce();
7479
final X509Certificate userCertificate = tokenValidator.validate(authToken, nonce);
75-
return WebEidAuthentication.fromCertificate(userCertificate, authorities);
80+
final String signingCertificate = Optional.ofNullable(authToken)
81+
.map(WebEidAuthToken::getUnverifiedSigningCertificate)
82+
.orElse(null);
83+
final List<SupportedSignatureAlgorithm> supportedSignatureAlgorithms = Optional.ofNullable(authToken)
84+
.map(WebEidAuthToken::getSupportedSignatureAlgorithms)
85+
.orElse(null);
86+
87+
if (webEidMobileProperties.requestSigningCert()) {
88+
LOG.info("request-signing-cert=true -> Skipping signing certificate in authentication (demo mode)");
89+
return WebEidAuthentication.fromCertificate(userCertificate, null, null, authorities);
90+
}
91+
92+
return WebEidAuthentication.fromCertificate(userCertificate, signingCertificate, supportedSignatureAlgorithms, authorities);
7693
} catch (AuthTokenException e) {
7794
throw new AuthenticationServiceException("Web eID token validation failed", e);
7895
} catch (CertificateEncodingException e) {

0 commit comments

Comments
 (0)