How do I use an External Identity Store for Authentication?

From Iguana 5.6 onwards it is possible to use an External Identity Store to validate user logins.

Use of an External Identity Store in Iguana is optional, and defaults to disabled. When this feature is enabled, Iguana will query the External Identity Store first and then revert to using the local Iguana validation if the external validation fails. Using external logins is transparent to the user as it uses the same credentials (username and password) as a local login. Iguana local logins continue to work in exactly the same manner as previous releases.

The sample script provides demonstrates the use of LDAP authentication, you can adapt this to your own requirements. Notice that the LDAP call has been commented out and replaced with hard-coded authentication to make the script “plug and play”.

How it Works

Configuration

First you need to configure Iguana Authentication to use your Identity Store.

Warning: You must use a web service (http:// or https://), a direct LDAP link (like ldap://10.211.55.4:6544/checkLogin) does not work.

There are two configuration options correspond to two option keys in IguanaConfiguration.xml:

[...]
<auth_config
      use_auth_url="true"
      auth_url="http://10.211.55.4:6544/checkLogin">
[...]

The use_auth_url key can only be set to true when a valid URL is also provided for auth_url (it is valid to have auth_url set while the use_auth_url is set to false). Some basic checking is done to ensure that a properly formed HTTP URL is used for the second setting.

Requests

When enabled, all login requests will query this URL to validate user credentials (and fallback to local Iguana validation if the query fails). Requests are formatted as HTTP GET, and credentials are passed in clear plaintext. The entity listening for these requests will test the credentials (or pass those credentials to some process that can) and respond with a plaintext line-formatted reply.

The query URL that is sent is built up from the base URL plus the user credentials. For example, for the username “John” with the password “Fnord”, the query sent by Iguana would be:

http://10.211.55.4:6544/checkLogin?name=John&password=Fnord

If a request fails, Iguana will fallback to local authentication using the same credentials, which behaves in the same manner as it always has. Basic success and failure is captured in the Iguana server log.

Responses

The response to this request must be formatted in a very specific and simple manner. The response can contain one or two pieces of information, each separated by a newline:

  • A flag that signals success or failure, formatted as a single-character string; “0” for failure and “1” for success
  • (Optional, on success) A list of Iguana registry roles this login can be associated with, each separated by a newline

For example, the following reply could be expected for the request shown above (control characters are shown for informational purposes only):

1<cr><lf>
Administrators<cr><lf>
Network Administrators<cr><lf>
Developers<cr><lf>

Upon receiving this reply, Iguana would know that John’s credentials are correct and he belongs to the following three Roles:

  • Administrators
  • Network Administrators
  • Developers

These Roles do not need to be present in the local registry, nor are they added to the registry if fetched from a reply. But Iguana will try to honour Role membership for externally authenticated users just as it always has for locally authenticated users.

All login successes and failures are logged to the Iguana server log, regardless of whether the authentication occurred via local or external authentication.

Code

Paste the code into a From HTTPS component it should work immediately (but only for the hard-coded users). Comments are provided inline.

 function main(Data)
   -- Get the request details
   local Request = net.http.parseRequest{data = Data}
   local name = Request.params['name'] or "UNKNOWN"
   local password = Request.params['password']

   local success = false

   --success, roles = validateViaPasswd(name, password)
   success = validateViaLdap(name, password)

   if success then
      body = '1'
      for _, role in pairs(roles) do
         body = body .. '\r\n' .. role
      end
   else
      body = '0'
   end

   local Response = net.http.respond{
      body = body,
      entity_type = "text/plain",
      debug_result = true,
      use_gzip = false,
   }

   iguana.logInfo('Returning "' .. body .. '" for: ' .. name)
end

-- Plaintext 'passwd' style authentication
function validateViaPasswd(name, password)
   local passwd = {
      ['foo'] = 'bar',
      ['fnord'] = 'fnord',
      ['John'] = 'password',
      ['fnørd'] = 'πåßß∑ø®∂',
   }

   isGoodPassword = false
   for uid, p in pairs(passwd) do
      if name == uid then
         -- Found a matching UID to the name.
         if password == p then
            -- And passwords match.
            isGoodPassword = true
            break
         end
      end
   end

   return isGoodPassword, getRoles(name)
end

-- Good-enough LDAP authentication via shell command
-- This works only if we can find ldapexop on the local machine.
function validateViaLdap(name, password)
   -- Directory and environment specific details. This will need to be
   -- tweaked.
   local ldapCommand = '/usr/bin/ldapexop'
   local baseDN = ',ou=people,dc=fnord,dc=test'
   local bindDN = 'cn=' .. name .. baseDN
   -- We quote the DN and password, just to be safe.
   local cmdArgs = ' -D "' .. bindDN .. '" -w "' .. password .. '" whoami'

   local h = io.popen(ldapCommand .. cmdArgs)
   local result = h:read()

   -- If the whoami query succeeds the command returns a valid DN, nil otherwise.
   if result == nil then
      return false
   else
      return true, getRoles(name)
   end
end

function getRoles(name)
   local roles = {}

   -- Only some users are Admins
   if name == 'John' or name == 'admin' then
      roles = {
         "A Test Role",
         "Fnord",
         "Administrators",
         "ƒ˜ø®∂",
         "Iguana Administrators",
      }
   else
      roles = {
         "A Test Role",
         "Fnord",
         "ƒ˜ø®∂",
         "Users",
         "Iguana Users",
      }
   end

   return roles
end