/*
* Copyright (C) 2011-2012 Intel Corporation
* All rights reserved.
*/
package com.intel.mtwilson.security.jersey;
import com.intel.mtwilson.model.Md5Digest;
import com.intel.mtwilson.datatypes.Role;
import com.intel.mtwilson.security.core.AuthorizationScheme;
import com.intel.mtwilson.security.core.IPAddressUtil;
import com.intel.mtwilson.security.core.RequestInfo;
import com.intel.mtwilson.security.core.RequestLog;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ContainerRequestContext;
//import com.sun.jersey.spi.container.ContainerRequest;
//import com.sun.jersey.spi.container.ContainerRequestFilter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is a SERVER SIDE FILTER to handle INCOMING REQUESTS.
* This class requires the following libraries:
* com.sun.jersey.spi.container.ContainerRequest and ContainerRequestFilter from the Jersey Server API's
* @since 0.5.1
* @author jbuhacoff
* @deprecated use apache shiro
*/
public class AuthenticationJerseyFilter implements ContainerRequestFilter {
private HmacRequestVerifier hmacAuthorization; // currently this is set via Spring Framework; example is new VerifyAuthorization(new LoginBO()); and new VerifyAuthorization(new HashMapSecretKeyFinder());
private PublicKeyRequestVerifier publickeyAuthorization; // currently this is set via Spring Framework; example is new VerifyAuthorization(new LoginBO()); and new VerifyAuthorization(new HashMapSecretKeyFinder());
private X509RequestVerifier x509Authorization;
private HttpBasicRequestVerifier httpBasicAuthorization;
private RequestLog requestLog;
private static Logger log = LoggerFactory.getLogger(AuthenticationJerseyFilter.class);
private String[] trustWhitelist = null;
private boolean sslRequired = true;
// private boolean antiReplayEnabled = false; // bug #380. permanently false; if customer needs anti-replay protection they should use TLS
public AuthenticationJerseyFilter() {
}
public void setRequestLog(RequestLog finder) { this.requestLog = finder; }
public void setRequestValidator(HmacRequestVerifier validator) { this.hmacAuthorization = validator; }
public void setRequestValidator(PublicKeyRequestVerifier validator) { this.publickeyAuthorization = validator; }
public void setRequestValidator(X509RequestVerifier validator) { this.x509Authorization = validator; }
public void setRequestValidator(HttpBasicRequestVerifier validator) {this.httpBasicAuthorization = validator;}
public void setTrustedRemoteAddress(String[] ipAddressOrSubnet) { trustWhitelist = ipAddressOrSubnet; } // should be set by mtwilson.api.trust=ipaddresslist
public void setSslRequired(boolean required) { sslRequired = required; } // should be set by mtwilson.ssl.required=true/false
// public void setAntiReplayEnabled(boolean enabled) { antiReplayEnabled = enabled; } // bug #380. should be set by configuration...
@javax.ws.rs.core.Context HttpServletRequest servletRequest;
/**
* This filter authenticates the request. If the request is from an authenticated
* user, it will be returned with the proper security context. If the request is
* from a trusted IP Address, it will be returned with a security context showing
* the IP Address as username and with all roles enabled. If the user is not found,
* it will return the request without any security context. If there is any error
* with the request itself such as improperly formed authorization header or any
* problem with the cryptography, it will throw a WebApplicationException with UNAUTHORIZED. If the
* request is insecure (non-SSL) and the server is configured to require secure
* connections, it will throw a WebApplicationException with FORBIDDEN.
* @param request
* @return request object if processing should continue
*/
@Override
public void filter(ContainerRequestContext request) {
log.debug("AuthenticationJerseyFilter request for {} {} with Authorization={}", request.getMethod(), request.getUriInfo().getPath(), request.getHeaderString("Authorization"));
log.debug("AuthenticationJerseyFilter: HTTP method="+request.getMethod());
log.debug("AuthenticationJerseyFilter: Request URI="+request.getUriInfo().getRequestUri());
log.debug("AuthenticationJerseyFilter: Secure/https="+request.getSecurityContext().isSecure());
if( servletRequest != null ) {
log.debug("AuthenticationJerseyFilter: Remote Address="+servletRequest.getRemoteAddr());
}
if( sslRequired && !request.getSecurityContext().isSecure() ) {
log.error("AuthenticationJerseyFilter: rejecting insecure (http) request");
throw new WebApplicationException(Response.status(Response.Status.FORBIDDEN).entity("Secure connection required").build());
}
String requestBody = readEntityBodyQuietly(request);
RequestInfo requestInfo = new RequestInfo();
requestInfo.instance = servletRequest.getLocalAddr();
requestInfo.received = new Date();
requestInfo.content = createRequestString(request, requestBody);
// the administrator may specify a list of IP addresses to trust without requiring an Authorization header. this requirement was added in 0.5.1-sp1 (0.5.2).
log.debug("Trusted remote addresses: {}", StringUtils.join(trustWhitelist, " and "));
log.debug("Client remote address: {}", servletRequest != null ? servletRequest.getRemoteAddr() : "(NO SERVLET REQUEST)");
if( trustWhitelist != null && trustWhitelist.length > 0 && servletRequest != null && servletRequest.getRemoteAddr() != null ) {
String trustedAddress = IPAddressUtil.matchAddressInList(servletRequest.getRemoteAddr(), trustWhitelist);
if( trustedAddress != null ) {
try {
log.debug("Request from trusted remote addr "+servletRequest.getRemoteAddr()+" matches "+trustedAddress+" in mtwilson.api.trust");
User user = new User(servletRequest.getRemoteAddr(), new Role[] { Role.Attestation, Role.Audit, Role.Report, Role.Security, Role.Whitelist }, servletRequest.getRemoteAddr(), Md5Digest.valueOf((request.getMethod()+" "+request.getUriInfo().getPath()+" "+String.valueOf(request.getHeaderString("Date"))).getBytes("UTF-8")));
request.setSecurityContext(new MtWilsonSecurityContext(user, request.getSecurityContext().isSecure()));
requestInfo.source = servletRequest.getRemoteAddr();
requestInfo.md5Hash = user.getMd5Hash().toByteArray();
// MtWilsonThreadLocal.set(new MtWilsonSecurityContext(user, request.isSecure()));
//requestLog.logRequestInfo(requestInfo); // bug #380. NOTE: for trusted ip clients, we DO NOT check for replay attacks.
// return request;
}
catch(Exception e) {
throw new WebApplicationException(Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Cannot log request: "+e.getMessage()).build());
}
}
}
if( request.getHeaderString("Authorization") != null ) {
try {
// support both the symmetric-key HMAC-SHA256 method "MtWilson" and the RSA method "PublicKey"
User user = null;
AuthorizationScheme scheme = getAuthorizationScheme(request.getHeaderString("Authorization"));
log.debug("Authorization scheme is {}", scheme.name());
if( x509Authorization != null && scheme.equals(AuthorizationScheme.X509) ) {
user = x509Authorization.getUserForRequest(request.getMethod(), request.getUriInfo().getRequestUri().toString(), request.getHeaders(), requestBody);
}
else if( publickeyAuthorization != null && scheme.equals(AuthorizationScheme.PublicKey) ) {
user = publickeyAuthorization.getUserForRequest(request.getMethod(), request.getUriInfo().getRequestUri().toString(), request.getHeaders(), requestBody);
}
else if( hmacAuthorization != null && scheme.equals(AuthorizationScheme.MtWilson) ) {
user = hmacAuthorization.getUserForRequest(request.getMethod(), request.getUriInfo().getRequestUri().toString(), request.getHeaders(), requestBody);
}
else if(httpBasicAuthorization != null && scheme.equals(AuthorizationScheme.Basic)) {
user = httpBasicAuthorization.getUserForRequest(request.getMethod(), request.getUriInfo().getRequestUri().toString(), request.getHeaders(), requestBody);
}
if( user != null ) {
requestInfo.source = user.getName();
requestInfo.md5Hash = user.getMd5Hash().toByteArray();
// at this point we have a regular authenticated request with a username (not a trusted-ip request)
// we have an opportunity to check for a replay attack, but:
// 1. if the request is over https, it is already protected against replay by the TLS connection itself.
// 2. if there is a TLS connection but it is only from a proxy to our server, and the client actually has SSL turned off, then we won't know that and the client will not get replay protection.
// 3. if the client's TLS connection extends only to a proxy, and between the proxy and our server there is no SSL, then we won't know the request is protected and we will be doing redundant work by checking.
// for these 3 reasons, the only sane thing to do is allow the system administrator to turn replay protection on or off in the configuration file.
// either way, replay protection only applies to a request using hmac, publickey, or x509 authentication. we don't bother with http basic since there is no way to protect against replay at this level -- it's either protected by TLS or it's not protected at all.
// finally -- anti replay protection is disabled right now (value is always false) ... if customer needs anti-replay protection they should just use TLS
/*
if( antiReplayEnabled && scheme.equals(AuthorizationScheme.Basic) ) { // bug #380. always false
List<RequestInfo> recentRequests = requestLog.findRequestFromSourceWithMd5HashAfter(requestInfo.source, requestInfo.md5Hash, requestInfo.received);
if( !recentRequests.isEmpty() ) {
log.warn("Request has {} duplicates", recentRequests.size());
throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED).entity("Duplicate request").build());
}
}
*/
//requestLog.logRequestInfo(requestInfo); // bug #380.
log.debug("User {} with roles {} is authenticated. Security context is being set.", user.getLoginName(), user.getRoles());
log.info("AuthenticationJerseyFilter: Got user, setting security context");
request.setSecurityContext(new MtWilsonSecurityContext(user, request.getSecurityContext().isSecure()));
log.info("AuthenticationJerseyFilter: Set security context");
// MtWilsonThreadLocal.set(new MtWilsonSecurityContext(user, request.isSecure()));
// return request;
}
}
catch(Exception e) {
throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED).entity(e.getMessage()).build());
}
}
log.info("AuthenticationJerseyFilter: request is NOT AUTHENTICATED (continuing)");
/*
// we need to send back a challenge but we're only going to challenge on the authentication schemes that are
// supported by the current runtime configuration
ResponseBuilder forbidden = Response.status(Response.Status.FORBIDDEN);
if( x509Authorization != null ) {
forbidden = forbidden.header("WWW-Authenticate", "X509 realm=\"Attestation\"");
}
if( publickeyAuthorization != null ) {
forbidden = forbidden.header("WWW-Authenticate", "PublicKey realm=\"Attestation\"");
}
if( hmacAuthorization != null ) {
forbidden = forbidden.header("WWW-Authenticate", "MtWilson realm=\"Attestation\"");
}
throw new WebApplicationException(forbidden.build()); // send one WWW-Authenticate header for each supported scheme.
*
*/
// return request; // let the roles allowed resource filter throw an exception if the user does not have the proper roles
}
/**
* Reads the request input stream and then resets it so that other filters can read it too.
* @param request
* @return
*/
private String readEntityBodyQuietly(ContainerRequestContext request) {
String requestBody=null;
try {
InputStream in = request.getEntityStream();
ByteArrayOutputStream content = new ByteArrayOutputStream();
IOUtils.copy(in, content);
byte[] contentBytes = content.toByteArray();
request.setEntityStream(new ByteArrayInputStream(contentBytes));
requestBody = new String(contentBytes);
//log.debug("AuthenticationJerseyFilter: content follows:\n"+requestBody+"\n");
}
catch(IOException e) {
log.info("AuthenticationJerseyFilter: cannot read input stream");
}
return requestBody;
}
/**
* Identifies the authorization scheme used by the client.
* Supported values are "MtWilson" for HMAC-SHA256 and "PublicKey" for RSA
* @param authorization the content of the Authorization header
* @return
*/
private AuthorizationScheme getAuthorizationScheme(String authorizationHeader) {
if( authorizationHeader == null ) {
throw new IllegalArgumentException("Authorization header is missing");
}
String[] terms = authorizationHeader.split(" ");
if( terms.length == 0 ) {
throw new IllegalArgumentException("Invalid authorization header");
}
String name = terms[0];
try {
AuthorizationScheme scheme = AuthorizationScheme.valueOf(name);
return scheme;
}
catch(Exception e) {
throw new UnsupportedOperationException("Unsupported authorization scheme: "+name, e);
}
}
private String createRequestString(ContainerRequestContext request, String requestBody) {
StringBuilder content = new StringBuilder();
content.append(String.format("%s %s\n", request.getMethod(), request.getUriInfo().getRequestUri()));
MultivaluedMap<String,String> headers = request.getHeaders();
ArrayList<String> headerNames = new ArrayList<String>(headers.keySet());
Collections.sort(headerNames);
for(String headerName : headerNames) {
List<String> values = headers.get(headerName);
if( values != null ) {
for(String value : values) {
content.append(String.format("%s: %s\n", headerName, value));
}
}
}
content.append(String.format("\n%s", requestBody)); // empty line separates body from headers
return content.toString();
}
}