ApiSix plug-in Basic-Auth adds a backup password field to achieve smooth password switching

ApiSix plug-in Basic-Auth adds a backup password field to achieve smooth password switching

  • Introduction
  • Technology stack introduction
    • ApacheAPISIX
    • Consumer Consumer
    • Plugin Basic-Auth
  • cause
  • measure
    • Modify the source code basic-auth.lua
    • Modify consumer configuration
    • test
  • other instructions

Introduction

This article introduces how to add the backup password field backuo_password to the plug-in Basic-Auth in ApiSix to achieve smooth password switching.

Technology stack introduction

Apache APISIX

Apache APISIX is a top-level project under the Apache Software Foundation. It is a cloud-native API gateway with dynamic, real-time, and high-performance characteristics. It provides dynamic routing, dynamic upstream, dynamic certificates, A/B testing, and grayscale publishing ( Canary release), blue-green deployment, speed limit, attack prevention, indicator collection, monitoring and alarm, observability, service governance and other functions.

Consumer Consumer

In Apache APISIX, Consumer is a consumer of a certain type of service and needs to be used in conjunction with user authentication.

Plug-in Basic-Auth

Basic_access_authentication can be added to a Route or Service using the basic-auth plugin. This plugin needs to be used with Consumer. Consumers of the API can add their secret key to request headers to authenticate their requests.

Cause

Usually, we configure the plug-in Basic-Auth for the Consumer to authenticate the Consumer. The password of the plug-in Basic-Auth is changed regularly according to management needs. When changing the password, the Consumer and ApiSix need to modify it at the same time, which increases the cost of the change.

Measures

This article adds a backup password backuo_password to the plug-in Basic-Auth. When the password needs to be changed, ApiSix can first configure the new password in the field password and the original password in the field backuo_password. At this time, the Consumer can use the new password or the old one. Password can be used normally.
Specific measures are as follows:

Modify the source code basic-auth.lua

local consumer_schema = {<!-- -->
    type = "object",
    title = "work with consumer object",
    properties = {<!-- -->
        username = {<!-- --> type = "string" },
        password = {<!-- --> type = "string" },
        backup_password = {<!-- --> type = "string" },
    },
    encrypt_fields = {<!-- -->"password","backup_password"},
    required = {<!-- -->"username", "password"},
}
 -- 4. check the password is correct
    if cur_consumer.auth_conf.password ~= password and cur_consumer.auth_conf.backup_password ~= password then
        return 401, {<!-- --> message = "Invalid user authorization" }
    end

Modify consumer configuration

{<!-- -->
  "username": "consumer_name",
  "desc": "",
  "plugins": {<!-- -->
    "basic-auth": {<!-- -->
      "username": "consumer_name",
      "password": "password1",
      "backup_password": "password2",
      "disable": false
    },
  }
}

Test

slightly

Other instructions

  1. The ApiSix version used in this article is 3.2.1, other versions should be similar
  2. Attached here is the complete basic-auth.lua source code
--
-- 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 consumer = require("apisix.consumer")
local lrucache = core.lrucache.new({
    ttl = 300, count = 512
})

local schema = {
    type = "object",
    title = "work with route or service object",
    properties = {
        hide_credentials = {
            type = "boolean",
            default = false,
        }
    },
}

local consumer_schema = {<!-- -->
    type = "object",
    title = "work with consumer object",
    properties = {<!-- -->
        username = {<!-- --> type = "string" },
        password = {<!-- --> type = "string" },
        backup_password = {<!-- --> type = "string" },
    },
    encrypt_fields = {<!-- -->"password","backup_password"},
    required = {<!-- -->"username", "password"},
}

local plugin_name = "basic-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 function do_extract(auth)
        local obj = { username = "", password = "" }

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

        if not m then
            return nil, "Invalid authorization header format"
        end

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

        if not decoded then
            return nil, "Failed to decode authentication header: " .. m[1]
        end

        local res
        res, err = ngx_re.split(decoded, ":")
        if err then
            return nil, "Split authorization err:" .. err
        end
        if #res < 2 then
            return nil, "Split authorization err: invalid decoded data: " .. decoded
        end

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

        return obj, nil
    end

    local matcher, err = lrucache(authorization, nil, do_extract, authorization)

    if matcher then
        return matcher.username, matcher.password, err
    else
        return "", "", err
    end

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 username, password, err = extract_auth_header(auth_header)
    if err then
        core.log.warn(err)
        return 401, { message = "Invalid authorization in request" }
    end

    -- 2. get user info from consumer plugin
    local consumer_conf = consumer.plugin(plugin_name)
    if not consumer_conf then
        return 401, { message = "Missing related consumer" }
    end

    local consumers = consumer.consumers_kv(plugin_name, consumer_conf, "username")

    -- 3. check user exists
    local cur_consumer = consumers[username]
    if not cur_consumer then
        return 401, { message = "Invalid user authorization" }
    end
    core.log.info("consumer: ", core.json.delay_encode(cur_consumer))

    -- 4. check the password is correct
    if cur_consumer.auth_conf.password ~= password and cur_consumer.auth_conf.backup_password ~= password then
        return 401, {<!-- --> message = "Invalid user authorization" }
    end

    -- 5. hide `Authorization` request header if `hide_credentials` is `true`
    if conf.hide_credentials then
        core.request.set_header(ctx, "Authorization", nil)
    end

    consumer.attach_consumer(ctx, cur_consumer, consumer_conf)

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

return_M