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
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ public SecurityFilterChain webEidPluginAndMobileSecurityFilterChain(
AuthenticationConfiguration authConfig,
ChallengeNonceGenerator challengeNonceGenerator,
ITemplateEngine templateEngine,
WebEidMobileProperties webEidMobileProperties
WebEidMobileProperties webEidMobileProperties,
WebEidAuthTokenProperties webEidAuthTokenProperties
) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
Expand All @@ -65,7 +66,7 @@ public SecurityFilterChain webEidPluginAndMobileSecurityFilterChain(
.anyRequest().authenticated()
)
.authenticationProvider(webEidAuthenticationProvider)
.addFilterBefore(new WebEidMobileAuthInitFilter("/auth/mobile/init", "/auth/mobile/login", challengeNonceGenerator, webEidMobileProperties), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new WebEidMobileAuthInitFilter("/auth/mobile/init", "/auth/mobile/login", challengeNonceGenerator, webEidMobileProperties, webEidAuthTokenProperties), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new WebEidChallengeNonceFilter("/auth/challenge", challengeNonceGenerator), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new WebEidLoginPageGeneratingFilter("/auth/mobile/login", "/auth/login", templateEngine), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new WebEidAjaxLoginProcessingFilter("/auth/login", authConfig.getAuthenticationManager()), UsernamePasswordAuthenticationFilter.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import eu.webeid.example.config.WebEidAuthTokenProperties;
import eu.webeid.example.config.WebEidMobileProperties;
import eu.webeid.security.challenge.ChallengeNonceGenerator;
import jakarta.servlet.FilterChain;
Expand All @@ -51,12 +52,15 @@ public final class WebEidMobileAuthInitFilter extends OncePerRequestFilter {
private final ChallengeNonceGenerator nonceGenerator;
private final String mobileLoginPath;
private final WebEidMobileProperties webEidMobileProperties;
private final WebEidAuthTokenProperties webEidAuthTokenProperties;

public WebEidMobileAuthInitFilter(String path, String mobileLoginPath, ChallengeNonceGenerator nonceGenerator, WebEidMobileProperties webEidMobileProperties) {
public WebEidMobileAuthInitFilter(String path, String mobileLoginPath, ChallengeNonceGenerator nonceGenerator,
WebEidMobileProperties webEidMobileProperties, WebEidAuthTokenProperties webEidAuthTokenProperties) {
this.requestMatcher = PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, path);
this.nonceGenerator = nonceGenerator;
this.mobileLoginPath = mobileLoginPath;
this.webEidMobileProperties = webEidMobileProperties;
this.webEidAuthTokenProperties = webEidAuthTokenProperties;
}

@Override
Expand All @@ -70,8 +74,11 @@ protected void doFilterInternal(@NonNull HttpServletRequest request,

var challenge = nonceGenerator.generateAndStoreNonce();

String loginUri = ServletUriComponentsBuilder.fromCurrentContextPath()

@mrts mrts Jun 16, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without .fromCurrentContextPath() deploying the example under a non-root context path will no longer work. If this is intended, it should be documented in README.

.path(mobileLoginPath).build().toUriString();
String loginUri = UriComponentsBuilder
.fromUriString(webEidAuthTokenProperties.validation().localOrigin())
.path(mobileLoginPath)
.build()
.toUriString();

String payloadJson = OBJECT_WRITER.writeValueAsString(
new AuthPayload(challenge.getBase64EncodedNonce(), loginUri,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import eu.webeid.example.config.WebEidAuthTokenProperties;
import eu.webeid.example.config.WebEidMobileProperties;
import eu.webeid.example.security.WebEidAuthentication;
import eu.webeid.example.service.dto.CertificateDTO;
Expand Down Expand Up @@ -54,10 +55,13 @@ public class MobileSigningService {
private static final ObjectWriter OBJECT_WRITER = new ObjectMapper().writerFor(RequestObject.class);
private final SigningService signingService;
private final WebEidMobileProperties webEidMobileProperties;
private final WebEidAuthTokenProperties webEidAuthTokenProperties;

public MobileSigningService(SigningService signingService, WebEidMobileProperties webEidMobileProperties) {
public MobileSigningService(SigningService signingService, WebEidMobileProperties webEidMobileProperties,
WebEidAuthTokenProperties webEidAuthTokenProperties) {
this.signingService = signingService;
this.webEidMobileProperties = webEidMobileProperties;
this.webEidAuthTokenProperties = webEidAuthTokenProperties;
}

public MobileInitRequest initCertificateOrSigningRequest(WebEidAuthentication authentication) throws IOException, CertificateException, NoSuchAlgorithmException {
Expand All @@ -75,7 +79,8 @@ public MobileInitRequest initCertificateOrSigningRequest(WebEidAuthentication au
public MobileInitRequest initSigningRequest(WebEidAuthentication authentication, CertificateDTO certificateDTO) throws IOException, CertificateException, NoSuchAlgorithmException {
Objects.requireNonNull(authentication, "authentication must not be null");
Objects.requireNonNull(certificateDTO, "certificateDTO must not be null");
final String responseUri = ServletUriComponentsBuilder.fromCurrentContextPath()
final String responseUri = UriComponentsBuilder
.fromUriString(webEidAuthTokenProperties.validation().localOrigin())
.path(SIGNATURE_RESPONSE_PATH)
.build()
.toUriString();
Expand All @@ -95,7 +100,8 @@ public MobileInitRequest initSigningRequest(WebEidAuthentication authentication,
}

private MobileInitRequest initCertificateRequest() throws IOException {
final String responseUri = ServletUriComponentsBuilder.fromCurrentContextPath()
final String responseUri = UriComponentsBuilder
.fromUriString(webEidAuthTokenProperties.validation().localOrigin())
.path(CERTIFICATE_RESPONSE_PATH)
.build()
.toUriString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,13 @@
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;

import javax.security.auth.x500.X500Principal;
import java.security.cert.CertPath;
import java.security.cert.CertPathValidator;
import java.security.cert.CertStore;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.PKIXParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
Expand All @@ -55,7 +59,7 @@

class AuthTokenVersion11Validator extends AuthTokenVersion1Validator implements AuthTokenVersionValidator {

private static final String V11_SUPPORTED_TOKEN_FORMAT_PREFIX = "web-eid:1.1";
private static final Set<String> V11_SUPPORTED_TOKEN_FORMAT = Set.of("web-eid:1.1");
private static final Set<String> SUPPORTED_SIGNING_CRYPTO_ALGORITHMS = Set.of("ECC", "RSA");
private static final Set<String> SUPPORTED_SIGNING_PADDING_SCHEMES = Set.of("NONE", "PKCS1.5", "PSS");
private static final Set<String> SUPPORTED_SIGNING_HASH_FUNCTIONS = Set.of(
Expand All @@ -64,6 +68,9 @@ class AuthTokenVersion11Validator extends AuthTokenVersion1Validator implements
);
private static final int KEY_USAGE_NON_REPUDIATION = 1;

private final Set<TrustAnchor> trustedCACertificateAnchors;
private final CertStore trustedCACertificateCertStore;

public AuthTokenVersion11Validator(
SubjectCertificateValidatorBatch simpleSubjectCertificateValidators,
Set<TrustAnchor> trustedCACertificateAnchors,
Expand All @@ -82,11 +89,13 @@ public AuthTokenVersion11Validator(
ocspClient,
ocspServiceProvider
);
this.trustedCACertificateAnchors = trustedCACertificateAnchors;
this.trustedCACertificateCertStore = trustedCACertificateCertStore;
}

@Override
protected String getSupportedFormatPrefix() {
return V11_SUPPORTED_TOKEN_FORMAT_PREFIX;
public boolean supports(String format) {
return format != null && V11_SUPPORTED_TOKEN_FORMAT.contains(format);
}

@Override
Expand All @@ -98,6 +107,7 @@ public X509Certificate validate(WebEidAuthToken token, String currentChallengeNo
validateSameIssuer(subjectCertificate, signingCertificate);
validateSigningCertificateValidity(signingCertificate);
validateKeyUsage(signingCertificate);
validateSigningCertificateChain(signingCertificate);
}

return subjectCertificate;
Expand Down Expand Up @@ -184,6 +194,24 @@ private static void validateKeyUsage(X509Certificate signingCertificate)
}
}

private void validateSigningCertificateChain(X509Certificate signingCertificate)
throws AuthTokenParseException {
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");

CertPath certPath = certificateFactory.generateCertPath(List.of(signingCertificate));

PKIXParameters parameters = new PKIXParameters(trustedCACertificateAnchors);
parameters.addCertStore(trustedCACertificateCertStore);
parameters.setRevocationEnabled(false);

CertPathValidator validator = CertPathValidator.getInstance("PKIX");
validator.validate(certPath, parameters);
} catch (Exception e) {
throw new AuthTokenParseException("Signing certificate chain validation failed", e);
}
}

private static boolean subjectAndSigningCertificateSubjectsMatch(
X500Principal authenticationCertificateSubject,
X500Principal signingCertificateSubject) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
class AuthTokenVersion1Validator implements AuthTokenVersionValidator {

private static final String V1_SUPPORTED_TOKEN_FORMAT_PREFIX = "web-eid:1";

private static final Set<String> SUPPORTED_TOKEN_FORMATS = Set.of(V1_SUPPORTED_TOKEN_FORMAT_PREFIX, "web-eid:1.0");
private final SubjectCertificateValidatorBatch simpleSubjectCertificateValidators;
private final Set<TrustAnchor> trustedCACertificateAnchors;
private final CertStore trustedCACertificateCertStore;
Expand Down Expand Up @@ -69,11 +69,7 @@ public AuthTokenVersion1Validator(

@Override
public boolean supports(String format) {
return format != null && format.startsWith(getSupportedFormatPrefix());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As documented in README.md, minor versions must be backward-compatible within the major version, this will reject future compatible web-eid:1.x tokens.

}

protected String getSupportedFormatPrefix() {
return V1_SUPPORTED_TOKEN_FORMAT_PREFIX;
return format != null && SUPPORTED_TOKEN_FORMATS.contains(format);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

package eu.webeid.security.validator.versionvalidators;

import eu.webeid.security.authtoken.SupportedSignatureAlgorithm;
import eu.webeid.security.authtoken.WebEidAuthToken;
import eu.webeid.security.certificate.CertificateLoader;
import eu.webeid.security.authtoken.UnverifiedSigningCertificate;
Expand All @@ -31,6 +32,7 @@
import eu.webeid.security.validator.certvalidators.SubjectCertificateValidatorBatch;
import eu.webeid.security.validator.ocsp.OcspClient;
import eu.webeid.security.validator.ocsp.OcspServiceProvider;
import org.bouncycastle.asn1.x509.Extension;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
Expand All @@ -39,6 +41,7 @@
import org.mockito.MockedStatic;
import org.mockito.Mockito;

import javax.security.auth.x500.X500Principal;
import java.security.cert.CertStore;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
Expand Down Expand Up @@ -79,14 +82,14 @@ void setUp() {
}

@ParameterizedTest
@ValueSource(strings = {"web-eid:1.1", "web-eid:1.1.0", "web-eid:1.10"})
@ValueSource(strings = {"web-eid:1.1"})
void whenFormatIsV11OrPrefixedVariant_thenSupportsReturnsTrue(String format) {
assertThat(validator.supports(format)).isTrue();
}

@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {"web-eid:1", "web-eid:1.0", "web-eid:2", "webauthn:1.1"})
@ValueSource(strings = {"web-eid:1", "web-eid:1.0", "web-eid:1.1.0", "web-eid:1.10", "web-eid:2", "webauthn:1.1"})
void whenFormatIsNullEmptyOrNotV11_thenSupportsReturnsFalse(String format) {
assertThat(validator.supports(format)).isFalse();
}
Expand Down Expand Up @@ -156,4 +159,51 @@ void whenSupportedSignatureAlgorithmsMissing_thenValidationFails() throws Except
.hasMessage("'supportedSignatureAlgorithms' field is missing");
}
}

@Test
void whenSigningCertificateChainValidationFails_thenValidationFails() throws Exception {
WebEidAuthToken token = mock(WebEidAuthToken.class);
when(token.getFormat()).thenReturn("web-eid:1.1");

SupportedSignatureAlgorithm algorithm = new SupportedSignatureAlgorithm();
algorithm.setCryptoAlgorithm("RSA");
algorithm.setHashFunction("SHA-256");
algorithm.setPaddingScheme("PKCS1.5");

UnverifiedSigningCertificate certificate = new UnverifiedSigningCertificate();
certificate.setCertificate("abc");
certificate.setSupportedSignatureAlgorithms(Collections.singletonList(algorithm));

when(token.getUnverifiedSigningCertificates()).thenReturn(Collections.singletonList(certificate));

X500Principal subject = new X500Principal("CN=TEST");
byte[] authorityKeyIdentifier = new byte[] {
0x04, 0x18, 0x30, 0x16, (byte) 0x80, 0x14,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 19, 20
};

X509Certificate subjectCertificate = mock(X509Certificate.class);
when(subjectCertificate.getSubjectX500Principal()).thenReturn(subject);
when(subjectCertificate.getExtensionValue(Extension.authorityKeyIdentifier.getId()))
.thenReturn(authorityKeyIdentifier);

X509Certificate signingCertificate = mock(X509Certificate.class);
when(signingCertificate.getSubjectX500Principal()).thenReturn(subject);
when(signingCertificate.getExtensionValue(Extension.authorityKeyIdentifier.getId()))
.thenReturn(authorityKeyIdentifier);
when(signingCertificate.getKeyUsage()).thenReturn(new boolean[] {false, true});

AuthTokenVersion11Validator spyValidator = Mockito.spy(validator);
doReturn(subjectCertificate).when(spyValidator).validateV1(any(), any());

try (MockedStatic<CertificateLoader> mocked = mockStatic(CertificateLoader.class)) {
mocked.when(() -> CertificateLoader.decodeCertificateFromBase64("abc"))
.thenReturn(signingCertificate);

assertThatThrownBy(() -> spyValidator.validate(token, "nonce"))
.isInstanceOf(AuthTokenParseException.class)
.hasMessage("Signing certificate chain validation failed");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,14 @@ class AuthTokenVersion1ValidatorTest {
);

@ParameterizedTest
@ValueSource(strings = {"web-eid:1", "web-eid:1.0", "web-eid:1.1", "web-eid:1.10"})
void whenFormatIsAnyMajorV1Variant_thenSupportsReturnsTrue(String format) {
@ValueSource(strings = {"web-eid:1", "web-eid:1.0"})
void whenFormatIsV1OrV10_thenSupportsReturnsTrue(String format) {
assertThat(validator.supports(format)).isTrue();
}

@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {"web-eid", "web-eid:0.9", "web-eid:2", "webauthn:1"})
@ValueSource(strings = {"web-eid", "web-eid:1.1", "web-eid:1.10", "web-eid:0.9", "web-eid:2", "webauthn:1"})
void whenFormatIsNullEmptyOrNotV1_thenSupportsReturnsFalse(String format) {
assertThat(validator.supports(format)).isFalse();
}
Expand Down
Loading