From df86b707a60989ab78c1b115690adc9370e7bd24 Mon Sep 17 00:00:00 2001 From: jomu Date: Tue, 18 Dec 2018 15:43:57 +0100 Subject: [PATCH] ensured standard API response header is returned if an APIKeyError occurs --- .../account/boundary/ApiKeyError.java | 54 ++++++++++++++++++ .../account/boundary/ApiKeyService.java | 35 ++++++++---- .../shiro/filter/JWTAuthenticationFilter.java | 56 ++++++++++++++++--- .../account/boundary/ApiKeyError.properties | 16 ++++++ .../boundary/ApiKeyError_de_DE.properties | 16 ++++++ .../boundary/ApiKeyError_en_US.properties | 16 ++++++ 6 files changed, 173 insertions(+), 20 deletions(-) create mode 100644 account/src/main/java/de/muehlencord/shared/account/business/account/boundary/ApiKeyError.java create mode 100644 account/src/main/resources/de/muehlencord/shared/account/business/account/boundary/ApiKeyError.properties create mode 100644 account/src/main/resources/de/muehlencord/shared/account/business/account/boundary/ApiKeyError_de_DE.properties create mode 100644 account/src/main/resources/de/muehlencord/shared/account/business/account/boundary/ApiKeyError_en_US.properties diff --git a/account/src/main/java/de/muehlencord/shared/account/business/account/boundary/ApiKeyError.java b/account/src/main/java/de/muehlencord/shared/account/business/account/boundary/ApiKeyError.java new file mode 100644 index 0000000..8590c87 --- /dev/null +++ b/account/src/main/java/de/muehlencord/shared/account/business/account/boundary/ApiKeyError.java @@ -0,0 +1,54 @@ +/* + * Copyright 2018 joern.muehlencord. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.muehlencord.shared.account.business.account.boundary; + +import de.muehlencord.shared.jeeutil.restexfw.APIError; +import javax.ws.rs.core.Response; + +/** + * + * @author joern.muehlencord + */ +public enum ApiKeyError implements APIError { + + JWT_NO_TOKEN(Response.Status.BAD_REQUEST, "1000", "jwt_no_token"), + JWT_TOKEN_INVALID (Response.Status.FORBIDDEN, "1001", "jwt_token_invalid"); + + private final Response.Status status; + private final String errorCode; + private final String messageKey; + + private ApiKeyError(Response.Status status, String errorCode, String messageKey) { + this.status = status; + this.errorCode = errorCode; + this.messageKey = messageKey; + } + + @Override + public Response.Status getStatus() { + return status; + } + + @Override + public String getErrorCode() { + return this.errorCode; + } + + @Override + public String getMessageKey() { + return this.messageKey; + } +} diff --git a/account/src/main/java/de/muehlencord/shared/account/business/account/boundary/ApiKeyService.java b/account/src/main/java/de/muehlencord/shared/account/business/account/boundary/ApiKeyService.java index 367e2f8..d8f041b 100644 --- a/account/src/main/java/de/muehlencord/shared/account/business/account/boundary/ApiKeyService.java +++ b/account/src/main/java/de/muehlencord/shared/account/business/account/boundary/ApiKeyService.java @@ -137,11 +137,19 @@ public class ApiKeyService implements Serializable { public boolean validateJWT(String encodedJWT) { JWTDecoder decoder = new JWTDecoder(password, issuer, encodedJWT); - ApiKeyEntity validKey = getValidKey(decoder.getSubject(), decoder.getUniqueId(), encodedJWT); + ApiKeyEntity validKey; + try { + validKey = getValidKey(decoder.getSubject(), decoder.getUniqueId(), encodedJWT); + } catch (JWTException ex) { + if (LOGGER.isTraceEnabled()) { + LOGGER.trace(ex.toString(), ex); + } + return false; + } return validKey != null; } - private ApiKeyEntity getValidKey(String userName, String apiKey, String authorizationHeader) { + private ApiKeyEntity getValidKey(String userName, String apiKey, String authorizationHeader) throws JWTException { AccountEntity userAccount = accountControl.getAccountEntity(userName, false); List apiKeys = getUsersApiKeys(userAccount); @@ -151,15 +159,9 @@ public class ApiKeyService implements Serializable { ApiKeyEntity key = it.next(); if (key.getApiKey().equals(apiKey)) { ZonedDateTime issuedOn = ZonedDateTime.ofInstant(key.getIssuedOn().toInstant(), ZoneOffset.UTC); - String testString; - - try { - testString = JWTEncoder.encode(password, issuer, issuedOn, key.getAccount().getUsername(), key.getApiKey(), key.getExpiration()); - if (authorizationHeader.equals(testString)) { - return key; - } - } catch (JWTException ex) { - + String testString = JWTEncoder.encode(password, issuer, issuedOn, key.getAccount().getUsername(), key.getApiKey(), key.getExpiration()); + if (authorizationHeader.equals(testString)) { + return key; } } } @@ -206,7 +208,16 @@ public class ApiKeyService implements Serializable { if (jwtObject.isValid()) { String userName = jwtObject.getUserName(); - ApiKeyEntity keyToLogout = getValidKey(userName, jwtObject.getUnqiueId(), authorizationHeader); + ApiKeyEntity keyToLogout; + try { + keyToLogout = getValidKey(userName, jwtObject.getUnqiueId(), authorizationHeader); + } catch (JWTException ex) { + if (LOGGER.isTraceEnabled()) { + LOGGER.trace(ex.getMessage(), ex); + } + + keyToLogout = null; + } if (keyToLogout == null) { // no valid key found - must not happen, JWTVeryfingFIlter should have catched this diff --git a/account/src/main/java/de/muehlencord/shared/account/shiro/filter/JWTAuthenticationFilter.java b/account/src/main/java/de/muehlencord/shared/account/shiro/filter/JWTAuthenticationFilter.java index f26157b..2cc7a21 100644 --- a/account/src/main/java/de/muehlencord/shared/account/shiro/filter/JWTAuthenticationFilter.java +++ b/account/src/main/java/de/muehlencord/shared/account/shiro/filter/JWTAuthenticationFilter.java @@ -15,22 +15,27 @@ */ package de.muehlencord.shared.account.shiro.filter; +import de.muehlencord.shared.account.business.account.boundary.ApiKeyError; import de.muehlencord.shared.account.business.account.boundary.ApiKeyService; import de.muehlencord.shared.account.business.account.entity.JWTObject; import de.muehlencord.shared.account.shiro.token.JWTAuthenticationToken; +import de.muehlencord.shared.account.util.AccountSecurityException; +import de.muehlencord.shared.jeeutil.restexfw.APIException; +import java.io.IOException; import java.io.StringReader; +import java.util.Locale; import javax.json.Json; import javax.json.JsonObject; import javax.json.JsonReader; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; +import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.io.IOUtils; -import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.web.filter.authc.AuthenticatingFilter; @@ -45,9 +50,9 @@ public final class JWTAuthenticationFilter extends AuthenticatingFilter { private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(JWTAuthenticationFilter.class); protected static final String AUTHORIZATION_HEADER = "Authorization"; // NOI18N + protected static final String USERNAME = "username"; // NOI18N + protected static final String PASSWORD = "password"; // NOI18N - public static final String USERNAME = "username"; - public static final String PASSWORD = "password"; private final ApiKeyService apiKeyService = lookupApiKeyServiceBean(); public JWTAuthenticationFilter() { @@ -70,7 +75,7 @@ public final class JWTAuthenticationFilter extends AuthenticatingFilter { return loggedIn; } - + @Override protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception { if (isLoginRequest(request, response)) { @@ -97,22 +102,57 @@ public final class JWTAuthenticationFilter extends AuthenticatingFilter { return new UsernamePasswordToken(); } - protected boolean isLoggedAttempt(ServletRequest request, ServletResponse response) { + private boolean isLoggedAttempt(ServletRequest request, ServletResponse response) { String authzHeader = getAuthzHeader(request); return authzHeader != null; } - protected String getAuthzHeader(ServletRequest request) { + private String getAuthzHeader(ServletRequest request) { HttpServletRequest httpRequest = WebUtils.toHttp(request); return httpRequest.getHeader(AUTHORIZATION_HEADER); } - public JWTAuthenticationToken createToken(String token) { + private JWTAuthenticationToken createToken(String token) throws AccountSecurityException { if (apiKeyService.validateJWT(token)) { JWTObject jwtObject = apiKeyService.getJWTObject(token); return new JWTAuthenticationToken(jwtObject.getUserName(), token); } else { - throw new AuthenticationException("provided API key invalid"); + throw new APIException(ApiKeyError.JWT_TOKEN_INVALID, Locale.ENGLISH); // TODO - how to get the correct locale + } + } + + /** + * Overwrite cleanup to ensure no exception is thrown if an + * AccountSecurityException / APIException is raised during login. As long + * as the user is not logged in JERSEYs ExceptionMapper and intercepor + * classes are overruled by Shiro. + * + * @param request the incoming request + * @param response the response to return + * @param existing the raised exception + * @throws ServletException may be thrown by AuthenticatingFilter.cleanup if + * existing is not a AccountSecurityException + * @throws IOException may be thrown by AuthenticatingFilter.cleanup if + * existing is not a AccountSecurityException + */ + @Override + protected void cleanup(ServletRequest request, ServletResponse response, Exception existing) throws ServletException, IOException { + if ((existing != null) && (existing.getClass().isAssignableFrom(AccountSecurityException.class))) { + HttpServletResponse httpResponse = WebUtils.toHttp(response); + httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } else if ((existing != null) && (existing.getClass().isAssignableFrom(APIException.class))) { + APIException apiException = (APIException) existing; + + HttpServletResponse httpResponse = WebUtils.toHttp(response); + httpResponse.setStatus(apiException.getHttpResponse().getStatus()); + httpResponse.addHeader(APIException.HTTP_HEADER_X_ERROR, apiException.getHttpResponse().getHeaderString(APIException.HTTP_HEADER_X_ERROR)); + httpResponse.addHeader(APIException.HTTP_HEADER_X_ERROR_CODE, apiException.getHttpResponse().getHeaderString(APIException.HTTP_HEADER_X_ERROR_CODE)); + + if(apiException.getHttpResponse().getHeaderString(APIException.HTTP_HEADER_X_ROOT_CAUSE) != null) { + httpResponse.addHeader(APIException.HTTP_HEADER_X_ROOT_CAUSE, apiException.getHttpResponse().getHeaderString(APIException.HTTP_HEADER_X_ROOT_CAUSE)); + } + } else { + super.cleanup(request, response, existing); } } diff --git a/account/src/main/resources/de/muehlencord/shared/account/business/account/boundary/ApiKeyError.properties b/account/src/main/resources/de/muehlencord/shared/account/business/account/boundary/ApiKeyError.properties new file mode 100644 index 0000000..4e9b313 --- /dev/null +++ b/account/src/main/resources/de/muehlencord/shared/account/business/account/boundary/ApiKeyError.properties @@ -0,0 +1,16 @@ +# Copyright 2018 joern.muehlencord. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +jwt_no_token=No token provided +jwt_token_invalid=The provided token is invalid diff --git a/account/src/main/resources/de/muehlencord/shared/account/business/account/boundary/ApiKeyError_de_DE.properties b/account/src/main/resources/de/muehlencord/shared/account/business/account/boundary/ApiKeyError_de_DE.properties new file mode 100644 index 0000000..9841a53 --- /dev/null +++ b/account/src/main/resources/de/muehlencord/shared/account/business/account/boundary/ApiKeyError_de_DE.properties @@ -0,0 +1,16 @@ +# Copyright 2018 joern.muehlencord. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +jwt_no_token=Kein Token vorhanden +jwt_token_invalid=Das Token ist ung\u00fcltig diff --git a/account/src/main/resources/de/muehlencord/shared/account/business/account/boundary/ApiKeyError_en_US.properties b/account/src/main/resources/de/muehlencord/shared/account/business/account/boundary/ApiKeyError_en_US.properties new file mode 100644 index 0000000..4e9b313 --- /dev/null +++ b/account/src/main/resources/de/muehlencord/shared/account/business/account/boundary/ApiKeyError_en_US.properties @@ -0,0 +1,16 @@ +# Copyright 2018 joern.muehlencord. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +jwt_no_token=No token provided +jwt_token_invalid=The provided token is invalid