Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ spotBugs = "4.9.4"
swagger-annotations = "2.2.34"
swagger-jaxrs = "1.6.16"
systemstubs = "2.1.8"
testcontainers = "1.21.0"
unboundid-ldap-sdk = "7.0.3"
victools = "4.38.0"
wiremock = "3.0.1"
zeroallocationhashing = "0.27ea0"
Expand Down Expand Up @@ -154,6 +156,9 @@ slf4j-jultoslf4j = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" }
swagger-annotations = { module = "io.swagger.core.v3:swagger-annotations", version.ref = "swagger-annotations" }
swagger-jaxrs = { module = "io.swagger:swagger-jaxrs", version.ref = "swagger-jaxrs" }
systemstubs = { module = "uk.org.webcompere:system-stubs-jupiter", version.ref = "systemstubs" }
testcontainers = { module = "org.testcontainers:testcontainers", version.ref = "testcontainers" }
testcontainers-junit-jupiter = { module = "org.testcontainers:junit-jupiter", version.ref = "testcontainers" }
unboundid-ldap-sdk = { module = "com.unboundid:unboundid-ldapsdk", version.ref = "unboundid-ldap-sdk" }
victools-jsonschema-generator = { module = "com.github.victools:jsonschema-generator", version.ref = "victools" }
victools-jsonschema-jackson = { module = "com.github.victools:jsonschema-module-jackson", version.ref = "victools" }
wiremock-jre8-standalone = { module = "com.github.tomakehurst:wiremock-jre8-standalone", version.ref = "wiremock" }
Expand Down
5 changes: 5 additions & 0 deletions hivemq-edge/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@ dependencies {
//JWT
implementation(libs.jose4j)

//LDAP
implementation(libs.unboundid.ldap.sdk)

//json schema
implementation(libs.json.schema.validator)
implementation(libs.victools.jsonschema.generator)
Expand Down Expand Up @@ -234,6 +237,8 @@ dependencies {
testImplementation(libs.awaitility)
testImplementation(libs.assertj)
testImplementation(libs.systemstubs)
testImplementation(libs.testcontainers)
testImplementation(libs.testcontainers.junit.jupiter)
}

tasks.test {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?xml version="1.0"?>
<!--
~ Copyright 2025-present HiveMQ GmbH
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->

<hivemq xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="config.xsd">

<!--
Example: Admin API with LDAP Authentication

This configuration enables LDAP-based authentication for the Admin API.
Instead of managing users in the config file, users are authenticated
against an LDAP directory server.

Supported TLS modes:
- NONE: Plain LDAP (port 389, not recommended for production)
- LDAPS: LDAP over TLS from connection start (port 636, recommended)
- START_TLS: Plain connection upgraded to TLS (port 389)
-->

<admin-api>
<enabled>true</enabled>

<!-- LDAP Authentication Configuration -->
<ldap>
<ldap-authentication>
<!-- LDAP Server Configuration -->
<host>ldap.example.com</host>
<port>636</port> <!-- 636 for LDAPS, 389 for plain LDAP or START_TLS -->
<tls-mode>LDAPS</tls-mode> <!-- Options: NONE, LDAPS, START_TLS -->

<!-- TLS/SSL Configuration (required for LDAPS and START_TLS) -->
<tls>
<!-- Path to truststore containing CA certificates -->
<truststore-path>/path/to/truststore.jks</truststore-path>
<truststore-password>changeit</truststore-password>
<truststore-type>JKS</truststore-type> <!-- JKS or PKCS12 -->
</tls>

<!-- Optional: Connection Timeouts (in milliseconds) -->
<connect-timeout-millis>5000</connect-timeout-millis> <!-- 5 seconds -->
<response-timeout-millis>10000</response-timeout-millis> <!-- 10 seconds -->

<!--
User DN Template
Defines how usernames are mapped to LDAP Distinguished Names.
Placeholders:
- {username}: The username entered in the login form
- {baseDn}: The base DN specified below

Examples:
- OpenLDAP: uid={username},ou=people,{baseDn}
- Active Directory: cn={username},cn=Users,{baseDn}
- Email-based: mail={username},ou=staff,{baseDn}
-->
<user-dn-template>uid={username},ou=people,{baseDn}</user-dn-template>

<!-- Base DN: The root of your LDAP directory tree -->
<base-dn>dc=example,dc=com</base-dn>
</ldap-authentication>
</ldap>
</admin-api>

</hivemq>
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?xml version="1.0"?>
<!--
~ Copyright 2025-present HiveMQ GmbH
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->

<hivemq xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="config.xsd">

<!--
Example: Admin API with LDAP Authentication using System CA Certificates

This configuration uses the system's default CA certificates for TLS validation.
This is suitable when your LDAP server uses certificates signed by well-known
certificate authorities (e.g., Let's Encrypt, DigiCert, etc.).

No custom truststore configuration is needed.
-->

<admin-api>
<enabled>true</enabled>

<ldap>
<ldap-authentication>
<!-- LDAP Server Configuration -->
<host>ldap.example.com</host>
<port>636</port>
<tls-mode>LDAPS</tls-mode>

<!-- No <tls> section needed - system CA certificates will be used -->

<!-- User DN Template for Active Directory -->
<user-dn-template>cn={username},cn=Users,{baseDn}</user-dn-template>

<!-- Base DN -->
<base-dn>dc=company,dc=local</base-dn>
</ldap-authentication>
</ldap>
</admin-api>

</hivemq>
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,17 @@
package com.hivemq.api.auth.handler.impl;

import com.google.common.base.Preconditions;
import com.hivemq.api.auth.ApiPrincipal;
import com.hivemq.api.auth.handler.AuthenticationResult;
import com.hivemq.api.auth.provider.IUsernamePasswordProvider;
import org.jetbrains.annotations.NotNull;
import com.hivemq.api.auth.provider.IUsernameRolesProvider;
import com.hivemq.http.HttpConstants;
import com.hivemq.http.core.UsernamePasswordRoles;

import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.SecurityContext;
import org.jetbrains.annotations.NotNull;

import java.util.Base64;
import java.util.Optional;

Expand All @@ -39,27 +38,26 @@ public class BasicAuthenticationHandler extends AbstractHeaderAuthenticationHand

static final String SEP = ":";
static final String METHOD = "Basic";
private final IUsernamePasswordProvider provider;
private final IUsernameRolesProvider provider;

@Inject
public BasicAuthenticationHandler(final @NotNull IUsernamePasswordProvider provider) {
public BasicAuthenticationHandler(final @NotNull IUsernameRolesProvider provider) {
this.provider = provider;
}

@Override
protected AuthenticationResult authenticateInternal(final @NotNull ContainerRequestContext requestContext, String authValue) {
Optional<UsernamePasswordRoles> usernamePassword = parseValue(authValue);
if(usernamePassword.isPresent()){
UsernamePasswordRoles supplied = usernamePassword.get();
Optional<UsernamePasswordRoles> record = provider.findByUsername(supplied.getUserName());
if(record.isPresent() && record.get().getPassword().equals(supplied.getPassword())){
AuthenticationResult result = AuthenticationResult.allowed(this);
ApiPrincipal principal = new ApiPrincipal(supplied.getUserName(), record.get().getRoles());
result.setPrincipal(principal);
return result;
}
}
return AuthenticationResult.denied(this);
protected AuthenticationResult authenticateInternal(final @NotNull ContainerRequestContext requestContext,
final @NotNull String authValue) {
return parseValue(authValue)
.flatMap(supplied ->
provider.findByUsernameAndPassword(
supplied.getUserName(),
supplied.getPassword()))
.map(record -> {
final var result = AuthenticationResult.allowed(this);
result.setPrincipal(record.toPrincipal());
return result;
}).orElseGet(() -> AuthenticationResult.denied(this));
}

@Override
Expand All @@ -72,12 +70,11 @@ public void decorateResponse(final AuthenticationResult result, final Response.R

protected static Optional<UsernamePasswordRoles> parseValue(final @NotNull String headerValue){
Preconditions.checkNotNull(headerValue);
String userPass = headerValue.trim();
userPass = new String(Base64.getDecoder().decode(userPass));
final var userPass = new String(Base64.getDecoder().decode(headerValue.trim()));
if(userPass.contains(SEP)){
String[] userNamePassword = userPass.split(SEP);
final var userNamePassword = userPass.split(SEP);
if(userNamePassword.length == 2){
UsernamePasswordRoles usernamePassword = new UsernamePasswordRoles();
final var usernamePassword = new UsernamePasswordRoles();
usernamePassword.setUserName(userNamePassword[0]);
usernamePassword.setPassword(userNamePassword[1]);
return Optional.of(usernamePassword);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,23 @@
*/
package com.hivemq.api.auth.provider;

import com.hivemq.api.auth.ApiPrincipal;
import org.jetbrains.annotations.NotNull;
import com.hivemq.http.core.UsernamePasswordRoles;

import java.util.Optional;
import java.util.Set;

/**
* @author Simon L Johnson
*/
public interface IUsernamePasswordProvider extends ICredentialsProvider {
public interface IUsernameRolesProvider extends ICredentialsProvider {

Optional<UsernamePasswordRoles> findByUsername(final @NotNull String userName);
record UsernameRoles(String username, Set<String> roles){
public ApiPrincipal toPrincipal(){
//decouple the password from the principal for the API
return new ApiPrincipal(username(), Set.copyOf(roles()));
}
}

Optional<UsernameRoles> findByUsernameAndPassword(final @NotNull String userName, final @NotNull String password);
}

This file was deleted.

Loading
Loading