Skip to content

feat: add saml-auth plugin#13346

Merged
AlinsRan merged 26 commits into
apache:masterfrom
AlinsRan:feat/saml-auth-plugin
Jun 1, 2026
Merged

feat: add saml-auth plugin#13346
AlinsRan merged 26 commits into
apache:masterfrom
AlinsRan:feat/saml-auth-plugin

Conversation

@AlinsRan
Copy link
Copy Markdown
Contributor

@AlinsRan AlinsRan commented May 9, 2026

Summary

This PR proposes a new saml-auth plugin for Apache APISIX to support SAML 2.0 authentication at the gateway layer.

The plugin acts as a SAML Service Provider (SP) and integrates with external Identity Providers (IdP) such as Keycloak, Okta, and Azure Active Directory. It allows APISIX to authenticate end users before a request reaches upstream services, while preserving the authenticated identity in ctx.external_user for downstream authorization plugins.

Motivation

APISIX already supports several authentication mechanisms such as key-based auth, JWT, OIDC, and CAS. However, many enterprises still rely on SAML-based identity systems for workforce SSO and federated login.

Without native SAML support at the gateway layer, operators currently have to:

  • implement SAML handling in each upstream service,
  • deploy an additional SAML-aware proxy in front of APISIX, or
  • migrate existing enterprise IdP integrations to another protocol.

This creates unnecessary operational complexity and makes policy enforcement inconsistent across services.

The saml-auth plugin fills this gap by allowing APISIX to terminate the SAML authentication flow directly.

What this plugin provides

The plugin supports:

  • HTTP-Redirect binding (default)
  • HTTP-POST binding
  • Single Sign-On (SSO)
  • Single Logout (SLO)
  • Session key rotation through secret_fallbacks
  • Encrypted-at-rest sensitive fields for sp_private_key, secret, and secret_fallbacks

After a successful login, the authenticated user information is stored in ctx.external_user, so other plugins (for example, acl) can make authorization decisions based on SAML-authenticated user attributes.

Design overview

At request time, the plugin checks whether a valid SAML session already exists.

  • If a valid session exists, the request continues and user attributes are exposed through ctx.external_user.
  • If no valid session exists, APISIX redirects the client to the configured IdP.
  • After the IdP authenticates the user, it sends a signed SAML response back to the configured callback URI.
  • The plugin validates the SAML response, establishes a session, and allows the request to proceed.
  • Logout requests can be initiated through the configured SLO endpoint, and the plugin supports completing the SAML logout flow with the IdP.

To improve operational robustness, the plugin also handles the lua-resty-saml dependency gracefully at runtime and returns a clear 503 when the library is unavailable.

Example use case

A company uses Keycloak as its enterprise IdP and wants all requests to /internal/* to require SAML login before they reach upstream services.

With this plugin, APISIX can:

  1. redirect unauthenticated users to Keycloak,
  2. validate the SAML assertion returned by Keycloak,
  3. establish the gateway-side session, and
  4. pass the authenticated identity to downstream authorization logic.

Example configuration

The following example protects a route with the saml-auth plugin:

curl "http://127.0.0.1:9180/apisix/admin/routes/1" \
  -H "X-API-KEY: $ADMIN_API_KEY" \
  -X PUT \
  -d '{
    "uri": "/internal/*",
    "plugins": {
      "saml-auth": {
        "sp_issuer": "https://sp.example.com",
        "idp_uri": "https://keycloak.example.com/realms/myrealm/protocol/saml",
        "idp_cert": "-----BEGIN CERTIFICATE-----\nMIIC...\n-----END CERTIFICATE-----",
        "login_callback_uri": "https://sp.example.com/login/callback",
        "logout_uri": "https://sp.example.com/logout",
        "logout_callback_uri": "https://sp.example.com/logout/callback",
        "logout_redirect_uri": "https://sp.example.com/logout/done",
        "sp_cert": "-----BEGIN CERTIFICATE-----\nMIIC...\n-----END CERTIFICATE-----",
        "sp_private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIE...\n-----END RSA PRIVATE KEY-----",
        "auth_protocol_binding_method": "HTTP-Redirect",
        "secret": "my-session-secret",
        "secret_fallbacks": ["my-previous-secret"]
      }
    },
    "upstream": {
      "type": "roundrobin",
      "nodes": {
        "127.0.0.1:1980": 1
      }
    }
  }'

Schema highlights

Required fields:

  • sp_issuer
  • idp_uri
  • idp_cert
  • login_callback_uri
  • logout_uri
  • logout_callback_uri
  • logout_redirect_uri
  • sp_cert
  • sp_private_key

Required fields:

  • secret: must be identical on all APISIX nodes; used for resty.session key derivation. Without a stable secret, each worker generates a random key at startup, breaking session verification across workers and after reloads.

Optional fields:

  • auth_protocol_binding_method (HTTP-Redirect or HTTP-POST)
  • secret_fallbacks

Dependency and build notes

This plugin depends on lua-resty-saml.

Because lua-resty-saml builds native xmlsec bindings, this PR also updates the relevant build and CI paths to install the required development dependencies such as:

  • OpenSSL development headers
  • libxml2 development headers
  • libxslt development headers
  • zlib development headers

Files changed

Core plugin and registration:

  • apisix/plugins/saml-auth.lua
  • apisix/cli/config.lua
  • conf/config.yaml.example
  • apisix-master-0.rockspec
  • t/admin/plugins.t

Tests:

  • t/plugin/saml-auth.t

Documentation:

  • docs/en/latest/plugins/saml-auth.md
  • docs/zh/latest/plugins/saml-auth.md
  • docs/en/latest/config.json
  • docs/zh/latest/config.json

Build / CI support:

  • Makefile
  • ci/linux-install-openresty.sh
  • ci/redhat-ci.sh
  • docker/debian-dev/Dockerfile
  • utils/install-dependencies.sh
  • utils/linux-install-luarocks.sh

Backward compatibility

This PR adds a new plugin and does not change the behavior of existing routes unless the plugin is explicitly enabled.

Checklist

  • I have explained the need for this PR and the problem it solves
  • I have explained the changes or the new features added in this PR
  • I have added or updated tests corresponding to this change
  • I have updated the documentation to reflect this change
  • I have verified that this change is backward compatible

@dosubot dosubot Bot added size:XL This PR changes 500-999 lines, ignoring generated files. enhancement New feature or request plugin labels May 9, 2026
@AlinsRan AlinsRan requested a review from Copilot May 13, 2026 08:06
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

Note

Copilot was unable to run its full agentic suite in this review.

Adds a new saml-auth APISIX plugin to enable SAML 2.0 authentication (SP-initiated) for protected routes, along with registration, docs, and basic schema tests.

Changes:

  • Introduces apisix/plugins/saml-auth.lua with schema + rewrite phase logic using lua-resty-saml
  • Registers the plugin in default/config plugin lists and docs navigation
  • Adds docs (EN/ZH) and a test file covering schema validation + missing dependency behavior

Reviewed changes

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

Show a summary per file
File Description
apisix/plugins/saml-auth.lua Implements the saml-auth plugin schema and rewrite handler (loads/initializes lua-resty-saml, sets ctx.external_user).
conf/config.yaml.example Adds saml-auth to the example plugin list at priority 2598.
apisix/cli/config.lua Adds saml-auth to the default enabled plugin list.
t/plugin/saml-auth.t Adds schema validation tests and missing lua-resty-saml graceful-failure tests.
docs/en/latest/plugins/saml-auth.md Adds English plugin documentation and configuration reference.
docs/zh/latest/plugins/saml-auth.md Adds Chinese plugin documentation and configuration reference.
docs/en/latest/config.json Adds plugins/saml-auth to the English docs sidebar/config.
docs/zh/latest/config.json Adds plugins/saml-auth to the Chinese docs sidebar/config.

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

Comment thread apisix/plugins/saml-auth.lua Outdated
Comment thread apisix/plugins/saml-auth.lua
Comment thread apisix/plugins/saml-auth.lua Outdated
Comment thread apisix/plugins/saml-auth.lua Outdated
Comment thread docs/en/latest/plugins/saml-auth.md
Comment thread docs/zh/latest/plugins/saml-auth.md
Comment thread t/plugin/saml-auth.t Outdated
Comment thread t/plugin/saml-auth.t Outdated
@AlinsRan AlinsRan force-pushed the feat/saml-auth-plugin branch 2 times, most recently from 0635136 to 8ff8ddb Compare May 25, 2026 06:40
@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. and removed size:XL This PR changes 500-999 lines, ignoring generated files. labels May 26, 2026
AlinsRan and others added 18 commits May 27, 2026 04:08
The saml-auth plugin enables SAML 2.0 authentication for API routes.
It integrates with external Identity Providers (IdP) such as Keycloak,
Okta, and Azure Active Directory.

The plugin supports:
- HTTP-Redirect and HTTP-POST SAML binding methods
- Single Sign-On (SSO) and Single Logout (SLO)
- Session key rotation via secret_fallbacks
- Encrypted storage of private keys and secrets

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add saml-auth to t/admin/plugins.t expected plugin list
- Fix response_body_like regex for required field error messages
  to tolerate quoted field names (e.g. "sp_issuer" vs sp_issuer)
- Fix preprocessor to also check error_log_like when deciding
  whether to set no_error_log so TEST 8 no longer conflicts

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add additionalProperties=false to schema to reject unknown fields
- Rename unused schema_type param to _ in check_schema
- Change debug=false in saml_lib.init to avoid leaking sensitive data
- Check saml:authenticate() return value and handle errors gracefully
- Add TEST 9 to cover normal rewrite flow with mocked saml library

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This change is not needed and deviates from APISIX plugin conventions.
It would also require downstream EE sync changes unnecessarily.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Change core.log.error to core.log.warn for missing lua-resty-saml
  so TEST 8 passes no_error_log "[error]" check while still matching
  error_log_like pattern (nginx writes warn to error.log)
- Provide required lrucache ctx fields (conf_type/conf_id/conf_version)
  in TEST 9 to fix nil concatenation crash in plugin_ctx_key_and_ver

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The saml-auth plugin requires lua-resty-saml but it was missing
from the rockspec dependencies. Add lua-resty-saml = 0.2.5 to match
the version used in the enterprise edition.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
lua-resty-saml bundles xmlsec1 which requires libxml2 and libxslt at
build time and libxml2 at runtime. Add the missing system dependencies
to the debian-dev Docker image.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
lua-resty-saml builds xmlsec1 from source which requires:
- libxml2-dev / libxml2-devel: XML parsing library
- libxslt-dev / libxslt-devel: XSLT processing library
- libssl-dev: OpenSSL (required by xmlsec1 crypto backend)

Add these to all CI environments: Ubuntu test runner, RedHat/UBI
runner, and the debian-dev Docker image.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
lua-resty-saml rockspec passes OPENSSL_DIR to its make build step
via build_variables. APISIX's make deps configured OPENSSL_LIBDIR and
OPENSSL_INCDIR but never OPENSSL_DIR, causing luarocks to pass an
empty OPENSSL_DIR to make. With an empty OPENSSL_DIR, the ?= default
in lua-resty-saml's Makefile is overridden (command-line assignment
takes precedence), so xmlsec1's configure receives --with-openssl=/
which fails with 'not found: //include/openssl/opensslv.h'.

Fix: add OPENSSL_DIR to the luarocks config alongside OPENSSL_LIBDIR
and OPENSSL_INCDIR so lua-resty-saml finds the OpenResty OpenSSL.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
lua-resty-saml compilation requires:
- libxslt-dev / libxslt-devel: needed for xmlsec/transforms.h
- libxml2-dev / libxml2-devel: needed for libxml2 headers
- zlib-dev / zlib1g-dev: needed for saml.c zlib.h include

Add these to all build environments: install-dependencies.sh
(apt/yum paths) and the debian-dev Docker build stage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The linux_apisix_current_luarocks CI path calls 'luarocks install'
directly (bypassing 'make deps'), so the OPENSSL_DIR luarocks variable
was never set, causing lua-resty-saml's xmlsec1 build to fail with
'not found: //include/openssl/opensslv.h'.

Add OPENSSL_DIR alongside the existing OPENSSL_LIBDIR/INCDIR config.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…apper

Use top-level pcall(require, "resty.saml") instead of a lazy-load
wrapper function. Lua's require already caches modules, so load_resty_saml()
added unnecessary indirection.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
resty.saml is a required dependency; remove pcall and nil-guard,
use a direct require at module level.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
resty.saml is now a direct required dependency (not optional). Remove
TEST 7/8 which tested graceful 503 when the library was absent, as that
behavior no longer exists.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Foo Bar and others added 2 commits May 27, 2026 04:13
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add apisix_keycloak_saml service (port 8087) to docker-compose.plugin.yml
  to avoid realm conflict with existing apisix_keycloak (port 8080)
- Add ci/pod/keycloak/kcadm_configure_saml.sh: creates realm=test, sp and sp2 clients
- Add ci/init-plugin-test-service.sh: wait for port 8087 and run saml configure script
- Add t/lib/keycloak_saml.lua: helper for login/logout/SLO flows
- Add TEST 9-14 in t/plugin/saml-auth.t: full Keycloak integration tests covering
  login, logout, SLO (single logout), and error cases

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AlinsRan AlinsRan force-pushed the feat/saml-auth-plugin branch from dc7ff00 to e6ac8e9 Compare May 26, 2026 20:14
Foo Bar and others added 3 commits May 27, 2026 11:03
- Add Apache License headers to ci/pod/keycloak/kcadm_configure_saml.sh
  and t/lib/keycloak_saml.lua (required by check-license CI)
- Fix kcadm_configure_saml.sh to use port 8087 instead of 8080:
  OSS uses network_mode=host with --http-port=8087, so kcadm must
  connect to localhost:8087 (not 8080 which was correct for EE where
  the container maps 8087->8080 internally)
- Add nil check in get_realm_cert() to log a clear error when the
  SAML descriptor does not contain a certificate

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
nic-6443
nic-6443 previously approved these changes May 27, 2026
@AlinsRan AlinsRan requested a review from membphis May 28, 2026 05:34
@membphis
Copy link
Copy Markdown
Member

Review notes from merge-risk check:

Dependency usage check: lua-resty-saml

This PR adds lua-resty-saml = 0.2.5. The object lifecycle is mostly aligned with api7/lua-resty-saml and API7 EE usage: initialize resty_saml once, create/cache a SAML object per APISIX plugin config, then call saml:authenticate() per request.

One concrete inconsistency still needs attention: upstream lua-resty-saml tests configure a stable session_config.secret, and lua-resty-saml passes opts.secret to resty.session. In this PR, secret is optional, which means resty.session can fall back to process-local random keying material. That is not safe for APISIX multi-worker / reload behavior; see P1 below.

[P1] secret is optional, which can make SAML sessions fail across workers/reloads

  • Problem: saml-auth marks secret as optional, but the underlying lua-resty-saml session uses it as the resty.session encryption/key-derivation secret.
  • Why this blocks merge: when secret is absent, lua-resty-session may use random process-local keying material. The worker that handles the ACS callback may be unable to read the saml_state cookie created by the worker that initiated login; reloads have the same problem.
  • Impact: SAML login can fail intermittently in normal multi-worker deployments, or after worker reload/restart.
  • Trigger: enable saml-auth without configuring secret.
  • Evidence: apisix/plugins/saml-auth.lua makes secret optional and passes conf into resty_saml.new; lua-resty-saml builds session_config.secret = opts.secret; lua-resty-session derives IKM from secret when present, otherwise defaults to random keying material. APISIX openid-connect already requires session.secret for non-bearer interactive login for the same class of reason.
  • Suggested fix: make secret required for saml-auth, require a stronger minimum length, document that all APISIX nodes must share the same value, and add schema coverage.

[P1] Runtime image misses libxslt, so default plugin loading can fail

  • Problem: lua-resty-saml links saml.so with -lxslt, but the final Debian runtime image only installs libxml2, not the runtime libxslt package.
  • Why this blocks merge: saml-auth is added to the default plugin list and directly requires resty.saml at module load time. If libxslt is missing, the gateway/container can fail during plugin load/startup.
  • Impact: official/minimal runtime images may fail to start after this PR, even when users do not configure the new plugin on any route.
  • Trigger: start APISIX with the default plugin list in a runtime image that lacks libxslt1.1.
  • Evidence: lua-resty-saml Makefile links -lxslt; this PR's docker/debian-dev/Dockerfile final stage installs libxml2 but not libxslt1.1; apisix/plugins/saml-auth.lua uses top-level require("resty.saml").
  • Suggested fix: install the runtime libxslt package in the final image and verify all dynamic runtime deps of saml.so are present. If graceful missing-dependency behavior is intended, keep pcall(require, ...) and align the PR description/tests with that behavior.

Foo Bar and others added 2 commits May 28, 2026 14:18
…kers

Without a fixed secret, lua-resty-session generates a random IKM per
worker process. Sessions signed by one worker cannot be verified by
another, causing users to be randomly kicked out in multi-worker
deployments.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…quired in docs

- docker/debian-dev/Dockerfile: add libxslt1.1 to the final runtime
  stage so saml.so can be loaded even when the plugin is not configured
- docs: update secret field Required column from False to True and add
  note that the value must be identical on all nodes

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AlinsRan AlinsRan dismissed stale reviews from shreemaan-abhishek and nic-6443 via 0f3050e May 28, 2026 06:27
@membphis
Copy link
Copy Markdown
Member

Follow-up after the latest update:

The two previous P1 code blockers look fixed now:

  • secret is required by the plugin schema and covered by schema tests.
  • The Debian runtime image installs libxslt1.1, so the lua-resty-saml native module should have the required runtime library.

One small docs mismatch still needs to be fixed before merge:

[P2] Chinese docs still mark secret as optional

  • Problem: docs/zh/latest/plugins/saml-auth.md still shows secret as 必填 = 否, while the plugin schema now requires secret and the English docs show it as required.
  • Why this blocks merge: Chinese users following the docs can omit secret, but the Admin API will reject the config. The row also misses the important note that the value must be identical on all APISIX nodes for session verification across workers/reloads.
  • Evidence: apisix/plugins/saml-auth.lua includes "secret" in the required list; docs/en/latest/plugins/saml-auth.md marks secret as True; docs/zh/latest/plugins/saml-auth.md still marks it as .
  • Suggested fix: change the Chinese secret row to 必填 = 是 and mirror the English description that the value must be identical on all APISIX nodes.

membphis
membphis previously approved these changes May 28, 2026
Copy link
Copy Markdown
Member

@membphis membphis left a comment

Choose a reason for hiding this comment

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

need to update the chinese doc too

docs/zh/latest/plugins/saml-auth.md, secret is a requied field

@AlinsRan AlinsRan merged commit 37c29f8 into apache:master Jun 1, 2026
23 checks passed
@AlinsRan AlinsRan deleted the feat/saml-auth-plugin branch June 1, 2026 06:44
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:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants