made JWTAuthentication filter work again. Ensured realms not supporting

the JTWtoken based are not considdered when logging in via API key
This commit is contained in:
2019-01-24 13:18:05 +01:00
parent 38592887c5
commit 333508632c
8 changed files with 264 additions and 129 deletions

View File

@ -129,7 +129,7 @@ public class AccountControl implements Serializable {
Subject currentUser = SecurityUtils.getSubject(); Subject currentUser = SecurityUtils.getSubject();
String currentLoggedInUser = currentUser.getPrincipal().toString(); String currentLoggedInUser = currentUser.getPrincipal().toString();
account.setLastUpdatedBy(currentLoggedInUser); account.setLastUpdatedBy(currentLoggedInUser); // FIXME - should be done via updateable
account.setLastUpdatedOn(now); account.setLastUpdatedOn(now);
boolean newAccount = (account.getCreatedOn() == null); boolean newAccount = (account.getCreatedOn() == null);

View File

@ -19,6 +19,7 @@ import java.io.Serializable;
import java.util.Date; import java.util.Date;
import java.util.UUID; import java.util.UUID;
import javax.persistence.Basic; import javax.persistence.Basic;
import javax.persistence.Cacheable;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.GeneratedValue; import javax.persistence.GeneratedValue;
@ -27,6 +28,7 @@ import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne; import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries; import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery; import javax.persistence.NamedQuery;
import javax.persistence.QueryHint;
import javax.persistence.Table; import javax.persistence.Table;
import javax.persistence.Temporal; import javax.persistence.Temporal;
import javax.persistence.TemporalType; import javax.persistence.TemporalType;
@ -41,13 +43,18 @@ import org.hibernate.annotations.Type;
* @author Joern Muehlencord <joern at muehlencord.de> * @author Joern Muehlencord <joern at muehlencord.de>
*/ */
@Entity @Entity
@Cacheable
@Table(name = "api_key") @Table(name = "api_key")
@XmlRootElement @XmlRootElement
@NamedQueries({ @NamedQueries({
@NamedQuery(name = "ApiKeyEntity.findAll", query = "SELECT a FROM ApiKeyEntity a"), @NamedQuery(name = "ApiKeyEntity.findAll", query = "SELECT a FROM ApiKeyEntity a"),
@NamedQuery(name = "ApiKeyEntity.findByApiKey", query = "SELECT a FROM ApiKeyEntity a WHERE a.apiKey = :apiKey"), @NamedQuery(name = "ApiKeyEntity.findByApiKey", query = "SELECT a FROM ApiKeyEntity a WHERE a.apiKey = :apiKey", hints = {
@QueryHint(name = "org.hibernate.cacheable", value = "true"),
@QueryHint(name = "org.hibernate.cacheRegion", value = "Queries")}),
@NamedQuery(name = "ApiKeyEntity.findByIssuedOn", query = "SELECT a FROM ApiKeyEntity a WHERE a.issuedOn = :issuedOn"), @NamedQuery(name = "ApiKeyEntity.findByIssuedOn", query = "SELECT a FROM ApiKeyEntity a WHERE a.issuedOn = :issuedOn"),
@NamedQuery(name = "ApiKeyEntity.findByAccount", query = "SELECT a FROM ApiKeyEntity a WHERE a.account = :account"), @NamedQuery(name = "ApiKeyEntity.findByAccount", query = "SELECT a FROM ApiKeyEntity a WHERE a.account = :account", hints = {
@QueryHint(name = "org.hibernate.cacheable", value = "true"),
@QueryHint(name = "org.hibernate.cacheRegion", value = "Queries")}),
@NamedQuery(name = "ApiKeyEntity.findByExpiration", query = "SELECT a FROM ApiKeyEntity a WHERE a.expiration = :expiration")}) @NamedQuery(name = "ApiKeyEntity.findByExpiration", query = "SELECT a FROM ApiKeyEntity a WHERE a.expiration = :expiration")})
public class ApiKeyEntity implements Serializable { public class ApiKeyEntity implements Serializable {
@ -72,7 +79,7 @@ public class ApiKeyEntity implements Serializable {
@Temporal(TemporalType.TIMESTAMP) @Temporal(TemporalType.TIMESTAMP)
private Date issuedOn; private Date issuedOn;
@Column(name = "expiration") @Column(name = "expiration")
private Short expiration; private Short expiration;
@JoinColumn(name = "account", referencedColumnName = "id") @JoinColumn(name = "account", referencedColumnName = "id")
@ManyToOne(optional = false) @ManyToOne(optional = false)
private AccountEntity account; private AccountEntity account;

View File

@ -94,8 +94,14 @@ public final class JWTAuthenticationFilter extends AuthenticatingFilter {
if (isLoggedAttempt(request, response)) { if (isLoggedAttempt(request, response)) {
String jwtToken = getAuthzHeader(request); String jwtToken = getAuthzHeader(request);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("found jwtToke in header = {}", jwtToken);
}
if (jwtToken != null) { if (jwtToken != null) {
return createToken(jwtToken); JWTObject jwtObject = apiKeyService.getJWTObject(jwtToken);
return new JWTAuthenticationToken(jwtObject.getUserName(), jwtToken);
// return createToken(jwtToken);
} }
} }
@ -142,13 +148,13 @@ public final class JWTAuthenticationFilter extends AuthenticatingFilter {
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
} else if ((existing != null) && (existing.getClass().isAssignableFrom(APIException.class))) { } else if ((existing != null) && (existing.getClass().isAssignableFrom(APIException.class))) {
APIException apiException = (APIException) existing; APIException apiException = (APIException) existing;
HttpServletResponse httpResponse = WebUtils.toHttp(response); HttpServletResponse httpResponse = WebUtils.toHttp(response);
httpResponse.setStatus(apiException.getHttpResponse().getStatus()); 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, 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)); 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) { 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)); httpResponse.addHeader(APIException.HTTP_HEADER_X_ROOT_CAUSE, apiException.getHttpResponse().getHeaderString(APIException.HTTP_HEADER_X_ROOT_CAUSE));
} }
} else { } else {

View File

@ -0,0 +1,82 @@
/*
* Copyright 2019 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.shiro.pam;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.pam.AbstractAuthenticationStrategy;
import org.apache.shiro.realm.Realm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @author joern.muehlencord
*/
public class AllSupportedSuccessfulStrategy extends AbstractAuthenticationStrategy {
private static final Logger LOGGER = LoggerFactory.getLogger(AllSupportedSuccessfulStrategy.class);
public AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
if (!realm.supports(token)) {
return info;
}
return info;
}
public AuthenticationInfo afterAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo info, AuthenticationInfo aggregate, Throwable t)
throws AuthenticationException {
if (t != null) {
if (t instanceof AuthenticationException) {
//propagate:
throw ((AuthenticationException) t);
} else {
String msg = "Unable to acquire account data from realm [" + realm + "]. The ["
+ getClass().getName() + " implementation requires all configured realm(s) to operate successfully "
+ "for a successful authentication.";
throw new AuthenticationException(msg, t);
}
}
if (info == null) {
if (realm.supports(token)) {
String msg = "Realm [" + realm + "] could not find any associated account data for the submitted "
+ "AuthenticationToken [" + token + "]. The [" + getClass().getName() + "] implementation requires "
+ "all configured realm(s) to acquire valid account data for a submitted token during the "
+ "log-in process.";
throw new UnknownAccountException(msg);
} else {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Realm [{}] does not support token of type [{}], skipped realm", realm, token);
}
}
} else {
LOGGER.debug("Account successfully authenticated using realm [{}]", realm);
}
// If non-null account is returned, then the realm was able to authenticate the
// user - so merge the account with any accumulated before
// if the realm does not support the token, null is returned
if (realm.supports(token)) {
merge(info, aggregate);
}
return aggregate;
}
}

View File

@ -54,17 +54,100 @@ public class AccountRealm extends JdbcRealm {
public AccountRealm() { public AccountRealm() {
this.authenticationQuery = "select al.account_password from account a, account_login al where al.account = a.id and a.username = ? and status not in ('LOCKED','DELETED','DISABLED')"; this.authenticationQuery = "select al.account_password from account a, account_login al where al.account = a.id and a.username = ? and status not in ('LOCKED','DELETED','DISABLED')";
this.userRolesQuery = "select r.role_name from application_role r, account_role ar, account a WHERE a.username = ? AND a.id = ar.account AND ar.account_role = r.id"; this.userRolesQuery = "select r.role_name from application_role r, account_role ar, account a WHERE a.username = ? AND a.id = ar.account AND ar.account_role = r.id AND r.application = ?";
this.permissionsQuery = "select permission_name from application_role appr, role_permission rp, application_permission appp WHERE appr.role_name = ? AND appr.application = ? AND rp.application_role = appr.id AND rp.role_permission = appp.id"; this.permissionsQuery = "select permission_name from application_role appr, role_permission rp, application_permission appp WHERE appr.role_name = ? AND appr.application = ? AND rp.application_role = appr.id AND rp.role_permission = appp.id";
this.permissionsLookupEnabled = true; this.permissionsLookupEnabled = true;
} }
@Override @Override
public boolean supports(AuthenticationToken token) { public boolean supports(AuthenticationToken token) {
super.supports(token); return (token != null && ((JWTAuthenticationToken.class.isAssignableFrom(token.getClass())) || (UsernamePasswordToken.class.isAssignableFrom(token.getClass()))));
return token != null && (token instanceof JWTAuthenticationToken || token instanceof UsernamePasswordToken); }
@Override
protected Set<String> getRoleNamesForUser(Connection conn, String username) throws SQLException {
PreparedStatement ps = null;
ResultSet rs = null;
Set<String> roleNames = new LinkedHashSet<>();
try {
ps = conn.prepareStatement(userRolesQuery);
ps.setString(1, username);
ps.setObject(2, UUID.fromString(applicationId)); // this is the changed line - rest is the same as in JDBCRealm
// Execute query
rs = ps.executeQuery();
// Loop over results and add each returned role to a set
while (rs.next()) {
String roleName = rs.getString(1);
// Add the role to the list of names if it isn't null
if (roleName != null) {
roleNames.add(roleName);
} else {
LOGGER.error("Null role name found while retrieving role names for user [{}]", username);
}
}
} finally {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(ps);
}
return roleNames;
} }
/**
* overwritten getPermissions. Only change is to inject the applicationId
* into the the query
*
* @param conn the connection to use
* @param username the user to lookup
* @param roleNames the users roles
* @return a list of permissions
* @throws SQLException if the SQL query fails
*/
@Override
protected Set<String> getPermissions(Connection conn, String username, Collection<String> roleNames) throws SQLException {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("user {} has the following roles: {}", username, roleNames.toString());
LOGGER.debug("looking up permissions for user");
}
PreparedStatement ps = null;
Set<String> permissions = new LinkedHashSet<>();
try {
ps = conn.prepareStatement(permissionsQuery);
for (String roleName : roleNames) {
ps.setString(1, roleName);
ps.setObject(2, UUID.fromString(applicationId)); // this is the changed line - rest is the same as in JDBCRealm
ResultSet rs = null;
try {
// Execute query
rs = ps.executeQuery();
// Loop over results and add each returned role to a set
while (rs.next()) {
String permissionString = rs.getString(1);
// Add the permission to the set of permissions
permissions.add(permissionString);
}
} finally {
JdbcUtils.closeResultSet(rs);
}
}
} finally {
JdbcUtils.closeStatement(ps);
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("user {} has the following permissions: {}", username, permissions.toString());
}
return permissions;
}
public boolean isJwtAuthentication(AuthenticationToken token) { public boolean isJwtAuthentication(AuthenticationToken token) {
if (token == null) { if (token == null) {
throw new AuthenticationException("empty tokens are not supported by this realm"); throw new AuthenticationException("empty tokens are not supported by this realm");
@ -83,54 +166,7 @@ public class AccountRealm extends JdbcRealm {
} else { } else {
throw new AuthenticationException("Handling of " + token.getClass() + " not supported by this realm"); throw new AuthenticationException("Handling of " + token.getClass() + " not supported by this realm");
} }
} }
/**
* overwritten getPermissions. Only change is to inject the applicationId
* into the the query
*
* @param conn the connection to use
* @param username the user to lookup
* @param roleNames the users roles
* @return a list of permissions
* @throws SQLException if the SQL query fails
*/
@Override
protected Set<String> getPermissions(Connection conn, String username, Collection<String> roleNames) throws SQLException {
PreparedStatement ps = null;
Set<String> permissions = new LinkedHashSet<>();
try {
ps = conn.prepareStatement(permissionsQuery);
for (String roleName : roleNames) {
ps.setString(1, roleName);
ps.setObject(2, UUID.fromString(applicationId)); // this is the changed line - rest is the same as in JDBCRealm
ResultSet rs = null;
try {
// Execute query
rs = ps.executeQuery();
// Loop over results and add each returned role to a set
while (rs.next()) {
String permissionString = rs.getString(1);
// Add the permission to the set of permissions
permissions.add(permissionString);
}
} finally {
JdbcUtils.closeResultSet(rs);
}
}
} finally {
JdbcUtils.closeStatement(ps);
}
return permissions;
}
@Override @Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

View File

@ -40,6 +40,11 @@ public class UserNameActiveDirectoryRealm extends ActiveDirectoryRealm {
private boolean permissionsLookupEnabled = true; private boolean permissionsLookupEnabled = true;
protected String fallbackPrincipalSuffix = null; protected String fallbackPrincipalSuffix = null;
@Override
public boolean supports(AuthenticationToken token) {
return (token != null && (UsernamePasswordToken.class.isAssignableFrom(token.getClass())));
}
@Override @Override
protected AuthenticationInfo queryForAuthenticationInfo(AuthenticationToken token, LdapContextFactory ldapContextFactory) throws NamingException { protected AuthenticationInfo queryForAuthenticationInfo(AuthenticationToken token, LdapContextFactory ldapContextFactory) throws NamingException {

View File

@ -43,6 +43,10 @@ public class APIExceptionInterceptor {
// if an exception is thrown during processing, this is passed in to the catch block below // if an exception is thrown during processing, this is passed in to the catch block below
proceedResponse = context.proceed(); proceedResponse = context.proceed();
} catch (Exception ex) { } catch (Exception ex) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Detailed stacktrace", new Object[]{ex});
}
Response errorResponse; Response errorResponse;
if (ex instanceof APIException) { if (ex instanceof APIException) {
errorResponse = ((APIException) ex).getHttpResponse(); errorResponse = ((APIException) ex).getHttpResponse();
@ -56,12 +60,7 @@ public class APIExceptionInterceptor {
throw (ConstraintViolationException) ex.getCause(); throw (ConstraintViolationException) ex.getCause();
} else { } else {
errorResponse = new APIException(ex, locale).getHttpResponse(); errorResponse = new APIException(ex, locale).getHttpResponse();
} }
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(ex.toString(), ex);
}
return errorResponse; return errorResponse;
} }
return proceedResponse; return proceedResponse;

View File

@ -1,63 +1,63 @@
/* /*
* Copyright 2018 jomu. * Copyright 2018 jomu.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package de.muehlencord.shared.jeeutil.restexfw; package de.muehlencord.shared.jeeutil.restexfw;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException; import javax.validation.ConstraintViolationException;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.GenericEntity; import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Request; import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.Variant; import javax.ws.rs.core.Variant;
import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider; import javax.ws.rs.ext.Provider;
/** /**
* *
* @author jomu * @author jomu
*/ */
@Provider @Provider
public class ConstraintViolationMapper implements ExceptionMapper<ConstraintViolationException> { public class ConstraintViolationMapper implements ExceptionMapper<ConstraintViolationException> {
private static List<Variant> acceptableMediaTypes = Variant.mediaTypes(MediaType.APPLICATION_JSON_TYPE, MediaType.APPLICATION_XML_TYPE).build(); private static final List<Variant> ACCEPTABLE_MEDIA_TYPES = Variant.mediaTypes(MediaType.APPLICATION_JSON_TYPE, MediaType.APPLICATION_XML_TYPE).build();
@Context @Context
protected Request request; protected Request request;
@Override @Override
public Response toResponse(ConstraintViolationException ex) { public Response toResponse(ConstraintViolationException ex) {
Set<ConstraintViolation<?>> constViolations = ex.getConstraintViolations(); Set<ConstraintViolation<?>> constViolations = ex.getConstraintViolations();
List<ConstraintViolationEntry> errorList = new ArrayList<>(); List<ConstraintViolationEntry> errorList = new ArrayList<>();
for (ConstraintViolation<?> constraintViolation : constViolations) { constViolations.forEach((constraintViolation) -> {
errorList.add(new ConstraintViolationEntry(constraintViolation)); errorList.add(new ConstraintViolationEntry(constraintViolation));
} });
GenericEntity<List<ConstraintViolationEntry>> entity = new GenericEntity<List<ConstraintViolationEntry>>(errorList) {}; GenericEntity<List<ConstraintViolationEntry>> entity = new GenericEntity<List<ConstraintViolationEntry>>(errorList) {};
return Response.status(Response.Status.BAD_REQUEST).entity(entity).type(getNegotiatedMediaType()).build(); return Response.status(Response.Status.BAD_REQUEST).entity(entity).type(getNegotiatedMediaType()).build();
} }
protected MediaType getNegotiatedMediaType() { protected MediaType getNegotiatedMediaType() {
final Variant selectedMediaType = request.selectVariant(acceptableMediaTypes); final Variant selectedMediaType = request.selectVariant(ACCEPTABLE_MEDIA_TYPES);
if (selectedMediaType == null) { if (selectedMediaType == null) {
return MediaType.APPLICATION_JSON_TYPE; return MediaType.APPLICATION_JSON_TYPE;
} }
return selectedMediaType.getMediaType(); return selectedMediaType.getMediaType();
} }
} }