Publishing security

Hermes has a feature to restrict publishing on specific topics to particular services: publishing permissions. How service name is extracted from request is entirely configurable and depends on security requirements within company's ecosystem.

Since Hermes uses Undertow as http server authentication implementation follows Undertow internal security model which is described here: Undertow Security.

Authentication has to be enabled using following configuration:

Option Description Options Default value
frontend.handlers.authentication.enabled enable authentication handler true, false false
frontend.handlers.authentication.mode in which circumstances perform authentication constraint_driven, pro_active constraint_driven

More about authentication mode can be read here: AuthenticationMode

Implementing IdentityManager

Authentication is handled by IdentityManager exposed via AuthenticationConfiguration from HermesFrontend.Builder. Hermes by default do not come with any concrete implementation so it has to be coded.

Frontend is extracting service name from HttpServerExchange using following method:

String serviceName = exchange.getSecurityContext().getAuthenticatedAccount().getPrincipal().getName();

Worth mentioning is that authenticated account has to contain role Roles.PUBLISH otherwise frontend will return 403 without evaluating topic specific authorisation configuration. This behavior can be used for instance to reject requests from development environment on production.

Below you can see naive implementation of authentication via Authorization header used in our integration tests.

@Configuration
@PropertySource("classpath:application-auth.properties")
public class AuthConfiguration {

    @Value("${auth.username}")
    private String username;

    @Value("${auth.password}")
    private String password;

    @Bean
    @Profile("authRequired")
    public AuthenticationConfiguration requiredAuthenticationConfiguration() {
        return getAuthConfig(true);
    }

    @Bean
    @Profile("authNonRequired")
    public AuthenticationConfiguration notRequiredAuthenticationConfiguration() {
        return getAuthConfig(false);
    }

    private AuthenticationConfiguration getAuthConfig (boolean isAuthenticationRequired) {
        return new AuthenticationConfiguration(
                exchange -> isAuthenticationRequired,
                Lists.newArrayList(new BasicAuthenticationMechanism("basicAuthRealm")),
                new SingleUserAwareIdentityManager(username, password));
    }
}
public class SingleUserAwareIdentityManager implements IdentityManager {
    private final String username;
    private final String password;

    SingleUserAwareIdentityManager(String username, String password) {
        this.username = username;
        this.password = password;
    }

    @Override
    public Account verify(Account account) {
        return null;
    }

    @Override
    public Account verify(String username, Credential credential) {
        String password = new String(((PasswordCredential) credential).getPassword());
        if (this.username.equals(username) && this.password.equals(password)) {
            return new SomeUserAccount(username);
        }
        return null;
    }

    @Override
    public Account verify(Credential credential) {
        return null;
    }

    private final class SomeUserAccount implements Account {
        private final Principal principal;

        private SomeUserAccount(String username) {
            this.principal = new SomeUserPrincipal(username);
        }

        @Override
        public Principal getPrincipal() {
            return principal;
        }

        @Override
        public Set<String> getRoles() {
            return Collections.singleton(Roles.PUBLISHER);
        }
    }

    private final class SomeUserPrincipal implements Principal {
        private final String username;

        private SomeUserPrincipal(String username) {
            this.username = username;
        }

        @Override
        public String getName() {
            return username;
        }
    }

    static Map<String, String> getHeadersWithAuthentication(String username, String password) {
        String credentials = username + ":" + password;
        String token = "Basic " + Base64.encodeBase64String(credentials.getBytes(StandardCharsets.UTF_8));

        Map<String, String> headers = new HashMap<>();
        headers.put("Authorization", token);
        return headers;
    }
}