or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

authorization.mdcsrf-protection.mdexpression-handling.mdindex.mdmessage-matching.mdreactive-support.mdsecurity-context.md
tile.json

security-context.mddocs/

Security Context Propagation

Manage authentication context across message processing threads and inject authenticated principals into message handler methods.

Capabilities

SecurityContextChannelInterceptor

Classic approach for security context propagation - obtains Authentication from message headers and populates SecurityContext during message processing.

/**
 * ChannelInterceptor that obtains Authentication from message headers
 * and populates SecurityContext during message processing.
 *
 * @since 4.0
 */
public final class SecurityContextChannelInterceptor
    implements ExecutorChannelInterceptor, ChannelInterceptor {

    /**
     * Default header name for storing authentication in message headers.
     */
    public static final String USER_HEADER = "simpUser";

    /**
     * Creates interceptor using default USER_HEADER for authentication.
     */
    public SecurityContextChannelInterceptor();

    /**
     * Creates interceptor with custom header name for authentication.
     *
     * @param authenticationHeaderName name of header containing authentication
     */
    public SecurityContextChannelInterceptor(String authenticationHeaderName);

    /**
     * Sets anonymous authentication to use when no authentication is present.
     *
     * @param authentication the anonymous authentication
     */
    public void setAnonymousAuthentication(Authentication authentication);

    /**
     * Sets the SecurityContextHolderStrategy for managing security context.
     *
     * @param strategy the strategy to use
     */
    public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy strategy);

    /**
     * Intercepts message before sending and sets up security context.
     *
     * @param message the message being sent
     * @param channel the channel
     * @return the message
     */
    public Message<?> preSend(Message<?> message, MessageChannel channel);

    /**
     * Cleans up security context after message send completes.
     *
     * @param message the message
     * @param channel the channel
     * @param sent whether send succeeded
     * @param ex exception if send failed
     */
    public void afterSendCompletion(
        Message<?> message,
        MessageChannel channel,
        boolean sent,
        Exception ex
    );

    /**
     * Sets up security context before handling message.
     *
     * @param message the message
     * @param channel the channel
     * @param handler the message handler
     * @return the message
     */
    public Message<?> beforeHandle(
        Message<?> message,
        MessageChannel channel,
        MessageHandler handler
    );

    /**
     * Cleans up security context after message handling.
     *
     * @param message the message
     * @param channel the channel
     * @param handler the message handler
     * @param ex exception if handling failed
     */
    public void afterMessageHandled(
        Message<?> message,
        MessageChannel channel,
        MessageHandler handler,
        Exception ex
    );
}

Usage Example:

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.security.config.annotation.web.socket.AbstractSecurityWebSocketMessageBrokerConfigurer;
import org.springframework.security.messaging.context.SecurityContextChannelInterceptor;

@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

    @Override
    protected void configureInbound(ChannelRegistration registration) {
        registration.interceptors(new SecurityContextChannelInterceptor());
    }
}

SecurityContextPropagationChannelInterceptor

Modern approach (since 6.2) - captures Authentication from current SecurityContext in preSend and propagates via message headers. Cannot be used together with SecurityContextChannelInterceptor.

/**
 * ExecutorChannelInterceptor that captures Authentication from current
 * SecurityContext and propagates it via message headers.
 *
 * @since 6.2
 */
public final class SecurityContextPropagationChannelInterceptor
    implements ExecutorChannelInterceptor {

    /**
     * Default header name for storing authentication in message headers.
     */
    public static final String USER_HEADER = "simpUser";

    /**
     * Creates interceptor using default USER_HEADER for authentication.
     */
    public SecurityContextPropagationChannelInterceptor();

    /**
     * Creates interceptor with custom header name for authentication.
     *
     * @param authenticationHeaderName name of header to store authentication
     */
    public SecurityContextPropagationChannelInterceptor(String authenticationHeaderName);

    /**
     * Sets the SecurityContextHolderStrategy for managing security context.
     *
     * @param strategy the strategy to use
     */
    public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy strategy);

    /**
     * Sets anonymous authentication to use when no authentication is present.
     *
     * @param authentication the anonymous authentication
     */
    public void setAnonymousAuthentication(Authentication authentication);

    /**
     * Captures current authentication and stores in message header.
     *
     * @param message the message
     * @param channel the channel
     * @return message with authentication in header
     */
    public Message<?> preSend(Message<?> message, MessageChannel channel);

    /**
     * Restores security context from message header before handling.
     *
     * @param message the message
     * @param channel the channel
     * @param handler the message handler
     * @return the message
     */
    public Message<?> beforeHandle(
        Message<?> message,
        MessageChannel channel,
        MessageHandler handler
    );

    /**
     * Restores security context from message header after receiving.
     *
     * @param message the message
     * @param channel the channel
     * @return the message
     */
    public Message<?> postReceive(Message<?> message, MessageChannel channel);

    /**
     * Cleans up security context after message handling.
     *
     * @param message the message
     * @param channel the channel
     * @param handler the message handler
     * @param ex exception if handling failed
     */
    public void afterMessageHandled(
        Message<?> message,
        MessageChannel channel,
        MessageHandler handler,
        Exception ex
    );
}

Usage Example:

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.security.config.annotation.web.socket.AbstractSecurityWebSocketMessageBrokerConfigurer;
import org.springframework.security.messaging.context.SecurityContextPropagationChannelInterceptor;

@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

    @Override
    protected void configureInbound(ChannelRegistration registration) {
        // Modern approach - captures from current SecurityContext
        registration.interceptors(new SecurityContextPropagationChannelInterceptor());
    }
}

AuthenticationPrincipalArgumentResolver

Resolves method parameters annotated with @AuthenticationPrincipal to inject the authenticated user's principal into message handler methods.

/**
 * HandlerMethodArgumentResolver that resolves parameters annotated
 * with @AuthenticationPrincipal to inject the authenticated principal.
 *
 * @since 4.0
 */
public final class AuthenticationPrincipalArgumentResolver
    implements HandlerMethodArgumentResolver {

    /**
     * Checks if this resolver supports the given parameter.
     *
     * @param parameter the method parameter
     * @return true if parameter is annotated with @AuthenticationPrincipal
     */
    public boolean supportsParameter(MethodParameter parameter);

    /**
     * Resolves the authenticated principal from SecurityContext.
     *
     * @param parameter the method parameter
     * @param message the current message
     * @return the authenticated principal
     */
    public Object resolveArgument(MethodParameter parameter, Message<?> message);

    /**
     * Sets the SecurityContextHolderStrategy for obtaining authentication.
     *
     * @param securityContextHolderStrategy the strategy to use
     * @since 5.8
     */
    public void setSecurityContextHolderStrategy(
        SecurityContextHolderStrategy securityContextHolderStrategy
    );

    /**
     * Sets template defaults for annotation expression evaluation.
     *
     * @param templateDefaults the template defaults
     * @since 6.4
     */
    public void setTemplateDefaults(
        AnnotationTemplateExpressionDefaults templateDefaults
    );
}

Usage Example:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.messaging.context.AuthenticationPrincipalArgumentResolver;
import org.springframework.stereotype.Controller;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

import java.util.List;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(new AuthenticationPrincipalArgumentResolver());
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws").withSockJS();
    }
}

@Controller
public class ChatController {

    @MessageMapping("/chat")
    public void handleChatMessage(
        @AuthenticationPrincipal UserDetails user,
        String message
    ) {
        String username = user.getUsername();
        // Process message from authenticated user...
    }

    @MessageMapping("/profile")
    public void handleProfileUpdate(
        @AuthenticationPrincipal(expression = "username") String username,
        ProfileUpdate update
    ) {
        // username is extracted directly from principal
        // Process profile update...
    }
}

Configuration Patterns

Basic Security Context Setup

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.security.config.annotation.web.socket.AbstractSecurityWebSocketMessageBrokerConfigurer;

@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

    @Override
    protected void configureInbound(ChannelRegistration registration) {
        // Choose ONE of these approaches:

        // Option 1: Classic approach
        registration.interceptors(new SecurityContextChannelInterceptor());

        // Option 2: Modern approach (recommended for new applications)
        // registration.interceptors(new SecurityContextPropagationChannelInterceptor());
    }
}

Custom Authentication Header

@Override
protected void configureInbound(ChannelRegistration registration) {
    // Use custom header name instead of default "simpUser"
    SecurityContextChannelInterceptor interceptor =
        new SecurityContextChannelInterceptor("customAuthHeader");

    registration.interceptors(interceptor);
}

Anonymous Authentication Support

import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;

@Override
protected void configureInbound(ChannelRegistration registration) {
    SecurityContextChannelInterceptor interceptor =
        new SecurityContextChannelInterceptor();

    // Set anonymous authentication for unauthenticated users
    AnonymousAuthenticationToken anonymousAuth = new AnonymousAuthenticationToken(
        "key",
        "anonymousUser",
        AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")
    );
    interceptor.setAnonymousAuthentication(anonymousAuth);

    registration.interceptors(interceptor);
}

Custom SecurityContextHolderStrategy

import org.springframework.security.core.context.SecurityContextHolderStrategy;

@Override
protected void configureInbound(ChannelRegistration registration) {
    SecurityContextChannelInterceptor interceptor =
        new SecurityContextChannelInterceptor();

    // Use custom strategy (e.g., for testing or custom security context management)
    SecurityContextHolderStrategy customStrategy = // ... custom implementation
    interceptor.setSecurityContextHolderStrategy(customStrategy);

    registration.interceptors(interceptor);
}

Principal Resolution in Message Handlers

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;

@Controller
public class MessageHandlers {

    // Inject entire UserDetails object
    @MessageMapping("/simple")
    public void handleSimple(@AuthenticationPrincipal UserDetails user) {
        String username = user.getUsername();
        // ...
    }

    // Extract specific property using SpEL expression
    @MessageMapping("/username")
    public void handleUsername(
        @AuthenticationPrincipal(expression = "username") String username
    ) {
        // username extracted directly
        // ...
    }

    // Work with custom principal types
    @MessageMapping("/custom")
    public void handleCustom(@AuthenticationPrincipal CustomUserPrincipal principal) {
        String email = principal.getEmail();
        // ...
    }

    // Can also inject full Authentication if needed
    @MessageMapping("/auth")
    public void handleAuth(Authentication authentication) {
        Object principal = authentication.getPrincipal();
        // ...
    }
}

Complete Configuration with All Features

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.security.config.annotation.web.socket.AbstractSecurityWebSocketMessageBrokerConfigurer;
import org.springframework.security.messaging.context.AuthenticationPrincipalArgumentResolver;
import org.springframework.security.messaging.context.SecurityContextChannelInterceptor;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;

import java.util.List;

@Configuration
public class CompleteWebSocketSecurityConfig
    extends AbstractSecurityWebSocketMessageBrokerConfigurer {

    @Override
    protected void configureInbound(ChannelRegistration registration) {
        // Add security context propagation
        registration.interceptors(new SecurityContextChannelInterceptor());
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        // Enable @AuthenticationPrincipal in message handlers
        argumentResolvers.add(new AuthenticationPrincipalArgumentResolver());
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws")
            .setAllowedOrigins("*")
            .withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic", "/queue");
        registry.setApplicationDestinationPrefixes("/app");
        registry.setUserDestinationPrefix("/user");
    }
}

Choosing Between SecurityContext Interceptors

SecurityContextChannelInterceptor (Classic)

  • Use when: Working with existing Spring Security WebSocket configurations
  • Behavior: Obtains authentication from message headers
  • Best for: Applications migrating from older Spring Security versions

SecurityContextPropagationChannelInterceptor (Modern)

  • Use when: Starting new applications or can update configurations
  • Behavior: Captures authentication from current SecurityContext and propagates it
  • Best for: New applications, better integration with modern Spring Security patterns
  • Note: Cannot be used together with SecurityContextChannelInterceptor

Important: Choose ONE interceptor - do not use both in the same configuration.

Migration Guide

Migrating from SecurityContextChannelInterceptor to SecurityContextPropagationChannelInterceptor

Step 1: Replace interceptor registration:

// Old (Classic)
@Override
protected void configureInbound(ChannelRegistration registration) {
    registration.interceptors(new SecurityContextChannelInterceptor());
}

// New (Modern)
@Override
protected void configureInbound(ChannelRegistration registration) {
    registration.interceptors(new SecurityContextPropagationChannelInterceptor());
}

Step 2: Verify authentication is available in SecurityContext before message processing

Step 3: Test thoroughly - behavior may differ slightly

Error Handling

Null Authentication Handling

Always check for null authentication:

@MessageMapping("/secure")
public void handleSecure(
    @AuthenticationPrincipal(required = false) UserDetails user,
    String message
) {
    if (user == null) {
        // Handle unauthenticated case
        logger.warn("Unauthenticated message received");
        return;
    }
    // Process authenticated message
}

Security Context Not Available

If security context is not propagated, @AuthenticationPrincipal will fail:

// Ensure interceptor is registered
@Override
protected void configureInbound(ChannelRegistration registration) {
    // Must have one of these:
    registration.interceptors(new SecurityContextChannelInterceptor());
    // OR
    registration.interceptors(new SecurityContextPropagationChannelInterceptor());
}

Troubleshooting

Issue: @AuthenticationPrincipal Returns Null

Causes:

  1. Security context interceptor not registered
  2. Authentication not in SecurityContext
  3. Anonymous authentication not configured

Solutions:

// Ensure interceptor is registered
registration.interceptors(new SecurityContextPropagationChannelInterceptor());

// Check if authentication exists
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null) {
    logger.warn("No authentication in security context");
}

// Configure anonymous authentication if needed
interceptor.setAnonymousAuthentication(anonymousAuth);

Issue: Security Context Not Propagated Across Threads

Causes:

  1. Using wrong SecurityContextHolderStrategy
  2. Thread-local context not copied

Solutions:

// Use InheritableThreadLocalSecurityContextHolderStrategy for thread pools
SecurityContextHolderStrategy strategy = 
    new InheritableThreadLocalSecurityContextHolderStrategy();
interceptor.setSecurityContextHolderStrategy(strategy);

Issue: Multiple Interceptors Conflict

Symptom: Security context not working correctly

Cause: Both interceptors registered

Solution: Remove one interceptor - use only SecurityContextChannelInterceptor OR SecurityContextPropagationChannelInterceptor, not both.