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
- The ApiSix version used in this article is 3.2.1, other versions should be similar
- 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