package org.appverse.web.framework.backend.rest.filters.auth;
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.RSASSASigner;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.glassfish.jersey.message.MessageBodyWorkers;
import org.glassfish.jersey.message.internal.MessageBodyFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.ws.rs.RuntimeType;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.core.*;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.net.URI;
import java.security.interfaces.RSAPrivateKey;
@Component
@Provider
public class JWSJerseyFilter implements ClientRequestFilter {
public static final String JWS_FILTER_KEY="jws.filter.key";
public static final String JWS_AUTHORIZATION_HEADER="Authorization";
public static final String JWS_AUTHORIZATION_START_TOKEN="Bearer ";
private static Logger logger = LoggerFactory.getLogger(JWSJerseyFilter.class);
public static final int PAYLOAD_HEADER_MAX_SIZE = 1024;
private static RSAPrivateKey key = null;
public JWSJerseyFilter(){
}
public JWSJerseyFilter(RSAPrivateKey key){
this.key = key;
}
@Context
private MessageBodyWorkers workers;
@Override
/**
* RSA signatures require a public and private RSA key pair, the public key must be made known to the JWS recipient in order to verify the signatures
**/
public void filter(final ClientRequestContext requestContext) throws IOException {
final Configuration config = requestContext.getConfiguration();
//filter only valid on client-side
if (RuntimeType.CLIENT != config.getRuntimeType()) {
return;
}
//tries to obtain a key
checkParams(requestContext);
//Create RSA-signer with the private key
JWSSigner signer = null;
if (key != null) {
signer = new RSASSASigner(key);
signer.setProvider(new BouncyCastleProvider());
} else {
requestContext.abortWith(
Response.status(Response.Status.BAD_REQUEST).entity("Error invalid " + JWS_FILTER_KEY + " param. Should be a valid RSAPrivateKey")
.build()
);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("URI:: " + getUri(requestContext));
}
Payload objectPay = obtainObjectPay(requestContext);
try
{
// Prepare JWS object with simple string as payload
JWSObject jwsObject = new JWSObject(new JWSHeader(JWSAlgorithm.RS256), objectPay);
// Compute the RSA signature
jwsObject.sign(signer);
String s = jwsObject.serialize();
if (logger.isDebugEnabled())
logger.debug("serialized compact form: " + s);
//add a header with computed signature
requestContext.getHeaders().add(JWS_AUTHORIZATION_HEADER, JWS_AUTHORIZATION_START_TOKEN + s);
} catch (Exception e)
{
logger.error("Error signing message", e);
requestContext.abortWith(
Response.status(Response.Status.BAD_REQUEST).entity("Error signing message")
.build());
}
}
/**
* Check parameters on request Context
* @param requestContext
*/
private void checkParams(ClientRequestContext requestContext){
if (key==null) {
Object keyObject = requestContext.getProperty(JWS_FILTER_KEY);
if (keyObject == null) {
requestContext.abortWith(
Response.status(Response.Status.BAD_REQUEST).entity("Error " + JWS_FILTER_KEY + " param is required")
.build()
);
}else if (keyObject instanceof RSAPrivateKey) {
key = (RSAPrivateKey)keyObject;
}
}
}
/**
* Generates a payload based on request context. On empty content use url as content.
* @param requestContext
* @return payload based on content
*/
private Payload obtainObjectPay(ClientRequestContext requestContext){
Payload objectPay = null;
Object object = requestContext.getEntity();
if (object != null)
{
// buffer into which myBean will be serialized
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Class<Object> type = (Class<Object>) requestContext.getEntityClass();
GenericType<Object> genericType = new GenericType<Object>(type) {
};
// get most appropriate MBW
final MessageBodyWriter<Object> messageBodyWriter =
workers.getMessageBodyWriter(type, type,
new Annotation[] {}, MediaType.APPLICATION_JSON_TYPE);
try {
// use the MBW to serialize myBean into baos
messageBodyWriter.writeTo(object,
object.getClass(), genericType.getType(), new Annotation[] {},
MediaType.APPLICATION_JSON_TYPE,
new MultivaluedHashMap<String, Object>(),
baos);
} catch (IOException e) {
throw new RuntimeException(
"Error while serializing MyBean.", e);
}
String stringJsonOutput = baos.toString();
if (logger.isDebugEnabled())
logger.debug("Entity.toString():: " + stringJsonOutput);
//There is a short limitation for http headers. It depends on server.
//As message payload grows, payload in header is growing too, so we must set a limit.
//It means that we are only signing, validating and checking message integrity of first 1024 characters
//Same logic is applied in server side
if (stringJsonOutput != null
&& stringJsonOutput.length() > PAYLOAD_HEADER_MAX_SIZE)
stringJsonOutput = stringJsonOutput.substring(0, PAYLOAD_HEADER_MAX_SIZE);
objectPay = new Payload(stringJsonOutput);
}
//If request entity is null (usually GET methods) use URI as payload
else {
String reqUri = getUri(requestContext);
objectPay = new Payload(reqUri);
}
return objectPay;
}
private String getUri(ClientRequestContext requestContext){
URI uri = requestContext.getUri();
return uri!=null?uri.getPath():null;
}
public MessageBodyWorkers getWorkers() {
return workers;
}
public void setWorkers(MessageBodyWorkers workers) {
this.workers = workers;
}
}