Custom Challenge
To facilitate users in implementing challenge mechanisms that meet their specific business requirements, OpenResty Edge introduced the Custom Challenge
feature in version 25.9
.
Currently, this feature can be enabled through the Edgelang interface. In future versions, we will provide corresponding configuration options in page rule actions.
Interface Description
See the Edgelang interface enable-custom-captcha for details.
Global Lua Module Description
Global Lua modules used for custom challenges must meet specific design requirements to work properly with OpenResty Edge. The specific requirements are as follows:
- Must export three interfaces:
invoke
,create
, andverify
, whereinvoke
andverify
are required, andcreate
can be used as needed. - Interface calls in HTML pages must use specified URLs and methods, as detailed in the subsequent interface descriptions.
invoke Interface Description
This interface is used to return an HTML page when a challenge is triggered.
function _M.invoke(params)
-- return HTML page
end
The params
parameter contains:
token
: Encrypted data that needs to be rendered into the HTML page and passed to subsequent challenge interface callstime
: Timestamp when the challenge was triggered, can be used to reject expired challenges in subsequent interfaces. Unit is seconds, precision is millisecondsprev_url
: The URL originally requested by the client, which usually needs to be redirected back to after the challenge passes. Example:/hello.html
clearance_time
: Valid time for verification results, in seconds. Example: 60
The return value is the final HTML page content to be responded.
create Interface Description
This interface is used to create challenge content or regenerate challenge content when refreshing.
If your challenge does not need to support refresh functionality, you can directly render the challenge content during the invoke
stage without triggering this interface.
function _M.create(params, uri_args)
-- return challenge content
end
The params
parameter contains:
time
: Timestamp when the challenge was triggered, can be used to reject expired challenges. Unit is seconds, precision is millisecondsclearance_time
: Valid time for verification results, in seconds. Example: 60
The uri_args
parameter must contain the token
from the params
parameter of the invoke
interface.
The return value is the generated challenge content.
After this interface responds, it needs to be processed by your predefined HTML page.
You need to use a fixed URI to call the create interface: GET /.edge-waf/create-captcha?token=TOKEN
- Request URI:
/.edge-waf/create-captcha
- Request method:
GET
- URL parameter:
token=TOKEN
verify Interface Description
This interface is used to verify challenge results.
function _M.verify(params, post_args)
local ok, err = result_verify(params, post_args)
if not ok then
return false
end
return true
end
The params
parameter contains:
time
: Timestamp when the challenge was triggered, can be used to reject expired challenges. Unit is seconds, precision is millisecondsclearance_time
: Valid time for verification results, in seconds. Example: 60
The post_args
parameter must contain token
and prev_url
from the params
parameter of the invoke
interface. After verification passes, prev_url
will be returned as the response body.
The return value is true
or false
. When false
is returned, the request will respond with a 403 status code. You can also respond with other status codes in advance as needed in this interface.
After this interface responds, it needs to be processed by your predefined HTML page.
You need to use a specific request format to call the verify interface:
POST /.edge-waf/edge-recaptcha
Content-Type: application/x-www-form-urlencoded
token=TOKEN&prev_url=PREV_URL
- Request URI:
/.edge-waf/edge-recaptcha
- Request method:
POST
- Request header:
Content-Type: application/x-www-form-urlencoded
- Request body: Must contain parameters
token=TOKEN
andprev_url=PREV_URL
Practical Application Example
The following demonstrates how to customize a CAPTCHA challenge.
Adding Global Lua Module
In Global Config > Global Lua Modules
, create a global Lua module named example-custom-captcha
:
Source code:
local resty_captcha = require "resty.captcha"
local ck = require "resty.cookie"
local resty_random = require "resty.random"
local resty_string = require "resty.string"
local font_path = ngx.config.prefix() .. "font/Vera.ttf"
local captcha_dict = ngx.shared.captcha_store
local str_lower = string.lower
local str_fmt = string.format
local _M = {}
function _M.invoke(params)
local html_template = [[
<!doctype html>
<html>
<head>
<title>Response</title>
</head>
<body>
<img id="captcha_image" src="/.edge-waf/create-captcha?token=%s" alt="captcha" class="captcha_image">
<form id="edge-recaptcha-form">
<input type="text" id="edge-input-captcha" name="captcha" class="outline-input text-short" style=" border: 1px solid;">
<input type="hidden" id="edge-input-prev_url" name="prev_url" value="%s">
<input type="hidden" id="edge-input-token" name="token" value="%s">
<input type="submit" style="border: 1px solid; padding: 0px 18px;">
<p id="captcha_error" style="color: red; margin-top: 15px; line-height: 1.5;">
<form>
<script type="text/javascript">
var URL = '/.edge-waf/edge-recaptcha';
var form = document.getElementById('edge-recaptcha-form');
form.addEventListener('submit', function (e) {
e.preventDefault();
var xhr = new XMLHttpRequest();
var captcha = document.getElementById('edge-input-captcha').value;
var prevUrl = document.getElementById('edge-input-prev_url').value;
var token = document.getElementById('edge-input-token').value;
if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else if (window.ActiveXObject) {
xhr = new ActiveXObject('Microsoft.XMLHTTP');
}
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
window.location.replace(xhr.responseText);
} else if (xhr.status > 400 && xhr.status < 500) {
document.getElementById('captcha_error').innerText = 'Verification failed, please refresh and try again.';
} else if (xhr.status >= 500) {
var reqId = xhr.getResponseHeader('req-id');
document.getElementById('captcha_error').innerText = 'Verification failed, please refresh and try again. Status code: ' + xhr.status + ', Request ID: ' + reqId;
}
}
};
xhr.open('POST', URL, true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('captcha=' + captcha + '&prev_url=' + prevUrl + '&token=' + token);
});
</script>
</body>
</html>
]]
return str_fmt(html_template, params.token, params.prev_url, params.token)
end
local function generate_session_id()
local bytes = resty_random.bytes(16)
return resty_string.to_hex(bytes)
end
function _M.create(params, uri_args)
local cap = resty_captcha.new()
cap:font(font_path)
cap:length(4)
cap:scribble()
cap:generate()
local session_id = generate_session_id()
local captcha_text = cap:getStr()
local ttl = params.clearance_time or 60
captcha_dict:set(session_id, str_lower(captcha_text), ttl)
local cookie = ck:new()
cookie:set({
key = "captcha_session",
value = session_id,
path = "/",
httponly = true,
max_age = ttl
})
return cap:jpegStr(70)
end
function _M.verify(params, post_args)
local cookie = ck:new()
local session_id = cookie:get("captcha_session")
if not session_id then
ngx.log(ngx.ERR, "missing captcha session")
ngx.exit(403)
end
local stored_captcha = captcha_dict:get(session_id)
if not stored_captcha then
ngx.log(ngx.ERR, "captcha session expired or invalid")
ngx.exit(403)
end
local user_input = str_lower(post_args["captcha"] or "")
captcha_dict:delete(session_id)
return user_input == stored_captcha
end
return _M
Features of this Lua module:
- Defines and exports 3 required functions:
invoke
,create
,verify
- In the HTML page, uses HTTP request formats that comply with design requirements to call
/.edge-waf/create-captcha
and/.edge-waf/edge-recaptcha
- Stores CAPTCHA information in shared memory
captcha_store
Adding Shared Memory
In Global Config > Global Lua Modules > Custom Shared Memory Dictionaries
, enter captcha_store
and save:
Using Custom Challenge in Application
First, add a rule to enable custom challenge:
Then add a rule that returns Hello
to simulate the page displayed after successful custom challenge verification:
Testing
Challenge page triggered:
After completing the challenge, automatically redirect to the protected resource page: