Forward External Auth User Info to Upstream
When you protect a route with openid-connect or saml-auth, API7 Gateway authenticates the request before proxying it upstream. In some cases, the upstream service also needs to know which user made the request.
Both plugins store the authenticated user in ctx.external_user. You can use that value with serverless-post-function to:
- Set
ctx.consumer_nameso the identity is available to logs and other plugins. - Add request headers for the upstream service.
For OpenID Connect, the plugin can already forward user information and tokens as headers. Use serverless-post-function when you need to set consumer_name or when you want to build custom headers for SAML.
Prerequisites
- An API7 Enterprise instance is running.
- A Gateway Group is created and a Gateway instance is running.
- A token from the Dashboard.
- A route is configured with either
openid-connectorsaml-authand authentication completes successfully end-to-end. For OpenID Connect, see OAuth 2.0 and OIDC.
Set the Consumer Name
Add serverless-post-function to the same route in the rewrite phase. By default, it runs after openid-connect and saml-auth, so ctx.external_user is already populated when the function executes. If you have customized plugin priorities, verify that ordering.
The following function uses sub for OpenID Connect or name_id for SAML and writes it to ctx.consumer_name:
phase: rewrite
functions:
- |
return function(conf, ctx)
local user = ctx.external_user
if type(user) ~= "table" then
return
end
local id = user.sub or user.name_id
if id then
ctx.consumer_name = id
end
end
After the configuration reaches the gateway, send an authenticated request through the route. Other plugins can then read ctx.consumer_name. If you want the value in the access log, include consumer_name in the log format.
Forward User Information to the Upstream
OpenID Connect
You do not need serverless-post-function for standard OpenID Connect header forwarding. The plugin can add the following headers to the upstream request:
| Flag | Default | Header set on the upstream request |
|---|---|---|
set_userinfo_header | true | X-Userinfo — Base64-encoded JSON of the userinfo document. |
set_access_token_header | true | X-Access-Token — the raw access token. (Set access_token_in_authorization_header: true to put it in Authorization instead.) |
set_id_token_header | true | X-ID-Token — the raw ID token, when available in the flow. |
set_refresh_token_header | false | X-Refresh-Token — the refresh token. Opt in only when the upstream genuinely needs to refresh on its own. |
Set any flag to false to suppress the corresponding header. See the openid-connect plugin reference for the full set of header-related options.
SAML
SAML attributes do not have an equivalent built-in header. Use serverless-post-function to read ctx.external_user and set the headers explicitly. Clear the headers first so client-supplied values never reach the upstream:
phase: rewrite
functions:
- |
return function(conf, ctx)
local core = require("apisix.core")
-- Clear any client-supplied values first.
core.request.set_header(ctx, "X-SAML-NameID", nil)
core.request.set_header(ctx, "X-SAML-Userinfo", nil)
local user = ctx.external_user
if user and user.authenticated then
core.request.set_header(ctx, "X-SAML-NameID", user.name_id or "")
local userinfo = {
name_id = user.name_id,
session_index = user.session_index,
issuer = user.issuer,
attrs = user.attrs,
}
core.request.set_header(
ctx,
"X-SAML-Userinfo",
ngx.encode_base64(core.json.encode(userinfo))
)
end
end
This example sends:
X-SAML-NameID— the raw NameID for quick lookups.X-SAML-Userinfo— a Base64-encoded JSON document with the NameID, session index, IdP issuer, and the IdP attribute map.
Restrict direct access to the upstream service so these headers are only trusted when the request comes through API7 Gateway.
Use Both Patterns on a SAML Route
If the route needs both consumer_name and SAML headers, combine them in one serverless-post-function instance:
phase: rewrite
functions:
- |
return function(conf, ctx)
local core = require("apisix.core")
local user = ctx.external_user
if type(user) ~= "table" then
return
end
local id = user.sub or user.name_id
if id then
ctx.consumer_name = id
end
core.request.set_header(ctx, "X-SAML-NameID", nil)
core.request.set_header(ctx, "X-SAML-Userinfo", nil)
if user.authenticated and user.name_id then
core.request.set_header(ctx, "X-SAML-NameID", user.name_id)
local userinfo = {
name_id = user.name_id,
session_index = user.session_index,
issuer = user.issuer,
attrs = user.attrs,
}
core.request.set_header(
ctx,
"X-SAML-Userinfo",
ngx.encode_base64(core.json.encode(userinfo))
)
end
end
On OpenID Connect routes, keep only the consumer_name block and rely on the plugin's built-in header flags.
Verify the Result
After you apply the configuration, confirm that the upstream receives the expected user information and that consumer_name is available where you need it.
Check the Upstream Headers
- Point the route at a request-echo upstream such as
httpbin.org/anythingso the response body lists every header the upstream received. - Send an authenticated request through the route.
- For
openid-connectwithbearer_only: true, obtain an access token from the IdP and call the route with-H "Authorization: Bearer <token>". - For
saml-author the interactive OpenID Connect flow, complete the login in a browser.
- For
- Check the echoed request headers in the upstream response.
- For OpenID Connect, look for
X-Userinfo,X-Access-Token, and, if present in the flow,X-ID-Token. - For SAML, look for the headers set by your function, such as
X-SAML-NameIDandX-SAML-Userinfo.
- For OpenID Connect, look for
Check consumer_name
If you set ctx.consumer_name, confirm the value appears where your deployment exposes it, such as in an access log field or another plugin that reads the consumer identity.
Troubleshooting
If the upstream headers or consumer_name do not appear as expected, inspect what ctx.external_user actually contains. Attach this serverless-post-function to the route, send one authenticated request, and read the gateway error log:
phase: rewrite
functions:
- |
return function(conf, ctx)
local core = require("apisix.core")
core.log.warn("ctx.external_user: ", core.json.encode(ctx.external_user, true))
end
The exact field set depends on the IdP, the scopes you request, and any claim or attribute mappers configured on the IdP side. Typical fields are:
| Plugin | Common fields |
|---|---|
openid-connect | sub, email, email_verified, name, preferred_username, given_name, family_name, iss, aud, plus any extra claim returned by the IdP (for example realm_access and resource_access from Keycloak, or roles from Auth0). |
saml-auth | name_id, session_index, issuer, attrs (IdP attribute map), authenticated. |
If the value is empty or nil, the authentication plugin did not run before serverless-post-function, or the request was rejected before reaching the rewrite phase. Check the route's plugin list and the IdP-side configuration. If the value is populated but the field you read (sub, name_id, a custom claim) is missing, adjust the IdP's claim or attribute mappers or read a different field from the dump.
If the configuration looks correct but nothing changes, wait a few seconds for the latest configuration to propagate to the gateway and retry.
Next Steps
- OAuth 2.0 and OIDC — configure an IdP-backed route end-to-end.
- Consumers and Credentials — understand how API7 Gateway tracks identities natively.
- Include Consumer Labels in Access Logs — surface consumer identity (and arbitrary consumer labels) in the gateway access log.
serverless-functionsplugin reference — learn the execution phases and thectxAPI available to your Lua snippets.