-
Notifications
You must be signed in to change notification settings - Fork 2.9k
feat: add saml-auth plugin #13346
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
AlinsRan
wants to merge
15
commits into
apache:master
Choose a base branch
from
AlinsRan:feat/saml-auth-plugin
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
feat: add saml-auth plugin #13346
Changes from 5 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
1a04e89
feat: add saml-auth plugin
AlinsRan ae3627d
docs: add saml-auth to config.json sidebar
AlinsRan 4668202
fix: make lua-resty-saml optional for saml-auth
afb47d5
fix(saml-auth): localize pcall and require to satisfy luacheck
f1d8161
docs(saml-auth): add canonical link
fda4fed
test: fix CI failures in saml-auth tests
56d8268
fix: address code review comments for saml-auth plugin
c18ee76
revert: remove additionalProperties=false from saml-auth schema
6c4e8ca
fix: resolve remaining TEST 8 and TEST 9 failures in saml-auth
3323fa6
fix: add lua-resty-saml dependency to rockspec
37822ea
fix: add libxml2-dev and libxslt1-dev for lua-resty-saml build
4687aa2
fix: add system dependencies required by lua-resty-saml
498ed60
fix: set OPENSSL_DIR luarocks variable for lua-resty-saml build
a8cb4c5
fix: add libxslt, libxml2, and zlib dev packages for lua-resty-saml
f807d9e
fix: set OPENSSL_DIR in linux-install-luarocks.sh for lua-resty-saml
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| -- | ||
| -- 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" }, | ||
| 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, schema_type) | ||
|
AlinsRan marked this conversation as resolved.
Outdated
|
||
| 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.error("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 = true, | ||
| data_dir = constants.apisix_lua_home .. "/deps/share/lua/5.1/resty/saml" | ||
|
AlinsRan marked this conversation as resolved.
Outdated
|
||
| }) | ||
| 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 = saml:authenticate() | ||
|
AlinsRan marked this conversation as resolved.
Outdated
|
||
|
|
||
| ctx.external_user = data | ||
| end | ||
|
|
||
| return _M | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | | ||
|
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 | ||
| } | ||
| } | ||
| }' | ||
| ``` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.