6
votes

I have a web application which sets a spring security context through a spring filter. Services are protected with spring annotations based on users roles. This works.

Asynchronous tasks are executed in JMS listeners (extend javax.jms.MessageListener). The setup of this listeners is done with Spring.

Messages are sent from the web application, at this time a user is authenticated. I need the same authentication in the JMS thread (user and roles) during message processing.

Today this is done by putting the spring authentication in the JMS ObjectMessage:

SecurityContext context = SecurityContextHolder.getContext();
Authentication auth = context.getAuthentication();
... put the auth object in jms message object

Then inside the JMS listener the authentication object is extracted and set in the context:

SecurityContext context = new SecurityContextImpl();
context.setAuthentication(auth);
SecurityContextHolder.setContext(context);

This works most of the time. But when there is a delay before the processing of a message, message will never be processed. I couldn't determine yet the cause of these messages loss, but I'm not sure the way we propagate authentication is good, even if it works in custer when the message is processed in another server.

Is this the right way to propagate a spring authentication ?

Regards, Mickaël

3
I'm not familiar with Spring in this context, but I've come across this problem before. A good security context will have an expiry time after which it can no longer be used. In general if a message delivery is delayed past the expiry the security context will have expired and the message not processed. This might be the problem here, in which case increasing that expiry time may fix it, or at least make it less common.Alasdair

3 Answers

2
votes

I did not find better solution, but this one works for me just fine.

By sending of JMS Message I'am storing Authentication as Header and respectively by receiving recreating Security Context. In order to store Authentication as Header you have to serialise it as Base64:

class AuthenticationSerializer {

 static String serialize(Authentication authentication) {
    byte[] bytes = SerializationUtils.serialize(authentication);
    return DatatypeConverter.printBase64Binary(bytes);
 }

 static Authentication deserialize(String authentication) {
    byte[] decoded = DatatypeConverter.parseBase64Binary(authentication);
    Authentication auth = (Authentication) SerializationUtils.deserialize(decoded);
    return auth;
  }
}

By sending just set Message header - you can create Decorator for Message Template, so that it will happen automatically. In you decorator just call such method:

private void attachAuthenticationContext(Message message){
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    String serialized = AuthenticationSerializer.serialize(auth);
    message.setStringProperty("authcontext", serialized);
}

Receiving gets more complicated, but it can be also done automatically. Instead of applying @EnableJMS use following Configuration:

@Configuration
class JmsBootstrapConfiguration {

    @Bean(name = JmsListenerConfigUtils.JMS_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME)
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public JmsListenerAnnotationBeanPostProcessor jmsListenerAnnotationProcessor() {
        return new JmsListenerPostProcessor();
    }

    @Bean(name = JmsListenerConfigUtils.JMS_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME)
    public JmsListenerEndpointRegistry defaultJmsListenerEndpointRegistry() {
        return new JmsListenerEndpointRegistry();
    }
}

class JmsListenerPostProcessor extends JmsListenerAnnotationBeanPostProcessor {


    @Override
    protected MethodJmsListenerEndpoint createMethodJmsListenerEndpoint() {
        return new ListenerEndpoint();
    }

}

class ListenerEndpoint extends MethodJmsListenerEndpoint {
    @Override
    protected MessagingMessageListenerAdapter createMessageListenerInstance() {
        return new ListenerAdapter();
    }
}

class ListenerAdapter extends MessagingMessageListenerAdapter {

    @Override
    public void onMessage(Message jmsMessage, Session session) throws JMSException {
        propagateSecurityContext(jmsMessage);
        super.onMessage(jmsMessage, session);
    }

    private void propagateSecurityContext(Message jmsMessage) throws JMSException {
        String authStr = jmsMessage.getStringProperty("authcontext");        
        Authentication auth = AuthenticationSerializer.deserialize(authStr);
        SecurityContextHolder.getContext().setAuthentication(auth);
    }     

}
1
votes

I have implemented for myself a different solution, which seems easier for me.

Already I have a message converter, the standard JSON Jackson message converter, which I need to configure on the JMSTemplate and the listeners.

So I created a MessageConverter implementation which wraps around another message converter, and propagates the security context via the JMS message properties. (In my case, the propagated context is a JWT token which I can extract from the current context and apply to the security context of the listening thread).

This way the entire responsibility for propagation of security context is elegantly implemented in a single class, and requires only a little bit of configuration.

0
votes

Thanks great but I am handling this in easy way . put one util file and solved .

    public class AuthenticationSerializerUtil {
    public static final String AUTH_CONTEXT = "authContext";

    public static String serialize(Authentication authentication) {
        byte[] bytes = SerializationUtils.serialize(authentication);
        return DatatypeConverter.printBase64Binary(bytes);
    }

    public static Authentication deserialize(String authentication) {
        byte[] decoded = DatatypeConverter.parseBase64Binary(authentication);
        Authentication auth = (Authentication) SerializationUtils.deserialize(decoded);
        return auth;
    }

    /**
     * taking message and return string json from message & set current context
     * @param message
     * @return
     */
    public static String jsonAndSetContext(Message message){
        LongString authContext = (LongString)message.getMessageProperties().getHeaders().get(AUTH_CONTEXT);
        Authentication auth = deserialize(authContext.toString());
        SecurityContextHolder.getContext().setAuthentication(auth);
        byte json[] = message.getBody();
        return new String(json);

    }
}