ensured standard API response header is returned if an APIKeyError occurs

This commit is contained in:
2018-12-18 15:43:57 +01:00
parent c3fafa4331
commit df86b707a6
6 changed files with 173 additions and 20 deletions

View File

@ -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;
}
}

View File

@ -137,11 +137,19 @@ public class ApiKeyService implements Serializable {
public boolean validateJWT(String encodedJWT) { public boolean validateJWT(String encodedJWT) {
JWTDecoder decoder = new JWTDecoder(password, issuer, 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; 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); AccountEntity userAccount = accountControl.getAccountEntity(userName, false);
List<ApiKeyEntity> apiKeys = getUsersApiKeys(userAccount); List<ApiKeyEntity> apiKeys = getUsersApiKeys(userAccount);
@ -151,15 +159,9 @@ public class ApiKeyService implements Serializable {
ApiKeyEntity key = it.next(); ApiKeyEntity key = it.next();
if (key.getApiKey().equals(apiKey)) { if (key.getApiKey().equals(apiKey)) {
ZonedDateTime issuedOn = ZonedDateTime.ofInstant(key.getIssuedOn().toInstant(), ZoneOffset.UTC); ZonedDateTime issuedOn = ZonedDateTime.ofInstant(key.getIssuedOn().toInstant(), ZoneOffset.UTC);
String testString; String testString = JWTEncoder.encode(password, issuer, issuedOn, key.getAccount().getUsername(), key.getApiKey(), key.getExpiration());
if (authorizationHeader.equals(testString)) {
try { return key;
testString = JWTEncoder.encode(password, issuer, issuedOn, key.getAccount().getUsername(), key.getApiKey(), key.getExpiration());
if (authorizationHeader.equals(testString)) {
return key;
}
} catch (JWTException ex) {
} }
} }
} }
@ -206,7 +208,16 @@ public class ApiKeyService implements Serializable {
if (jwtObject.isValid()) { if (jwtObject.isValid()) {
String userName = jwtObject.getUserName(); 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) { if (keyToLogout == null) {
// no valid key found - must not happen, JWTVeryfingFIlter should have catched this // no valid key found - must not happen, JWTVeryfingFIlter should have catched this

View File

@ -15,22 +15,27 @@
*/ */
package de.muehlencord.shared.account.shiro.filter; 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.boundary.ApiKeyService;
import de.muehlencord.shared.account.business.account.entity.JWTObject; import de.muehlencord.shared.account.business.account.entity.JWTObject;
import de.muehlencord.shared.account.shiro.token.JWTAuthenticationToken; 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.io.StringReader;
import java.util.Locale;
import javax.json.Json; import javax.json.Json;
import javax.json.JsonObject; import javax.json.JsonObject;
import javax.json.JsonReader; import javax.json.JsonReader;
import javax.naming.Context; import javax.naming.Context;
import javax.naming.InitialContext; import javax.naming.InitialContext;
import javax.naming.NamingException; import javax.naming.NamingException;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest; import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse; import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter; 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); private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(JWTAuthenticationFilter.class);
protected static final String AUTHORIZATION_HEADER = "Authorization"; // NOI18N 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(); private final ApiKeyService apiKeyService = lookupApiKeyServiceBean();
public JWTAuthenticationFilter() { public JWTAuthenticationFilter() {
@ -97,22 +102,57 @@ public final class JWTAuthenticationFilter extends AuthenticatingFilter {
return new UsernamePasswordToken(); return new UsernamePasswordToken();
} }
protected boolean isLoggedAttempt(ServletRequest request, ServletResponse response) { private boolean isLoggedAttempt(ServletRequest request, ServletResponse response) {
String authzHeader = getAuthzHeader(request); String authzHeader = getAuthzHeader(request);
return authzHeader != null; return authzHeader != null;
} }
protected String getAuthzHeader(ServletRequest request) { private String getAuthzHeader(ServletRequest request) {
HttpServletRequest httpRequest = WebUtils.toHttp(request); HttpServletRequest httpRequest = WebUtils.toHttp(request);
return httpRequest.getHeader(AUTHORIZATION_HEADER); return httpRequest.getHeader(AUTHORIZATION_HEADER);
} }
public JWTAuthenticationToken createToken(String token) { private JWTAuthenticationToken createToken(String token) throws AccountSecurityException {
if (apiKeyService.validateJWT(token)) { if (apiKeyService.validateJWT(token)) {
JWTObject jwtObject = apiKeyService.getJWTObject(token); JWTObject jwtObject = apiKeyService.getJWTObject(token);
return new JWTAuthenticationToken(jwtObject.getUserName(), token); return new JWTAuthenticationToken(jwtObject.getUserName(), token);
} else { } 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);
} }
} }

View File

@ -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

View File

@ -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

View File

@ -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