Skip to content

Proposition ldap-auth plugin #3861

Description

@jp-gouin

Hi ,
I've developped a ldap-auth plugin that allow to use a ldap server to authenticate the user.
This plugin is based on lualdap to try the authentication against the ldap server.

What i did is that i create a temporary consumer so that it allow the use of authorisation plugin such as consumer-restriction

Here is the ldap-auth.lua file

--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements.  See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You 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.
--
local core = require("apisix.core")
local ngx = ngx
local ngx_re = require("ngx.re")
local ipairs = ipairs
local consumer = require("apisix.consumer")
local lualdap = require "lualdap"

local lrucache = core.lrucache.new({
    ttl = 300, count = 512
})
local consumers_lrucache = core.lrucache.new({
    type = "plugin",
})

local schema = {
    type = "object",
    title = "work with route or service object",
    properties = {
        basedn = { type = "string" },
        ldapuri = { type = "string" },
        usetls = { type = "boolean" },
        uid = { type = "string" },
    },
    required = {"basedn","ldapuri"},
    additionalProperties = false,
}

local consumer_schema = {
    type = "object",
    title = "work with consumer object",
    properties = {
        userdn = { type = "string" },
    },
    required = {"userdn"},
    additionalProperties = false,
}

local plugin_name = "ldap-auth"

local _M = {
    version = 0.1,
    priority = 2520,
    type = 'auth',
    name = plugin_name,
    schema = schema,
    consumer_schema = consumer_schema
}

function _M.check_schema(conf, schema_type)
    local ok, err
    if schema_type == core.schema.TYPE_CONSUMER then
        ok, err = core.schema.check(consumer_schema, conf)
    else
        ok, err = core.schema.check(schema, conf)
    end

    if not ok then
        return false, err
    end

    return true
end

local function extract_auth_header(authorization)
    local obj = { username = "", password = "" }

    local m, err = ngx.re.match(authorization, "Basic\\s(.+)", "jo")
    if err then
        -- error authorization
        return nil, err
    end

    local decoded = ngx.decode_base64(m[1])

    local res
    res, err = ngx_re.split(decoded, ":")
    if err then
        return nil, "split authorization err:" .. err
    end

    obj.username = ngx.re.gsub(res[1], "\\s+", "", "jo")
    obj.password = ngx.re.gsub(res[2], "\\s+", "", "jo")
    core.log.error("plugin access phase, authorization: ",
                  obj.username, ": ", obj.password)

    return obj, nil
end

function _M.rewrite(conf, ctx)
    core.log.info("plugin access phase, conf: ", core.json.delay_encode(conf))

    -- 1. extract authorization from header
    local auth_header = core.request.header(ctx, "Authorization")
    if not auth_header then
        core.response.set_header("WWW-Authenticate", "Basic realm='.'")
        return 401, { message = "Missing authorization in request" }
    end

    local user, err = extract_auth_header(auth_header)
    if err then
        return 401, { message = err }
    end

    -- 2. try authenticate the user against the ldap server
    local uid = "cn"
    if conf.uid then 
        uid = conf.uid
    end
    local userdn =  uid .. "=" .. user.username .. "," .. conf.basedn
    local ld = lualdap.open_simple (conf.ldapuri, userdn, user.password)
    if not ld then
        return 401, { message = "Invalid user authorization" }
    end

    -- 3. Create temporary consumer for authorization plugin
    local tmpCustomer = {consumer_name = "", auth_conf = {username = ""}}
    local tmpCustomerConf = {conf_version = "1", consumer_name="ldapauth"}
    tmpCustomer.consumer_name = uid .. "=" .. user.username .. "," .. conf.basedn
    tmpCustomer.auth_conf.username = uid .. "=" .. user.username .. "," .. conf.basedn

    consumer.attach_consumer(ctx, tmpCustomer, tmpCustomerConf)

    core.log.info("hit basic-auth access")
end

return _M

The plugin is then configured with :

curl http://127.0.0.1:9080/apisix/admin/routes/12 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
    "uri": "/*",
    "plugins": {
        "ldap-auth": {
          "basedn": "dc=example,dc=org",
          "ldapuri": "172.19.0.1",
          "uid": "cn"
        },
        "consumer-restriction": {
            "whitelist": [
                "cn=admin,dc=example,dc=org"
            ]
        }
    },
    "host": "web1.lvh.me",
    "service_id": "1"
}'

The plugin is configured with basedn, 'ldapuri, the uid (optional) and usetls (optional)

Do you think it's a good approach ? (for a first version)
-> probably it would be good to use groups property for the autorisation part

If so , i can create a PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions