diff --git a/apisix/plugins/multi-auth.lua b/apisix/plugins/multi-auth.lua new file mode 100644 index 000000000000..5c6a825791d7 --- /dev/null +++ b/apisix/plugins/multi-auth.lua @@ -0,0 +1,89 @@ +-- +-- 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 require = require +local pairs = pairs + +local schema = { + type = "object", + title = "work with route or service object", + properties = { + auth_plugins = { type = "array", minItems = 2 } + }, + required = { "auth_plugins" }, +} + + +local plugin_name = "multi-auth" + +local _M = { + version = 0.1, + priority = 2600, + type = 'auth', + name = plugin_name, + schema = schema +} + +function _M.check_schema(conf) + local ok, err = core.schema.check(schema, conf) + if not ok then + return false, err + end + + local auth_plugins = conf.auth_plugins + for k, auth_plugin in pairs(auth_plugins) do + for auth_plugin_name, auth_plugin_conf in pairs(auth_plugin) do + local auth = require("apisix.plugins." .. auth_plugin_name) + if auth == nil then + return false, auth_plugin_name .. " plugin did not found" + else + if auth.type ~= 'auth' then + return false, auth_plugin_name .. " plugin is not supported" + end + end + end + end + + return true +end + +function _M.rewrite(conf, ctx) + local auth_plugins = conf.auth_plugins + local status_code + for k, auth_plugin in pairs(auth_plugins) do + for auth_plugin_name, auth_plugin_conf in pairs(auth_plugin) do + local auth = require("apisix.plugins." .. auth_plugin_name) + -- returns 401 HTTP status code if authentication failed, otherwise returns nothing. + local auth_code = auth.rewrite(auth_plugin_conf, ctx) + status_code = auth_code + if auth_code == nil then + core.log.debug(auth_plugin_name .. " succeed to authenticate the request") + goto authenticated + else + core.log.debug(auth_plugin_name .. " failed to authenticate the request, code: " + .. auth_code) + end + end + end + + :: authenticated :: + if status_code ~= nil then + return 401, { message = "Authorization Failed" } + end +end + +return _M diff --git a/conf/config-default.yaml b/conf/config-default.yaml index 4e257f4d4335..8717d1398b3f 100755 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -457,6 +457,7 @@ plugins: # plugin list (sorted by priority) - uri-blocker # priority: 2900 - request-validation # priority: 2800 - chaitin-waf # priority: 2700 + - multi-auth # priority: 2600 - openid-connect # priority: 2599 - cas-auth # priority: 2597 - authz-casbin # priority: 2560 diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json index e1c8391f275b..05f63bd63fed 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -111,7 +111,8 @@ "plugins/authz-casbin", "plugins/ldap-auth", "plugins/opa", - "plugins/forward-auth" + "plugins/forward-auth", + "plugins/multi-auth" ] }, { diff --git a/docs/en/latest/getting-started/key-authentication.md b/docs/en/latest/getting-started/key-authentication.md index 725bcd3527b9..f6e8a8383f0f 100644 --- a/docs/en/latest/getting-started/key-authentication.md +++ b/docs/en/latest/getting-started/key-authentication.md @@ -28,6 +28,7 @@ APISIX has a flexible plugin extension system and a number of existing plugins f - [LDAP](https://apisix.apache.org/docs/apisix/plugins/ldap-auth/) - [Open Policy Agent (OPA)](https://apisix.apache.org/docs/apisix/plugins/opa/) - [Forward Authentication](https://apisix.apache.org/docs/apisix/plugins/forward-auth/) +- [Multiple Authentications](https://apisix.apache.org/docs/apisix/plugins/multi-auth/) In this tutorial, you will create a _consumer_ with _key authentication_, and learn how to enable and disable key authentication. diff --git a/docs/en/latest/plugins/multi-auth.md b/docs/en/latest/plugins/multi-auth.md new file mode 100644 index 000000000000..703fac19ae2f --- /dev/null +++ b/docs/en/latest/plugins/multi-auth.md @@ -0,0 +1,144 @@ +--- +title: multi-auth +keywords: + - Apache APISIX + - API Gateway + - Plugin + - Multi Auth + - multi-auth +description: This document contains information about the Apache APISIX multi-auth Plugin. +--- + + + +## Description + +The `multi-auth` Plugin is used to add multiple authentication methods to a Route or a Service. It supports plugins of type 'auth'. You can combine different authentication methods using "or" relationship with `multi-auth` plugin. If you want to use multiple methods in an "and" relationship, apply specific authentication plugins directly to the route or service. + +## Attributes + +For Route: + +| Name | Type | Required | Default | Description | +|--------------|-------|----------|---------|-----------------------------------------------------------------------| +| auth_plugins | array | True | - | Add supporting auth plugins configuration. expects at least 2 plugins | + +## Enable Plugin + +To enable the Plugin, you have to create a Consumer object with multiple authentication configurations: + +```shell +curl http://127.0.0.1:9180/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "username": "foo", + "plugins": { + "basic-auth": { + "username": "foo", + "password": "bar" + }, + "key-auth": { + "key": "auth-one" + } + } +}' +``` + +You can also use the [APISIX Dashboard](/docs/dashboard/USER_GUIDE) to complete the operation through a web UI. + +Once you have created a Consumer object, you can then configure a Route or a Service to authenticate requests: + +```shell +curl http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "methods": ["GET"], + "uri": "/hello", + "plugins": { + "multi-auth":{ + "auth_plugins":[ + { + "basic-auth":{ } + }, + { + "key-auth":{ + "query":"apikey", + "hide_credentials":true, + "header":"apikey" + } + } + ] + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } +}' +``` + +## Example usage + +After you have configured the Plugin as mentioned above, you can make a request to the Route as shown below: + +request with basic-auth + +```shell +curl -i -ufoo:bar http://127.0.0.1:9080/hello +``` + +request with key-auth + +```shell +curl http://127.0.0.2:9080/hello -H 'apikey: auth-one' -i +``` + +``` +HTTP/1.1 200 OK +... +hello, world +``` + +If the request is not authorized, an error will be thrown: + +```shell +HTTP/1.1 401 Unauthorized +... +{"message":"Authorization Failed"} +``` + +## Delete Plugin + +To remove the `multi-auth` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect. + +```shell +curl http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "methods": ["GET"], + "uri": "/hello", + "plugins": {}, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } +}' +``` diff --git a/docs/zh/latest/getting-started/key-authentication.md b/docs/zh/latest/getting-started/key-authentication.md index ddcce9029db0..022f020ae909 100644 --- a/docs/zh/latest/getting-started/key-authentication.md +++ b/docs/zh/latest/getting-started/key-authentication.md @@ -28,6 +28,7 @@ APISIX 拥有灵活的插件扩展系统,目前有很多可用于用户身份 - [LDAP](https://apisix.apache.org/zh/docs/apisix/plugins/ldap-auth/) - [Open Policy Agent (OPA)](https://apisix.apache.org/zh/docs/apisix/plugins/opa/) - [Forward Authentication](https://apisix.apache.org/zh/docs/apisix/plugins/forward-auth/) +- [Multiple Authentications](https://apisix.apache.org/docs/apisix/plugins/multi-auth/) 本教程中,你将创建一个带有 _密钥验证_ 插件的 _消费者_,并学习如何启用和停用身份验证插件。 diff --git a/t/admin/plugins.t b/t/admin/plugins.t index e7bcf09e97b4..e7b736fd959c 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -75,6 +75,7 @@ csrf uri-blocker request-validation chaitin-waf +multi-auth openid-connect cas-auth authz-casbin @@ -311,7 +312,7 @@ qr/\{"metadata_schema":\{"properties":\{"ikey":\{"minimum":0,"type":"number"\}," } } --- response_body eval -qr/\[\{"name":"wolf-rbac","priority":2555\},\{"name":"ldap-auth","priority":2540\},\{"name":"hmac-auth","priority":2530\},\{"name":"basic-auth","priority":2520\},\{"name":"jwt-auth","priority":2510\},\{"name":"key-auth","priority":2500\}\]/ +qr/\[\{"name":"multi-auth","priority":2600\},\{"name":"wolf-rbac","priority":2555\},\{"name":"ldap-auth","priority":2540\},\{"name":"hmac-auth","priority":2530\},\{"name":"basic-auth","priority":2520\},\{"name":"jwt-auth","priority":2510\},\{"name":"key-auth","priority":2500\}\]/ diff --git a/t/debug/debug-mode.t b/t/debug/debug-mode.t index 5d645e8e62ec..f2bdbb2c9a75 100644 --- a/t/debug/debug-mode.t +++ b/t/debug/debug-mode.t @@ -53,6 +53,7 @@ loaded plugin and sort by priority: 3000 name: ip-restriction loaded plugin and sort by priority: 2990 name: referer-restriction loaded plugin and sort by priority: 2900 name: uri-blocker loaded plugin and sort by priority: 2800 name: request-validation +loaded plugin and sort by priority: 2600 name: multi-auth loaded plugin and sort by priority: 2599 name: openid-connect loaded plugin and sort by priority: 2555 name: wolf-rbac loaded plugin and sort by priority: 2530 name: hmac-auth diff --git a/t/plugin/multi-auth.t b/t/plugin/multi-auth.t new file mode 100644 index 000000000000..78ec19481b4a --- /dev/null +++ b/t/plugin/multi-auth.t @@ -0,0 +1,414 @@ +# +# 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. +# +use t::APISIX 'no_plan'; + +repeat_each(2); +no_long_string(); +no_root_location(); +no_shuffle(); +run_tests; + +__DATA__ + +=== TEST 1: add consumer with basic-auth and key-auth plugins +--- 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": "bar" + }, + "key-auth": { + "key": "auth-one" + } + } + }]] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 2: enable multi auth plugin using admin api +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "multi-auth": { + "auth_plugins": [ + { + "basic-auth": {} + }, + { + "key-auth": { + "query": "apikey", + "hide_credentials": true, + "header": "apikey" + } + }, + { + "jwt-auth": { + "cookie": "jwt", + "query": "jwt", + "hide_credentials": true, + "header": "authorization" + } + } + ] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 3: verify, missing authorization +--- request +GET /hello +--- error_code: 401 +--- response_body +{"message":"Authorization Failed"} + + + +=== TEST 4: verify basic-auth +--- request +GET /hello +--- more_headers +Authorization: Basic Zm9vOmJhcg== +--- response_body +hello world +--- error_log +find consumer foo + + + +=== TEST 5: verify key-auth +--- request +GET /hello +--- more_headers +apikey: auth-one +--- response_body +hello world + + + +=== TEST 6: verify, invalid basic credentials +--- request +GET /hello +--- more_headers +Authorization: Basic YmFyOmJhcgo= +--- error_code: 401 +--- response_body +{"message":"Authorization Failed"} + + + +=== TEST 7: verify, invalid api key +--- request +GET /hello +--- more_headers +apikey: auth-two +--- error_code: 401 +--- response_body +{"message":"Authorization Failed"} + + + +=== TEST 8: enable multi auth plugin using admin api, without any auth_plugins configuration +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "multi-auth": { } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- error_code: 400 +--- response_body_like eval +qr/\{"error_msg":"failed to check the configuration of plugin multi-auth err: property \\"auth_plugins\\" is required"\}/ + + + +=== TEST 9: enable multi auth plugin using admin api, with auth_plugins configuration but with one authorization plugin +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "multi-auth": { + "auth_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) + } + } +--- request +GET /t +--- error_code: 400 +--- response_body_like eval +qr/\{"error_msg":"failed to check the configuration of plugin multi-auth err: property \\"auth_plugins\\" validation failed: expect array to have at least 2 items"\}/ + + + +=== TEST 10: create public API route (jwt-auth sign) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/2', + ngx.HTTP_PUT, + [[{ + "plugins": { + "public-api": {} + }, + "uri": "/apisix/plugin/jwt/sign" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 11: add consumer with username and jwt-auth plugins +--- 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": "jack", + "plugins": { + "jwt-auth": { + "key": "user-key", + "secret": "my-secret-key" + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 12: sign / verify jwt-auth +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, err, sign = t('/apisix/plugin/jwt/sign?key=user-key', + ngx.HTTP_GET + ) + + if code > 200 then + ngx.status = code + ngx.say(err) + return + end + + local code, _, res = t('/hello?jwt=' .. sign, + ngx.HTTP_GET + ) + + ngx.status = code + ngx.print(res) + } + } +--- request +GET /t +--- response_body +hello world + + + +=== TEST 13: verify multi-auth with plugin config will cause the conf_version change +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + + local code, err = t('/apisix/admin/plugin_configs/1', + ngx.HTTP_PUT, + [[{ + "desc": "Multiple Authentication", + "plugins": { + "multi-auth": { + "auth_plugins": [ + { + "basic-auth": {} + }, + { + "key-auth": { + "query": "apikey", + "hide_credentials": true, + "header": "apikey" + } + }, + { + "jwt-auth": { + "cookie": "jwt", + "query": "jwt", + "hide_credentials": true, + "header": "authorization" + } + } + ] + } + } + }]] + ) + if code > 300 then + ngx.log(ngx.ERR, err) + return + end + + local code, err = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/hello", + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "plugin_config_id": 1 + }]] + ) + if code > 300 then + ngx.log(ngx.ERR, err) + return + end + ngx.sleep(0.1) + + local code, err, sign = t('/apisix/plugin/jwt/sign?key=user-key', + ngx.HTTP_GET + ) + + if code > 200 then + ngx.status = code + ngx.say(err) + return + end + + local code, _, res = t('/hello?jwt=' .. sign, + ngx.HTTP_GET + ) + + ngx.status = code + ngx.print(res) + } + } +--- request +GET /t +--- response_body +hello world