ensured standard API response header is returned if an APIKeyError occurs
This commit is contained in:
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,16 +159,10 @@ 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());
|
||||||
|
|
||||||
try {
|
|
||||||
testString = JWTEncoder.encode(password, issuer, issuedOn, key.getAccount().getUsername(), key.getApiKey(), key.getExpiration());
|
|
||||||
if (authorizationHeader.equals(testString)) {
|
if (authorizationHeader.equals(testString)) {
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
} catch (JWTException ex) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -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
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
Reference in New Issue
Block a user