diff --git a/apisix/plugins/basic-auth.lua b/apisix/plugins/basic-auth.lua index 5d3f46a94201..ba22c0ab7714 100644 --- a/apisix/plugins/basic-auth.lua +++ b/apisix/plugins/basic-auth.lua @@ -43,7 +43,7 @@ local consumer_schema = { title = "work with consumer object", properties = { username = { type = "string" }, - password = { type = "string" }, + password = { type = "string", minLength = 1 }, }, encrypt_fields = {"password"}, required = {"username", "password"}, @@ -148,7 +148,20 @@ local function find_consumer(ctx) return nil, nil, "Invalid user authorization" end - if cur_consumer.auth_conf.password ~= password then + -- the schema rejects an empty password on write and on load, but a secret + -- reference ($secret:// or $env://) is resolved after validation and can + -- yield "": fail closed so such a consumer never authenticates + local expected = cur_consumer.auth_conf.password + if expected == "" then + err = "empty password configured for consumer: " .. cur_consumer.consumer_name + if auth_utils.is_running_under_multi_auth(ctx) then + return nil, nil, err + end + core.log.warn(err) + return nil, nil, "Invalid user authorization" + end + + if expected ~= password then return nil, nil, "Invalid user authorization" end diff --git a/docs/en/latest/plugins/basic-auth.md b/docs/en/latest/plugins/basic-auth.md index a50e99f20256..1e5056f8a83f 100644 --- a/docs/en/latest/plugins/basic-auth.md +++ b/docs/en/latest/plugins/basic-auth.md @@ -48,7 +48,7 @@ For Consumer/Credentials: | Name | Type | Required | Default | Valid values | Description | |------|------|----------|---------|--------------|-------------| | username | string | True | | | Unique basic auth username for a Consumer. | -| password | string | True | | | Basic auth password for the Consumer. The password is encrypted with AES before being stored in etcd. You can also store it in an environment variable and reference it using the `env://` prefix, or in a secret manager such as HashiCorp Vault's KV secrets engine, and reference it using the `secret://` prefix. | +| password | string | True | | non-empty string | Basic auth password for the Consumer. The password is encrypted with AES before being stored in etcd. You can also store it in an environment variable and reference it using the `env://` prefix, or in a secret manager such as HashiCorp Vault's KV secrets engine, and reference it using the `secret://` prefix. | For Route: diff --git a/docs/zh/latest/plugins/basic-auth.md b/docs/zh/latest/plugins/basic-auth.md index d554b55e2270..cc499d926018 100644 --- a/docs/zh/latest/plugins/basic-auth.md +++ b/docs/zh/latest/plugins/basic-auth.md @@ -48,7 +48,7 @@ import TabItem from '@theme/TabItem'; | 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | |------|------|--------|--------|--------|------| | username | string | 是 | | | 消费者的唯一基本认证用户名。 | -| password | string | 是 | | | 消费者的基本认证密码。密码在存储到 etcd 之前会使用 AES 加密。你也可以将其存储在环境变量中并使用 `env://` 前缀引用,或存储在 HashiCorp Vault 等密钥管理器中并使用 `secret://` 前缀引用。 | +| password | string | 是 | | 非空字符串 | 消费者的基本认证密码。密码在存储到 etcd 之前会使用 AES 加密。你也可以将其存储在环境变量中并使用 `env://` 前缀引用,或存储在 HashiCorp Vault 等密钥管理器中并使用 `secret://` 前缀引用。 | Route 端: diff --git a/t/admin/plugins.t b/t/admin/plugins.t index ef70e46d9175..b5d96031e0d2 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -264,7 +264,7 @@ plugins: ngx.HTTP_GET, nil, [[ -{"title":"work with consumer object","required":["username","password"],"properties":{"username":{"type":"string"},"password":{"type":"string"}},"type":"object"} +{"title":"work with consumer object","required":["username","password"],"properties":{"username":{"type":"string"},"password":{"type":"string","minLength":1}},"type":"object"} ]] ) @@ -371,7 +371,7 @@ qr/\[\{"name":"multi-auth","priority":2600\},\{"name":"wolf-rbac","priority":255 } } --- response_body eval -qr/\{"encrypt_fields":\["password"\],"properties":\{"password":\{"type":"string"\},"username":\{"type":"string"\}\},"required":\["username","password"\],"title":"work with consumer object","type":"object"\}/ +qr/\{"encrypt_fields":\["password"\],"properties":\{"password":\{"minLength":1,"type":"string"\},"username":\{"type":"string"\}\},"required":\["username","password"\],"title":"work with consumer object","type":"object"\}/ diff --git a/t/plugin/basic-auth.t b/t/plugin/basic-auth.t index 2e19a2f6d7f1..8a141859e971 100644 --- a/t/plugin/basic-auth.t +++ b/t/plugin/basic-auth.t @@ -344,7 +344,7 @@ GET /t ngx.HTTP_GET, nil, [[ -{"title":"work with consumer object","required":["username","password"],"properties":{"username":{"type":"string"},"password":{"type":"string"}},"type":"object"} +{"title":"work with consumer object","required":["username","password"],"properties":{"username":{"type":"string"},"password":{"type":"string","minLength":1}},"type":"object"} ]] ) ngx.status = code @@ -708,3 +708,211 @@ Authorization: bASiC Zm9vOmJhcg== hello world --- error_log find consumer foo + + + +=== TEST 31: reject an empty password on the consumer +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "foo", + "plugins": { + "basic-auth": { + "username": "foo", + "password": "" + } + } + }]] + ) + ngx.status = code + ngx.print(body) + } + } +--- request +GET /t +--- error_code: 400 +--- response_body +{"error_msg":"invalid plugins configuration: failed to check the configuration of plugin basic-auth err: property \"password\" validation failed: string too short, expected at least 1, got 0"} + + + +=== TEST 32: reject an empty password on the credential +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers/foo/credentials/cred_a', + ngx.HTTP_PUT, + [[{ + "plugins": { + "basic-auth": { + "username": "bar", + "password": "" + } + } + }]] + ) + ngx.status = code + ngx.print(body) + } + } +--- request +GET /t +--- error_code: 400 +--- response_body +{"error_msg":"invalid plugins configuration: failed to check the configuration of plugin basic-auth err: property \"password\" validation failed: string too short, expected at least 1, got 0"} + + + +=== TEST 33: store an empty secret into vault +--- exec +VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/empty passwd= +--- response_body +Success! Data written to: kv/apisix/empty + + + +=== TEST 34: set basic-auth conf: password uses a secret ref that resolves to an empty string +--- request +GET /t +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/secrets/vault/test1', + ngx.HTTP_PUT, + [[{ + "uri": "http://127.0.0.1:8200", + "prefix" : "kv/apisix", + "token" : "root" + }]] + ) + if code >= 300 then + ngx.status = code + return ngx.say(body) + end + + code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "foo", + "plugins": { + "basic-auth": { + "username": "foo", + "password": "$secret://vault/test1/empty/passwd" + } + } + }]] + ) + if code >= 300 then + ngx.status = code + return ngx.say(body) + end + + code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "basic-auth": {} + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 35: verify, empty password on the wire (foo:) is rejected +--- request +GET /hello +--- more_headers +Authorization: Basic Zm9vOg== +--- error_code: 401 + + + +=== TEST 36: verify, whitespace-only password on the wire (foo: ) is rejected +--- request +GET /hello +--- more_headers +Authorization: Basic Zm9vOiA= +--- error_code: 401 +--- response_body +{"message":"Invalid user authorization"} +--- error_log +empty password configured for consumer: foo + + + +=== TEST 37: set basic-auth conf: password uses an env ref that resolves to an empty string +--- main_config +env BASIC_AUTH_EMPTY_PASSWORD=; +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "foo", + "plugins": { + "basic-auth": { + "username": "foo", + "password": "$env://BASIC_AUTH_EMPTY_PASSWORD" + } + } + }]] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 38: verify, empty password on the wire (foo:) is rejected +--- main_config +env BASIC_AUTH_EMPTY_PASSWORD=; +--- request +GET /hello +--- more_headers +Authorization: Basic Zm9vOg== +--- error_code: 401 + + + +=== TEST 39: verify, whitespace-only password on the wire (foo: ) is rejected +--- main_config +env BASIC_AUTH_EMPTY_PASSWORD=; +--- request +GET /hello +--- more_headers +Authorization: Basic Zm9vOiA= +--- error_code: 401 +--- response_body +{"message":"Invalid user authorization"} +--- error_log +empty password configured for consumer: foo