Role-based access control
Fluxzero allows you to restrict message handling based on the authenticated user’s roles. This access control happens before the message reaches the handler — similar to how payload validation is enforced.
There are several annotations for declaring user and role requirements.
@RequiresAnyRole
Section titled “@RequiresAnyRole”Use this annotation to ensure that a handler is only invoked if the user has at least one of the specified roles.
@HandleCommand@RequiresAnyRole({"admin", "editor"})void handle(UpdateArticle command) { ... }@RequiresAnyRole("admin")public record DeleteAccount(String userId) {}@HandleCommand@RequiresAnyRole(["admin", "editor"])fun handle(command: UpdateArticle) { ... }@RequiresAnyRole("admin")data class DeleteAccount(val userId: String)@ForbidsAnyRole
Section titled “@ForbidsAnyRole”This annotation works the other way around — it prevents message handling if the user has any of the specified roles.
@ForbidsAnyRole("guest")@HandleCommandvoid handle(SensitiveOperation command) { ... }@ForbidsAnyRole("guest")@HandleCommandfun handle(command: SensitiveOperation) { ... }@RequiresUser
Section titled “@RequiresUser”Ensures that a message can only be handled if an authenticated user is present. If no user is found, the message is rejected with an UnauthenticatedException.
@RequiresUser@HandleCommandvoid handle(UpdateProfile command) { ... }@RequiresUser@HandleCommandfun handle(command: UpdateProfile) { ... }@NoUserRequired
Section titled “@NoUserRequired”Allows a message to be processed even if no authenticated user is present — ideal for public APIs or health checks.
@NoUserRequired@HandleCommandvoid handle(SignUpUser command) { ... }@NoUserRequired@HandleCommandfun handle(command: SignUpUser) { ... }@ForbidsUser
Section titled “@ForbidsUser”Prevents message handling if an authenticated user is present. This is useful for restricting certain flows to unauthenticated users — such as guest signups.
@ForbidsUser@HandleCommandvoid handle(SignUpAsGuest command) { ... }@ForbidsUser@HandleCommandfun handle(command: SignUpAsGuest) { ... }Controlling behavior on unauthorized access
Section titled “Controlling behavior on unauthorized access”All authorization annotations include an optional throwIfUnauthorized() property (default: true) that controls what happens when access is denied.
-
If
throwIfUnauthorized = true:- If a user is required but not present, an
UnauthenticatedExceptionis thrown. - If a user is present but lacks required roles, an
UnauthorizedExceptionis thrown.
- If a user is required but not present, an
-
If
throwIfUnauthorized = false:- The message is silently skipped, allowing delegation to other eligible handlers (if any).
Role annotations support nesting and overrides
Section titled “Role annotations support nesting and overrides”Fluxzero evaluates annotations hierarchically:
- If
@RequiresAnyRole("admin")is placed on a package, it applies to all handlers and payloads in that package. - You can override it on specific classes or methods.
@RequiresUserpackage com.myapp.handlers;@NoUserRequired@HandleCommandvoid handle(PublicPing ping) { ... } // Overrides the package-level requirement// No direct package annotations in Kotlin, use class-level instead@RequiresUserclass SecuredHandler { @HandleCommand fun handle(command: UpdateAccount) { ... }}
@NoUserRequiredclass PublicHandler { @HandleCommand fun handle(command: PublicPing) { ... }}Enum-based role annotations
Section titled “Enum-based role annotations”You can define custom annotations using enums for structured roles.
public enum Role { ADMIN, EDITOR, USER}
@RequiresAnyRole@Target({ElementType.TYPE, ElementType.METHOD})public @interface RequiresRole { Role[] value();}@HandleCommand@RequiresRole(Role.ADMIN)void handle(DeleteAccount command) { ... }enum class Role { ADMIN, EDITOR, USER}
@RequiresAnyRole@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)annotation class RequiresRole(val value: Array<Role>)@HandleCommand@RequiresRole([Role.ADMIN])fun handle(command: DeleteAccount) { ... }Best practices
Section titled “Best practices”- Use role annotations on payload classes to guarantee strict access checks across environments.
- Apply them on handlers to allow fallback logic or specialization by role.
- Set default access rules at the package level, and override them as needed.
- Create custom annotations to avoid repeating role strings throughout your codebase.
Where does user info come from?
Section titled “Where does user info come from?”User roles are resolved by the configured UserProvider, which extracts user info from message metadata (e.g. headers or tokens). Fluxzero uses a pluggable SPI to register this provider.
Use User.id() for ownership checks, audit references, and other behavior that needs a stable user identity. In the 1.x
SDK it defaults to Principal.getName() for compatibility with existing User implementations. Override id() when
the principal name is a display or provider-facing name. AbstractUserProvider stores this ID in user metadata and
resolves it through getUserById(...); keep accepting earlier getName() values while older messages can still be in
flight.
Providing your own user logic
Section titled “Providing your own user logic”You can implement a custom UserProvider to extract users from headers, JWT tokens, cookies, etc.
public class MyUserProvider extends AbstractUserProvider { public MyUserProvider() { super("Authorization", MyUser.class); }
@Override public User fromMessage(HasMessage message) { if (message.toMessage() instanceof WebRequest request) { return decodeToken(request.getHeader("Authorization")); } return super.fromMessage(message); }
private User decodeToken(String header) { // Parse and validate JWT token here return ...; }}class MyUserProvider : AbstractUserProvider("Authorization", MyUser::class.java) { override fun fromMessage(message: HasMessage): User { val request = message.toMessage() as? WebRequest return request?.getHeader("Authorization")?.let { decodeToken(it) } ?: super.fromMessage(message) }
private fun decodeToken(header: String): User { // Your decoding logic here return ... }}System and testing support
Section titled “System and testing support”Your UserProvider can also implement:
getSystemUser()— returns the default system-level user (used in tests and scheduled handlers)getUserById(...)— used by test utilities likefixture.whenCommandByUser(...)
This ensures consistent behavior across environments.
Registering your user provider
Section titled “Registering your user provider”To register your custom UserProvider, use Java’s SPI mechanism:
src/main/resources/META-INF/services/io.fluxzero.sdk.tracking.handling.authentication.UserProviderAdd each provider class (one per line):
com© 2026 Fluxzero