Skip to content

Commit 102c405

Browse files
NFC-47 Move /auth/challenge and /auth/mobile/auth/init into Security filters; remove ChallengeController
1 parent 016dda8 commit 102c405

File tree

5 files changed

+175
-89
lines changed

5 files changed

+175
-89
lines changed

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +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;
2729
import eu.webeid.example.security.ui.WebEidLoginPageGeneratingFilter;
30+
import eu.webeid.security.challenge.ChallengeNonceGenerator;
2831
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
2932
import org.springframework.context.annotation.Bean;
3033
import org.springframework.context.annotation.Configuration;
@@ -35,8 +38,10 @@
3538
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
3639
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
3740
import org.springframework.security.web.SecurityFilterChain;
41+
import org.springframework.security.web.access.intercept.AuthorizationFilter;
3842
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
3943
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
44+
import org.springframework.security.web.csrf.CsrfFilter;
4045
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
4146
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
4247

@@ -46,7 +51,7 @@
4651
public class ApplicationConfiguration implements WebMvcConfigurer {
4752

4853
@Bean
49-
public SecurityFilterChain filterChain(HttpSecurity http, AuthTokenDTOAuthenticationProvider authTokenDTOAuthenticationProvider, AuthenticationConfiguration authConfig) throws Exception {
54+
public SecurityFilterChain filterChain(HttpSecurity http, AuthTokenDTOAuthenticationProvider authTokenDTOAuthenticationProvider, AuthenticationConfiguration authConfig, ChallengeNonceGenerator challengeNonceGenerator) throws Exception {
5055

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

@@ -63,6 +68,8 @@ public SecurityFilterChain filterChain(HttpSecurity http, AuthTokenDTOAuthentica
6368
)
6469
.authenticationProvider(authTokenDTOAuthenticationProvider)
6570
.addFilterBefore(new WebEidLoginPageGeneratingFilter(), UsernamePasswordAuthenticationFilter.class)
71+
.addFilterBefore(new WebEidChallengeNonceFilter(challengeNonceGenerator), AuthorizationFilter.class)
72+
.addFilterAfter(new WebEidMobileAuthInitFilter(challengeNonceGenerator), CsrfFilter.class)
6673
.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class)
6774
.headers(h -> h.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
6875
.logout(l -> l.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()))
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package eu.webeid.example.security;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import com.fasterxml.jackson.databind.ObjectWriter;
5+
import eu.webeid.example.service.dto.ChallengeDTO;
6+
import eu.webeid.security.challenge.ChallengeNonceGenerator;
7+
import jakarta.servlet.FilterChain;
8+
import jakarta.servlet.ServletException;
9+
import jakarta.servlet.http.HttpServletRequest;
10+
import jakarta.servlet.http.HttpServletResponse;
11+
import org.springframework.http.HttpMethod;
12+
import org.springframework.lang.NonNull;
13+
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
14+
import org.springframework.security.web.util.matcher.RequestMatcher;
15+
import org.springframework.web.filter.OncePerRequestFilter;
16+
17+
import java.io.IOException;
18+
19+
public final class WebEidChallengeNonceFilter extends OncePerRequestFilter {
20+
21+
private static final ObjectWriter JSON = new ObjectMapper().writer();
22+
private final RequestMatcher matcher =
23+
PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.GET, "/auth/challenge");
24+
25+
private final ChallengeNonceGenerator nonceGenerator;
26+
27+
public WebEidChallengeNonceFilter(ChallengeNonceGenerator nonceGenerator) {
28+
this.nonceGenerator = nonceGenerator;
29+
}
30+
31+
@Override
32+
protected void doFilterInternal(@NonNull HttpServletRequest request,
33+
@NonNull HttpServletResponse response,
34+
@NonNull FilterChain chain) throws ServletException, IOException {
35+
if (!matcher.matches(request)) {
36+
chain.doFilter(request, response);
37+
return;
38+
}
39+
40+
var dto = new ChallengeDTO();
41+
dto.setNonce(nonceGenerator.generateAndStoreNonce().getBase64EncodedNonce());
42+
43+
response.setContentType("application/json");
44+
JSON.writeValue(response.getWriter(), dto);
45+
}
46+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package eu.webeid.example.security;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import eu.webeid.example.service.dto.MobileAuthInitResponse;
5+
import eu.webeid.security.challenge.ChallengeNonceGenerator;
6+
import jakarta.servlet.FilterChain;
7+
import jakarta.servlet.ServletException;
8+
import jakarta.servlet.http.HttpServletRequest;
9+
import jakarta.servlet.http.HttpServletResponse;
10+
import org.springframework.http.HttpMethod;
11+
import org.springframework.lang.NonNull;
12+
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
13+
import org.springframework.security.web.util.matcher.RequestMatcher;
14+
import org.springframework.web.filter.OncePerRequestFilter;
15+
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
16+
17+
import java.io.IOException;
18+
import java.nio.charset.StandardCharsets;
19+
import java.util.Base64;
20+
import java.util.Map;
21+
22+
public final class WebEidMobileAuthInitFilter extends OncePerRequestFilter {
23+
24+
private static final ObjectMapper MAPPER = new ObjectMapper();
25+
private final RequestMatcher matcher =
26+
PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, "/auth/mobile/auth/init");
27+
28+
private final ChallengeNonceGenerator nonceGenerator;
29+
30+
public WebEidMobileAuthInitFilter(ChallengeNonceGenerator nonceGenerator) {
31+
this.nonceGenerator = nonceGenerator;
32+
}
33+
34+
@Override
35+
protected void doFilterInternal(@NonNull HttpServletRequest request,
36+
@NonNull HttpServletResponse response,
37+
@NonNull FilterChain chain) throws ServletException, IOException {
38+
if (!matcher.matches(request)) {
39+
chain.doFilter(request, response);
40+
return;
41+
}
42+
43+
String nonce = nonceGenerator.generateAndStoreNonce().getBase64EncodedNonce();
44+
45+
String loginUri = ServletUriComponentsBuilder
46+
.fromCurrentContextPath()
47+
.path("/auth/eid/login")
48+
.build()
49+
.toUriString();
50+
51+
String payloadJson = MAPPER.writeValueAsString(Map.of(
52+
"challenge", nonce,
53+
"login_uri", loginUri
54+
));
55+
String encoded = Base64.getEncoder().encodeToString(payloadJson.getBytes(StandardCharsets.UTF_8));
56+
String eidAuthUri = "web-eid-mobile://auth#" + encoded;
57+
58+
response.setContentType("application/json");
59+
MAPPER.writeValue(response.getWriter(), new MobileAuthInitResponse(eidAuthUri));
60+
}
61+
62+
@Override
63+
protected boolean shouldNotFilterAsyncDispatch() {
64+
return false;
65+
}
66+
}

example/src/main/java/eu/webeid/example/web/rest/ChallengeController.java

Lines changed: 0 additions & 79 deletions
This file was deleted.

example/src/test/java/eu/webeid/example/AuthenticationRestControllerTest.java

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,28 +22,74 @@
2222

2323
package eu.webeid.example;
2424

25+
import com.fasterxml.jackson.databind.JsonNode;
26+
import com.fasterxml.jackson.databind.ObjectMapper;
2527
import org.junit.jupiter.api.Test;
2628
import org.springframework.beans.factory.annotation.Autowired;
29+
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
2730
import org.springframework.boot.test.context.SpringBootTest;
28-
import eu.webeid.example.web.rest.ChallengeController;
31+
import org.springframework.http.MediaType;
32+
import org.springframework.test.web.servlet.MockMvc;
33+
import org.springframework.test.web.servlet.MvcResult;
34+
35+
import java.nio.charset.StandardCharsets;
36+
import java.util.Base64;
2937

30-
import static org.assertj.core.api.Assertions.assertThat;
3138
import static eu.webeid.security.challenge.ChallengeNonceGenerator.NONCE_LENGTH;
39+
import static org.assertj.core.api.Assertions.assertThat;
40+
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
41+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
42+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
43+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
44+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
3245

3346
@SpringBootTest
34-
class AuthenticationRestControllerTest {
47+
@AutoConfigureMockMvc
48+
class AuthenticationEndpointsFilterTest {
3549

3650
@Autowired
37-
ChallengeController authRestController;
51+
private ObjectMapper mapper;
52+
53+
@Autowired
54+
private MockMvc mvc;
3855

3956
@Test
40-
void testChallengeNonceLength() {
41-
assertThat(authRestController.challenge().getNonce().length())
42-
.isEqualTo(nonceGeneratorNonceBase64Length());
57+
void challengeReturnsNonceWithExpectedBase64Length() throws Exception {
58+
MvcResult result = mvc.perform(get("/auth/challenge").accept(MediaType.APPLICATION_JSON))
59+
.andExpect(status().isOk())
60+
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
61+
.andReturn();
62+
63+
JsonNode json = mapper.readTree(result.getResponse().getContentAsByteArray());
64+
String nonce = json.get("nonce").asText();
65+
66+
assertThat(nonce).isNotBlank();
67+
assertThat(nonce).hasSize(expectedBase64Len());
4368
}
4469

45-
private int nonceGeneratorNonceBase64Length() {
46-
return (NONCE_LENGTH * 8 + 6 - 1) / 6 + 1;
70+
@Test
71+
void mobileInitBuildsDeepLinkWithEmbeddedChallenge() throws Exception {
72+
MvcResult result = mvc.perform(post("/auth/mobile/auth/init")
73+
.with(csrf())
74+
.accept(MediaType.APPLICATION_JSON))
75+
.andExpect(status().isOk())
76+
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
77+
.andReturn();
78+
79+
JsonNode json = mapper.readTree(result.getResponse().getContentAsByteArray());
80+
String eidAuthUri = json.get("eidAuthUri").asText();
81+
82+
assertThat(eidAuthUri).startsWith("web-eid-mobile://auth#");
83+
84+
String fragment = eidAuthUri.substring(eidAuthUri.indexOf('#') + 1);
85+
String payloadJson = new String(Base64.getDecoder().decode(fragment), StandardCharsets.UTF_8);
86+
JsonNode payload = mapper.readTree(payloadJson);
87+
88+
assertThat(payload.get("challenge").asText()).hasSize(expectedBase64Len());
89+
assertThat(payload.get("login_uri").asText()).endsWith("/auth/eid/login");
4790
}
4891

92+
private static int expectedBase64Len() {
93+
return Base64.getEncoder().encodeToString(new byte[NONCE_LENGTH]).length();
94+
}
4995
}

0 commit comments

Comments
 (0)