diff --git a/.github/workflows/coverity-analysis.yml b/.github/workflows/coverity-analysis.yml index 2e8e2285..9f3758c1 100644 --- a/.github/workflows/coverity-analysis.yml +++ b/.github/workflows/coverity-analysis.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/setup-java@v4 with: distribution: zulu - java-version: 11 + java-version: 17 - name: Cache Maven packages uses: actions/cache@v4 diff --git a/.github/workflows/maven-build.yml b/.github/workflows/maven-build.yml index 850ad585..a89b2777 100644 --- a/.github/workflows/maven-build.yml +++ b/.github/workflows/maven-build.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/setup-java@v4 with: distribution: zulu - java-version: 11 + java-version: 17 - name: Cache Maven packages uses: actions/cache@v4 diff --git a/.github/workflows/maven-deploy.yml b/.github/workflows/maven-deploy.yml index d2c3af53..573938af 100644 --- a/.github/workflows/maven-deploy.yml +++ b/.github/workflows/maven-deploy.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/setup-java@v4 with: distribution: zulu - java-version: 11 + java-version: 17 - name: Cache Maven packages uses: actions/cache@v4 diff --git a/README.md b/README.md index e66db524..f4c5105a 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ More information about the Web eID project is available on the project [website] Complete the steps below to add support for secure authentication with eID cards to your Java web application back end. Instructions for the front end are available [here](https://github.com/web-eid/web-eid.js). -A Java web application that uses Maven or Gradle to manage packages is needed for running this quickstart. Examples are for Maven, but they are straightforward to translate to Gradle. +A Java 17 or newer web application that uses Maven or Gradle to manage packages is needed for running this quickstart. Examples are for Maven, but they are straightforward to translate to Gradle. In the following example we are using the [Spring Framework](https://spring.io/), but the examples can be easily ported to other Java web application frameworks. @@ -48,7 +48,7 @@ Implement the session-backed challenge nonce store as follows: import org.springframework.beans.factory.ObjectFactory; import eu.webeid.security.challenge.ChallengeNonce; import eu.webeid.security.challenge.ChallengeNonceStore; -import javax.servlet.http.HttpSession; +import jakarta.servlet.http.HttpSession; public class SessionBackedChallengeNonceStore implements ChallengeNonceStore { @@ -99,16 +99,18 @@ import eu.webeid.security.challenge.ChallengeNonceStore; ## 4. Add trusted certificate authority certificates -You must explicitly specify which **intermediate** certificate authorities (CAs) are trusted to issue the eID authentication and OCSP responder certificates. CA certificates can be loaded from either the truststore file, resources or any stream source. We use the [`CertificateLoader`](https://github.com/web-eid/web-eid-authtoken-validation-java/blob/main/src/main/java/eu/webeid/security/certificate/CertificateLoader.java) helper class to load CA certificates from resources here, but consider using [the truststore file](./blob/example/main/src/main/java/eu/webeid/example/config/ValidationConfiguration.java#L104-L123) instead. +You must explicitly specify which **intermediate** certificate authorities (CAs) are trusted to issue the eID authentication and OCSP responder certificates. CA certificates can be loaded from either the truststore file, resources or any stream source. We use the [`CertificateLoader`](src/main/java/eu/webeid/security/certificate/CertificateLoader.java) helper class to load CA certificates from resources here, but consider using [the truststore file](example/src/main/java/eu/webeid/example/config/ValidationConfiguration.java) instead. First, copy the trusted certificates, for example `ESTEID2018.cer`, to `resources/cacerts/`, then load the certificates as follows: ```java +import java.io.IOException; +import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import eu.webeid.security.certificate.CertificateLoader; ... - private X509Certificate[] trustedIntermediateCACertificates() { + private X509Certificate[] trustedIntermediateCACertificates() throws CertificateException, IOException { return CertificateLoader.loadCertificatesFromResources("cacerts/ESTEID2018.cer"); } ... @@ -120,14 +122,20 @@ Once the prerequisites have been met, the authentication token validator itself The mandatory parameters are the website origin (the URL serving the web application, see section [_Basic usage_](#basic-usage) below) and trusted certificate authorities. The authentication token validator will be used in the login processing component of your web application authentication framework; it is thread-safe and should be scoped as a singleton. +Certificate revocation checking is enabled automatically using the platform OCSP implementation. No additional OCSP configuration is needed for normal use. + ```java +import java.io.IOException; +import java.net.URI; +import java.security.cert.CertificateException; +import eu.webeid.security.exceptions.JceException; import eu.webeid.security.validator.AuthTokenValidator; import eu.webeid.security.validator.AuthTokenValidatorBuilder; ... - public AuthTokenValidator tokenValidator() throws JceException { + public AuthTokenValidator tokenValidator() throws JceException, CertificateException, IOException { return new AuthTokenValidatorBuilder() - .withSiteOrigin("https://example.org") + .withSiteOrigin(URI.create("https://example.org")) .withTrustedCertificateAuthorities(trustedIntermediateCACertificates()) .build(); } @@ -146,7 +154,7 @@ For internationalized domain names, configure the Punycode form, for example A REST endpoint that issues challenge nonces is required for authentication. The endpoint must support `GET` requests. -In the following example, we are using the [Spring RESTful Web Services framework](https://spring.io/guides/gs/rest-service/) to implement the endpoint, see also the full implementation [here](example/blob/main/src/main/java/eu/webeid/example/web/rest/ChallengeController.java). +In the following example, we are using the [Spring RESTful Web Services framework](https://spring.io/guides/gs/rest-service/) to implement the endpoint, see also the full implementation [here](example/src/main/java/eu/webeid/example/web/rest/ChallengeController.java). ```java import org.springframework.web.bind.annotation.GetMapping; @@ -180,19 +188,22 @@ Authentication consists of calling the `validate()` method of the authentication When using [Spring Security](https://spring.io/guides/topicals/spring-security-architecture) with standard cookie-based authentication, -- implement a custom authentication provider that uses the authentication token validator for authentication as shown [here](example/blob/main/src/main/java/eu/webeid/example/security/AuthTokenDTOAuthenticationProvider.java), -- implement an AJAX authentication processing filter that extracts the authentication token and passes it to the authentication manager as shown [here](example/blob/main/src/main/java/eu/webeid/example/security/WebEidAjaxLoginProcessingFilter.java), -- configure the authentication provider and authentication processing filter in the application configuration as shown [here](example/blob/main/src/main/java/eu/webeid/example/config/ApplicationConfiguration.java). +- implement a custom authentication provider that uses the authentication token validator for authentication as shown [here](example/src/main/java/eu/webeid/example/security/AuthTokenDTOAuthenticationProvider.java), +- implement an AJAX authentication processing filter that extracts the authentication token and passes it to the authentication manager as shown [here](example/src/main/java/eu/webeid/example/security/WebEidAjaxLoginProcessingFilter.java), +- configure the authentication provider and authentication processing filter in the application configuration as shown [here](example/src/main/java/eu/webeid/example/config/ApplicationConfiguration.java). -The gist of the validation is [in the `authenticate()` method](example/blob/main/src/main/java/eu/webeid/example/security/AuthTokenDTOAuthenticationProvider.java#L74-L76) of the authentication provider: +The gist of the validation is [in the `authenticate()` method](example/src/main/java/eu/webeid/example/security/AuthTokenDTOAuthenticationProvider.java) of the authentication provider: ```java try { String nonce = challengeNonceStore.getAndRemove().getBase64EncodedNonce(); - X509Certificate userCertificate = tokenValidator.validate(authToken, nonce); - return WebEidAuthentication.fromCertificate(userCertificate, authorities); + ValidationInfo validationInfo = tokenValidator.validate(authToken, nonce); + return WebEidAuthentication.fromCertificate(validationInfo.subjectCertificate(), authorities); } catch (AuthTokenException e) { - ... + throw new AuthenticationServiceException("Web eID token validation failed", e); +} catch (CertificateEncodingException e) { + throw new AuthenticationServiceException("Invalid certificate subject fields", e); +} ``` # Table of contents @@ -203,7 +214,7 @@ try { - [Authentication token validation](#authentication-token-validation) - [Basic usage](#basic-usage) - [Extended configuration](#extended-configuration) - - [Certificates' Authority Information Access (AIA) extension](#certificates-authority-information-access-aia-extension) + - [Advanced OCSP configuration](src/main/java/eu/webeid/ocsp/README.md) - [Possible validation errors](#possible-validation-errors) - [Stateful and stateless authentication](#stateful-and-stateless-authentication) - [Challenge nonce generation](#challenge-nonce-generation) @@ -281,13 +292,18 @@ The authentication token validator configuration and construction is described i ```java String challengeNonce = challengeNonceStore.getAndRemove().getBase64EncodedNonce(); WebEidAuthToken token = tokenValidator.parse(tokenString); -X509Certificate userCertificate = tokenValidator.validate(token, challengeNonce); +ValidationInfo validationInfo = tokenValidator.validate(token, challengeNonce); +X509Certificate userCertificate = validationInfo.subjectCertificate(); ``` -The `validate()` method returns the validated user certificate object if validation is successful or throws an exception as described in section *[Possible validation errors](#possible-validation-errors)* below if validation fails. The `CertificateData` and `TitleCase` classes can be used for extracting user information from the user certificate object: +The `validate()` method returns a `ValidationInfo` object on success. Use `subjectCertificate()` to obtain the validated certificate. Validation failures throw an exception, as described in [Possible validation errors](#possible-validation-errors). + +Additional revocation information is available to custom integrations through `revocationInfoList()`; see the [OCSP guide](src/main/java/eu/webeid/ocsp/README.md#revocation-information). + +The `CertificateData` and `Strings` classes provide helpers for extracting and formatting user information: ```java -import eu.webeid.security.certificate; +import eu.webeid.security.certificate.CertificateData; import static eu.webeid.security.util.Strings.toTitleCase; ... @@ -300,42 +316,19 @@ toTitleCase(CertificateData.getSubjectGivenName(userCertificate).orElseThrow()); toTitleCase(CertificateData.getSubjectSurname(userCertificate).orElseThrow()); // "Jõeorg" ``` -## Extended configuration +## Extended configuration -The following additional configuration options are available in `AuthTokenValidatorBuilder`: +The default validator uses the platform OCSP implementation to check certificate revocation. A revoked certificate or an unsuccessful revocation check causes authentication to fail. -- `withoutUserCertificateRevocationCheckWithOcsp()` – turns off user certificate revocation check with OCSP. OCSP check is enabled by default and the OCSP responder access location URL is extracted from the user certificate AIA extension unless a designated OCSP service is activated. -- `withDesignatedOcspServiceConfiguration(DesignatedOcspServiceConfiguration serviceConfiguration)` – activates the provided designated OCSP responder service configuration for user certificate revocation check with OCSP. The designated service is only used for checking the status of the certificates whose issuers are supported by the service, for other certificates the default AIA extension service access location will be used. See configuration examples in `testutil.OcspServiceMaker.getDesignatedOcspServiceConfiguration()`. -- `withOcspClient(OcspClient ocspClient)` - uses the provided OCSP client instance during user certificate revocation check with OCSP. The provided client instance must be thread-safe. This gives the possibility to configure the request timeouts, proxies etc of the `HttpClient` instance or provide an implementation that uses an altogether different HTTP client. See examples in `OcspClientOverrideTest`. -- `withOcspRequestTimeout(Duration ocspRequestTimeout)` – sets both the connection and response timeout of user certificate revocation check OCSP requests. Default is 5 seconds. -- `withDisallowedCertificatePolicies(ASN1ObjectIdentifier... policies)` – adds the given policies to the list of disallowed user certificate policies. In order for the user certificate to be considered valid, it must not contain any policies present in this list. Contains the Estonian Mobile-ID policies by default as it must not be possible to authenticate with a Mobile-ID certificate when an eID smart card is expected. -- `withNonceDisabledOcspUrls(URI... urls)` – adds the given URLs to the list of OCSP responder access location URLs for which the nonce protocol extension will be disabled. Some OCSP responders don't support the nonce extension. -- `withAllowedOcspResponseTimeSkew(Duration allowedTimeSkew)` – sets the allowed time skew for OCSP response's `thisUpdate` and `nextUpdate` times to allow discrepancies between the system clock and the OCSP responder's clock or revocation updates that are not published in real time. The default allowed time skew is 15 minutes. The relatively long default is specifically chosen to account for one particular OCSP responder that used CRLs for authoritative revocation info, these CRLs were updated every 15 minutes. -- `withMaxOcspResponseThisUpdateAge(Duration maxThisUpdateAge)` – sets the maximum age for the OCSP response's `thisUpdate` time before it is considered too old to rely on. The default maximum age is 2 minutes. +Use `withDisallowedCertificatePolicies(ASN1ObjectIdentifier... policies)` to add disallowed certificate policies. Estonian Mobile-ID policies are disallowed by default because smart-card authentication must not accept Mobile-ID certificates. -Extended configuration example: +For more advanced revocation requirements, supply a `CertificateRevocationChecker` with `withCertificateRevocationChecker(...)`. The [OCSP configuration guide](src/main/java/eu/webeid/ocsp/README.md) covers custom implementations, the bundled OCSP checker, custom PKIX checkers, responder selection, HTTP settings, and nonce policies. -```java -AuthTokenValidator validator = new AuthTokenValidatorBuilder() - .withSiteOrigin("https://example.org") - .withTrustedCertificateAuthorities(trustedCertificateAuthorities()) - .withoutUserCertificateRevocationCheckWithOcsp() - .withDisallowedCertificatePolicies(new ASN1ObjectIdentifier("1.2.3")) - .withNonceDisabledOcspUrls(URI.create("http://aia.example.org/cert")) - .withAllowedOcspResponseTimeSkew(Duration.ofMinutes(10)) - .withMaxOcspResponseThisUpdateAge(Duration.ofMinutes(5)) - .build(); -``` - -### Certificates' *Authority Information Access* (AIA) extension - -Unless a designated OCSP responder service is in use, it is required that the AIA extension that contains the certificate’s OCSP responder access location is present in the user certificate. The AIA OCSP URL will be used to check the certificate revocation status with OCSP. +## Possible validation errors -Note that there may be limitations to using AIA URLs as the services behind these URLs provide different security and SLA guarantees than dedicated OCSP responder services. In case you need a SLA guarantee, use a designated OCSP responder service. +Certificate and token validation failures are reported through `AuthTokenException` subclasses. `CertificateRevokedException` means the certificate is revoked; `CertificateRevocationCheckFailedException` means its status could not be established. Other failures are documented in the [exception classes](src/main/java/eu/webeid/security/exceptions/). -## Possible validation errors - -The `validate()` method of `AuthTokenValidator` returns the validated user certificate object if validation is successful or throws an exception if validation fails. All exceptions that can occur during validation derive from `AuthTokenException`, the list of available exceptions is available [here](src/main/java/eu/webeid/security/exceptions/). Each exception file contains a documentation comment that describes under which conditions the exception is thrown. +Log the exception itself, for example `LOG.warn("Web eID authentication failed", e)`, to preserve its cause chain. When wrapping it, retain the cause as shown in the authentication example above. Return a generic authentication failure to the client; keep diagnostic details in server logs. See the [OCSP diagnostics guide](src/main/java/eu/webeid/ocsp/README.md#errors-and-diagnostics) for revocation-specific details. ## Stateful and stateless authentication @@ -348,7 +341,11 @@ A common alternative to stateful authentication is stateless authentication with The authentication protocol requires support for generating challenge nonces, large random numbers that can be used only once, and storing them for later use during token validation. The validation library uses the *java.security.SecureRandom* API as the secure random source and the `ChallengeNonceStore` interface for storing issued challenge nonces. -The `-Djava.security.egd=file:/dev/./urandom` command line argument is added to `pom.xml` to avoid the risk of having the code execution blocked unexpectedly during random generation. Without this, the JVM uses `/dev/random`, which can block, to seed the `SecureRandom` class. +No additional JVM configuration is normally required. The selected random-number generator and its entropy source depend on the JDK, operating system and security-provider configuration; some implementations may block while gathering entropy. See the [JDK's `SecureRandom` documentation](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/security/SecureRandom.html). + +If nonce generation stalls, inspect the application's thread dump and selected `SecureRandom` implementation before changing its configuration. On Linux with the OpenJDK SUN provider, `-Djava.security.egd=file:/dev/urandom` can be supplied when starting the application to select `/dev/urandom` as the entropy source for implementations that honor this property. This setting affects the whole JVM. Alternatively, configure the challenge nonce generator with a suitable `SecureRandom` instance using `withSecureRandom(...)`. + +This repository's `pom.xml` supplies `-Djava.security.egd=file:/dev/urandom` to the test JVM. That test setting is not automatically applied to applications using the library. The authentication protocol requires a REST endpoint that issues challenge nonces as described in section *[6. Add a REST endpoint for issuing challenge nonces](#6-add-a-rest-endpoint-for-issuing-challenge-nonces)*. @@ -356,11 +353,11 @@ Nonce usage is described in more detail in the [Web eID system architecture docu ## Basic usage -As described in section *[3. Configure the nonce generator](#3-configure-the-nonce-generator)*, the only mandatory configuration parameter of the challenge nonce generator is the challenge nonce store. +As described in section *[3. Configure the challenge nonce generator](#3-configure-the-challenge-nonce-generator)*, the only mandatory configuration parameter of the challenge nonce generator is the challenge nonce store. The challenge nonce store is used to save the nonce value along with the nonce expiry time. It must be possible to look up the challenge nonce data structure from the store using an identifier specific to the browser session. The values from the store are used by the token validator as described in the section *[Authentication token validation > Basic usage](#basic-usage)* that also contains recommendations for store usage and configuration. -The nonce generator configuration and construction is described in more detail in section *[3. Configure the nonce generator](#3-configure-the-nonce-generator)*. Once the generator object has been constructed, it can be used for generating nonces as follows: +The nonce generator configuration and construction is described in more detail in section *[3. Configure the challenge nonce generator](#3-configure-the-challenge-nonce-generator)*. Once the generator object has been constructed, it can be used for generating nonces as follows: ```java ChallengeNonce challengeNonce = nonceGenerator.generateAndStoreNonce(); @@ -370,14 +367,14 @@ The `generateAndStoreNonce()` method both generates the nonce and saves it in th ## Extended configuration -The following additional configuration options are available in `NonceGeneratorBuilder`: +The following additional configuration options are available in `ChallengeNonceGeneratorBuilder`: - `withNonceTtl(Duration duration)` – overrides the default challenge nonce time-to-live duration. When the time-to-live passes, the nonce is considered to be expired. Default challenge nonce time-to-live is 5 minutes. - `withSecureRandom(SecureRandom)` - allows to specify a custom `SecureRandom` instance. Extended configuration example: ```java -NonceGenerator generator = new NonceGeneratorBuilder() +ChallengeNonceGenerator generator = new ChallengeNonceGeneratorBuilder() .withChallengeNonceStore(store) .withNonceTtl(Duration.ofMinutes(5)) .withSecureRandom(customSecureRandom) diff --git a/example/README.md b/example/README.md index b0b0e6a1..f4c9ebbe 100644 --- a/example/README.md +++ b/example/README.md @@ -142,7 +142,10 @@ The main configuration file `src/main/resources/application.yaml` is shared by a Besides configuration settings, the trusted certificate authority certificates may need to be configured as described in section [_3. Configure the trusted certificate authority certificates_](#3-configure-the-trusted-certificate-authority-certificates) above. -Spring Security has CSRF protection enabled by default. Web eID requires CSRF protection. +Spring Security has CSRF protection enabled by default. Web eID requires CSRF protection. By default, the frontend reads +CSRF tokens from Thymeleaf meta tags. Set `web-eid-auth-token.csrf.use-spa-configuration=true` to use Spring Security's +SPA-compatible CSRF setup with a JavaScript-readable `WEBEID-XSRF-TOKEN` cookie. +Set `web-eid-auth-token.csrf.cookie-domain` to configure the cookie Domain to one level higher than the local-origin. ### Integration with Web eID components diff --git a/example/pom.xml b/example/pom.xml index 59daad1b..6ba58bad 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -12,7 +12,7 @@ eu.webeid.example web-eid-springboot-example - 3.2.3 + 4.0.0-SNAPSHOT web-eid-springboot-example Example Spring Boot application that demonstrates how to use Web eID for authentication and digital signing @@ -21,10 +21,9 @@ 17 3.6.0 - 3.2.2 + 4.0.0-SNAPSHOT 6.2.0 1.86 - 1.44 3.5.2 3.6.3 @@ -83,12 +82,6 @@ spring-security-test test - - org.jmockit - jmockit - ${jmockit.version} - test - @@ -102,9 +95,6 @@ maven-surefire-plugin ${maven-surefire-plugin.version} - - -javaagent:${settings.localRepository}/org/jmockit/jmockit/${jmockit.version}/jmockit-${jmockit.version}.jar - true diff --git a/example/src/main/java/eu/webeid/example/config/ApplicationConfiguration.java b/example/src/main/java/eu/webeid/example/config/ApplicationConfiguration.java index d5d2b584..d42b19c0 100644 --- a/example/src/main/java/eu/webeid/example/config/ApplicationConfiguration.java +++ b/example/src/main/java/eu/webeid/example/config/ApplicationConfiguration.java @@ -5,39 +5,116 @@ import eu.webeid.example.security.AuthTokenDTOAuthenticationProvider; import eu.webeid.example.security.WebEidAjaxLoginProcessingFilter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; +import org.springframework.security.web.csrf.CsrfTokenRequestHandler; +import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler; +import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.util.function.Supplier; + @Configuration @EnableWebSecurity @EnableMethodSecurity(securedEnabled = true) public class ApplicationConfiguration implements WebMvcConfigurer { + private static final String CSRF_COOKIE_NAME = "WEBEID-XSRF-TOKEN"; + @Bean - public SecurityFilterChain filterChain(HttpSecurity http, AuthTokenDTOAuthenticationProvider authTokenDTOAuthenticationProvider, AuthenticationConfiguration authConfig) throws Exception { + public SecurityFilterChain filterChain( + HttpSecurity http, + AuthTokenDTOAuthenticationProvider authTokenDTOAuthenticationProvider, + AuthenticationConfiguration authConfig, + YAMLConfig validationConfig, + CsrfConfigurationProperties csrfConfig, + ServerProperties serverConfig + ) throws Exception { + if (csrfConfig.isUseSpaConfiguration()) { + http + .csrf(csrf -> csrf + .csrfTokenRepository(createDefaultCsrfTokenRepository(csrfConfig.getCookieDomain())) + .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())) + .cors(Customizer.withDefaults()); + } return http .authenticationProvider(authTokenDTOAuthenticationProvider) .addFilterBefore(new WebEidAjaxLoginProcessingFilter("/auth/login", authConfig.getAuthenticationManager()), UsernamePasswordAuthenticationFilter.class) - .logout(logout -> logout.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())) + .logout(logout -> logout + .deleteCookies(StringUtils.defaultIfBlank(serverConfig.getServlet().getSession().getCookie().getName(), "JSESSIONID")) + .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())) .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)) + .exceptionHandling(ex -> ex.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))) .build(); } + @ConditionalOnProperty(name = "web-eid-auth-token.csrf.use-spa-configuration", havingValue = "true") + @Bean + public WebMvcConfigurer corsConfigurer(YAMLConfig config) { + return new WebMvcConfigurer() { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins(config.getCorsAllowedOrigin()) + .allowCredentials(true); + } + }; + } + @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("index"); registry.addViewController("/welcome").setViewName("welcome"); } + private CookieCsrfTokenRepository createDefaultCsrfTokenRepository(String cookieDomain) { + CookieCsrfTokenRepository cookieCsrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse(); + cookieCsrfTokenRepository.setCookieName(CSRF_COOKIE_NAME); + cookieCsrfTokenRepository.setCookieCustomizer(cookie -> cookie + .domain(cookieDomain) + .sameSite("Lax") + ); + return cookieCsrfTokenRepository; + } + + private static final class SpaCsrfTokenRequestHandler implements CsrfTokenRequestHandler { + private final CsrfTokenRequestHandler plain = new CsrfTokenRequestAttributeHandler(); + private final CsrfTokenRequestHandler xor = new XorCsrfTokenRequestAttributeHandler(); + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, Supplier csrfToken) { + xor.handle(request, response, csrfToken); + csrfToken.get(); + } + + @Override + public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) { + String headerValue = request.getHeader(csrfToken.getHeaderName()); + return (StringUtils.isNotBlank(headerValue) ? plain : xor).resolveCsrfTokenValue(request, csrfToken); + } + } + } diff --git a/example/src/main/java/eu/webeid/example/config/CsrfConfigurationProperties.java b/example/src/main/java/eu/webeid/example/config/CsrfConfigurationProperties.java new file mode 100644 index 00000000..b19ecf4b --- /dev/null +++ b/example/src/main/java/eu/webeid/example/config/CsrfConfigurationProperties.java @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: Estonian Information System Authority +// SPDX-License-Identifier: MIT + +package eu.webeid.example.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "web-eid-auth-token.csrf") +public class CsrfConfigurationProperties { + + private boolean useSpaConfiguration; + private String cookieDomain; + + public boolean isUseSpaConfiguration() { + return useSpaConfiguration; + } + + public void setUseSpaConfiguration(boolean useSpaConfiguration) { + this.useSpaConfiguration = useSpaConfiguration; + } + + public String getCookieDomain() { + return cookieDomain; + } + + public void setCookieDomain(String cookieDomain) { + this.cookieDomain = cookieDomain; + } +} diff --git a/example/src/main/java/eu/webeid/example/config/ValidationConfiguration.java b/example/src/main/java/eu/webeid/example/config/ValidationConfiguration.java index 540d97f9..188d2df2 100644 --- a/example/src/main/java/eu/webeid/example/config/ValidationConfiguration.java +++ b/example/src/main/java/eu/webeid/example/config/ValidationConfiguration.java @@ -65,19 +65,13 @@ public AuthTokenValidator validator(YAMLConfig yamlConfig) { .withSiteOrigin(URI.create(yamlConfig.getLocalOrigin())) .withTrustedCertificateAuthorities(loadTrustedCACertificatesFromCerFiles()) .withTrustedCertificateAuthorities(loadTrustedCACertificatesFromTrustStore(yamlConfig)) - .withOcspRequestTimeout(yamlConfig.getOcspRequestTimeout()) .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() { + X509Certificate[] loadTrustedCACertificatesFromCerFiles() { List caCertificates = new ArrayList<>(); try { @@ -87,18 +81,19 @@ private X509Certificate[] loadTrustedCACertificatesFromCerFiles() { Resource[] resources = resolver.getResources(CERTS_RESOURCE_PATH + activeProfile + "/*.cer"); for (Resource resource : resources) { - X509Certificate caCertificate = (X509Certificate) certFactory.generateCertificate(resource.getInputStream()); - caCertificates.add(caCertificate); + try (InputStream stream = resource.getInputStream()) { + caCertificates.add((X509Certificate) certFactory.generateCertificate(stream)); + } } } catch (CertificateException | IOException e) { throw new RuntimeException("Error initializing trusted CA certificates.", e); } - return caCertificates.toArray(new X509Certificate[0]); + return caCertificates.toArray(X509Certificate[]::new); } - private X509Certificate[] loadTrustedCACertificatesFromTrustStore(YAMLConfig yamlConfig) { + X509Certificate[] loadTrustedCACertificatesFromTrustStore(YAMLConfig yamlConfig) { List caCertificates = new ArrayList<>(); try (InputStream is = ValidationConfiguration.class.getResourceAsStream(CERTS_RESOURCE_PATH + activeProfile + "/" + TRUSTED_CERTIFICATES_JKS)) { @@ -118,7 +113,7 @@ private X509Certificate[] loadTrustedCACertificatesFromTrustStore(YAMLConfig yam throw new RuntimeException("Error initializing trusted CA certificates from trust store.", e); } - return caCertificates.toArray(new X509Certificate[0]); + return caCertificates.toArray(X509Certificate[]::new); } diff --git a/example/src/main/java/eu/webeid/example/config/YAMLConfig.java b/example/src/main/java/eu/webeid/example/config/YAMLConfig.java index 4cf7c660..9aad60b4 100644 --- a/example/src/main/java/eu/webeid/example/config/YAMLConfig.java +++ b/example/src/main/java/eu/webeid/example/config/YAMLConfig.java @@ -3,7 +3,6 @@ package eu.webeid.example.config; -import java.time.Duration; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -17,14 +16,14 @@ public class YAMLConfig { @Value("local-origin") private String localOrigin; + private String corsAllowedOrigin; + @Value("site-cert-hash") private String siteCertHash; @Value("truststore-password") private String trustStorePassword; - private Duration ocspRequestTimeout = Duration.ofSeconds(5L); - @Value("#{new Boolean('${web-eid-auth-token.validation.use-digidoc4j-prod-configuration}'.trim())}") private Boolean useDigiDoc4jProdConfiguration; @@ -34,6 +33,11 @@ public String getLocalOrigin() { public void setLocalOrigin(String localOrigin) { this.localOrigin = localOrigin; + this.corsAllowedOrigin = localOrigin; + } + + public String getCorsAllowedOrigin() { + return corsAllowedOrigin; } public String getSiteCertHash() { @@ -59,12 +63,4 @@ public boolean getUseDigiDoc4jProdConfiguration() { public void setUseDigiDoc4jProdConfiguration(boolean useDigiDoc4jProdConfiguration) { this.useDigiDoc4jProdConfiguration = useDigiDoc4jProdConfiguration; } - - public Duration getOcspRequestTimeout() { - return ocspRequestTimeout; - } - - public void setOcspRequestTimeout(Duration ocspRequestTimeout) { - this.ocspRequestTimeout = ocspRequestTimeout; - } } diff --git a/example/src/main/java/eu/webeid/example/security/AuthTokenDTOAuthenticationProvider.java b/example/src/main/java/eu/webeid/example/security/AuthTokenDTOAuthenticationProvider.java index 7104b110..86ed45eb 100644 --- a/example/src/main/java/eu/webeid/example/security/AuthTokenDTOAuthenticationProvider.java +++ b/example/src/main/java/eu/webeid/example/security/AuthTokenDTOAuthenticationProvider.java @@ -8,6 +8,7 @@ import eu.webeid.security.challenge.ChallengeNonceStore; import eu.webeid.security.exceptions.AuthTokenException; import eu.webeid.security.validator.AuthTokenValidator; +import eu.webeid.security.validator.ValidationInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.authentication.AuthenticationProvider; @@ -20,12 +21,10 @@ import org.springframework.stereotype.Component; import java.security.cert.CertificateEncodingException; -import java.security.cert.X509Certificate; -import java.util.Collections; import java.util.List; /** - * Parses JWT from token string inside AuthTokenDTO and attempts authentication. + * Validates the Web eID authentication token supplied in AuthTokenDTO. */ @Component public class AuthTokenDTOAuthenticationProvider implements AuthenticationProvider { @@ -46,15 +45,17 @@ public AuthTokenDTOAuthenticationProvider(AuthTokenValidator tokenValidator, Cha public Authentication authenticate(Authentication auth) throws AuthenticationException { LOG.info("authenticate(): {}", auth); - final PreAuthenticatedAuthenticationToken authentication = (PreAuthenticatedAuthenticationToken) auth; - final WebEidAuthToken authToken = ((AuthTokenDTO) authentication.getCredentials()).getToken(); + if (!(auth.getCredentials() instanceof AuthTokenDTO credentials) || credentials.token() == null) { + throw new AuthenticationServiceException("Authentication token is missing"); + } + final WebEidAuthToken authToken = credentials.token(); - final List authorities = Collections.singletonList(USER_ROLE); + final List authorities = List.of(USER_ROLE); try { final String nonce = challengeNonceStore.getAndRemove().getBase64EncodedNonce(); - final X509Certificate userCertificate = tokenValidator.validate(authToken, nonce); - return WebEidAuthentication.fromCertificate(userCertificate, authorities); + final ValidationInfo validationInfo = tokenValidator.validate(authToken, nonce); + return WebEidAuthentication.fromCertificate(validationInfo.subjectCertificate(), authorities); } catch (AuthTokenException e) { throw new AuthenticationServiceException("Web eID token validation failed", e); } catch (CertificateEncodingException e) { diff --git a/example/src/main/java/eu/webeid/example/security/WebEidAjaxLoginProcessingFilter.java b/example/src/main/java/eu/webeid/example/security/WebEidAjaxLoginProcessingFilter.java index 2af96cfc..3f503c37 100644 --- a/example/src/main/java/eu/webeid/example/security/WebEidAjaxLoginProcessingFilter.java +++ b/example/src/main/java/eu/webeid/example/security/WebEidAjaxLoginProcessingFilter.java @@ -3,6 +3,7 @@ package eu.webeid.example.security; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import eu.webeid.example.security.ajax.AjaxAuthenticationFailureHandler; @@ -30,7 +31,7 @@ public class WebEidAjaxLoginProcessingFilter extends AbstractAuthenticationProcessingFilter { private static final Logger LOG = LoggerFactory.getLogger(WebEidAjaxLoginProcessingFilter.class); - private final ObjectReader OBJECT_READER = new ObjectMapper().readerFor(AuthTokenDTO.class); + private static final ObjectReader OBJECT_READER = new ObjectMapper().readerFor(AuthTokenDTO.class); private final SecurityContextRepository securityContextRepository; public WebEidAjaxLoginProcessingFilter( @@ -59,7 +60,15 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ } LOG.info("attemptAuthentication(): Reading request body"); - final AuthTokenDTO authTokenDTO = OBJECT_READER.readValue(request.getReader()); + final AuthTokenDTO authTokenDTO; + try { + authTokenDTO = OBJECT_READER.readValue(request.getReader()); + } catch (JsonProcessingException e) { + throw new AuthenticationServiceException("Invalid authentication request", e); + } + if (authTokenDTO == null || authTokenDTO.token() == null) { + throw new AuthenticationServiceException("Authentication token is missing"); + } LOG.info("attemptAuthentication(): Creating token"); final PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(null, authTokenDTO); LOG.info("attemptAuthentication(): Calling authentication manager"); diff --git a/example/src/main/java/eu/webeid/example/security/WebEidAuthentication.java b/example/src/main/java/eu/webeid/example/security/WebEidAuthentication.java index e83c16f6..dc23d944 100644 --- a/example/src/main/java/eu/webeid/example/security/WebEidAuthentication.java +++ b/example/src/main/java/eu/webeid/example/security/WebEidAuthentication.java @@ -49,9 +49,9 @@ private static String getPrincipalNameFromCertificate(X509Certificate userCertif @Override public boolean equals(Object o) { - if (!super.equals(o)) return false; - WebEidAuthentication that = (WebEidAuthentication) o; - return Objects.equals(idCode, that.idCode); + return o instanceof WebEidAuthentication that + && super.equals(that) + && Objects.equals(idCode, that.idCode); } @Override diff --git a/example/src/main/java/eu/webeid/example/security/ajax/AjaxAuthenticationFailureHandler.java b/example/src/main/java/eu/webeid/example/security/ajax/AjaxAuthenticationFailureHandler.java index 29061dab..5925b4c5 100644 --- a/example/src/main/java/eu/webeid/example/security/ajax/AjaxAuthenticationFailureHandler.java +++ b/example/src/main/java/eu/webeid/example/security/ajax/AjaxAuthenticationFailureHandler.java @@ -22,10 +22,7 @@ public class AjaxAuthenticationFailureHandler extends SimpleUrlAuthenticationFai public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException { final String message = AUTHENTICATION_FAILED + exception.getMessage(); - LOG.warn("onAuthenticationFailure(): exception {}, returning {} {}", - exception, - HttpServletResponse.SC_UNAUTHORIZED, - message); + LOG.warn("Authentication failed; returning HTTP 401", exception); final HttpSession session = request.getSession(false); if (session != null) { LOG.info("Invalidating session"); diff --git a/example/src/main/java/eu/webeid/example/security/ajax/AjaxAuthenticationSuccessHandler.java b/example/src/main/java/eu/webeid/example/security/ajax/AjaxAuthenticationSuccessHandler.java index 38ef6e16..cabcf48f 100644 --- a/example/src/main/java/eu/webeid/example/security/ajax/AjaxAuthenticationSuccessHandler.java +++ b/example/src/main/java/eu/webeid/example/security/ajax/AjaxAuthenticationSuccessHandler.java @@ -3,7 +3,6 @@ package eu.webeid.example.security.ajax; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; @@ -42,19 +41,11 @@ public void onAuthenticationSuccess( response.getWriter().write(AuthSuccessDTO.asJson(authentication)); } - public static class AuthSuccessDTO { + public record AuthSuccessDTO(String sub, String auth) { private static final ObjectWriter OBJECT_WRITER = new ObjectMapper().writerFor(AuthSuccessDTO.class); - @JsonProperty("sub") - private String sub; - - @JsonProperty("auth") - private String auth; - public static String asJson(Authentication authentication) throws JsonProcessingException { - final AuthSuccessDTO dto = new AuthSuccessDTO(); - dto.sub = authentication.getName(); - dto.auth = authentication.getAuthorities().toString(); + final AuthSuccessDTO dto = new AuthSuccessDTO(authentication.getName(), authentication.getAuthorities().toString()); return OBJECT_WRITER.writeValueAsString(dto); } } diff --git a/example/src/main/java/eu/webeid/example/security/dto/AuthTokenDTO.java b/example/src/main/java/eu/webeid/example/security/dto/AuthTokenDTO.java index 07dae682..78664488 100644 --- a/example/src/main/java/eu/webeid/example/security/dto/AuthTokenDTO.java +++ b/example/src/main/java/eu/webeid/example/security/dto/AuthTokenDTO.java @@ -6,15 +6,5 @@ import com.fasterxml.jackson.annotation.JsonProperty; import eu.webeid.security.authtoken.WebEidAuthToken; -public class AuthTokenDTO { - @JsonProperty("auth-token") - private WebEidAuthToken token; - - public WebEidAuthToken getToken() { - return token; - } - - public void setToken(WebEidAuthToken token) { - this.token = token; - } +public record AuthTokenDTO(@JsonProperty("auth-token") WebEidAuthToken token) { } diff --git a/example/src/main/java/eu/webeid/example/service/SigningService.java b/example/src/main/java/eu/webeid/example/service/SigningService.java index cd2b5a78..e2b3ad71 100644 --- a/example/src/main/java/eu/webeid/example/service/SigningService.java +++ b/example/src/main/java/eu/webeid/example/service/SigningService.java @@ -108,11 +108,7 @@ public DigestDTO prepareContainer(CertificateDTO certificateDTO, WebEidAuthentic final byte[] digest = signatureDigestAlgorithm.getDssDigestAlgorithm().getMessageDigest() .digest(dataToSign.getDataToSign()); - final DigestDTO digestDTO = new DigestDTO(); - digestDTO.setHash(DatatypeConverter.printBase64Binary(digest)); - digestDTO.setHashFunction(digestAlgorithmName); - - return digestDTO; + return new DigestDTO(DatatypeConverter.printBase64Binary(digest), digestAlgorithmName); } /** @@ -127,7 +123,7 @@ public FileDTO signContainer(SignatureDTO signatureDTO) { Container containerToSign = (Container) Objects.requireNonNull(currentSession().getAttribute(SESSION_ATTR_CONTAINER)); DataToSign dataToSign = (DataToSign) Objects.requireNonNull(currentSession().getAttribute(SESSION_ATTR_DATA)); - byte[] signatureBytes = DatatypeConverter.parseBase64Binary(signatureDTO.getBase64Signature()); + byte[] signatureBytes = DatatypeConverter.parseBase64Binary(signatureDTO.base64Signature()); Signature signature = dataToSign.finalize(signatureBytes); containerToSign.addSignature(signature); currentSession().setAttribute(SESSION_ATTR_CONTAINER, containerToSign); diff --git a/example/src/main/java/eu/webeid/example/service/dto/CertificateDTO.java b/example/src/main/java/eu/webeid/example/service/dto/CertificateDTO.java index 8bbf1ff8..1e36c6d9 100644 --- a/example/src/main/java/eu/webeid/example/service/dto/CertificateDTO.java +++ b/example/src/main/java/eu/webeid/example/service/dto/CertificateDTO.java @@ -3,15 +3,15 @@ package eu.webeid.example.service.dto; +import com.fasterxml.jackson.annotation.JsonProperty; + import java.io.ByteArrayInputStream; import java.io.InputStream; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; -import java.util.ArrayList; import java.util.Base64; import java.util.List; -import java.util.stream.Collectors; public class CertificateDTO { @@ -41,11 +41,12 @@ public X509Certificate toX509Certificate() throws CertificateException { return (X509Certificate) cf.generateCertificate(inStream); } + @JsonProperty(access = JsonProperty.Access.READ_ONLY) public List getSupportedHashFunctionNames() { - return supportedSignatureAlgorithms == null ? new ArrayList<>() : supportedSignatureAlgorithms + return supportedSignatureAlgorithms == null ? List.of() : supportedSignatureAlgorithms .stream() .map(SignatureAlgorithmDTO::getHashFunction) .distinct() - .collect(Collectors.toList()); + .toList(); } } diff --git a/example/src/main/java/eu/webeid/example/service/dto/ChallengeDTO.java b/example/src/main/java/eu/webeid/example/service/dto/ChallengeDTO.java index 53a2bd0e..c80ee496 100644 --- a/example/src/main/java/eu/webeid/example/service/dto/ChallengeDTO.java +++ b/example/src/main/java/eu/webeid/example/service/dto/ChallengeDTO.java @@ -3,14 +3,5 @@ package eu.webeid.example.service.dto; -public class ChallengeDTO { - private String nonce; - - public String getNonce() { - return nonce; - } - - public void setNonce(String nonce) { - this.nonce = nonce; - } +public record ChallengeDTO(String nonce) { } diff --git a/example/src/main/java/eu/webeid/example/service/dto/DigestDTO.java b/example/src/main/java/eu/webeid/example/service/dto/DigestDTO.java index 20f68dd2..0eb97c80 100644 --- a/example/src/main/java/eu/webeid/example/service/dto/DigestDTO.java +++ b/example/src/main/java/eu/webeid/example/service/dto/DigestDTO.java @@ -3,23 +3,5 @@ package eu.webeid.example.service.dto; -public class DigestDTO { - private String hash; - private String hashFunction; - - public String getHash() { - return hash; - } - - public void setHash(String hash) { - this.hash = hash; - } - - public String getHashFunction() { - return hashFunction; - } - - public void setHashFunction(String hashFunction) { - this.hashFunction = hashFunction; - } +public record DigestDTO(String hash, String hashFunction) { } diff --git a/example/src/main/java/eu/webeid/example/service/dto/FileDTO.java b/example/src/main/java/eu/webeid/example/service/dto/FileDTO.java index 44888758..d82f2060 100644 --- a/example/src/main/java/eu/webeid/example/service/dto/FileDTO.java +++ b/example/src/main/java/eu/webeid/example/service/dto/FileDTO.java @@ -8,10 +8,8 @@ import org.springframework.web.multipart.MultipartFile; import java.io.IOException; +import java.io.InputStream; import java.io.Serializable; -import java.net.URI; -import java.nio.file.Files; -import java.nio.file.Paths; import java.util.Objects; public class FileDTO implements Serializable { @@ -40,12 +38,9 @@ public static FileDTO fromMultipartFile(MultipartFile file) throws IOException { } public static FileDTO getExampleForSigningFromResources() throws IOException { - final URI resourceUri = new ClassPathResource("/static/files/" + EXAMPLE_FILENAME).getURI(); - return new FileDTO( - EXAMPLE_FILENAME, - MimeTypeUtils.TEXT_PLAIN_VALUE, - Files.readAllBytes(Paths.get(resourceUri)) - ); + try (InputStream stream = new ClassPathResource("/static/files/" + EXAMPLE_FILENAME).getInputStream()) { + return new FileDTO(EXAMPLE_FILENAME, MimeTypeUtils.TEXT_PLAIN_VALUE, stream.readAllBytes()); + } } public String getName() { diff --git a/example/src/main/java/eu/webeid/example/service/dto/SignatureAlgorithmDTO.java b/example/src/main/java/eu/webeid/example/service/dto/SignatureAlgorithmDTO.java index 1e5d2e4f..1df2e358 100644 --- a/example/src/main/java/eu/webeid/example/service/dto/SignatureAlgorithmDTO.java +++ b/example/src/main/java/eu/webeid/example/service/dto/SignatureAlgorithmDTO.java @@ -3,24 +3,22 @@ package eu.webeid.example.service.dto; -import java.util.Arrays; -import java.util.HashSet; import java.util.Set; public class SignatureAlgorithmDTO { // See https://github.com/web-eid/web-eid-app/blob/main/src/controller/command-handlers/signauthutils.cpp#L121-L127 - private static final Set SUPPORTED_CRYPTO_ALGOS = new HashSet<>(Arrays.asList( + private static final Set SUPPORTED_CRYPTO_ALGOS = Set.of( "ECC", "RSA" - )); - private static final Set SUPPORTED_PADDING_SCHEMES = new HashSet<>(Arrays.asList( + ); + private static final Set SUPPORTED_PADDING_SCHEMES = Set.of( "NONE", "PKCS1.5", "PSS" - )); + ); // See https://github.com/web-eid/libelectronic-id/tree/main/src/electronic-id.cpp#L131 - private static final Set SUPPORTED_HASH_FUNCTIONS = new HashSet<>(Arrays.asList( + private static final Set SUPPORTED_HASH_FUNCTIONS = Set.of( "SHA-224", "SHA-256", "SHA-384", "SHA-512", "SHA3-224", "SHA3-256", "SHA3-384", "SHA3-512" - )); + ); private String cryptoAlgorithm; @@ -33,7 +31,7 @@ public String getCryptoAlgorithm() { } public void setCryptoAlgorithm(String cryptoAlgorithm) { - if (!SUPPORTED_CRYPTO_ALGOS.contains(cryptoAlgorithm)) { + if (cryptoAlgorithm == null || !SUPPORTED_CRYPTO_ALGOS.contains(cryptoAlgorithm)) { throw new IllegalArgumentException("The provided crypto algorithm is not supported"); } this.cryptoAlgorithm = cryptoAlgorithm; @@ -44,7 +42,7 @@ public String getHashFunction() { } public void setHashFunction(String hashFunction) { - if (!SUPPORTED_HASH_FUNCTIONS.contains(hashFunction)) { + if (hashFunction == null || !SUPPORTED_HASH_FUNCTIONS.contains(hashFunction)) { throw new IllegalArgumentException("The provided hash function is not supported"); } this.hashFunction = hashFunction; @@ -55,7 +53,7 @@ public String getPaddingScheme() { } public void setPaddingScheme(String paddingScheme) { - if (!SUPPORTED_PADDING_SCHEMES.contains(paddingScheme)) { + if (paddingScheme == null || !SUPPORTED_PADDING_SCHEMES.contains(paddingScheme)) { throw new IllegalArgumentException("The provided padding scheme is not supported"); } this.paddingScheme = paddingScheme; diff --git a/example/src/main/java/eu/webeid/example/service/dto/SignatureDTO.java b/example/src/main/java/eu/webeid/example/service/dto/SignatureDTO.java index 03b641b0..2953c176 100644 --- a/example/src/main/java/eu/webeid/example/service/dto/SignatureDTO.java +++ b/example/src/main/java/eu/webeid/example/service/dto/SignatureDTO.java @@ -7,16 +7,5 @@ import com.fasterxml.jackson.annotation.JsonProperty; @JsonIgnoreProperties(ignoreUnknown = true) -public class SignatureDTO { - - @JsonProperty("signature") - private String base64Signature; - - public String getBase64Signature() { - return base64Signature; - } - - public void setBase64Signature(String base64Signature) { - this.base64Signature = base64Signature; - } +public record SignatureDTO(@JsonProperty("signature") String base64Signature) { } diff --git a/example/src/main/java/eu/webeid/example/web/rest/ChallengeController.java b/example/src/main/java/eu/webeid/example/web/rest/ChallengeController.java index be3a4a42..6789b8fc 100644 --- a/example/src/main/java/eu/webeid/example/web/rest/ChallengeController.java +++ b/example/src/main/java/eu/webeid/example/web/rest/ChallengeController.java @@ -3,8 +3,12 @@ package eu.webeid.example.web.rest; +import com.fasterxml.jackson.core.JsonProcessingException; +import eu.webeid.example.security.ajax.AjaxAuthenticationSuccessHandler.AuthSuccessDTO; import eu.webeid.example.service.dto.ChallengeDTO; import eu.webeid.security.challenge.ChallengeNonceGenerator; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -21,8 +25,12 @@ public ChallengeController(ChallengeNonceGenerator challengeNonceGenerator) { @GetMapping("challenge") public ChallengeDTO challenge() { - final ChallengeDTO challenge = new ChallengeDTO(); - challenge.setNonce(challengeNonceGenerator.generateAndStoreNonce().getBase64EncodedNonce()); - return challenge; + return new ChallengeDTO(challengeNonceGenerator.generateAndStoreNonce().getBase64EncodedNonce()); + } + + @PreAuthorize("isAuthenticated()") + @GetMapping("user") + public String user(Authentication authentication) throws JsonProcessingException { + return AuthSuccessDTO.asJson(authentication); } } diff --git a/example/src/main/resources/application.yaml b/example/src/main/resources/application.yaml index 89329c6c..44c0217c 100644 --- a/example/src/main/resources/application.yaml +++ b/example/src/main/resources/application.yaml @@ -18,5 +18,6 @@ server: logging: level: eu.webeid.security: DEBUG + eu.webeid.ocsp: DEBUG eu.webeid.example: DEBUG org.springframework.security.web.csrf.CsrfFilter: DEBUG diff --git a/example/src/main/resources/static/js/csrf.js b/example/src/main/resources/static/js/csrf.js new file mode 100644 index 00000000..3a9c2dd4 --- /dev/null +++ b/example/src/main/resources/static/js/csrf.js @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: Estonian Information System Authority +// SPDX-License-Identifier: MIT + +"use strict"; + +const CSRF_COOKIE_NAME = "WEBEID-XSRF-TOKEN"; +const CSRF_COOKIE_HEADER_NAME = "X-XSRF-TOKEN"; + +export function csrfHeader() { + const metaToken = document.querySelector("#csrftoken")?.content; + const metaHeaderName = document.querySelector("#csrfheadername")?.content; + if (metaToken && metaHeaderName) { + return {[metaHeaderName]: metaToken}; + } + + const cookieToken = getCookie(CSRF_COOKIE_NAME); + if (cookieToken) { + return {[CSRF_COOKIE_HEADER_NAME]: cookieToken}; + } + + return {}; +} + +function getCookie(name) { + const encodedName = encodeURIComponent(name) + "="; + return document.cookie + .split(";") + .map(cookie => cookie.trim()) + .filter(cookie => cookie.startsWith(encodedName)) + .map(cookie => decodeCookieValue(cookie.substring(encodedName.length))) + .shift(); +} + +function decodeCookieValue(value) { + try { + return decodeURIComponent(value); + } catch { + return value; + } +} diff --git a/example/src/main/resources/templates/index.html b/example/src/main/resources/templates/index.html index d8acaf78..9c35681f 100644 --- a/example/src/main/resources/templates/index.html +++ b/example/src/main/resources/templates/index.html @@ -6,8 +6,10 @@ - - + + + + Web eID: electronic ID smart cards on the Web For developers