Skip to content

Commit 0187597

Browse files
NFC-47 Start using state instead of sending the challenge.
1 parent 5108d00 commit 0187597

File tree

6 files changed

+95
-36
lines changed

6 files changed

+95
-36
lines changed

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@
2626
import eu.webeid.example.security.WebEidAjaxLoginProcessingFilter;
2727
import eu.webeid.example.security.WebEidChallengeNonceFilter;
2828
import eu.webeid.example.security.WebEidMobileAuthInitFilter;
29+
import eu.webeid.example.security.state.MobileAuthStateStore;
2930
import eu.webeid.example.security.ui.WebEidLoginPageGeneratingFilter;
3031
import eu.webeid.security.challenge.ChallengeNonceGenerator;
32+
import eu.webeid.security.challenge.ChallengeNonceStore;
3133
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
3234
import org.springframework.context.annotation.Bean;
3335
import org.springframework.context.annotation.Configuration;
@@ -51,7 +53,7 @@
5153
public class ApplicationConfiguration implements WebMvcConfigurer {
5254

5355
@Bean
54-
public SecurityFilterChain filterChain(HttpSecurity http, AuthTokenDTOAuthenticationProvider authTokenDTOAuthenticationProvider, AuthenticationConfiguration authConfig, ChallengeNonceGenerator challengeNonceGenerator) throws Exception {
56+
public SecurityFilterChain filterChain(HttpSecurity http, AuthTokenDTOAuthenticationProvider authTokenDTOAuthenticationProvider, AuthenticationConfiguration authConfig, ChallengeNonceGenerator challengeNonceGenerator, ChallengeNonceStore challengeNonceStore, MobileAuthStateStore mobileAuthStateStore) throws Exception {
5557

5658
var filter = new WebEidAjaxLoginProcessingFilter("/auth/login", authConfig.getAuthenticationManager());
5759

@@ -67,15 +69,20 @@ public SecurityFilterChain filterChain(HttpSecurity http, AuthTokenDTOAuthentica
6769
.anyRequest().permitAll()
6870
)
6971
.authenticationProvider(authTokenDTOAuthenticationProvider)
70-
.addFilterBefore(new WebEidLoginPageGeneratingFilter(), UsernamePasswordAuthenticationFilter.class)
72+
.addFilterBefore(new WebEidLoginPageGeneratingFilter(mobileAuthStateStore, challengeNonceStore), UsernamePasswordAuthenticationFilter.class)
7173
.addFilterBefore(new WebEidChallengeNonceFilter(challengeNonceGenerator), AuthorizationFilter.class)
72-
.addFilterAfter(new WebEidMobileAuthInitFilter(challengeNonceGenerator), CsrfFilter.class)
74+
.addFilterAfter(new WebEidMobileAuthInitFilter(challengeNonceGenerator, mobileAuthStateStore), CsrfFilter.class)
7375
.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class)
7476
.headers(h -> h.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
7577
.logout(l -> l.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()))
7678
.build();
7779
}
7880

81+
@Bean
82+
public MobileAuthStateStore mobileAuthStateStore() {
83+
return new MobileAuthStateStore(5 * 60L);
84+
}
85+
7986
@Override
8087
public void addViewControllers(ViewControllerRegistry registry) {
8188
registry.addViewController("/").setViewName("index");

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

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,7 @@ public Authentication authenticate(Authentication auth) throws AuthenticationExc
7171
final List<GrantedAuthority> authorities = Collections.singletonList(USER_ROLE);
7272

7373
try {
74-
String nonce;
75-
if (authToken.getChallenge() != null && !authToken.getChallenge().isEmpty()) {
76-
nonce = authToken.getChallenge();
77-
LOG.info("Using challenge from token itself");
78-
} else {
79-
nonce = challengeNonceStore.getAndRemove().getBase64EncodedNonce();
80-
LOG.info("Using challenge from session store");
81-
}
74+
final String nonce = challengeNonceStore.getAndRemove().getBase64EncodedNonce();
8275
final X509Certificate userCertificate = tokenValidator.validate(authToken, nonce);
8376
return WebEidAuthentication.fromCertificate(userCertificate, authorities);
8477
} catch (AuthTokenException e) {

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@
2929
import eu.webeid.example.security.dto.AuthTokenDTO;
3030
import jakarta.servlet.FilterChain;
3131
import jakarta.servlet.ServletException;
32-
import jakarta.servlet.ServletRequest;
33-
import jakarta.servlet.ServletResponse;
3432
import jakarta.servlet.http.HttpServletRequest;
3533
import jakarta.servlet.http.HttpServletResponse;
3634
import org.slf4j.Logger;
@@ -81,6 +79,7 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ
8179
LOG.info("attemptAuthentication(): Creating token");
8280
final PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(null, authTokenDTO);
8381
LOG.info("attemptAuthentication(): Calling authentication manager");
82+
token.setDetails(new org.springframework.security.web.authentication.WebAuthenticationDetailsSource().buildDetails(request));
8483
return getAuthenticationManager().authenticate(token);
8584
}
8685

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

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package eu.webeid.example.security;
22

33
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import eu.webeid.example.security.state.MobileAuthStateStore;
45
import eu.webeid.example.service.dto.MobileAuthInitResponse;
56
import eu.webeid.security.challenge.ChallengeNonceGenerator;
67
import jakarta.servlet.FilterChain;
78
import jakarta.servlet.ServletException;
89
import jakarta.servlet.http.HttpServletRequest;
910
import jakarta.servlet.http.HttpServletResponse;
11+
import org.slf4j.Logger;
12+
import org.slf4j.LoggerFactory;
1013
import org.springframework.http.HttpMethod;
1114
import org.springframework.lang.NonNull;
1215
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
@@ -20,43 +23,45 @@
2023
import java.util.Map;
2124

2225
public final class WebEidMobileAuthInitFilter extends OncePerRequestFilter {
23-
26+
private static final Logger LOG = LoggerFactory.getLogger(WebEidMobileAuthInitFilter.class);
2427
private static final ObjectMapper MAPPER = new ObjectMapper();
2528
private final RequestMatcher matcher =
26-
PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, "/auth/mobile/auth/init");
29+
PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, "/auth/mobile/auth/init");
2730

2831
private final ChallengeNonceGenerator nonceGenerator;
32+
private final MobileAuthStateStore stateStore;
2933

30-
public WebEidMobileAuthInitFilter(ChallengeNonceGenerator nonceGenerator) {
34+
public WebEidMobileAuthInitFilter(ChallengeNonceGenerator nonceGenerator, MobileAuthStateStore stateStore) {
3135
this.nonceGenerator = nonceGenerator;
36+
this.stateStore = stateStore;
3237
}
3338

3439
@Override
3540
protected void doFilterInternal(@NonNull HttpServletRequest request,
3641
@NonNull HttpServletResponse response,
3742
@NonNull FilterChain chain) throws ServletException, IOException {
38-
if (!matcher.matches(request)) {
39-
chain.doFilter(request, response);
40-
return;
41-
}
43+
if (!matcher.matches(request)) { chain.doFilter(request, response); return; }
4244

43-
String nonce = nonceGenerator.generateAndStoreNonce().getBase64EncodedNonce();
45+
var challenge = nonceGenerator.generateAndStoreNonce();
46+
String state = stateStore.put(challenge);
4447

45-
String loginUri = ServletUriComponentsBuilder
46-
.fromCurrentContextPath()
47-
.path("/auth/eid/login")
48-
.build()
49-
.toUriString();
48+
String loginUri = ServletUriComponentsBuilder.fromCurrentContextPath()
49+
.path("/auth/eid/login").queryParam("state", state).build().toUriString();
5050

5151
String payloadJson = MAPPER.writeValueAsString(Map.of(
52-
"challenge", nonce,
53-
"login_uri", loginUri
52+
"challenge", challenge.getBase64EncodedNonce(),
53+
"login_uri", loginUri,
54+
"state", state
5455
));
5556
String encoded = Base64.getEncoder().encodeToString(payloadJson.getBytes(StandardCharsets.UTF_8));
5657
String eidAuthUri = "web-eid-mobile://auth#" + encoded;
5758

5859
response.setContentType("application/json");
5960
MAPPER.writeValue(response.getWriter(), new MobileAuthInitResponse(eidAuthUri));
61+
62+
var session = request.getSession(false);
63+
var sid = (session != null ? session.getId() : "null");
64+
LOG.info("MOBILE INIT: sessionId={}, issuing nonce, state={}", sid, state);
6065
}
6166

6267
@Override
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package eu.webeid.example.security.state;
2+
3+
import eu.webeid.security.challenge.ChallengeNonce;
4+
5+
import java.time.Instant;
6+
import java.util.Map;
7+
import java.util.UUID;
8+
import java.util.concurrent.ConcurrentHashMap;
9+
10+
public final class MobileAuthStateStore {
11+
private static final class Entry {
12+
final ChallengeNonce challenge;
13+
final Instant ts;
14+
Entry(ChallengeNonce c) { this.challenge = c; this.ts = Instant.now(); }
15+
}
16+
17+
private final Map<String, Entry> store = new ConcurrentHashMap<>();
18+
private final long ttlSeconds;
19+
20+
public MobileAuthStateStore(long ttlSeconds) { this.ttlSeconds = ttlSeconds; }
21+
22+
public String put(ChallengeNonce challenge) {
23+
String state = UUID.randomUUID().toString();
24+
store.put(state, new Entry(challenge));
25+
return state;
26+
}
27+
28+
public ChallengeNonce consume(String state) {
29+
if (state == null || state.isBlank()) return null;
30+
Entry e = store.remove(state);
31+
if (e == null) return null;
32+
if (Instant.now().isAfter(e.ts.plusSeconds(ttlSeconds))) return null;
33+
return e.challenge;
34+
}
35+
}

example/src/main/java/eu/webeid/example/security/ui/WebEidLoginPageGeneratingFilter.java

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package eu.webeid.example.security.ui;
22

3+
import eu.webeid.example.security.state.MobileAuthStateStore;
4+
import eu.webeid.security.challenge.ChallengeNonceStore;
35
import jakarta.servlet.FilterChain;
46
import jakarta.servlet.ServletException;
57
import jakarta.servlet.http.HttpServletRequest;
68
import jakarta.servlet.http.HttpServletResponse;
9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
711
import org.springframework.http.HttpMethod;
812
import org.springframework.lang.NonNull;
913
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
@@ -13,8 +17,10 @@
1317
import java.io.IOException;
1418

1519
public final class WebEidLoginPageGeneratingFilter extends OncePerRequestFilter {
16-
20+
private static final Logger LOG = LoggerFactory.getLogger(WebEidLoginPageGeneratingFilter.class);
1721
private final RequestMatcher requestMatcher = PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.GET, "/auth/eid/login");
22+
private final MobileAuthStateStore stateStore;
23+
private final ChallengeNonceStore challengeNonceStore;
1824
private static final String LOGIN_PAGE_HTML = """
1925
<!doctype html>
2026
<html lang="en">
@@ -55,21 +61,35 @@ public final class WebEidLoginPageGeneratingFilter extends OncePerRequestFilter
5561
</html>
5662
""";
5763

64+
public WebEidLoginPageGeneratingFilter(MobileAuthStateStore stateStore, ChallengeNonceStore challengeNonceStore) {
65+
this.stateStore = stateStore;
66+
this.challengeNonceStore = challengeNonceStore;
67+
}
68+
5869
@Override
59-
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain chain) throws IOException, ServletException {
70+
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain chain)
71+
throws IOException, ServletException {
6072
if (!requestMatcher.matches(request)) {
6173
chain.doFilter(request, response);
6274
return;
6375
}
6476

65-
var csrf = (org.springframework.security.web.csrf.CsrfToken) request.getAttribute(org.springframework.security.web.csrf.CsrfToken.class.getName());
66-
if (csrf == null) {
67-
csrf = (org.springframework.security.web.csrf.CsrfToken) request.getAttribute("_csrf");
77+
String state = request.getParameter("state");
78+
if (state != null && !state.isBlank()) {
79+
var challenge = stateStore.consume(state);
80+
var session = request.getSession(true);
81+
var sid = (session != null ? session.getId() : "null");
82+
if (challenge != null) {
83+
challengeNonceStore.put(challenge);
84+
LOG.info("LOGIN PAGE: rehydrated challenge via state={}, sessionId={}", state, sid);
85+
} else {
86+
LOG.warn("LOGIN PAGE: state missing/expired: {}, sessionId={}", state, sid);
87+
}
6888
}
69-
String token = csrf != null ? csrf.getToken() : "";
70-
String header = csrf != null ? csrf.getHeaderName() : "X-CSRF-TOKEN";
7189

72-
String html = generateHtml(token, header);
90+
var csrf = (org.springframework.security.web.csrf.CsrfToken) request.getAttribute(org.springframework.security.web.csrf.CsrfToken.class.getName());
91+
if (csrf == null) csrf = (org.springframework.security.web.csrf.CsrfToken) request.getAttribute("_csrf");
92+
String html = generateHtml(csrf != null ? csrf.getToken() : "", csrf != null ? csrf.getHeaderName() : "X-CSRF-TOKEN");
7393
response.setContentType("text/html;charset=UTF-8");
7494
response.getWriter().write(html);
7595
}

0 commit comments

Comments
 (0)