Skip to content

feat: add dingtalk-auth plugin#13381

Open
AlinsRan wants to merge 3 commits into
masterfrom
feat/dingtalk-auth
Open

feat: add dingtalk-auth plugin#13381
AlinsRan wants to merge 3 commits into
masterfrom
feat/dingtalk-auth

Conversation

@AlinsRan
Copy link
Copy Markdown
Contributor

Summary

Add the dingtalk-auth plugin that integrates DingTalk (Ding Talk / 钉钉) OAuth 2.0 authentication into APISIX routes.

How it works

  1. When a request arrives without a valid session cookie, the plugin checks for a DingTalk authorization code in a configurable query parameter (default: code) or HTTP header (default: X-DingTalk-Code).
  2. If no code is found, the request is redirected (302) to the configured redirect_uri (typically the DingTalk OAuth login page).
  3. If a code is present, the plugin exchanges it for an access token via the DingTalk token API (access_token_url), then retrieves user information from the DingTalk user info API (userinfo_url).
  4. The access token is cached in an LRU cache (TTL: 7000 s) to avoid redundant requests.
  5. Verified user information is stored in an encrypted lua-resty-session v4 cookie session. Subsequent requests carrying the session cookie bypass all DingTalk API calls.
  6. When set_userinfo_header is true (default), the upstream receives the user information in the X-Userinfo header as a Base64-encoded JSON object.

Key attributes

Attribute Default Notes
app_key Required
app_secret Required; stored encrypted
secret Required; 8–32 chars; stored encrypted
redirect_uri Required
code_query code Query param name for the auth code
code_header X-DingTalk-Code Header name for the auth code
cookie_expires_in 86400 Session cookie TTL in seconds
secret_fallbacks Supports zero-downtime key rotation

Plugin priority: 2430 (between key-auth 2500 and consumer-restriction 2400).

Changes

  • apisix/plugins/dingtalk-auth.lua — plugin implementation
  • t/plugin/dingtalk-auth.t — test suite (13 test cases)
  • docs/en/latest/plugins/dingtalk-auth.md — English documentation
  • conf/config.yaml.example — register plugin in default list
  • docs/en/latest/config.json — add to sidebar navigation
  • t/admin/plugins.t — register plugin in admin test list

Add the dingtalk-auth plugin that integrates DingTalk OAuth 2.0
authentication into APISIX routes. The plugin:

- Validates a DingTalk authorization code from a configurable query
  parameter (default: "code") or request header (default:
  "X-DingTalk-Code")
- Exchanges the code for an access token via the DingTalk token API
  and caches it with a 7000-second TTL to avoid repeated token fetches
- Retrieves DingTalk user information and stores it in an encrypted
  cookie session (lua-resty-session v4)
- Forwards user information to upstream in the X-Userinfo header
  (Base64-encoded JSON) when set_userinfo_header is true
- Supports secret_fallbacks for zero-downtime session key rotation

Priority: 2430 (between key-auth 2500 and consumer-restriction 2400)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dosubot dosubot Bot added size:XL This PR changes 500-999 lines, ignoring generated files. enhancement New feature or request plugin labels May 17, 2026
AlinsRan and others added 2 commits May 18, 2026 03:41
…nfo encoding error

- Check return value of sess:save() and return 500 if it fails, rather
  than silently proceeding with an unsaved session
- Handle encoding error for the X-Userinfo header gracefully instead of
  passing nil to base64_encode

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add dingtalk-auth to apisix/cli/config.lua so the plugin is loaded
by the APISIX runtime and recognized by the Admin API.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new dingtalk-auth plugin that integrates DingTalk OAuth 2.0 authentication into APISIX routes. The plugin extracts an authorization code from query/header, exchanges it for an access token (with an LRU cache), retrieves user info from DingTalk, and persists the verified user in an encrypted lua-resty-session v4 cookie. It also optionally forwards user info to the upstream via an X-Userinfo header.

Changes:

  • New plugin implementation apisix/plugins/dingtalk-auth.lua with priority 2430 and encrypted app_secret/secret fields.
  • New test suite t/plugin/dingtalk-auth.t (13 cases) and registration in t/admin/plugins.t, apisix/cli/config.lua, and conf/config.yaml.example.
  • New English documentation docs/en/latest/plugins/dingtalk-auth.md and sidebar entry in docs/en/latest/config.json.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
apisix/plugins/dingtalk-auth.lua New plugin implementing DingTalk OAuth2 code-to-userinfo flow with session cookie.
apisix/cli/config.lua Registers dingtalk-auth in the default plugin list at priority 2430.
conf/config.yaml.example Adds dingtalk-auth to the example plugin list.
t/plugin/dingtalk-auth.t New test suite mocking DingTalk endpoints and exercising schema, redirect, code, session, and custom code-source paths.
t/admin/plugins.t Adds plugin to expected admin plugin list.
docs/en/latest/plugins/dingtalk-auth.md New English plugin documentation.
docs/en/latest/config.json Adds doc page to the English sidebar.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +210 to +277
function _M.rewrite(conf, ctx)
local userinfo, err

local sess, sess_err = session.open(
{
secret = conf.secret,
secret_fallbacks = conf.secret_fallbacks,
cookie_name = "dingtalk_session",
absolute_timeout = conf.cookie_expires_in,
}
)
if not sess then
core.log.error("failed to open session: ", sess_err)
return 500, {message = "Failed to open session"}
end

local raw = sess:get("userinfo")
if raw then
userinfo, err = core.json.decode(raw)
if not userinfo then
sess:destroy()
core.log.error("failed to decode userinfo in session: ", err)
return 500, {message = "Invalid userinfo in session"}
end
else
local code = get_code(conf, ctx)
if not code then
core.response.set_header("Location", conf.redirect_uri)
return 302
end

local key = core.table.concat({
conf.access_token_url,
conf.app_key,
conf.app_secret,
}, "#")
local access_token, err = access_token_cache(key, nil,
fetch_access_token, conf)
if not access_token then
core.log.error("failed to get dingtalk access token: ", err)
return 500, {
message = "Invalid configuration",
}
end

local new_userinfo, err = fetch_userinfo(conf, access_token, code)
if not new_userinfo then
core.log.warn("failed to get dingtalk userinfo: ", err)
return 401, {
message = "Invalid authorization code",
}
end
userinfo = new_userinfo
local raw, err = core.json.encode(userinfo)
if not raw then
core.log.error("failed to encode userinfo: ", err)
return 500, {message = "Invalid userinfo"}
end

sess:set("userinfo", raw)
local ok, save_err = sess:save()
if not ok then
core.log.error("failed to save session: ", save_err)
return 500, {message = "Failed to save session"}
end
core.log.info("verified dingtalk user, code: ", code,
", app_key: ", conf.app_key)
end
if not access_token then
core.log.error("failed to get dingtalk access token: ", err)
return 500, {
message = "Invalid configuration",
core.log.warn("failed to encode userinfo for X-Userinfo header: ", encode_err)
end
end
ctx.external_user = userinfo
Comment on lines +62 to +84
timeout = {type = "integer", default = 6000},
ssl_verify = {type = "boolean", default = true},
secret = {
type = "string",
description = "Secret used for key derivation.",
minLength = 8,
maxLength = 32,
},
secret_fallbacks = {
type = "array",
items = {
type = "string",
minLength = 8,
maxLength = 32,
},
description = "List of secrets for alternative secrets used when doing key rotation"
},
cookie_expires_in = {
type = "integer",
description = "Valid duration (in seconds) for the authorization cookie."
.. "This value defines how long the cookie remains valid after creation.",
default = 86400,
},
})

local DEFAULT_USERINFO_URL = "https://oapi.dingtalk.com/topapi/v2/user/getuserinfo"
local DEFAULT_TOKEN_URL = "https://api.dingtalk.com/v1.0/oauth2/accessToken"
Comment thread t/plugin/dingtalk-auth.t
Comment on lines +111 to +145
=== TEST 2: schema check - missing required field app_key
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.dingtalk-auth")
local ok, err = plugin.check_schema({
app_secret = "appsecret456",
secret = "session-secret-key",
redirect_uri = "/login",
})
ngx.say(ok)
}
}
--- response_body
false



=== TEST 3: schema check - secret too short
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.dingtalk-auth")
local ok, err = plugin.check_schema({
app_key = "appkey123",
app_secret = "appsecret456",
secret = "short",
redirect_uri = "/login",
})
ngx.say(ok)
}
}
--- response_body
false

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.

Comment on lines +70 to +86
secret_fallbacks = {
type = "array",
items = {
type = "string",
minLength = 8,
maxLength = 32,
},
description = "List of secrets for alternative secrets used when doing key rotation"
},
cookie_expires_in = {
type = "integer",
description = "Valid duration (in seconds) for the authorization cookie."
.. "This value defines how long the cookie remains valid after creation.",
default = 86400,
},
},
encrypt_fields = {"app_secret", "secret"},
Comment on lines +23 to +28
-- the access token from dingtalk has a TTL of 7200 seconds,
-- we set the cache TTL to 7000 seconds to avoid edge cases of token expiration during use.
local access_token_cache = core.lrucache.new({
ttl = 7000,
invalid_stale = true,
})
Comment on lines +38 to +62
code_header = {
type = "string",
description = "HTTP header name to extract dingtalk authorization code from.",
default = "X-DingTalk-Code"
},
code_query = {
type = "string",
description = "Query parameter name to extract dingtalk authorization code from.",
default = "code"
},
userinfo_url = {
type = "string",
default = DEFAULT_USERINFO_URL
},
access_token_url = {
type = "string",
default = DEFAULT_TOKEN_URL
},
set_userinfo_header = {
type = "boolean",
description = "Whether to set dingtalk user information in request headers",
default = true
},
redirect_uri = {type = "string"},
timeout = {type = "integer", default = 6000},
", app_key: ", conf.app_key)
end

if userinfo and conf.set_userinfo_header ~= false then
Comment on lines +279 to +285
if userinfo and conf.set_userinfo_header ~= false then
local raw_for_header, encode_err = core.json.encode(userinfo)
if raw_for_header then
core.request.set_header(ctx, "X-Userinfo", base64_encode(raw_for_header))
else
core.log.warn("failed to encode userinfo for X-Userinfo header: ", encode_err)
end
Comment on lines +231 to +232
core.log.error("failed to decode userinfo in session: ", err)
return 500, {message = "Invalid userinfo in session"}
Comment on lines +258 to +259
return 401, {
message = "Invalid authorization code",
Comment thread t/plugin/dingtalk-auth.t
Comment on lines +53 to +54
errcode = 403,
errmsg = "Unauthorized"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request plugin size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants