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
14 changes: 5 additions & 9 deletions .github/workflows/maven-build-example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ on:
- 'example/**'
- '.github/workflows/*example*'

defaults:
run:
working-directory: ./example

jobs:
build:
runs-on: ubuntu-latest
Expand All @@ -33,9 +29,9 @@ jobs:
key: ${{ runner.os }}-m2-v17-${{ secrets.CACHE_VERSION }}-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2-v17-${{ secrets.CACHE_VERSION }}

- name: Build
run: mvn --batch-mode compile

- name: Test and package
run: mvn --batch-mode package
- name: Install library
run: mvn -B -ntp install

- name: Build example project
working-directory: ./example
run: mvn -B -ntp package
94 changes: 72 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -134,36 +134,86 @@ import eu.webeid.security.validator.AuthTokenValidatorBuilder;
...
```

## 6. Add a REST endpoint for issuing challenge nonces
## 6. Add a filter for issuing challenge nonces

A REST endpoint that issues challenge nonces is required for authentication. The endpoint must support `GET` requests.
Request Filters that issue challenge nonces for regular Web eID and Web eID for Mobile authentication flows are required for authentication.
The filters must support POST 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).
The `WebEidChallengeNonceFilter` handles `/auth/challenge` requests and issues a new nonce for regular Web eID authentication flow.
See the full implementation [here](example/src/main/java/eu/webeid/example/security/WebEidChallengeNonceFilter.java).

```java
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import eu.webeid.security.challenge.ChallengeNonceGenerator;
...
public final class WebEidChallengeNonceFilter extends OncePerRequestFilter {
private static final ObjectWriter OBJECT_WRITER = new ObjectMapper().writer();
private final RequestMatcher requestMatcher;
private final ChallengeNonceGenerator nonceGenerator;

public WebEidChallengeNonceFilter(String path, ChallengeNonceGenerator nonceGenerator) {
this.requestMatcher = PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, path);
this.nonceGenerator = nonceGenerator;
}

@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain chain
) throws ServletException, IOException {
if (!requestMatcher.matches(request)) {
chain.doFilter(request, response);
return;
}

var dto = new ChallengeDTO(nonceGenerator.generateAndStoreNonce().getBase64EncodedNonce());

response.setContentType(MediaType.APPLICATION_JSON_VALUE);
OBJECT_WRITER.writeValue(response.getWriter(), dto);
}

@RestController
@RequestMapping("auth")
public class ChallengeController {
public record ChallengeDTO(String nonce) {}
}
```

@Autowired // for brevity, prefer constructor dependency injection
private ChallengeNonceGenerator nonceGenerator;
Similarly, the `WebEidMobileAuthInitFilter` handles `/auth/mobile/init` requests for Web eID for Mobile authentication flow by generating a challenge nonce and returning a deep link URI. This deep link contains both the challenge nonce and a login URI for the mobile authentication flow.
See the full implementation [here](example/src/main/java/eu/webeid/example/security/WebEidMobileAuthInitFilter.java).

@GetMapping("challenge")
public ChallengeDTO challenge() {
// a simple DTO with a single 'nonce' field
final ChallengeDTO challenge = new ChallengeDTO();
challenge.setNonce(nonceGenerator.generateAndStoreNonce().getBase64EncodedNonce());
return challenge;
```java
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain chain) throws IOException, ServletException {
if (!requestMatcher.matches(request)) {
chain.doFilter(request, response);
return;
}

var challenge = nonceGenerator.generateAndStoreNonce();

String loginUri = ServletUriComponentsBuilder.fromCurrentContextPath()
.path(mobileLoginPath).build().toUriString();

String payloadJson = OBJECT_WRITER.writeValueAsString(
new AuthPayload(challenge.getBase64EncodedNonce(), loginUri,
webEidMobileProperties.requestSigningCert() ? Boolean.TRUE : null)
);
String encoded = Base64.getEncoder().encodeToString(payloadJson.getBytes(StandardCharsets.UTF_8));
String authUri = getAuthUri(encoded);

response.setContentType(MediaType.APPLICATION_JSON_VALUE);
OBJECT_WRITER.writeValue(response.getWriter(), new AuthUri(authUri));
}
```

Both filters are registered in the Spring Security filter chain in ApplicationConfiguration
See the full implementation [here](example/src/main/java/eu/webeid/example/config/ApplicationConfiguration.java):
```java
http
.addFilterBefore(new WebEidMobileAuthInitFilter("/auth/mobile/init", "/auth/mobile/login", challengeNonceGenerator, webEidMobileProperties),
UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new WebEidChallengeNonceFilter("/auth/challenge", challengeNonceGenerator),
UsernamePasswordAuthenticationFilter.class)
```

Also, see general guidelines for implementing secure authentication services [here](https://github.com/SK-EID/smart-id-documentation/wiki/Secure-Implementation-Guide).

## 7. Implement authentication
Expand All @@ -172,11 +222,11 @@ 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 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/WebEidAuthenticationProvider.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).

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/blob/main/src/main/java/eu/webeid/example/security/WebEidAuthenticationProvider.java#L74-L76) of the authentication provider:

```java
try {
Expand Down
28 changes: 22 additions & 6 deletions example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ When the application has started, open the _ngrok_ HTTPS URL in your preferred w
- [Using DigiDoc4j in production mode with the `prod` profile](#using-digidoc4j-in-production-mode-with-the-prod-profile)
+ [Stateful and stateless authentication](#stateful-and-stateless-authentication)
+ [Assuring that the signing and authentication certificate subjects match](#assuring-that-the-signing-and-authentication-certificate-subjects-match)
+ [Requesting the signing certificate in a separate step](#requesting-the-signing-certificate-in-a-separate-step)
* [HTTPS support](#https-support)
+ [How to verify that HTTPS is configured properly](#how-to-verify-that-https-is-configured-properly)
* [Deployment](#deployment)
Expand All @@ -100,7 +101,8 @@ This repository contains the code of a minimal Spring Boot web application that
- Spring Security,
- the Web eID authentication token validation library [_web-eid-authtoken-validation-java_](https://github.com/web-eid/web-eid-authtoken-validation-java),
- the Web eID JavaScript library [_web-eid.js_](https://github.com/web-eid/web-eid.js),
- the digital signing library [_DigiDoc4j_](https://github.com/open-eid/digidoc4j).
- the digital signing library [_DigiDoc4j_](https://github.com/open-eid/digidoc4j),
- the Android application [_MOPP-Android_](https://github.com/open-eid/MOPP-Android/).

The project uses Maven for managing the dependencies and building the application. Maven project configuration file `pom.xml` is in the root of the project.

Expand All @@ -113,11 +115,15 @@ The source code folder `src` contains the application source code and resources
The `src/main/java/eu/webeid/example` directory contains the Spring Boot application Java class and the following subdirectories:

- `config`: Spring and HTTP security configuration, Web eID authentication token validation library configuration, trusted CA certificates loading etc,
- `security`: Web eID authentication token validation library integration with Spring Security via an `AuthenticationProvider` and `AuthenticationProcessingFilter`,
- `service`: Web eID signing service implementation that uses DigiDoc4j, and DigiDoc4j runtime configuration,
- `web`: Spring Web MVC controller for the welcome page and Spring Web REST controllers that provide endpoints
- for getting the challenge nonce used by the authentication token validation library,
- for digital signing.
- `security`: Web eID authentication token validation library integration with Spring Security
- `AuthenticationProvider` and `AuthenticationProcessingFilter` for handling Web eID authentication tokens,
- `WebEidChallengeNonceFilter` for issuing the challenge nonce required by the authentication flow,
- `WebEidMobileAuthInitFilter` for issuing the challenge nonce and generating the deep link with the authentication request, used to initiate the mobile authentication flow,
- `WebEidAjaxLoginProcessingFilter` and `WebEidLoginPageGeneratingFilter` for handling login requests.
- `service`: Web eID signing service implementation that uses DigiDoc4j, and DigiDoc4j runtime configuration.
- `SigningService`: prepares ASiC-E containers and finalizes signatures.
- `MobileSigningService`: orchestrates the mobile signing flow (builds mobile signing requests/responses) and supports requesting the signing certificate in a separate step when enabled by configuration.
- `web`: Spring Web MVC controller for the welcome page and Spring Web REST controller that provides a digital signing endpoint.

The `src/resources` directory contains the resources used by the application:

Expand Down Expand Up @@ -174,6 +180,16 @@ A common alternative to stateful authentication is stateless authentication with

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

### Requesting the signing certificate in a separate step

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

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

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

This behavior is controlled by the `request-signing-cert` flag in the `application.yaml` configuration files (`application-dev.yaml`, `application-prod.yaml`). When the flag is set to **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.

## HTTPS support

There are two ways of adding HTTPS support to a Spring Boot application:
Expand Down
8 changes: 6 additions & 2 deletions example/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</parent>
<groupId>eu.webeid.example</groupId>
<artifactId>web-eid-springboot-example</artifactId>
<version>3.2.0</version>
<version>4.0.0-SNAPSHOT</version>
<name>web-eid-springboot-example</name>
<description>Example Spring Boot application that demonstrates how to use Web eID for authentication and digital
signing
Expand All @@ -19,7 +19,7 @@
<properties>
<java.version>17</java.version>
<maven-surefire-plugin.version>3.5.3</maven-surefire-plugin.version>
<webeid.version>3.2.0</webeid.version>
<webeid.version>4.0.0-SNAPSHOT</webeid.version>
<digidoc4j.version>6.0.1</digidoc4j.version>
<jmockit.version>1.44</jmockit.version> <!-- Keep version 1.44, otherwise mocking will fail. -->
<jib.version>3.4.6</jib.version>
Expand All @@ -38,6 +38,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<dependency>
<groupId>org.digidoc4j</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,13 @@

package eu.webeid.example.config;

import eu.webeid.example.security.AuthTokenDTOAuthenticationProvider;
import eu.webeid.example.security.WebEidAjaxLoginProcessingFilter;
import eu.webeid.example.security.WebEidAuthenticationProvider;
import eu.webeid.example.security.WebEidChallengeNonceFilter;
import eu.webeid.example.security.WebEidMobileAuthInitFilter;
import eu.webeid.example.security.ui.WebEidLoginPageGeneratingFilter;
import eu.webeid.security.challenge.ChallengeNonceGenerator;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
Expand All @@ -34,29 +39,36 @@
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.thymeleaf.ITemplateEngine;

@Configuration
@ConfigurationPropertiesScan
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true)
public class ApplicationConfiguration implements WebMvcConfigurer {
public class ApplicationConfiguration {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http, AuthTokenDTOAuthenticationProvider authTokenDTOAuthenticationProvider, AuthenticationConfiguration authConfig) throws Exception {
public SecurityFilterChain filterChain(
HttpSecurity http,
WebEidAuthenticationProvider webEidAuthenticationProvider,
AuthenticationConfiguration authConfig,
ChallengeNonceGenerator challengeNonceGenerator,
ITemplateEngine templateEngine,
WebEidMobileProperties webEidMobileProperties
) throws Exception {
return http
.authenticationProvider(authTokenDTOAuthenticationProvider)
.addFilterBefore(new WebEidAjaxLoginProcessingFilter("/auth/login", authConfig.getAuthenticationManager()),
UsernamePasswordAuthenticationFilter.class)
.logout(logout -> logout.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()))
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
.build();
.authorizeHttpRequests(auth -> auth
.requestMatchers("/css/**", "/files/**", "/img/**", "/js/**", "/scripts/**").permitAll()
.requestMatchers("/").permitAll()
.anyRequest().authenticated()
)
.authenticationProvider(webEidAuthenticationProvider)
.addFilterBefore(new WebEidMobileAuthInitFilter("/auth/mobile/init", "/auth/mobile/login", challengeNonceGenerator, webEidMobileProperties), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new WebEidChallengeNonceFilter("/auth/challenge", challengeNonceGenerator), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new WebEidLoginPageGeneratingFilter("/auth/mobile/login", "/auth/login", templateEngine), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new WebEidAjaxLoginProcessingFilter("/auth/login", authConfig.getAuthenticationManager()), UsernamePasswordAuthenticationFilter.class)
.logout(l -> l.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()))
.headers(h -> h.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
.build();
}

@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
registry.addViewController("/welcome").setViewName("welcome");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,17 @@
import org.springframework.boot.web.embedded.tomcat.TomcatContextCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class SameSiteCookieConfiguration implements WebMvcConfigurer {
public class SameSiteCookieConfiguration {

@Bean
public TomcatContextCustomizer configureSameSiteCookies() {
return context -> {
final Rfc6265CookieProcessor cookieProcessor = new Rfc6265CookieProcessor();
cookieProcessor.setSameSiteCookies("strict");
// Set to "strict" if Web eID for Mobile flow is not used - this would restrict sending back the
// authentication response in the Web eID for Mobile flow.
cookieProcessor.setSameSiteCookies("lax");
context.setCookieProcessor(cookieProcessor);
};
}
Expand Down
Loading