Skip to content

Commit d3edcb7

Browse files
NFC-47 NFC support for web-eid example
Signed-off-by: Sander Kondratjev <sander.kondratjev@nortal.com>
1 parent 2d8b399 commit d3edcb7

27 files changed

+1059
-368
lines changed

README.md

Lines changed: 87 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ Implement the session-backed challenge nonce store as follows:
4848
import org.springframework.beans.factory.ObjectFactory;
4949
import eu.webeid.security.challenge.ChallengeNonce;
5050
import eu.webeid.security.challenge.ChallengeNonceStore;
51-
import javax.servlet.http.HttpSession;
51+
import jakarta.servlet.http.HttpSession;
5252

5353
public class SessionBackedChallengeNonceStore implements ChallengeNonceStore {
5454

@@ -134,36 +134,103 @@ import eu.webeid.security.validator.AuthTokenValidatorBuilder;
134134
...
135135
```
136136

137-
## 6. Add a REST endpoint for issuing challenge nonces
137+
## 6. Add a filter for issuing challenge nonces
138138

139-
A REST endpoint that issues challenge nonces is required for authentication. The endpoint must support `GET` requests.
139+
Request Filters that issue challenge nonces for regular Web eID and Web eID for Mobile authentication flows are required for authentication.
140+
The filters must support POST requests.
140141

141-
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).
142+
The `WebEidChallengeNonceFilter` handles `/auth/challenge` requests and issues a new nonce for regualar Web eID authentication flow.
143+
See the full implementation [here](example/src/main/java/eu/webeid/example/security/WebEidChallengeNonceFilter.java).
142144

143145
```java
144-
import org.springframework.web.bind.annotation.GetMapping;
145-
import org.springframework.web.bind.annotation.RequestMapping;
146-
import org.springframework.web.bind.annotation.RestController;
147-
import eu.webeid.security.challenge.ChallengeNonceGenerator;
148-
...
146+
public final class WebEidChallengeNonceFilter extends OncePerRequestFilter {
147+
private static final ObjectWriter OBJECT_WRITER = new ObjectMapper().writer();
148+
private final RequestMatcher requestMatcher;
149+
private final ChallengeNonceGenerator nonceGenerator;
150+
151+
public WebEidChallengeNonceFilter(String path, ChallengeNonceGenerator nonceGenerator) {
152+
this.requestMatcher = PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, path);
153+
this.nonceGenerator = nonceGenerator;
154+
}
155+
156+
@Override
157+
protected void doFilterInternal(
158+
@NonNull HttpServletRequest request,
159+
@NonNull HttpServletResponse response,
160+
@NonNull FilterChain chain
161+
) throws ServletException, IOException {
162+
if (!requestMatcher.matches(request)) {
163+
chain.doFilter(request, response);
164+
return;
165+
}
166+
167+
var dto = new ChallengeDTO(nonceGenerator.generateAndStoreNonce().getBase64EncodedNonce());
168+
169+
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
170+
OBJECT_WRITER.writeValue(response.getWriter(), dto);
171+
}
172+
173+
public record ChallengeDTO(String nonce) {}
174+
}
175+
```
149176

150-
@RestController
151-
@RequestMapping("auth")
152-
public class ChallengeController {
177+
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.
178+
See the full implementation [here](example/src/main/java/eu/webeid/example/security/WebEidMobileAuthInitFilter.java).
153179

154-
@Autowired // for brevity, prefer constructor dependency injection
155-
private ChallengeNonceGenerator nonceGenerator;
180+
```java
181+
public final class WebEidMobileAuthInitFilter extends OncePerRequestFilter {
182+
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
183+
private final RequestMatcher requestMatcher;
184+
private final ChallengeNonceGenerator nonceGenerator;
185+
private final String loginPath;
186+
187+
public WebEidMobileAuthInitFilter(String path, String loginPath, ChallengeNonceGenerator nonceGenerator) {
188+
this.requestMatcher = PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, path);
189+
this.nonceGenerator = nonceGenerator;
190+
this.loginPath = loginPath;
191+
}
156192

157-
@GetMapping("challenge")
158-
public ChallengeDTO challenge() {
159-
// a simple DTO with a single 'nonce' field
160-
final ChallengeDTO challenge = new ChallengeDTO();
161-
challenge.setNonce(nonceGenerator.generateAndStoreNonce().getBase64EncodedNonce());
162-
return challenge;
193+
@Override
194+
protected void doFilterInternal(
195+
@NonNull HttpServletRequest request,
196+
@NonNull HttpServletResponse response,
197+
@NonNull FilterChain chain
198+
) throws IOException, ServletException {
199+
if (!requestMatcher.matches(request)) {
200+
chain.doFilter(request, response);
201+
return;
202+
}
203+
204+
var challenge = nonceGenerator.generateAndStoreNonce();
205+
206+
String loginUri = ServletUriComponentsBuilder.fromCurrentContextPath()
207+
.path(loginPath).build().toUriString();
208+
209+
String payloadJson = OBJECT_MAPPER.writeValueAsString(
210+
new AuthPayload(challenge.getBase64EncodedNonce(), loginUri)
211+
);
212+
String encoded = Base64.getEncoder().encodeToString(payloadJson.getBytes(StandardCharsets.UTF_8));
213+
String eidAuthUri = "web-eid-mobile://auth#" + encoded;
214+
215+
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
216+
OBJECT_MAPPER.writeValue(response.getWriter(), new AuthUri(eidAuthUri));
163217
}
218+
219+
record AuthPayload(String challenge, @JsonProperty("login_uri") String loginUri) {}
220+
record AuthUri(@JsonProperty("auth_uri") String authUri) {}
164221
}
165222
```
166223

224+
Both filters are registered in the Spring Security filter chain in ApplicationConfiguration
225+
See the full implementation [here](example/src/main/java/eu/webeid/example/config/ApplicationConfiguration.java):
226+
```java
227+
http
228+
.addFilterBefore(new WebEidMobileAuthInitFilter("/auth/mobile/init", "/auth/mobile/login", challengeNonceGenerator),
229+
UsernamePasswordAuthenticationFilter.class)
230+
.addFilterBefore(new WebEidChallengeNonceFilter("/auth/challenge", challengeNonceGenerator),
231+
UsernamePasswordAuthenticationFilter.class);
232+
```
233+
167234
Also, see general guidelines for implementing secure authentication services [here](https://github.com/SK-EID/smart-id-documentation/wiki/Secure-Implementation-Guide).
168235

169236
## 7. Implement authentication

example/README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ This repository contains the code of a minimal Spring Boot web application that
100100
- Spring Security,
101101
- the Web eID authentication token validation library [_web-eid-authtoken-validation-java_](https://github.com/web-eid/web-eid-authtoken-validation-java),
102102
- the Web eID JavaScript library [_web-eid.js_](https://github.com/web-eid/web-eid.js),
103-
- the digital signing library [_DigiDoc4j_](https://github.com/open-eid/digidoc4j).
103+
- the digital signing library [_DigiDoc4j_](https://github.com/open-eid/digidoc4j),
104+
- the Android application [_MOPP-Android_](https://github.com/open-eid/MOPP-Android/).
104105

105106
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.
106107

@@ -113,11 +114,13 @@ The source code folder `src` contains the application source code and resources
113114
The `src/main/java/eu/webeid/example` directory contains the Spring Boot application Java class and the following subdirectories:
114115

115116
- `config`: Spring and HTTP security configuration, Web eID authentication token validation library configuration, trusted CA certificates loading etc,
116-
- `security`: Web eID authentication token validation library integration with Spring Security via an `AuthenticationProvider` and `AuthenticationProcessingFilter`,
117+
- `security`: Web eID authentication token validation library integration with Spring Security
118+
- `AuthenticationProvider` and `AuthenticationProcessingFilter` for handling Web eID authentication tokens,
119+
- `WebEidChallengeNonceFilter` for issuing the challenge nonce required by the authentication flow,
120+
- `WebEidMobileAuthInitFilter` for issuing the challenge nonce and generating the deep link with the authentication request, used to initiate the mobile authentication flow,
121+
- `WebEidAjaxLoginProcessingFilter` and `WebEidLoginPageGeneratingFilter` for handling login requests.
117122
- `service`: Web eID signing service implementation that uses DigiDoc4j, and DigiDoc4j runtime configuration,
118-
- `web`: Spring Web MVC controller for the welcome page and Spring Web REST controllers that provide endpoints
119-
- for getting the challenge nonce used by the authentication token validation library,
120-
- for digital signing.
123+
- `web`: Spring Web MVC controller for the welcome page and Spring Web REST controller that provides a digital signing endpoint.
121124

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

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

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424

2525
import eu.webeid.example.security.AuthTokenDTOAuthenticationProvider;
2626
import eu.webeid.example.security.WebEidAjaxLoginProcessingFilter;
27+
import eu.webeid.example.security.WebEidChallengeNonceFilter;
28+
import eu.webeid.example.security.WebEidMobileAuthInitFilter;
29+
import eu.webeid.example.security.ui.WebEidLoginPageGeneratingFilter;
30+
import eu.webeid.security.challenge.ChallengeNonceGenerator;
2731
import org.springframework.context.annotation.Bean;
2832
import org.springframework.context.annotation.Configuration;
2933
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
@@ -34,29 +38,32 @@
3438
import org.springframework.security.web.SecurityFilterChain;
3539
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
3640
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
37-
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
38-
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
3941

4042
@Configuration
4143
@EnableWebSecurity
4244
@EnableMethodSecurity(securedEnabled = true)
43-
public class ApplicationConfiguration implements WebMvcConfigurer {
45+
public class ApplicationConfiguration {
4446

4547
@Bean
46-
public SecurityFilterChain filterChain(HttpSecurity http, AuthTokenDTOAuthenticationProvider authTokenDTOAuthenticationProvider, AuthenticationConfiguration authConfig) throws Exception {
48+
public SecurityFilterChain filterChain(
49+
HttpSecurity http,
50+
AuthTokenDTOAuthenticationProvider authTokenDTOAuthenticationProvider,
51+
AuthenticationConfiguration authConfig,
52+
ChallengeNonceGenerator challengeNonceGenerator
53+
) throws Exception {
4754
return http
48-
.authenticationProvider(authTokenDTOAuthenticationProvider)
49-
.addFilterBefore(new WebEidAjaxLoginProcessingFilter("/auth/login", authConfig.getAuthenticationManager()),
50-
UsernamePasswordAuthenticationFilter.class)
51-
.logout(logout -> logout.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()))
52-
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
53-
.build();
55+
.authorizeHttpRequests(auth -> auth
56+
.requestMatchers("/css/**", "/files/**", "/img/**", "/js/**", "/scripts/**").permitAll()
57+
.requestMatchers("/").permitAll()
58+
.anyRequest().authenticated()
59+
)
60+
.authenticationProvider(authTokenDTOAuthenticationProvider)
61+
.addFilterBefore(new WebEidMobileAuthInitFilter("/auth/mobile/init", "/auth/mobile/login", challengeNonceGenerator), UsernamePasswordAuthenticationFilter.class)
62+
.addFilterBefore(new WebEidChallengeNonceFilter("/auth/challenge", challengeNonceGenerator), UsernamePasswordAuthenticationFilter.class)
63+
.addFilterBefore(new WebEidLoginPageGeneratingFilter("/auth/mobile/login"), UsernamePasswordAuthenticationFilter.class)
64+
.addFilterBefore(new WebEidAjaxLoginProcessingFilter("/auth/login", authConfig.getAuthenticationManager()), UsernamePasswordAuthenticationFilter.class)
65+
.headers(h -> h.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
66+
.logout(l -> l.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()))
67+
.build();
5468
}
55-
56-
@Override
57-
public void addViewControllers(ViewControllerRegistry registry) {
58-
registry.addViewController("/").setViewName("index");
59-
registry.addViewController("/welcome").setViewName("welcome");
60-
}
61-
6269
}

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,15 @@
2626
import org.springframework.boot.web.embedded.tomcat.TomcatContextCustomizer;
2727
import org.springframework.context.annotation.Bean;
2828
import org.springframework.context.annotation.Configuration;
29-
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
3029

3130
@Configuration
32-
public class SameSiteCookieConfiguration implements WebMvcConfigurer {
31+
public class SameSiteCookieConfiguration {
3332

3433
@Bean
3534
public TomcatContextCustomizer configureSameSiteCookies() {
3635
return context -> {
3736
final Rfc6265CookieProcessor cookieProcessor = new Rfc6265CookieProcessor();
38-
cookieProcessor.setSameSiteCookies("strict");
37+
cookieProcessor.setSameSiteCookies("lax");
3938
context.setCookieProcessor(cookieProcessor);
4039
};
4140
}

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

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333
import jakarta.servlet.http.HttpServletResponse;
3434
import org.slf4j.Logger;
3535
import org.slf4j.LoggerFactory;
36+
import org.springframework.http.HttpHeaders;
3637
import org.springframework.http.HttpMethod;
38+
import org.springframework.http.MediaType;
3739
import org.springframework.security.authentication.AuthenticationManager;
3840
import org.springframework.security.authentication.AuthenticationServiceException;
3941
import org.springframework.security.core.Authentication;
@@ -44,6 +46,7 @@
4446
import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy;
4547
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
4648
import org.springframework.security.web.context.SecurityContextRepository;
49+
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
4750

4851
import java.io.IOException;
4952

@@ -53,10 +56,10 @@ public class WebEidAjaxLoginProcessingFilter extends AbstractAuthenticationProce
5356
private final SecurityContextRepository securityContextRepository;
5457

5558
public WebEidAjaxLoginProcessingFilter(
56-
String defaultFilterProcessesUrl,
57-
AuthenticationManager authenticationManager
59+
String defaultFilterProcessesUrl,
60+
AuthenticationManager authenticationManager
5861
) {
59-
super(defaultFilterProcessesUrl);
62+
super(PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, defaultFilterProcessesUrl));
6063
this.setAuthenticationManager(authenticationManager);
6164
this.setAuthenticationSuccessHandler(new AjaxAuthenticationSuccessHandler());
6265
this.setAuthenticationFailureHandler(new AjaxAuthenticationFailureHandler());
@@ -66,13 +69,9 @@ public WebEidAjaxLoginProcessingFilter(
6669

6770
@Override
6871
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
69-
throws AuthenticationException, IOException {
70-
if (!HttpMethod.POST.name().equals(request.getMethod())) {
71-
LOG.warn("HttpMethod not supported: {}", request.getMethod());
72-
throw new AuthenticationServiceException("HttpMethod not supported: " + request.getMethod());
73-
}
74-
final String contentType = request.getHeader("Content-type");
75-
if (contentType == null || !contentType.startsWith("application/json")) {
72+
throws AuthenticationException, IOException {
73+
final String contentType = request.getHeader(HttpHeaders.CONTENT_TYPE);
74+
if (contentType == null || !contentType.startsWith(MediaType.APPLICATION_JSON_VALUE)) {
7675
LOG.warn("Content type not supported: {}", contentType);
7776
throw new AuthenticationServiceException("Content type not supported: " + contentType);
7877
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright (c) 2020-2025 Estonian Information System Authority
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in all
12+
* copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
* SOFTWARE.
21+
*/
22+
23+
package eu.webeid.example.security;
24+
25+
import com.fasterxml.jackson.databind.ObjectMapper;
26+
import com.fasterxml.jackson.databind.ObjectWriter;
27+
import eu.webeid.security.challenge.ChallengeNonceGenerator;
28+
import jakarta.servlet.FilterChain;
29+
import jakarta.servlet.ServletException;
30+
import jakarta.servlet.http.HttpServletRequest;
31+
import jakarta.servlet.http.HttpServletResponse;
32+
import org.springframework.http.HttpMethod;
33+
import org.springframework.http.MediaType;
34+
import org.springframework.lang.NonNull;
35+
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
36+
import org.springframework.security.web.util.matcher.RequestMatcher;
37+
import org.springframework.web.filter.OncePerRequestFilter;
38+
39+
import java.io.IOException;
40+
41+
public final class WebEidChallengeNonceFilter extends OncePerRequestFilter {
42+
private static final ObjectWriter OBJECT_WRITER = new ObjectMapper().writer();
43+
private final RequestMatcher requestMatcher;
44+
private final ChallengeNonceGenerator nonceGenerator;
45+
46+
public WebEidChallengeNonceFilter(String path, ChallengeNonceGenerator nonceGenerator) {
47+
this.requestMatcher = PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, path);
48+
this.nonceGenerator = nonceGenerator;
49+
}
50+
51+
@Override
52+
protected void doFilterInternal(@NonNull HttpServletRequest request,
53+
@NonNull HttpServletResponse response,
54+
@NonNull FilterChain chain) throws ServletException, IOException {
55+
if (!requestMatcher.matches(request)) {
56+
chain.doFilter(request, response);
57+
return;
58+
}
59+
60+
var dto = new ChallengeDTO(nonceGenerator.generateAndStoreNonce().getBase64EncodedNonce());
61+
62+
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
63+
OBJECT_WRITER.writeValue(response.getWriter(), dto);
64+
}
65+
66+
public record ChallengeDTO(String nonce) {}
67+
}

0 commit comments

Comments
 (0)