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

reactive-support.mddocs/

Reactive Support

Support for reactive messaging applications using Project Reactor, providing reactive argument resolvers for injecting authentication into handler methods.

Overview

The reactive support package provides argument resolvers for reactive message handlers that return Mono or Flux. These are used in reactive messaging applications built with Project Reactor.

Package: org.springframework.security.messaging.handler.invocation.reactive

Capabilities

AuthenticationPrincipalArgumentResolver (Reactive)

Resolves @AuthenticationPrincipal annotated parameters for reactive message handlers, returning the authenticated principal wrapped in a Mono.

/**
 * Reactive HandlerMethodArgumentResolver for @AuthenticationPrincipal parameters.
 * Returns Mono<Object> to support reactive message handling.
 *
 * @since 5.2
 */
public class AuthenticationPrincipalArgumentResolver
    implements HandlerMethodArgumentResolver {

    /**
     * Sets the BeanResolver for resolving beans in SpEL expressions.
     *
     * @param beanResolver the bean resolver
     */
    public void setBeanResolver(BeanResolver beanResolver);

    /**
     * Sets the ReactiveAdapterRegistry for reactive type conversions.
     *
     * @param adapterRegistry the adapter registry
     */
    public void setAdapterRegistry(ReactiveAdapterRegistry adapterRegistry);

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

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

    /**
     * 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.reactive.ArgumentResolverConfigurer;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.messaging.handler.invocation.reactive.AuthenticationPrincipalArgumentResolver;
import org.springframework.stereotype.Controller;
import reactor.core.publisher.Mono;

@Configuration
public class ReactiveMessageConfig {

    @Bean
    public ArgumentResolverConfigurer argumentResolverConfigurer() {
        ArgumentResolverConfigurer configurer = new ArgumentResolverConfigurer();
        configurer.addCustomResolver(new AuthenticationPrincipalArgumentResolver());
        return configurer;
    }
}

@Controller
public class ReactiveMessageHandler {

    @MessageMapping("chat.message")
    public Mono<Void> handleMessage(
        @AuthenticationPrincipal Mono<UserDetails> userMono,
        Mono<String> messageMono
    ) {
        return Mono.zip(userMono, messageMono)
            .flatMap(tuple -> {
                UserDetails user = tuple.getT1();
                String message = tuple.getT2();

                // Process reactive message with authenticated user
                return processMessage(user.getUsername(), message);
            });
    }

    @MessageMapping("profile.update")
    public Mono<Void> handleProfileUpdate(
        @AuthenticationPrincipal(expression = "username") Mono<String> usernameMono,
        Mono<ProfileUpdate> updateMono
    ) {
        return Mono.zip(usernameMono, updateMono)
            .flatMap(tuple -> {
                String username = tuple.getT1();
                ProfileUpdate update = tuple.getT2();

                return updateProfile(username, update);
            });
    }

    private Mono<Void> processMessage(String username, String message) {
        // Implementation
        return Mono.empty();
    }

    private Mono<Void> updateProfile(String username, ProfileUpdate update) {
        // Implementation
        return Mono.empty();
    }
}

CurrentSecurityContextArgumentResolver (Reactive)

Resolves @CurrentSecurityContext annotated parameters for reactive message handlers, providing access to the full SecurityContext.

/**
 * Reactive HandlerMethodArgumentResolver for @CurrentSecurityContext parameters.
 * Returns Mono<SecurityContext> to support reactive message handling.
 *
 * @since 5.2
 */
public class CurrentSecurityContextArgumentResolver
    implements HandlerMethodArgumentResolver {

    /**
     * Sets the BeanResolver for resolving beans in SpEL expressions.
     *
     * @param beanResolver the bean resolver
     */
    public void setBeanResolver(BeanResolver beanResolver);

    /**
     * Sets the ReactiveAdapterRegistry for reactive type conversions.
     *
     * @param adapterRegistry the adapter registry
     */
    public void setAdapterRegistry(ReactiveAdapterRegistry adapterRegistry);

    /**
     * Checks if this resolver supports the given parameter.
     * Supports parameters annotated with @CurrentSecurityContext or Mono<SecurityContext>.
     *
     * @param parameter the method parameter
     * @return true if parameter is supported
     */
    public boolean supportsParameter(MethodParameter parameter);

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

    /**
     * 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.messaging.handler.annotation.MessageMapping;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.CurrentSecurityContext;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.stereotype.Controller;
import reactor.core.publisher.Mono;

@Controller
public class ReactiveSecurityContextHandler {

    @MessageMapping("admin.action")
    public Mono<Void> handleAdminAction(
        @CurrentSecurityContext Mono<SecurityContext> contextMono,
        Mono<AdminAction> actionMono
    ) {
        return Mono.zip(contextMono, actionMono)
            .flatMap(tuple -> {
                SecurityContext context = tuple.getT1();
                AdminAction action = tuple.getT2();

                Authentication auth = context.getAuthentication();
                // Perform action with full security context
                return performAction(auth, action);
            });
    }

    @MessageMapping("audit.log")
    public Mono<Void> handleAuditLog(
        @CurrentSecurityContext(expression = "authentication")
        Mono<Authentication> authMono,
        Mono<AuditEntry> entryMono
    ) {
        return Mono.zip(authMono, entryMono)
            .flatMap(tuple -> {
                Authentication auth = tuple.getT1();
                AuditEntry entry = tuple.getT2();

                // Log with authentication info
                return logAuditEntry(auth.getName(), entry);
            });
    }

    // Direct Mono<SecurityContext> parameter (no annotation needed)
    @MessageMapping("security.check")
    public Mono<Boolean> checkSecurity(Mono<SecurityContext> contextMono) {
        return contextMono
            .map(context -> context.getAuthentication() != null &&
                            context.getAuthentication().isAuthenticated());
    }

    private Mono<Void> performAction(Authentication auth, AdminAction action) {
        // Implementation
        return Mono.empty();
    }

    private Mono<Void> logAuditEntry(String username, AuditEntry entry) {
        // Implementation
        return Mono.empty();
    }
}

Configuration

Complete Reactive Messaging Security Configuration

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.handler.invocation.reactive.ArgumentResolverConfigurer;
import org.springframework.messaging.rsocket.RSocketStrategies;
import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.rsocket.EnableRSocketSecurity;
import org.springframework.security.config.annotation.rsocket.RSocketSecurity;
import org.springframework.security.messaging.handler.invocation.reactive.AuthenticationPrincipalArgumentResolver;
import org.springframework.security.messaging.handler.invocation.reactive.CurrentSecurityContextArgumentResolver;
import org.springframework.security.rsocket.core.PayloadSocketAcceptorInterceptor;

@Configuration
@EnableRSocketSecurity
public class ReactiveMessagingSecurityConfig {

    @Bean
    public PayloadSocketAcceptorInterceptor authorization(RSocketSecurity security) {
        security
            .authorizePayload(authorize -> authorize
                .route("chat.**").authenticated()
                .route("admin.**").hasRole("ADMIN")
                .anyExchange().permitAll()
            )
            .simpleAuthentication(Customizer.withDefaults());

        return security.build();
    }

    @Bean
    public RSocketMessageHandler messageHandler(RSocketStrategies strategies) {
        RSocketMessageHandler handler = new RSocketMessageHandler();
        handler.setRSocketStrategies(strategies);

        // Add reactive argument resolvers
        ArgumentResolverConfigurer configurer = new ArgumentResolverConfigurer();
        configurer.addCustomResolver(new AuthenticationPrincipalArgumentResolver());
        configurer.addCustomResolver(new CurrentSecurityContextArgumentResolver());

        handler.setArgumentResolverConfigurer(configurer);
        return handler;
    }
}

WebFlux Reactive WebSocket Configuration

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.handler.invocation.reactive.ArgumentResolverConfigurer;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.messaging.handler.invocation.reactive.AuthenticationPrincipalArgumentResolver;
import org.springframework.security.messaging.handler.invocation.reactive.CurrentSecurityContextArgumentResolver;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class ReactiveWebSocketConfig {

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        return http
            .authorizeExchange(exchanges -> exchanges
                .pathMatchers("/ws/**").authenticated()
                .anyExchange().permitAll()
            )
            .httpBasic(Customizer.withDefaults())
            .build();
    }

    @Bean
    public HandlerMapping webSocketHandlerMapping(WebSocketHandler wsHandler) {
        Map<String, WebSocketHandler> map = new HashMap<>();
        map.put("/ws/chat", wsHandler);

        SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping();
        handlerMapping.setOrder(1);
        handlerMapping.setUrlMap(map);
        return handlerMapping;
    }

    @Bean
    public WebSocketHandlerAdapter handlerAdapter() {
        return new WebSocketHandlerAdapter();
    }

    @Bean
    public ArgumentResolverConfigurer argumentResolverConfigurer() {
        ArgumentResolverConfigurer configurer = new ArgumentResolverConfigurer();
        configurer.addCustomResolver(new AuthenticationPrincipalArgumentResolver());
        configurer.addCustomResolver(new CurrentSecurityContextArgumentResolver());
        return configurer;
    }
}

Usage Patterns

Basic Principal Access

@Controller
public class ChatController {

    @MessageMapping("chat.send")
    public Mono<ChatMessage> sendMessage(
        @AuthenticationPrincipal Mono<UserDetails> userMono,
        Mono<String> contentMono
    ) {
        return Mono.zip(userMono, contentMono)
            .map(tuple -> {
                UserDetails user = tuple.getT1();
                String content = tuple.getT2();

                return new ChatMessage(user.getUsername(), content);
            });
    }
}

Extracting Principal Properties with SpEL

@Controller
public class UserController {

    @MessageMapping("user.update")
    public Mono<Void> updateUser(
        @AuthenticationPrincipal(expression = "username") Mono<String> usernameMono,
        Mono<UserUpdate> updateMono
    ) {
        return Mono.zip(usernameMono, updateMono)
            .flatMap(tuple -> userService.update(tuple.getT1(), tuple.getT2()));
    }
}

Working with SecurityContext

@Controller
public class AuditController {

    @MessageMapping("audit.create")
    public Mono<Void> createAuditEntry(
        @CurrentSecurityContext Mono<SecurityContext> contextMono,
        Mono<AuditData> dataMono
    ) {
        return Mono.zip(contextMono, dataMono)
            .flatMap(tuple -> {
                SecurityContext context = tuple.getT1();
                AuditData data = tuple.getT2();

                Authentication auth = context.getAuthentication();
                return auditService.create(auth, data);
            });
    }
}

Custom Principal Types

public class CustomUserDetails implements UserDetails {
    private String username;
    private String email;
    private Long userId;

    // ... getters and UserDetails methods
}

@Controller
public class ProfileController {

    @MessageMapping("profile.get")
    public Mono<Profile> getProfile(
        @AuthenticationPrincipal Mono<CustomUserDetails> userMono
    ) {
        return userMono
            .flatMap(user -> profileService.getProfile(user.getUserId()));
    }

    @MessageMapping("profile.email")
    public Mono<String> getEmail(
        @AuthenticationPrincipal(expression = "email") Mono<String> emailMono
    ) {
        return emailMono;
    }
}

Combining Multiple Reactive Parameters

@Controller
public class CollaborationController {

    @MessageMapping("collab.join")
    public Mono<CollaborationSession> joinSession(
        @AuthenticationPrincipal Mono<UserDetails> userMono,
        @CurrentSecurityContext(expression = "authentication.authorities")
        Mono<Collection<? extends GrantedAuthority>> authoritiesMono,
        Mono<String> sessionIdMono
    ) {
        return Mono.zip(userMono, authoritiesMono, sessionIdMono)
            .flatMap(tuple -> {
                UserDetails user = tuple.getT1();
                Collection<? extends GrantedAuthority> authorities = tuple.getT2();
                String sessionId = tuple.getT3();

                return collaborationService.join(
                    user.getUsername(),
                    authorities,
                    sessionId
                );
            });
    }
}

Error Handling with Reactive Security

@Controller
public class SecureController {

    @MessageMapping("secure.action")
    public Mono<Response> secureAction(
        @AuthenticationPrincipal Mono<UserDetails> userMono,
        Mono<ActionRequest> requestMono
    ) {
        return Mono.zip(userMono, requestMono)
            .flatMap(tuple -> {
                UserDetails user = tuple.getT1();
                ActionRequest request = tuple.getT2();

                return performAction(user, request);
            })
            .onErrorResume(AccessDeniedException.class, e ->
                Mono.just(Response.denied("Access denied"))
            )
            .onErrorResume(AuthenticationException.class, e ->
                Mono.just(Response.error("Authentication failed"))
            );
    }

    private Mono<Response> performAction(UserDetails user, ActionRequest request) {
        // Implementation
        return Mono.just(Response.success());
    }

    static class Response {
        static Response denied(String message) { return new Response(); }
        static Response error(String message) { return new Response(); }
        static Response success() { return new Response(); }
    }
}

ReactiveSecurityContextHolder

The reactive argument resolvers obtain authentication from ReactiveSecurityContextHolder. In reactive applications, security context is typically stored in Reactor's context.

Example of setting security context:

import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import reactor.core.publisher.Mono;

public Mono<Void> processWithAuth(Authentication authentication) {
    SecurityContext context = // ... create security context with authentication

    return someReactiveOperation()
        .contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(context)));
}

Differences from Imperative Support

FeatureImperativeReactive
Return TypeObjectMono<Object>
Package.context.handler.invocation.reactive
Context StorageSecurityContextHolderReactiveSecurityContextHolder
Thread ModelThreadLocalReactor Context
Use CaseSTOMP/WebSocketRSocket, WebFlux

Dependencies

Reactive support requires:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-messaging</artifactId>
    <version>7.0.0</version>
</dependency>

<dependency>
    <groupId>io.projectreactor</groupId>
    <artifactId>reactor-core</artifactId>
</dependency>

Error Handling

Handling Null Principals

Reactive resolvers may emit null if no authentication is available:

@MessageMapping("secure.action")
public Mono<Void> handleAction(
    @AuthenticationPrincipal Mono<UserDetails> userMono
) {
    return userMono
        .switchIfEmpty(Mono.error(new AuthenticationException("Not authenticated")))
        .flatMap(user -> {
            // Process with authenticated user
            return performAction(user);
        });
}

Error Handling in Reactive Chains

Always handle errors in reactive chains:

@MessageMapping("secure.operation")
public Mono<Response> handleOperation(
    @AuthenticationPrincipal Mono<UserDetails> userMono,
    Mono<Request> requestMono
) {
    return Mono.zip(userMono, requestMono)
        .flatMap(tuple -> processRequest(tuple.getT1(), tuple.getT2()))
        .onErrorResume(AuthenticationException.class, e -> {
            logger.warn("Authentication failed", e);
            return Mono.just(Response.error("Authentication required"));
        })
        .onErrorResume(AccessDeniedException.class, e -> {
            logger.warn("Access denied", e);
            return Mono.just(Response.denied("Access denied"));
        })
        .onErrorResume(Exception.class, e -> {
            logger.error("Unexpected error", e);
            return Mono.just(Response.error("Internal error"));
        });
}

Troubleshooting

Issue: Principal Resolver Returns Empty Mono

Causes:

  1. Security context not in Reactor context
  2. No authentication available

Solutions:

// Ensure security context is propagated
return someReactiveOperation()
    .contextWrite(ReactiveSecurityContextHolder.withSecurityContext(
        Mono.just(securityContext)
    ));

// Check if authentication is available
@MessageMapping("check")
public Mono<Boolean> checkAuth(
    @AuthenticationPrincipal Mono<UserDetails> userMono
) {
    return userMono
        .hasElement()  // Check if Mono has value
        .map(hasAuth -> {
            logger.debug("Has authentication: {}", hasAuth);
            return hasAuth;
        });
}

Issue: Expression Evaluation Fails

Causes:

  1. Invalid SpEL expression
  2. Missing bean resolver

Solutions:

// Configure bean resolver
AuthenticationPrincipalArgumentResolver resolver = 
    new AuthenticationPrincipalArgumentResolver();
resolver.setBeanResolver(applicationContext);

// Validate expression syntax
try {
    Expression expr = parser.parseExpression("username");
    // Expression is valid
} catch (SpELParseException e) {
    logger.error("Invalid expression", e);
}