Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ deps: install-runtime
$(eval ENV_LUAROCKS_VER := $(shell $(ENV_LUAROCKS) --version | grep -E -o "luarocks [0-9]+."))
@if [ '$(ENV_LUAROCKS_VER)' = 'luarocks 3.' ]; then \
mkdir -p ~/.luarocks; \
$(ENV_LUAROCKS) config $(ENV_LUAROCKS_FLAG_LOCAL) variables.OPENSSL_DIR $(ENV_OPENSSL_PREFIX); \
$(ENV_LUAROCKS) config $(ENV_LUAROCKS_FLAG_LOCAL) variables.OPENSSL_LIBDIR $(addprefix $(ENV_OPENSSL_PREFIX), /lib); \
$(ENV_LUAROCKS) config $(ENV_LUAROCKS_FLAG_LOCAL) variables.OPENSSL_INCDIR $(addprefix $(ENV_OPENSSL_PREFIX), /include); \
$(ENV_LUAROCKS) config $(ENV_LUAROCKS_FLAG_LOCAL) variables.YAML_DIR $(ENV_LIBYAML_INSTALL_PREFIX); \
Expand Down
1 change: 1 addition & 0 deletions apisix-master-0.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ dependencies = {
"lua-resty-radixtree = 2.9.2-0",
"lua-protobuf = 0.5.3-1",
"lua-resty-openidc = 1.8.0-1",
"lua-resty-saml = 0.2.5",
"luafilesystem = 1.8.0-1",
"nginx-lua-prometheus-api7 = 0.20250302-1",
"jsonschema = 0.9.13-0",
Expand Down
1 change: 1 addition & 0 deletions apisix/cli/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ local _M = {
"chaitin-waf",
"multi-auth",
"openid-connect",
"saml-auth",
"cas-auth",
"authz-casbin",
"authz-casdoor",
Expand Down
140 changes: 140 additions & 0 deletions apisix/plugins/saml-auth.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
--
-- 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 require = require
local pcall = pcall
local core = require("apisix.core")
local constants = require("apisix.constants")

local is_resty_saml_init = false
local resty_saml

local lrucache = core.lrucache.new({
ttl = 300, count = 512
})

local schema = {
type = "object",
properties = {
sp_issuer = { type = "string" },
idp_uri = { type = "string" },
idp_cert = { type = "string" },
login_callback_uri = { type = "string" },
Comment thread
AlinsRan marked this conversation as resolved.
logout_uri = { type = "string" },
logout_callback_uri = { type = "string" },
logout_redirect_uri = { type = "string" },
sp_cert = { type = "string" },
sp_private_key = { type = "string" },
auth_protocol_binding_method = {
type = "string",
default = "HTTP-Redirect",
enum = {"HTTP-Redirect", "HTTP-POST",},
description = "Binding method for authentication protocol, setting to HTTP-POST " ..
"will set cookie samesite to None and cookie secure to 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"
}
},
encrypt_fields = {"sp_private_key", "secret", "secret_fallbacks"},
required = {
"sp_issuer",
"idp_uri",
"idp_cert",
"login_callback_uri",
"logout_uri",
"logout_callback_uri",
"logout_redirect_uri",
"sp_cert",
"sp_private_key",
}
}

local plugin_name = "saml-auth"

local _M = {
version = 0.1,
priority = 2598,
name = plugin_name,
schema = schema,
}


local function load_resty_saml()
if resty_saml then
return resty_saml
end

local ok, saml = pcall(require, "resty.saml")
if not ok then
return nil, saml
end

resty_saml = saml
return resty_saml
end

function _M.check_schema(conf, _)
return core.schema.check(schema, conf)
end

function _M.rewrite(conf, ctx)
local saml_lib, err = load_resty_saml()
if not saml_lib then
core.log.warn("failed to load lua-resty-saml: ", err)
return 503, {message = "lua-resty-saml is required for saml-auth"}
end

if not is_resty_saml_init then
local err = saml_lib.init({
debug = false,
data_dir = constants.apisix_lua_home .. "/deps/share/lua/5.1/resty/saml"
})
if err then
core.log.error("saml init: ", err)
return 503, {message = "saml init failed"}
end
is_resty_saml_init = true
end

local saml = core.lrucache.plugin_ctx(lrucache, ctx, nil, saml_lib.new, conf)
if not saml then
core.log.error("saml new failed")
return 500, {message = "create saml object failed"}
end

local data, err = saml:authenticate()
if err then
core.log.error("saml authenticate failed: ", err)
return 500, {message = "saml authentication failed"}
end

ctx.external_user = data
end

return _M
2 changes: 1 addition & 1 deletion ci/linux-install-openresty.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ sudo add-apt-repository -y "deb https://openresty.org/package/${arch_path}ubuntu
sudo add-apt-repository -y "deb http://repos.apiseven.com/packages/${arch_path}debian bullseye main"

sudo apt-get update
sudo apt-get install -y openresty-pcre-dev openresty-zlib-dev build-essential gcc g++ cpanminus
sudo apt-get install -y openresty-pcre-dev openresty-zlib-dev build-essential gcc g++ cpanminus libxml2-dev libxslt-dev

SSL_LIB_VERSION=${SSL_LIB_VERSION-openssl}
ENABLE_FIPS=${ENABLE_FIPS:-"false"}
Expand Down
2 changes: 1 addition & 1 deletion ci/redhat-ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ install_dependencies() {
yum install -y --disablerepo=* --enablerepo=ubi-8-appstream-rpms --enablerepo=ubi-8-baseos-rpms \
wget tar gcc gcc-c++ automake autoconf libtool make unzip git sudo openldap-devel hostname patch \
which ca-certificates pcre pcre-devel pcre2 pcre2-devel xz \
openssl-devel
openssl-devel libxml2-devel libxslt-devel
yum install -y libyaml-devel
yum install -y --disablerepo=* --enablerepo=ubi-8-appstream-rpms --enablerepo=ubi-8-baseos-rpms cpanminus perl

Expand Down
1 change: 1 addition & 0 deletions conf/config.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,7 @@ plugins: # plugin list (sorted by priority)
- chaitin-waf # priority: 2700
- multi-auth # priority: 2600
- openid-connect # priority: 2599
- saml-auth # priority: 2598
- cas-auth # priority: 2597
- authz-casbin # priority: 2560
- authz-casdoor # priority: 2559
Expand Down
7 changes: 6 additions & 1 deletion docker/debian-dev/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ RUN set -x \
git \
sudo \
libyaml-dev \
libxml2-dev \
libxslt1-dev \
pkg-config \
libssl-dev \
zlib1g-dev \
&& ls -al \
&& make deps \
&& mkdir -p ${ENV_INST_LUADIR} \
Expand All @@ -46,7 +51,7 @@ ARG INSTALL_BROTLI=./install-brotli.sh

# Install the runtime libyaml package
RUN apt-get -y update --fix-missing \
&& apt-get install -y libldap2-dev libyaml-0-2 \
&& apt-get install -y libldap2-dev libyaml-0-2 libxml2 \
&& apt-get remove --purge --auto-remove -y \
&& mkdir -p /usr/local/apisix/ui

Expand Down
3 changes: 2 additions & 1 deletion docs/en/latest/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,8 @@
"plugins/ldap-auth",
"plugins/opa",
"plugins/forward-auth",
"plugins/multi-auth"
"plugins/multi-auth",
"plugins/saml-auth"
]
},
{
Expand Down
153 changes: 153 additions & 0 deletions docs/en/latest/plugins/saml-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
---
title: saml-auth
keywords:
- Apache APISIX
- API Gateway
- SAML
- SAML 2.0
- SSO
- Single Sign-On
description: The saml-auth Plugin enables SAML 2.0 authentication for API routes, integrating with external Identity Providers (IdP) such as Keycloak, Okta, and Azure Active Directory.
---

<!--
#
# 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.
#
-->

<head>
<link rel="canonical" href="https://docs.api7.ai/hub/saml-auth" />
</head>

## Description

The `saml-auth` Plugin enables [SAML 2.0](https://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html) (Security Assertion Markup Language) authentication for API routes. It acts as a SAML Service Provider (SP) and integrates with external Identity Providers (IdP) such as Keycloak, Okta, and Azure Active Directory to authenticate users before allowing access to upstream resources.

When a request arrives at a protected route, the Plugin checks for a valid SAML session. If no session exists, it redirects the user to the IdP for authentication. After the user authenticates at the IdP, the IdP posts a signed SAML assertion back to the SP's Assertion Consumer Service (ACS) URL. The Plugin validates the assertion and establishes a session for the user.

The Plugin supports:

- **HTTP-Redirect binding** (default) — SAML messages are transmitted as URL query parameters.
- **HTTP-POST binding** — SAML messages are transmitted as HTML form values.
- **Single Logout (SLO)** — logout requests can be initiated by the SP or the IdP.
- **Session key rotation** via `secret_fallbacks`.

Authenticated user data is stored in `ctx.external_user` and can be used by downstream authorization plugins such as `acl`.

## Attributes

| Name | Type | Required | Encrypted | Default | Valid Values | Description |
|------|------|----------|-----------|---------|--------------|-------------|
| sp_issuer | string | True | | | | Service Provider (SP) entity ID/issuer URI. Must match the SP entity ID registered with the IdP. |
Comment thread
AlinsRan marked this conversation as resolved.
| idp_uri | string | True | | | | Identity Provider SSO endpoint URL. This is the URL to which SAML authentication requests are sent. |
| idp_cert | string | True | | | | IdP's X.509 certificate in PEM format, used to verify signatures on SAML assertions. |
| login_callback_uri | string | True | | | | SP's Assertion Consumer Service (ACS) URL. The IdP posts SAML responses to this URL after authentication. Must be registered with the IdP. |
| logout_uri | string | True | | | | SP's Single Logout (SLO) endpoint. Requests to this URI initiate the logout flow. |
| logout_callback_uri | string | True | | | | SP's SLO callback URL. The IdP sends logout responses to this URL. Must be registered with the IdP. |
| logout_redirect_uri | string | True | | | | URL to redirect users to after a successful logout. |
| sp_cert | string | True | | | | SP's X.509 certificate in PEM format. Used by the IdP to verify requests signed by the SP. |
| sp_private_key | string | True | Yes | | | SP's private key in PEM format, used to sign SAML requests. This field is encrypted at rest. |
| auth_protocol_binding_method | string | False | | `HTTP-Redirect` | `HTTP-Redirect`, `HTTP-POST` | SAML binding method for the authentication request. When set to `HTTP-POST`, the session cookie `SameSite` attribute is set to `None` and `Secure` is set to `true`. |
| secret | string | False | Yes | | 8–32 characters | Secret used for session key derivation. This field is encrypted at rest. |
| secret_fallbacks | array[string] | False | Yes | | Each item: 8–32 characters | List of previous secrets used during key rotation. Allows sessions encrypted with old secrets to remain valid. This field is encrypted at rest. |

## Prerequisites

Install `lua-resty-saml` on every APISIX node before enabling this Plugin:

```shell
luarocks install lua-resty-saml 0.2.5
```

`lua-resty-saml` builds native xmlsec bindings, so the build environment must provide the OpenSSL, libxml2, and libxslt development files required by LuaRocks.

Before configuring the `saml-auth` Plugin, you need to register APISIX as a Service Provider with your Identity Provider. The exact steps depend on your IdP; the following example uses [Keycloak](https://www.keycloak.org/).

### Set Up Keycloak

1. Log in to the Keycloak Admin Console.
2. Create or select a realm (for example, `myrealm`).
3. Navigate to **Clients** and click **Create client**.
4. Set **Client type** to `SAML`.
5. Set **Client ID** to match the `sp_issuer` value you will use in the Plugin configuration (for example, `https://sp.example.com`).
6. Under **Client** > **Settings**:
- Set **Root URL** to `https://sp.example.com`.
- Set **Valid redirect URIs** to include the `login_callback_uri` (for example, `https://sp.example.com/login/callback`).
- Set **Master SAML Processing URL** to `https://sp.example.com/login/callback`.
7. Under **Client** > **Keys**, upload the SP certificate (`sp_cert`) and enable **Sign assertions**.
8. Export the IdP metadata to obtain the `idp_uri` (SSO URL) and `idp_cert` (signing certificate).
9. Create users in Keycloak that will be allowed to authenticate.

## Enable the Plugin

The following example creates a route protected by the `saml-auth` Plugin using a Keycloak IdP:

:::note

Replace the placeholder certificate and key values with your actual SP certificate, SP private key, and IdP certificate.

:::

```shell
curl "http://127.0.0.1:9180/apisix/admin/routes/1" \
-H "X-API-KEY: $ADMIN_API_KEY" \
-X PUT \
-d '{
"uri": "/*",
"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"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"127.0.0.1:1980": 1
}
}
}'
```

## Disable the Plugin

To disable the `saml-auth` Plugin, remove it from the route configuration:

```shell
curl "http://127.0.0.1:9180/apisix/admin/routes/1" \
-H "X-API-KEY: $ADMIN_API_KEY" \
-X PUT \
-d '{
"uri": "/*",
"plugins": {},
"upstream": {
"type": "roundrobin",
"nodes": {
"127.0.0.1:1980": 1
}
}
}'
```
3 changes: 2 additions & 1 deletion docs/zh/latest/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@
"plugins/ldap-auth",
"plugins/opa",
"plugins/forward-auth",
"plugins/multi-auth"
"plugins/multi-auth",
"plugins/saml-auth"
]
},
{
Expand Down
Loading
Loading