Skip to content

Commit

Permalink
New LdapService.authenticate() API + TLS support (#384)
Browse files Browse the repository at this point in the history
  • Loading branch information
lbwexler committed Aug 30, 2024
1 parent fcfa671 commit 86beab1
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 10 deletions.
13 changes: 9 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@

### 💥 Breaking Changes (upgrade difficulty: 🟢 LOW - latest Hoist React + DB col additions)

* Requires `hoist-react >= 67.0.0` for client-side changes to accommodate updated `track`
and `submitError` APIs. See below for database column additions to support the same.

* Requires `hoist-react >= 67.0` to use corresponding role delete bug fix.
* Requires `hoist-react >= 67.0.0`.
* Requires minor DB schema additions (see below).

### 🎁 New Features

Expand All @@ -23,11 +21,18 @@
```sql
ALTER TABLE `xh_track_log` ADD COLUMN `correlation_id` VARCHAR(100) NULL;
```
* New `LdapService.authenticate()` API supports a new way to validate a domain user's credentials by
confirming they can be used to bind to a configured LDAP server.
### 🐞 Bug Fixes
* Fixed bug where a role with a dot in its name could not be deleted.
### ⚙️ Technical
* `LdapService` now binds to configured servers with TLS and supports new `skipTlsCertVerification`
flag in its config to allow for self-signed or otherwise untrusted certificates.
## 20.4.0 - 2024-07-31
### 🐞 Bug Fixes
Expand Down
57 changes: 51 additions & 6 deletions grails-app/services/io/xh/hoist/ldap/LdapService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import io.xh.hoist.BaseService
import io.xh.hoist.cache.Cache
import org.apache.directory.api.ldap.model.entry.Attribute
import org.apache.directory.api.ldap.model.message.SearchScope
import org.apache.directory.api.ldap.model.exception.LdapAuthenticationException
import org.apache.directory.ldap.client.api.LdapConnectionConfig
import org.apache.directory.ldap.client.api.NoVerificationTrustManager
import org.apache.directory.ldap.client.api.LdapNetworkConnection

import static grails.async.Promises.task
Expand All @@ -17,6 +20,7 @@ import static io.xh.hoist.util.DateTimeUtils.SECONDS
* - enabled - true to enable
* - timeoutMs - time to wait for any individual search to resolve.
* - cacheExpireSecs - length of time to cache results. Set to -1 to disable caching.
* - skipTlsCertVerification - true to accept untrusted certificates when binding
* - servers - list of servers to be queried, each containing:
* - host
* - baseUserDn
Expand Down Expand Up @@ -114,6 +118,37 @@ class LdapService extends BaseService {
return ret
}

/**
* Validate a domain user's password by confirming it can be used to bind to a configured LDAP
* server. Note this does *not* on its own cause the user to become authenticated to this
* application - it is intended to support an alternate form-based login strategy as a backup
* to primary OAuth/SSO authentication.
*
* @param username - sAMAccountName for user
* @param password - credentials for user
* @return true if the password is valid and the test connection succeeds
*/
boolean authenticate(String username, String password) {
for (Map server in config.servers) {
String host = server.host
List<LdapPerson> matches = doQuery(server, "(sAMAccountName=$username)", LdapPerson, true)
if (matches) {
if (matches.size() > 1) throw new RuntimeException("Multiple user records found for $username")
LdapPerson user = matches.first()
try (def conn = createConnection(host)) {
conn.bind(user.distinguishedname, password)
conn.unBind()
return true
} catch (LdapAuthenticationException ignored) {
logDebug('Authentication failed, incorrect credentials', [username: username])
return false
}
}
}
logDebug('Authentication failed, no user found', [username: username])
return false
}

//----------------------
// Implementation
//----------------------
Expand All @@ -139,12 +174,9 @@ class LdapService extends BaseService {
if (ret != null) return ret

withDebug(["Querying LDAP", [host: host, filter: filter]]) {
try (LdapNetworkConnection conn = new LdapNetworkConnection(host)) {
try (def conn = createConnection(host)) {
String baseDn = isPerson ? server.baseUserDn : server.baseGroupDn
String[] keys = objType.keys.toArray() as String[]

conn.timeOut = config.timeoutMs as Long

boolean didBind = false
try {
conn.bind(queryUsername, queryUserPwd)
Expand All @@ -153,8 +185,7 @@ class LdapService extends BaseService {
.collect { objType.create(it.attributes as Collection<Attribute>) }
cache.put(key, ret)
} finally {
// Calling unBind on an unbound connection will throw an exception
if (didBind) conn.unBind()
if (didBind) conn.unBind() // If unbound will throw an exception
}
} catch (Exception e) {
if (strictMode) throw e
Expand All @@ -165,6 +196,20 @@ class LdapService extends BaseService {
return ret
}

private LdapNetworkConnection createConnection(String host) {
def ret = new LdapConnectionConfig()
ret.ldapHost = host
ret.ldapPort = ret.defaultLdapPort
ret.timeout = config.timeoutMs as Long
ret.useTls = true

if (config.skipTlsCertVerification) {
ret.setTrustManagers(new NoVerificationTrustManager())
}

return new LdapNetworkConnection(ret)
}

private Map getConfig() {
configService.getMap('xhLdapConfig')
}
Expand Down

0 comments on commit 86beab1

Please sign in to comment.