saml-auth
The saml-auth plugin enables APISIX and API7 Gateway to act as a SAML 2.0 service provider (SP). When a user requests a protected route, the gateway redirects the browser to an identity provider (IdP). The gateway validates the signed SAML response and creates an authenticated session before proxying the request upstream.
The plugin supports the HTTP-Redirect binding by default, the HTTP-POST binding, service provider-initiated single logout, and session-secret rotation. Authenticated user data is available in ctx.external_user for other plugins on the same request.
Response Validation
Starting in API7 Enterprise 3.9.21 and APISIX 3.19.0, the plugin checks the following on every login response without additional configuration:
- An assertion that carries an
AudienceRestrictionnamessp_issueror one of the values insp_audiences. - The assertion is used within its
NotBeforeandNotOnOrAfterwindow, with a tolerance ofclock_skewseconds. - The
Recipientof the assertion, and theDestinationof the response if present, match the assertion consumer service URL. - If the assertion names the authentication request it answers (
InResponseTo), that request is the one started by the same browser session.
Unless sp_acs_url is set, the assertion consumer service URL is built from the scheme and host of the callback request. If the gateway runs behind a proxy that terminates TLS without setting X-Forwarded-Proto, or that rewrites the host, set sp_acs_url to the public callback URL. Otherwise, the URL built by the gateway differs from the one the IdP writes into Recipient, and logins are refused.
These response-validation controls and replay-tracking options are not available in API7 Enterprise 3.10.7.
Replay tracking is optional in the releases that support it. Set replay_dict to plugin-saml-auth-replay to record accepted assertions on each gateway instance. Records are not shared across instances, and a full dictionary can allow a login without recording it. See the configuration reference for retention and capacity considerations.
Example
The following example configures SAML single sign-on and single logout with Keycloak. It uses HTTP for local testing and the default HTTP-Redirect binding. Use HTTPS for the gateway and Keycloak in production.
Before proceeding:
- Install Docker, cURL, jq, and OpenSSL.
- Follow the Getting Started tutorial to start APISIX with Docker.
- If you plan to use ADC, install and configure ADC before continuing.
- If you plan to use the Ingress Controller examples, set up the Ingress Controller and gateway in the
aicnamespace.
Start Keycloak
Start Keycloak in the environment that matches the gateway deployment.
- Docker
- Kubernetes
Start Keycloak in development mode:
docker run -d --name apisix-saml-keycloak \
-e 'KC_BOOTSTRAP_ADMIN_USERNAME=quickstart-admin' \
-e 'KC_BOOTSTRAP_ADMIN_PASSWORD=quickstart-admin-pass' \
-p 127.0.0.1:8080:8080 \
quay.io/keycloak/keycloak:26.7.3 start-dev
Create the namespace if it does not already exist:
kubectl create namespace aic --dry-run=client -o yaml | kubectl apply -f -
Create keycloak.yaml with a Keycloak Deployment and Service:
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: aic
name: keycloak
spec:
replicas: 1
selector:
matchLabels:
app: keycloak
template:
metadata:
labels:
app: keycloak
spec:
containers:
- name: keycloak
image: quay.io/keycloak/keycloak:26.7.3
args:
- start-dev
env:
- name: KC_BOOTSTRAP_ADMIN_USERNAME
value: quickstart-admin
- name: KC_BOOTSTRAP_ADMIN_PASSWORD
value: quickstart-admin-pass
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: keycloak
spec:
selector:
app: keycloak
ports:
- port: 8080
targetPort: 8080
Apply the manifest and wait for Keycloak to become available:
kubectl apply -f keycloak.yaml
kubectl rollout status -n aic deployment/keycloak
In a separate terminal, forward the Keycloak port so that the browser can reach the IdP during the SAML flow:
kubectl port-forward -n aic service/keycloak 8080:8080
The development server and example administrator credentials are intended only for local testing. For a production deployment, use Keycloak production mode with HTTPS, a production database, and appropriately managed administrator credentials.
Create the Service Provider Certificate
Generate the certificate and private key that the gateway will use to sign SAML requests:
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout sp-private-key.pem \
-out sp-cert.pem \
-days 365 \
-subj "/CN=APISIX SAML"
Store the private key securely. Replace the self-signed certificate with a certificate that follows the organization's certificate policy for production use.
Configure Keycloak
Open http://localhost:8080/admin/ and sign in with username quickstart-admin and password quickstart-admin-pass.
Create a realm for the example:
- Select Manage realms → Create realm.
- Enter
apisix-samlas the realm name and select Create.
Create a user who can authenticate through the IdP:
- Select Users → Add user.
- Enter
aliceas the username, complete the required profile fields, and select Create. - Open the Credentials tab and select Set password.
- Enter
alice-pass, turn off Temporary, and save the password.
Register the gateway as a SAML client:
- Select Clients → Create client.
- Select
SAMLas the client type, enterapisix-samlas the client ID, and select Save. The client ID must match thesp_issuervalue configured on the plugin. - In Access settings, add these valid redirect URIs:
http://127.0.0.1:9080/anything/login_callbackhttp://127.0.0.1:9080/anything/logout_callback
- In SAML capabilities, keep Force POST binding off.
- In Signature and encryption, turn on Sign documents and Sign assertions, and select
RSA_SHA256as the signature algorithm. - In Advanced → Fine Grain SAML Endpoint Configuration, set Logout Service Redirect Binding URL to
http://127.0.0.1:9080/anything/logout_callback. - Save the client.
- Open the Keys tab, turn on Client signature required, and import
sp-cert.pemas a certificate in PEM format.
The plugin signs authentication and logout requests with sp-private-key.pem. Keycloak verifies those signatures with the imported service provider certificate. The plugin also rejects unsigned Keycloak responses, so Keycloak must sign the SAML documents returned to the gateway.
Save the Keycloak Certificate
Open http://localhost:8080/realms/apisix-saml/protocol/saml/descriptor. Copy the signing certificate from the X509Certificate element and save it as idp-cert.pem with the PEM header and footer:
-----BEGIN CERTIFICATE-----
replace-with-the-keycloak-signing-certificate
-----END CERTIFICATE-----
The metadata also lists the SAML single sign-on endpoint. This example uses http://localhost:8080/realms/apisix-saml/protocol/saml, which the user's browser can reach.
Generate a session secret and load the certificates into environment variables for the gateway configuration:
export SAML_SESSION_SECRET="$(openssl rand -hex 16)"
export IDP_CERT="$(cat idp-cert.pem)"
export SP_CERT="$(cat sp-cert.pem)"
export SP_PRIVATE_KEY="$(cat sp-private-key.pem)"
Use the same session secret on every gateway node. During secret rotation, move the previous value to secret_fallbacks so existing sessions remain readable.
Configure the Gateway
The configuration protects /anything/* and leaves /anything/logout-complete public so that users have a landing page after single logout.
Choose the API used to configure the routes.
- Admin API
- ADC
- Ingress Controller
Create the public logout destination:
curl "http://127.0.0.1:9180/apisix/admin/routes/saml-logout-complete" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"uri": "/anything/logout-complete",
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
Create the protected route. jq reads the certificate and private-key files without changing their PEM formatting:
jq -n \
--arg secret "$SAML_SESSION_SECRET" \
--rawfile idp_cert idp-cert.pem \
--rawfile sp_cert sp-cert.pem \
--rawfile sp_private_key sp-private-key.pem \
'{
"uri": "/anything/*",
"plugins": {
"saml-auth": {
"secret": $secret,
"sp_issuer": "apisix-saml",
"idp_uri": "http://localhost:8080/realms/apisix-saml/protocol/saml",
"login_callback_uri": "/anything/login_callback",
"logout_uri": "/anything/logout",
"logout_callback_uri": "/anything/logout_callback",
"logout_redirect_uri": "/anything/logout-complete",
"idp_cert": $idp_cert,
"sp_cert": $sp_cert,
"sp_private_key": $sp_private_key
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}' | \
curl "http://127.0.0.1:9180/apisix/admin/routes/saml-auth" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-H "Content-Type: application/json" \
--data-binary @-
Create adc.yaml with the public logout destination and protected route:
services:
- name: saml-auth-httpbin
labels:
docs-example: saml-auth
routes:
- name: saml-logout-complete
uris:
- /anything/logout-complete
- name: saml-auth
uris:
- /anything/*
plugins:
saml-auth:
secret: "${SAML_SESSION_SECRET}"
sp_issuer: apisix-saml
idp_uri: http://localhost:8080/realms/apisix-saml/protocol/saml
login_callback_uri: /anything/login_callback
logout_uri: /anything/logout
logout_callback_uri: /anything/logout_callback
logout_redirect_uri: /anything/logout-complete
idp_cert: "${IDP_CERT}"
sp_cert: "${SP_CERT}"
sp_private_key: "${SP_PRIVATE_KEY}"
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
ADC reconciles services as desired state. The label selector limits this example to its own labeled resources. Preview the scoped changes and confirm that they contain no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=saml-auth
Synchronize the reviewed service configuration:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=saml-auth
Replace the certificate, private-key, and session-secret placeholders before applying either manifest. Preserve the PEM header, footer, and \n separators when replacing the certificate and key values.
- Gateway API
- APISIX CRD
Create saml-auth-ic.yaml:
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: saml-auth
spec:
plugins:
- name: saml-auth
config:
secret: replace-with-session-secret
sp_issuer: apisix-saml
idp_uri: http://localhost:8080/realms/apisix-saml/protocol/saml
login_callback_uri: /anything/login_callback
logout_uri: /anything/logout
logout_callback_uri: /anything/logout_callback
logout_redirect_uri: /anything/logout-complete
idp_cert: "-----BEGIN CERTIFICATE-----\nreplace-with-keycloak-certificate\n-----END CERTIFICATE-----"
sp_cert: "-----BEGIN CERTIFICATE-----\nreplace-with-service-provider-certificate\n-----END CERTIFICATE-----"
sp_private_key: "-----BEGIN PRIVATE KEY-----\nreplace-with-service-provider-private-key\n-----END PRIVATE KEY-----"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: saml-auth
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /anything/logout-complete
backendRefs:
- name: httpbin-external-domain
port: 80
- matches:
- path:
type: PathPrefix
value: /anything/
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: saml-auth
backendRefs:
- name: httpbin-external-domain
port: 80
Apply the manifest:
kubectl apply -f saml-auth-ic.yaml
Create saml-auth-ic.yaml:
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: saml-auth
spec:
ingressClassName: apisix
http:
- name: saml-logout-complete
match:
paths:
- /anything/logout-complete
upstreams:
- name: httpbin-external-domain
- name: saml-auth
match:
paths:
- /anything/*
upstreams:
- name: httpbin-external-domain
plugins:
- name: saml-auth
enable: true
config:
secret: replace-with-session-secret
sp_issuer: apisix-saml
idp_uri: http://localhost:8080/realms/apisix-saml/protocol/saml
login_callback_uri: /anything/login_callback
logout_uri: /anything/logout
logout_callback_uri: /anything/logout_callback
logout_redirect_uri: /anything/logout-complete
idp_cert: "-----BEGIN CERTIFICATE-----\nreplace-with-keycloak-certificate\n-----END CERTIFICATE-----"
sp_cert: "-----BEGIN CERTIFICATE-----\nreplace-with-service-provider-certificate\n-----END CERTIFICATE-----"
sp_private_key: "-----BEGIN PRIVATE KEY-----\nreplace-with-service-provider-private-key\n-----END PRIVATE KEY-----"
Apply the manifest:
kubectl apply -f saml-auth-ic.yaml
Verify Single Sign-On and Logout
Open http://127.0.0.1:9080/anything/saml-test in a browser. Keycloak redirects the browser to its login page. Sign in with username alice and password alice-pass.
After authentication, the browser returns to the protected route. The upstream response contains the request details and a saml_session cookie similar to the following:
{
"headers": {
"Cookie": "saml_session=...",
"Host": "127.0.0.1",
"X-Forwarded-Host": "127.0.0.1:9080"
},
"method": "GET",
"url": "http://127.0.0.1/anything/saml-test"
}
Open http://127.0.0.1:9080/anything/logout in the same browser. The gateway sends a signed logout request to Keycloak, processes the signed logout response at /anything/logout_callback, clears the SAML session, and redirects the browser to /anything/logout-complete.
Return to http://127.0.0.1:9080/anything/saml-test. Keycloak should prompt for credentials again, confirming that the gateway and Keycloak sessions were terminated.