Skip to content

Commit bc8ec12

Browse files
committed
fixes #3
1 parent bdcf660 commit bc8ec12

File tree

6 files changed

+130
-15
lines changed

6 files changed

+130
-15
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
## Unreleased
44
### ⚠ Breaking
55
### ⭐ New Features
6+
- Allow to change the default behaviour when an authentication failure occurs (Web Servlet only) (fixes [#3](https://github.com/sephiroth-j/spring-security-ltpa2-core/issues/3))
7+
68
### 🐞 Bugs Fixed
79

810
## v1.0.0 - 2020-01-05

src/main/java/de/sephirothj/spring/security/ltpa2/Ltpa2Configurer.java

+14
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121
import lombok.experimental.Accessors;
2222
import org.springframework.http.HttpHeaders;
2323
import org.springframework.lang.NonNull;
24+
import org.springframework.lang.Nullable;
2425
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
2526
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
2627
import org.springframework.security.core.userdetails.UserDetailsService;
28+
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
2729
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
2830
import org.springframework.util.Assert;
2931

@@ -111,6 +113,15 @@ public class Ltpa2Configurer extends AbstractHttpConfigurer<Ltpa2Configurer, Htt
111113
@Setter
112114
private boolean allowExpiredToken = false;
113115

116+
/**
117+
* allows to change the default behaviour when an authentication failure occurs.
118+
* <p>
119+
* The default is to respond with 403 status code</p>
120+
*/
121+
@Setter
122+
@Nullable
123+
private AuthenticationFailureHandler authFailureHandler;
124+
114125
@Override
115126
public void configure(HttpSecurity builder) throws Exception
116127
{
@@ -123,6 +134,9 @@ public void configure(HttpSecurity builder) throws Exception
123134
ltpaFilter.setSharedKey(sharedKey);
124135
ltpaFilter.setSignerKey(signerKey);
125136
ltpaFilter.setAllowExpiredToken(allowExpiredToken);
137+
if (authFailureHandler != null) {
138+
ltpaFilter.setAuthFailureHandler(authFailureHandler);
139+
}
126140
ltpaFilter.afterPropertiesSet();
127141
builder.addFilterAt(ltpaFilter, AbstractPreAuthenticatedProcessingFilter.class);
128142
}

src/main/java/de/sephirothj/spring/security/ltpa2/Ltpa2Filter.java

+27-6
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import lombok.Setter;
2828
import lombok.extern.slf4j.Slf4j;
2929
import org.springframework.http.HttpHeaders;
30+
import org.springframework.http.HttpStatus;
3031
import org.springframework.lang.NonNull;
3132
import org.springframework.lang.Nullable;
3233
import org.springframework.security.authentication.InsufficientAuthenticationException;
@@ -35,6 +36,8 @@
3536
import org.springframework.security.core.context.SecurityContextHolder;
3637
import org.springframework.security.core.userdetails.UserDetails;
3738
import org.springframework.security.core.userdetails.UserDetailsService;
39+
import org.springframework.security.core.userdetails.UsernameNotFoundException;
40+
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
3841
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
3942
import org.springframework.util.Assert;
4043
import org.springframework.web.filter.OncePerRequestFilter;
@@ -107,6 +110,18 @@ public final class Ltpa2Filter extends OncePerRequestFilter
107110
@Setter
108111
private boolean allowExpiredToken = false;
109112

113+
/**
114+
* allows to change the default behaviour when an authentication failure occurs.
115+
* <p>
116+
* The default is to respond with 403 status code</p>
117+
*/
118+
@Setter
119+
@NonNull
120+
private AuthenticationFailureHandler authFailureHandler = (request, response, exception) ->
121+
{
122+
response.sendError(HttpStatus.FORBIDDEN.value(), exception.getLocalizedMessage());
123+
};
124+
110125
@Override
111126
public void afterPropertiesSet()
112127
{
@@ -116,6 +131,7 @@ public void afterPropertiesSet()
116131
Assert.notNull(headerValueIdentifier, "The headerValueIdentifier must not be null");
117132
Assert.notNull(signerKey, "A signerKey is required");
118133
Assert.notNull(sharedKey, "A sharedKey is required");
134+
Assert.notNull(authFailureHandler, "An authFailureHandler is required");
119135
if (allowExpiredToken)
120136
{
121137
log.warn("Expired LTPA2 tokens are allowed, this should only be used for testing!");
@@ -173,7 +189,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
173189
catch (AuthenticationException invalidTokenEx)
174190
{
175191
SecurityContextHolder.clearContext();
176-
response.sendError(HttpServletResponse.SC_FORBIDDEN, invalidTokenEx.getLocalizedMessage());
192+
authFailureHandler.onAuthenticationFailure(request, response, invalidTokenEx);
177193
return;
178194
}
179195

@@ -186,9 +202,9 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
186202
* @param encryptedToken the encrpyted token that sould be verified
187203
* @return a user record but never {@code null}
188204
* @throws AuthenticationException if the token was malformed
189-
* @throws AuthenticationException if the token is expired
190-
* @throws AuthenticationException if the token signature is invalid
191-
* @throws AuthenticationException if the user was not found or has not granted authorities
205+
* @throws InsufficientAuthenticationException if the token is expired
206+
* @throws InsufficientAuthenticationException if the token signature is invalid
207+
* @throws UsernameNotFoundException if the user was not found or has not granted authorities
192208
*/
193209
@NonNull
194210
private UserDetails validateLtpaTokenAndLoadUser(@NonNull final String encryptedToken) throws AuthenticationException
@@ -209,10 +225,15 @@ private UserDetails validateLtpaTokenAndLoadUser(@NonNull final String encrypted
209225
final Ltpa2Token token = Ltpa2Utils.makeInstance(ltpaToken);
210226
return userDetailsService.loadUserByUsername(token.getUser());
211227
}
228+
catch (UsernameNotFoundException ex)
229+
{
230+
log.debug("User not found", ex);
231+
throw ex;
232+
}
212233
catch (AuthenticationException ex)
213234
{
214-
log.debug("User not found or token is malformed", ex);
215-
throw new InsufficientAuthenticationException("token invalid");
235+
log.debug("token is malformed", ex);
236+
throw new InsufficientAuthenticationException("token invalid", ex);
216237
}
217238
}
218239
}

src/main/java/de/sephirothj/spring/security/ltpa2/reactive/Ltpa2AuthConverter.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ private void checkForExpiredToken(final String ltpaToken, final SynchronousSink<
128128
catch (AuthenticationException e)
129129
{
130130
// do not produce mono error, just log and produce empty mono as by contract of ServerAuthenticationConverter
131-
log.warn(e.getLocalizedMessage());
131+
log.warn(e.getLocalizedMessage(), e);
132132
}
133133
}
134134

@@ -148,7 +148,7 @@ private void checkTokenSignature(final String ltpaToken, final SynchronousSink<S
148148
catch (AuthenticationException e)
149149
{
150150
// do not produce mono error, just log and produce empty mono as by contract of ServerAuthenticationConverter
151-
log.warn(e.getLocalizedMessage());
151+
log.warn(e.getLocalizedMessage(), e);
152152
}
153153
}
154154

@@ -171,7 +171,7 @@ public Mono<Authentication> convert(ServerWebExchange exchange)
171171
.map(encryptedToken -> Ltpa2Utils.decryptLtpa2Token(encryptedToken, sharedKey))
172172
.onErrorResume(e ->
173173
{
174-
log.warn(e.getLocalizedMessage());
174+
log.warn(e.getLocalizedMessage(), e);
175175
return Mono.empty();
176176
})
177177
.handle(this::checkForExpiredToken)

src/test/java/de/sephirothj/spring/security/ltpa2/Ltpa2ConfigurerTest.java

+54-6
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,18 @@
1515
*/
1616
package de.sephirothj.spring.security.ltpa2;
1717

18+
import java.security.PublicKey;
19+
import javax.crypto.SecretKey;
1820
import org.junit.jupiter.api.Test;
19-
import org.mockito.ArgumentMatchers;
21+
import org.mockito.ArgumentCaptor;
2022
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
2123
import org.springframework.security.core.userdetails.UserDetailsService;
24+
import org.springframework.security.web.authentication.AuthenticationEntryPointFailureHandler;
25+
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
26+
import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint;
2227
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
2328

29+
import static org.assertj.core.api.Assertions.assertThat;
2430
import static org.mockito.ArgumentMatchers.eq;
2531
import static org.mockito.BDDMockito.given;
2632
import static org.mockito.Mockito.mock;
@@ -37,15 +43,57 @@ void testConfigure() throws Exception
3743
{
3844
HttpSecurity httpSecurity = mock(HttpSecurity.class);
3945
given(httpSecurity.getSharedObject(UserDetailsService.class)).will(invocation -> mock(invocation.getArgument(0)));
46+
final String headerName = "header";
47+
final String cookieName = "cookie";
48+
final SecretKey sharedKey = LtpaKeyUtils.decryptSharedKey(Constants.ENCRYPTED_SHARED_KEY, Constants.ENCRYPTION_PASSWORD);
49+
final PublicKey publicKey = LtpaKeyUtils.decodePublicKey(Constants.ENCODED_PUBLIC_KEY);
4050

4151
new Ltpa2Configurer()
42-
.headerName("header")
43-
.cookieName("cookie")
52+
.headerName(headerName)
53+
.cookieName(cookieName)
4454
.allowExpiredToken(true)
45-
.sharedKey(LtpaKeyUtils.decryptSharedKey(Constants.ENCRYPTED_SHARED_KEY, Constants.ENCRYPTION_PASSWORD))
46-
.signerKey(LtpaKeyUtils.decodePublicKey(Constants.ENCODED_PUBLIC_KEY))
55+
.sharedKey(sharedKey)
56+
.signerKey(publicKey)
4757
.configure(httpSecurity);
4858

49-
verify(httpSecurity).addFilterAt(ArgumentMatchers.isA(Ltpa2Filter.class), eq(AbstractPreAuthenticatedProcessingFilter.class));
59+
ArgumentCaptor<Ltpa2Filter> configuredFilter = ArgumentCaptor.forClass(Ltpa2Filter.class);
60+
verify(httpSecurity).addFilterAt(configuredFilter.capture(), eq(AbstractPreAuthenticatedProcessingFilter.class));
61+
assertThat(configuredFilter.getValue())
62+
.hasFieldOrPropertyWithValue("headerName", headerName)
63+
.hasFieldOrPropertyWithValue("headerValueIdentifier", "LtpaToken2 ")
64+
.hasFieldOrPropertyWithValue("cookieName", cookieName)
65+
.hasFieldOrPropertyWithValue("allowExpiredToken", true)
66+
.hasFieldOrPropertyWithValue("sharedKey", sharedKey)
67+
.hasFieldOrPropertyWithValue("signerKey", publicKey)
68+
.extracting("authFailureHandler").isNotNull()
69+
;
70+
}
71+
72+
@Test
73+
void testConfigureWithAuthFaulureHandler() throws Exception
74+
{
75+
HttpSecurity httpSecurity = mock(HttpSecurity.class);
76+
given(httpSecurity.getSharedObject(UserDetailsService.class)).will(invocation -> mock(invocation.getArgument(0)));
77+
final SecretKey sharedKey = LtpaKeyUtils.decryptSharedKey(Constants.ENCRYPTED_SHARED_KEY, Constants.ENCRYPTION_PASSWORD);
78+
final PublicKey publicKey = LtpaKeyUtils.decodePublicKey(Constants.ENCODED_PUBLIC_KEY);
79+
final AuthenticationFailureHandler failureHandler = new AuthenticationEntryPointFailureHandler(new Http403ForbiddenEntryPoint());
80+
81+
new Ltpa2Configurer()
82+
.sharedKey(sharedKey)
83+
.signerKey(publicKey)
84+
.authFailureHandler(failureHandler)
85+
.configure(httpSecurity);
86+
87+
ArgumentCaptor<Ltpa2Filter> configuredFilter = ArgumentCaptor.forClass(Ltpa2Filter.class);
88+
verify(httpSecurity).addFilterAt(configuredFilter.capture(), eq(AbstractPreAuthenticatedProcessingFilter.class));
89+
assertThat(configuredFilter.getValue())
90+
.hasFieldOrPropertyWithValue("headerName", "Authorization")
91+
.hasFieldOrPropertyWithValue("headerValueIdentifier", "LtpaToken2 ")
92+
.hasFieldOrPropertyWithValue("cookieName", "LtpaToken2")
93+
.hasFieldOrPropertyWithValue("allowExpiredToken", false)
94+
.hasFieldOrPropertyWithValue("sharedKey", sharedKey)
95+
.hasFieldOrPropertyWithValue("signerKey", publicKey)
96+
.hasFieldOrPropertyWithValue("authFailureHandler", failureHandler)
97+
;
5098
}
5199
}

src/test/java/de/sephirothj/spring/security/ltpa2/Ltpa2FilterTest.java

+30
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import java.nio.charset.StandardCharsets;
2121
import java.security.GeneralSecurityException;
2222
import java.security.PublicKey;
23+
import java.util.concurrent.CountDownLatch;
24+
import java.util.concurrent.TimeUnit;
2325
import javax.crypto.Cipher;
2426
import javax.crypto.SecretKey;
2527
import javax.crypto.spec.IvParameterSpec;
@@ -293,4 +295,32 @@ void doFilterInternalShouldCause403ForUnknownUsers() throws ServletException, IO
293295
assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
294296
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN);
295297
}
298+
299+
@Test
300+
void doFilterInternalShouldCause401ForUnknownUsersWithCustomFailureHandler() throws ServletException, IOException, GeneralSecurityException, InterruptedException
301+
{
302+
HttpServletRequest request = MockMvcRequestBuilders.get("/").header(HttpHeaders.AUTHORIZATION, "LtpaToken2 ".concat(Constants.TEST_TOKEN)).buildRequest(new MockServletContext());
303+
HttpServletResponse response = new MockHttpServletResponse();
304+
UserDetailsService userDetailsService = Mockito.mock(UserDetailsService.class);
305+
given(userDetailsService.loadUserByUsername(anyString())).willThrow(UsernameNotFoundException.class);
306+
Ltpa2Filter uut = new Ltpa2Filter();
307+
uut.setAllowExpiredToken(true);
308+
uut.setUserDetailsService(userDetailsService);
309+
uut.setSharedKey(LtpaKeyUtils.decryptSharedKey(Constants.ENCRYPTED_SHARED_KEY, Constants.ENCRYPTION_PASSWORD));
310+
uut.setSignerKey(LtpaKeyUtils.decodePublicKey(Constants.ENCODED_PUBLIC_KEY));
311+
CountDownLatch authFailureHandlerInvocations = new CountDownLatch(1);
312+
uut.setAuthFailureHandler((req, res, ex) ->
313+
{
314+
authFailureHandlerInvocations.countDown();
315+
assertThat(ex).isInstanceOf(UsernameNotFoundException.class);
316+
res.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
317+
});
318+
319+
uut.doFilter(request, response, new MockFilterChain());
320+
321+
verify(userDetailsService).loadUserByUsername(anyString());
322+
assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
323+
authFailureHandlerInvocations.await(1, TimeUnit.SECONDS);
324+
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
325+
}
296326
}

0 commit comments

Comments
 (0)