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

expression-handling.mddocs/

Expression-Based Authorization

Use Spring Expression Language (SpEL) for complex authorization rules with custom security expressions and evaluation contexts.

Overview

Spring Security Messaging supports SpEL expressions for authorization decisions, allowing complex rules that go beyond simple role checks. Expression handlers create evaluation contexts that provide access to message properties, authentication details, and custom security methods.

Package: org.springframework.security.messaging.access.expression

Capabilities

DefaultMessageSecurityExpressionHandler

Default implementation of SecurityExpressionHandler that uses MessageSecurityExpressionRoot for evaluating security expressions on messages.

/**
 * Default SecurityExpressionHandler for Message-based security expressions.
 * Creates evaluation contexts with MessageSecurityExpressionRoot.
 *
 * @param <T> the message payload type
 * @since 4.0
 */
public class DefaultMessageSecurityExpressionHandler<T>
    extends AbstractSecurityExpressionHandler<Message<T>> {

    /**
     * Creates evaluation context for the given authentication and message.
     * The context includes MessageSecurityExpressionRoot with access to
     * message properties and standard security expressions.
     *
     * @param authentication supplier of authentication to evaluate
     * @param message the message being evaluated
     * @return evaluation context for SpEL expressions
     */
    public EvaluationContext createEvaluationContext(
        Supplier<? extends Authentication> authentication,
        Message<T> message
    );

    /**
     * Sets the AuthenticationTrustResolver for checking authentication types.
     * @deprecated since 7.0, use setAuthorizationManagerFactory instead
     *
     * @param trustResolver the trust resolver
     */
    @Deprecated
    public void setTrustResolver(AuthenticationTrustResolver trustResolver);
}

Usage Example:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.messaging.access.expression.DefaultMessageSecurityExpressionHandler;
import org.springframework.messaging.Message;

@Configuration
public class ExpressionConfig {

    @Bean
    public SecurityExpressionHandler<Message<?>> messageSecurityExpressionHandler() {
        DefaultMessageSecurityExpressionHandler<Object> handler =
            new DefaultMessageSecurityExpressionHandler<>();

        // Customize if needed (e.g., add custom permission evaluator)
        handler.setPermissionEvaluator(customPermissionEvaluator());

        return handler;
    }

    private PermissionEvaluator customPermissionEvaluator() {
        // Custom permission evaluator implementation
        return new CustomPermissionEvaluator();
    }
}

MessageAuthorizationContextSecurityExpressionHandler

Expression handler specifically for MessageAuthorizationContext, supporting path variable extraction. Wraps a delegate handler for Message expressions.

/**
 * SecurityExpressionHandler for MessageAuthorizationContext that supports
 * path variables extracted from destination patterns.
 *
 * @since 5.8
 */
public final class MessageAuthorizationContextSecurityExpressionHandler
    implements SecurityExpressionHandler<MessageAuthorizationContext<?>> {

    /**
     * Creates handler with default DefaultMessageSecurityExpressionHandler.
     */
    public MessageAuthorizationContextSecurityExpressionHandler();

    /**
     * Creates handler wrapping the specified delegate.
     *
     * @param expressionHandler delegate handler for Message expressions
     */
    public MessageAuthorizationContextSecurityExpressionHandler(
        SecurityExpressionHandler<Message<?>> expressionHandler
    );

    /**
     * Returns the expression parser used for parsing SpEL expressions.
     *
     * @return the expression parser
     */
    public ExpressionParser getExpressionParser();

    /**
     * Creates evaluation context for the given authentication and context.
     * The context includes access to both message properties and path variables.
     *
     * @param authentication the authentication to evaluate
     * @param context the message authorization context
     * @return evaluation context for SpEL expressions
     */
    public EvaluationContext createEvaluationContext(
        Authentication authentication,
        MessageAuthorizationContext<?> context
    );

    /**
     * Creates evaluation context with authentication supplier.
     *
     * @param authentication supplier of authentication to evaluate
     * @param context the message authorization context
     * @return evaluation context for SpEL expressions
     */
    public EvaluationContext createEvaluationContext(
        Supplier<? extends Authentication> authentication,
        MessageAuthorizationContext<?> context
    );
}

Usage Example:

import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.messaging.access.expression.DefaultMessageSecurityExpressionHandler;
import org.springframework.security.messaging.access.expression.MessageAuthorizationContextSecurityExpressionHandler;
import org.springframework.security.messaging.access.intercept.MessageAuthorizationContext;

import java.util.function.Supplier;

public class CustomExpressionAuthorizationManager
    implements AuthorizationManager<MessageAuthorizationContext<?>> {

    private final MessageAuthorizationContextSecurityExpressionHandler expressionHandler;
    private final Expression expression;

    public CustomExpressionAuthorizationManager(String expressionString) {
        DefaultMessageSecurityExpressionHandler<Object> delegate =
            new DefaultMessageSecurityExpressionHandler<>();

        this.expressionHandler =
            new MessageAuthorizationContextSecurityExpressionHandler(delegate);

        this.expression = expressionHandler
            .getExpressionParser()
            .parseExpression(expressionString);
    }

    @Override
    public AuthorizationDecision check(
        Supplier<Authentication> authentication,
        MessageAuthorizationContext<?> context
    ) {
        EvaluationContext evalContext =
            expressionHandler.createEvaluationContext(authentication, context);

        boolean granted = expression.getValue(evalContext, Boolean.class);
        return new AuthorizationDecision(granted);
    }
}

// Usage with path variables
MessageMatcherDelegatingAuthorizationManager authManager =
    MessageMatcherDelegatingAuthorizationManager.builder()
        // Expression can access path variables via #variables
        .simpDestMatchers("/app/user/{userId}/**")
            .access(new CustomExpressionAuthorizationManager(
                "#variables['userId'] == authentication.name"
            ))
        .build();

MessageSecurityExpressionRoot

The root object for message security expressions, providing access to message properties and standard security expression methods.

/**
 * Root object for message security SpEL expressions.
 * Provides access to message properties and standard security checks.
 *
 * @param <T> the message payload type
 * @since 4.0
 */
public class MessageSecurityExpressionRoot extends SecurityExpressionRoot<Message<T>> {

    /**
     * The message being evaluated (accessible in expressions as 'message').
     */
    public final Message<T> message;

    /**
     * Creates root with authentication and message.
     *
     * @param authentication the authentication
     * @param message the message
     */
    public MessageSecurityExpressionRoot(Authentication authentication, Message<T> message);

    /**
     * Creates root with authentication supplier and message.
     *
     * @param authentication supplier of authentication
     * @param message the message
     * @since 5.8
     */
    public MessageSecurityExpressionRoot(
        Supplier<? extends Authentication> authentication,
        Message<T> message
    );
}

The expression root provides access to standard security expression methods inherited from SecurityExpressionRoot:

  • hasRole(String role) - Check if user has role
  • hasAnyRole(String... roles) - Check if user has any role
  • hasAuthority(String authority) - Check if user has authority
  • hasAnyAuthority(String... authorities) - Check if user has any authority
  • principal - The authentication principal
  • authentication - The Authentication object
  • permitAll - Always returns true
  • denyAll - Always returns false
  • isAnonymous() - Check if user is anonymous
  • isAuthenticated() - Check if user is authenticated
  • isRememberMe() - Check if user authenticated via remember-me
  • isFullyAuthenticated() - Check if user is fully authenticated (not remember-me)

Expression Authorization

Using Expressions with Authorization Manager

import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.messaging.access.expression.MessageAuthorizationContextSecurityExpressionHandler;
import org.springframework.security.messaging.access.intercept.MessageAuthorizationContext;
import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager;

// Custom expression-based authorization manager
AuthorizationManager<MessageAuthorizationContext<?>> expressionAuthManager =
    new ExpressionBasedAuthorizationManager(
        "hasRole('ADMIN') or #variables['userId'] == authentication.name"
    );

MessageMatcherDelegatingAuthorizationManager authManager =
    MessageMatcherDelegatingAuthorizationManager.builder()
        .simpDestMatchers("/app/user/{userId}/**").access(expressionAuthManager)
        .build();

Common Expression Patterns

Access Own Resources

// User can only access their own user ID
.simpDestMatchers("/app/user/{userId}/**")
    .access(expressionAuthManager("#variables['userId'] == authentication.name"))

Role-Based with Additional Checks

// Admin or owner can access
.simpDestMatchers("/app/resource/{ownerId}/**")
    .access(expressionAuthManager(
        "hasRole('ADMIN') or #variables['ownerId'] == authentication.name"
    ))

Multiple Path Variables

// Check multiple variables
.simpDestMatchers("/app/team/{teamId}/member/{memberId}/**")
    .access(expressionAuthManager(
        "#variables['memberId'] == authentication.name and " +
        "hasPermission(#variables['teamId'], 'TEAM', 'READ')"
    ))

Access Message Headers

// Access message headers in expression
.simpDestMatchers("/app/custom/**")
    .access(expressionAuthManager(
        "message.headers['customHeader'] == 'expectedValue'"
    ))

Complex Business Logic

// Call custom bean methods
.simpDestMatchers("/app/document/{docId}/**")
    .access(expressionAuthManager(
        "@documentService.canAccess(authentication.name, #variables['docId'])"
    ))

Custom Expression Methods

Adding Custom Security Methods

import org.springframework.security.access.expression.AbstractSecurityExpressionHandler;
import org.springframework.security.access.expression.SecurityExpressionRoot;
import org.springframework.security.core.Authentication;
import org.springframework.messaging.Message;

public class CustomMessageSecurityExpressionRoot<T>
    extends MessageSecurityExpressionRoot<T> {

    public CustomMessageSecurityExpressionRoot(
        Authentication authentication,
        Message<T> message
    ) {
        super(authentication, message);
    }

    /**
     * Custom expression method: check if user belongs to tenant
     */
    public boolean belongsToTenant(String tenantId) {
        // Custom logic to check tenant membership
        Object principal = getAuthentication().getPrincipal();
        if (principal instanceof CustomUserDetails) {
            return ((CustomUserDetails) principal).getTenantId().equals(tenantId);
        }
        return false;
    }

    /**
     * Custom expression method: check if user can access resource
     */
    public boolean canAccessResource(String resourceId) {
        // Custom authorization logic
        return checkResourceAccess(getAuthentication(), resourceId);
    }

    private boolean checkResourceAccess(Authentication auth, String resourceId) {
        // Implementation
        return true;
    }
}

public class CustomMessageSecurityExpressionHandler<T>
    extends DefaultMessageSecurityExpressionHandler<T> {

    @Override
    protected SecurityExpressionRoot createSecurityExpressionRoot(
        Authentication authentication,
        Message<T> invocation
    ) {
        CustomMessageSecurityExpressionRoot<T> root =
            new CustomMessageSecurityExpressionRoot<>(authentication, invocation);

        root.setPermissionEvaluator(getPermissionEvaluator());
        root.setTrustResolver(getTrustResolver());
        root.setRoleHierarchy(getRoleHierarchy());

        return root;
    }
}

// Configure custom handler
@Configuration
public class CustomExpressionConfig {

    @Bean
    public SecurityExpressionHandler<Message<?>> messageSecurityExpressionHandler() {
        return new CustomMessageSecurityExpressionHandler<>();
    }
}

// Use custom methods in expressions
MessageMatcherDelegatingAuthorizationManager authManager =
    MessageMatcherDelegatingAuthorizationManager.builder()
        .simpDestMatchers("/app/tenant/{tenantId}/**")
            .access(expressionAuthManager("belongsToTenant(#variables['tenantId'])"))
        .build();

Using @Bean in Expressions

@Service("messageAccessChecker")
public class MessageAccessChecker {

    public boolean canAccessDestination(String username, String destination) {
        // Custom logic
        return checkAccess(username, destination);
    }

    public boolean hasTeamAccess(String username, String teamId) {
        // Custom logic
        return checkTeamMembership(username, teamId);
    }

    private boolean checkAccess(String username, String destination) {
        // Implementation
        return true;
    }

    private boolean checkTeamMembership(String username, String teamId) {
        // Implementation
        return true;
    }
}

// Use in expressions with @beanName syntax
MessageMatcherDelegatingAuthorizationManager authManager =
    MessageMatcherDelegatingAuthorizationManager.builder()
        .simpDestMatchers("/app/team/{teamId}/**")
            .access(expressionAuthManager(
                "@messageAccessChecker.hasTeamAccess(" +
                "authentication.name, #variables['teamId'])"
            ))
        .build();

Expression Variables

Available Variables in Expressions

When using MessageAuthorizationContextSecurityExpressionHandler:

  • authentication - The Authentication object
  • principal - The authentication principal
  • message - The Message<?> being evaluated
  • #variables - Map of extracted path variables
  • #root - The MessageSecurityExpressionRoot

Example:

// Access variables
"#variables['userId'] == authentication.name"

// Access message headers
"message.headers['customHeader'] == 'value'"

// Access principal properties
"principal.email == 'admin@example.com'"

// Combine multiple conditions
"hasRole('USER') and #variables['ownerId'] == authentication.name"

Complete Expression-Based Configuration

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.messaging.access.expression.DefaultMessageSecurityExpressionHandler;
import org.springframework.security.messaging.access.expression.MessageAuthorizationContextSecurityExpressionHandler;
import org.springframework.security.messaging.access.intercept.MessageAuthorizationContext;
import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager;
import org.springframework.expression.Expression;
import org.springframework.expression.EvaluationContext;

import java.util.function.Supplier;

@Configuration
public class ExpressionBasedSecurityConfig {

    @Bean
    public MessageMatcherDelegatingAuthorizationManager authorizationManager() {
        return MessageMatcherDelegatingAuthorizationManager.builder()
            // Simple role check
            .simpDestMatchers("/app/admin/**").hasRole("ADMIN")

            // Expression: user can access own resources
            .simpDestMatchers("/app/user/{userId}/**")
                .access(expressionAuthManager(
                    "#variables['userId'] == authentication.name"
                ))

            // Expression: admin or owner can access
            .simpDestMatchers("/app/document/{ownerId}/**")
                .access(expressionAuthManager(
                    "hasRole('ADMIN') or #variables['ownerId'] == authentication.name"
                ))

            // Expression: complex business logic via bean
            .simpDestMatchers("/app/team/{teamId}/**")
                .access(expressionAuthManager(
                    "@teamService.isMember(authentication.name, #variables['teamId'])"
                ))

            // Expression: check message header
            .simpDestMatchers("/app/custom/**")
                .access(expressionAuthManager(
                    "hasRole('USER') and message.headers['version'] == '2.0'"
                ))

            .anyMessage().denyAll()
            .build();
    }

    private AuthorizationManager<MessageAuthorizationContext<?>> expressionAuthManager(
        String expressionString
    ) {
        return new ExpressionAuthorizationManager(expressionString);
    }

    private static class ExpressionAuthorizationManager
        implements AuthorizationManager<MessageAuthorizationContext<?>> {

        private final MessageAuthorizationContextSecurityExpressionHandler expressionHandler;
        private final Expression expression;

        public ExpressionAuthorizationManager(String expressionString) {
            DefaultMessageSecurityExpressionHandler<Object> delegate =
                new DefaultMessageSecurityExpressionHandler<>();

            this.expressionHandler =
                new MessageAuthorizationContextSecurityExpressionHandler(delegate);

            this.expression = expressionHandler
                .getExpressionParser()
                .parseExpression(expressionString);
        }

        @Override
        public AuthorizationDecision check(
            Supplier<Authentication> authentication,
            MessageAuthorizationContext<?> context
        ) {
            EvaluationContext evalContext =
                expressionHandler.createEvaluationContext(authentication, context);

            Boolean granted = expression.getValue(evalContext, Boolean.class);
            return new AuthorizationDecision(Boolean.TRUE.equals(granted));
        }
    }
}

Expression Best Practices

  1. Keep expressions simple - Complex logic should be in @Bean methods
  2. Use path variables - Access via #variables['name']
  3. Validate carefully - Expressions failures can lead to security issues
  4. Test thoroughly - Write tests for all expression-based rules
  5. Document expressions - Add comments explaining complex expressions
  6. Avoid side effects - Expressions should only read state, not modify it
  7. Handle nulls - Use safe navigation operators (?.) when accessing properties

Debugging Expressions

Enable debug logging to see expression evaluation:

logging.level.org.springframework.security.messaging.access.expression=DEBUG

This will log:

  • Expression text being evaluated
  • Variables available in evaluation context
  • Expression evaluation results

Error Handling

Expression Evaluation Exceptions

Expressions may throw exceptions during evaluation. Always handle these gracefully:

public class SafeExpressionAuthorizationManager
    implements AuthorizationManager<MessageAuthorizationContext<?>> {
    
    private final Expression expression;
    private final MessageAuthorizationContextSecurityExpressionHandler handler;
    
    @Override
    public AuthorizationDecision check(
        Supplier<Authentication> authentication,
        MessageAuthorizationContext<?> context
    ) {
        try {
            EvaluationContext evalContext = handler.createEvaluationContext(authentication, context);
            Boolean result = expression.getValue(evalContext, Boolean.class);
            return new AuthorizationDecision(Boolean.TRUE.equals(result));
        } catch (EvaluationException e) {
            logger.error("Expression evaluation failed", e);
            // Deny access on evaluation failure (fail-secure)
            return new AuthorizationDecision(false);
        } catch (Exception e) {
            logger.error("Unexpected error during expression evaluation", e);
            return new AuthorizationDecision(false);
        }
    }
}

Null Safety in Expressions

Always handle null values in expressions:

// Safe: Check for null before accessing
"#variables['userId'] != null and #variables['userId'] == authentication.name"

// Safe: Use safe navigation
"authentication?.name == #variables['userId']"

// Unsafe: May throw NullPointerException
"#variables['userId'] == authentication.name"  // If userId is null, may fail

Common Expression Errors

  1. NullPointerException: Accessing properties on null objects

    • Solution: Use null checks or safe navigation (?.)
  2. SpELParseException: Invalid expression syntax

    • Solution: Validate expressions at configuration time
  3. EvaluationException: Runtime evaluation errors

    • Solution: Wrap evaluation in try-catch, fail-secure

Troubleshooting

Issue: Expression Always Returns False

Causes:

  1. Null values in variables
  2. Type mismatches
  3. Missing path variables

Solutions:

// Add null checks
"#variables['userId'] != null and #variables['userId'] == authentication.name"

// Verify variable extraction
logger.debug("Variables: {}", context.getVariables());

// Test expression in isolation
Expression testExpr = parser.parseExpression("#variables['userId'] == authentication.name");

Issue: Bean Not Found in Expression

Symptom: @beanName method calls fail

Causes:

  1. Bean not registered
  2. Wrong bean name
  3. Method not accessible

Solutions:

// Ensure bean is registered
@Service("myService")
public class MyService { ... }

// Use correct bean name
"@myService.canAccess(authentication.name, #variables['id'])"

// Verify bean resolver is configured
handler.setBeanResolver(applicationContext);

Issue: Path Variables Not Available

Symptom: #variables map is empty

Causes:

  1. Pattern doesn't extract variables
  2. Using wrong matcher type

Solutions:

// Use path variable syntax
.simpDestMatchers("/app/user/{userId}/**")  // Correct

// Not: .simpDestMatchers("/app/user/*/**")  // Wrong - no variable extraction