From f2a53644b618d2959b3d3af36f084106b09785ff Mon Sep 17 00:00:00 2001 From: Konstantin Schaper Date: Thu, 8 Oct 2020 09:58:51 +0200 Subject: [PATCH 1/2] introduce api for handling token validation failed exception --- .../TokenValidationFailedException.java | 51 +++++++++++++++++++ .../scm/web/filter/AuthenticationFilter.java | 13 ++++- .../scm/security/JwtAccessTokenResolver.java | 2 +- .../ScmAtLeastOneSuccessfulStrategy.java | 20 ++++++-- .../security/JwtAccessTokenResolverTest.java | 2 +- .../ScmAtLeastOneSuccessfulStrategyTest.java | 22 +++++++- 6 files changed, 102 insertions(+), 8 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/security/TokenValidationFailedException.java diff --git a/scm-core/src/main/java/sonia/scm/security/TokenValidationFailedException.java b/scm-core/src/main/java/sonia/scm/security/TokenValidationFailedException.java new file mode 100644 index 0000000000..41e83d5100 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/TokenValidationFailedException.java @@ -0,0 +1,51 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.security; + +import org.apache.shiro.authc.AuthenticationException; + +/** + * Thrown by the {@link AccessTokenResolver} when an {@link AccessTokenValidator} fails to validate an access token. + * @since 2.6.2 + */ +@SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here +public class TokenValidationFailedException extends AuthenticationException { + private final AccessTokenValidator validator; + private final AccessToken accessToken; + + public TokenValidationFailedException(AccessTokenValidator validator, AccessToken accessToken) { + super(String.format("Token validator %s failed for access token %s", validator.getClass(), accessToken.getId())); + this.validator = validator; + this.accessToken = accessToken; + } + + public AccessTokenValidator getValidator() { + return validator; + } + + public AccessToken getAccessToken() { + return accessToken; + } +} diff --git a/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java index 30e8df9fe7..25587455c7 100644 --- a/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java +++ b/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java @@ -39,6 +39,7 @@ import sonia.scm.config.ScmConfiguration; import sonia.scm.security.AnonymousMode; import sonia.scm.security.AnonymousToken; import sonia.scm.security.TokenExpiredException; +import sonia.scm.security.TokenValidationFailedException; import sonia.scm.util.HttpUtil; import sonia.scm.util.Util; import sonia.scm.web.WebTokenGenerator; @@ -168,9 +169,15 @@ public class AuthenticationFilter extends HttpFilter { protected void handleTokenExpiredException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, TokenExpiredException tokenExpiredException) throws IOException, ServletException { + logger.trace("rethrow token expired exception"); throw tokenExpiredException; } + protected void handleTokenValidationFailedException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, TokenValidationFailedException tokenValidationFailedException) throws IOException, ServletException { + logger.trace("send unauthorized, because of a failed token validation"); + handleUnauthorized(request, response, chain); + } + /** * Iterates all {@link WebTokenGenerator} and creates an * {@link AuthenticationToken} from the given request. @@ -216,7 +223,11 @@ public class AuthenticationFilter extends HttpFilter { processChain(request, response, chain, subject); } catch (TokenExpiredException ex) { // Rethrow to be caught by TokenExpiredFilter + logger.trace("handle token expired exception"); handleTokenExpiredException(request, response, chain, ex); + } catch (TokenValidationFailedException ex) { + logger.trace("handle token validation failed exception"); + handleTokenValidationFailedException(request, response, chain, ex); } catch (AuthenticationException ex) { logger.warn("authentication failed", ex); handleUnauthorized(request, response, chain); @@ -259,7 +270,7 @@ public class AuthenticationFilter extends HttpFilter { * * @return {@code true} if anonymous access is enabled */ - private boolean isAnonymousAccessEnabled() { + protected boolean isAnonymousAccessEnabled() { return (configuration != null) && configuration.getAnonymousMode() != AnonymousMode.OFF; } } diff --git a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenResolver.java b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenResolver.java index 8f89e7f557..3e8b5f1722 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenResolver.java +++ b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenResolver.java @@ -89,7 +89,7 @@ public final class JwtAccessTokenResolver implements AccessTokenResolver { if (!validator.validate(accessToken)) { String msg = createValidationFailedMessage(validator, accessToken); LOG.debug(msg); - throw new AuthenticationException(msg); + throw new TokenValidationFailedException(validator, accessToken); } } diff --git a/scm-webapp/src/main/java/sonia/scm/security/ScmAtLeastOneSuccessfulStrategy.java b/scm-webapp/src/main/java/sonia/scm/security/ScmAtLeastOneSuccessfulStrategy.java index 98306e077a..818777e671 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/ScmAtLeastOneSuccessfulStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/security/ScmAtLeastOneSuccessfulStrategy.java @@ -55,16 +55,16 @@ public class ScmAtLeastOneSuccessfulStrategy extends AbstractAuthenticationStrat } @Override - public AuthenticationInfo afterAllAttempts(AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException { + public AuthenticationInfo afterAllAttempts(AuthenticationToken token, AuthenticationInfo aggregate) { final List throwables = threadLocal.get(); threadLocal.remove(); if (isAuthenticationSuccessful(aggregate)) { return aggregate; } - Optional tokenExpiredException = findTokenExpiredException(throwables); + Optional specializedException = findSpecializedException(throwables); - if (tokenExpiredException.isPresent()) { - throw tokenExpiredException.get(); + if (specializedException.isPresent()) { + throw specializedException.get(); } else { throw createAuthenticationException(token); } @@ -82,6 +82,18 @@ public class ScmAtLeastOneSuccessfulStrategy extends AbstractAuthenticationStrat return throwables.stream().filter(t -> t instanceof TokenExpiredException).findFirst().map(t -> (TokenExpiredException) t); } + private static Optional findTokenValidationFailedException(List throwables) { + return throwables.stream().filter(t -> t instanceof TokenValidationFailedException).findFirst().map(t -> (TokenValidationFailedException) t); + } + + private static Optional findSpecializedException(List throwables) { + Optional tokenExpiredException = findTokenExpiredException(throwables); + if (tokenExpiredException.isPresent()) { + return tokenExpiredException; + } + return findTokenValidationFailedException(throwables); + } + private static AuthenticationException createAuthenticationException(AuthenticationToken token) { return new AuthenticationException("Authentication token of type [" + token.getClass() + "] " + "could not be authenticated by any configured realms. Please ensure that at least one realm can " + diff --git a/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenResolverTest.java b/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenResolverTest.java index 15d4419952..cfeab5e35e 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenResolverTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenResolverTest.java @@ -110,7 +110,7 @@ public class JwtAccessTokenResolverTest { when(validator.validate(Mockito.any(AccessToken.class))).thenReturn(false); // expect exception - expectedException.expect(AuthenticationException.class); + expectedException.expect(TokenValidationFailedException.class); expectedException.expectMessage(Matchers.containsString("token")); BearerToken bearer = BearerToken.valueOf(compact); diff --git a/scm-webapp/src/test/java/sonia/scm/security/ScmAtLeastOneSuccessfulStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/security/ScmAtLeastOneSuccessfulStrategyTest.java index fde6a9c9f9..21cefb6cfb 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/ScmAtLeastOneSuccessfulStrategyTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/ScmAtLeastOneSuccessfulStrategyTest.java @@ -36,6 +36,7 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import java.util.ArrayList; +import java.util.Arrays; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; @@ -59,6 +60,9 @@ public class ScmAtLeastOneSuccessfulStrategyTest { @Mock TokenExpiredException tokenExpiredException; + @Mock + TokenValidationFailedException tokenValidationFailedException; + @Mock AuthenticationException authenticationException; @@ -77,13 +81,29 @@ public class ScmAtLeastOneSuccessfulStrategyTest { } @Test(expected = TokenExpiredException.class) - public void shouldRethrowException() { + public void shouldRethrowTokenExpiredException() { final ScmAtLeastOneSuccessfulStrategy strategy = new ScmAtLeastOneSuccessfulStrategy(); strategy.threadLocal.set(singletonList(tokenExpiredException)); strategy.afterAllAttempts(token, aggregateInfo); } + @Test(expected = TokenValidationFailedException.class) + public void shouldRethrowTokenValidationFailedException() { + final ScmAtLeastOneSuccessfulStrategy strategy = new ScmAtLeastOneSuccessfulStrategy(); + strategy.threadLocal.set(singletonList(tokenValidationFailedException)); + + strategy.afterAllAttempts(token, aggregateInfo); + } + + @Test(expected = TokenExpiredException.class) + public void shouldPrioritizeRethrowingTokenExpiredExceptionOverTokenValidationFailedException() { + final ScmAtLeastOneSuccessfulStrategy strategy = new ScmAtLeastOneSuccessfulStrategy(); + strategy.threadLocal.set(Arrays.asList(tokenValidationFailedException, tokenExpiredException)); + + strategy.afterAllAttempts(token, aggregateInfo); + } + @Test(expected = AuthenticationException.class) public void shouldThrowGenericErrorIfNonTokenExpiredExceptionWasCaught() { final ScmAtLeastOneSuccessfulStrategy strategy = new ScmAtLeastOneSuccessfulStrategy(); From 11430200bf33d8aea0cb7b354db06f62035e98c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 9 Oct 2020 07:36:46 +0200 Subject: [PATCH 2/2] Log change --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5eb83ea16..860408ff73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased +### Added +- Introduce api for handling token validation failed exception ([#1362](https://github.com/scm-manager/scm-manager/pull/1362)) + ## [2.6.1] - 2020-09-30 ### Fixed - Not found error when using browse command in empty hg repository ([#1355](https://github.com/scm-manager/scm-manager/pull/1355))