WP Manifestindependent plugin directory
manifest / security / saml-ldap-sso

SAML LDAP SSO

WordPress SAML 2.0 service provider with optional LDAP. Signed assertions, pinned IdP certificates, NameID account binding, and group-to-role mapping.

by Consid Borås · github.com/considbrs-webdev/saml-ldap-sso · website

0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/considbrs-webdev/saml-ldap-sso/archive/refs/heads/main.zip

Readme

SAML LDAP SSO

A WordPress plugin that acts as a SAML 2.0 Service Provider with optional LDAP fallback. It redirects wp-login.php to your Identity Provider (for example F5 APM), validates signed assertions, binds accounts to NameID, and can map directory groups to WordPress roles.

Author: Consid Borås. Source: github.com/Considbrs-Webdev/saml-ldap-sso.

Table of Contents

Requirements

  • PHP 8.2 or higher with ext-ldap, ext-openssl, and ext-dom
  • WordPress 6.0 or higher
  • HTTPS
  • Composer
  • Node.js & npm (for development)
  • An Identity Provider that signs Assertions (not only the Response)

Installation

  1. Clone or download the plugin to your WordPress plugins directory (wp-content/plugins/saml-ldap-sso)
  2. Install PHP dependencies:
    composer install --no-dev

    vendor/ and vendor-prefixed/ are generated locally and are not in git. xmlseclibs is a runtime require; every install downloads bin/strauss.phar (gitignored, pinned 0.22.2) and copies xmlseclibs to vendor-prefixed/, deleting vendor/robrichards. Do not run strauss include-autoloader. Bump with composer update robrichards/xmlseclibs. Keep the phar pinned — 0.29.x fails under wp-content/plugins/.

  3. Install Node dependencies (for development):
    npm install
  4. Build assets:
    npm run build
  5. Activate the plugin through the WordPress admin panel
  6. Configure SAML (and optionally LDAP) under Settings → SAML LDAP SSO
  7. Import SP metadata in your IdP and copy the Emergency local login URL from Security settings

For a Swedish operations guide (Nginx logs, F5, emergency login), see docs/instructions.md.

Development

Asset Building

The plugin uses Vite for asset bundling. Assets are located in resources/assets/ and are compiled to the dist/ directory. Built files in dist/ are checked in.

Available Scripts

Development Mode

npm run dev

Starts Vite in development mode with hot module replacement. This watches for changes and automatically rebuilds assets.

Production Build

npm run build

Builds optimized production assets with minification and hashing. The build includes:

  • Admin JavaScript (resources/assets/js/admin.jsdist/js/admin.[hash].js)
  • Admin SCSS (resources/assets/scss/admin.scssdist/css/admin.[hash].css)

Creating Distribution Package

GitHub is source only. wordpress.org gets a zip you upload via SVN — not a GitHub release. From the plugin directory:

npm run build
composer install --no-dev
wp dist-archive . ./releases/saml-ldap-sso.zip --create-target-dir --format=zip

composer install --no-dev must run before the zip. It installs runtime PHP deps, downloads bin/strauss.phar if needed, writes vendor-prefixed/, and deletes unprefixed vendor/robrichards. Those directories are gitignored; .distignore still ships them in the zip (except leftover vendor/robrichards and Strauss aliases). bin/ stays out of the zip.

Then upload the zip contents through wordpress.org SVN. Restore full Composer deps afterwards with composer install if you continue developing.

wordpress.org (first submission, 0.1.11)

No banner, icon, or screenshots in this round (skip SVN assets/).

  1. Log in as considboras. Add considadam as a plugin contributor.
  2. Submit the zip at Add your plugin with slug saml-ldap-sso.
  3. After approval, commit the unzipped plugin to https://plugins.svn.wordpress.org/saml-ldap-sso/ (trunk/ plus tag 0.1.11). Do not create GitHub Releases for the wordpress.org zip.
  4. Review typically takes two to four weeks and at least one round of feedback.

Asset Loading

Admin assets are enqueued using the ViteManifest utility class, which reads the generated manifest file to get the correct hashed filenames.

Translations

The plugin uses WordPress i18n. The text domain is saml-ldap-sso. Translation files are located in resources/languages/ (saml-ldap-sso.pot, saml-ldap-sso-sv_SE.po / .mo).

Translation Scripts

Generate/Update POT Template

npm run translate:pot

Scans src/ and resources/ for translatable strings and writes resources/languages/saml-ldap-sso.pot.

Update PO Files

npm run translate:update

Updates existing .po files from the .pot template.

Complete Translation Workflow

npm run translate

Runs POT generation and PO updates.

Compile Translations

npm run translate:compile

Compiles .po files to .mo (same as npm run translate:mo).

Translation Workflow Example

  1. Add translatable strings in PHP:
    __('My translatable text', 'saml-ldap-sso')
  2. Generate/update the POT file:
    npm run translate:pot
  3. Update existing translations:
    npm run translate:update
  4. Edit .po files
  5. Compile:
    npm run translate:compile

Tests

From the plugin directory, after composer install (dev dependencies):

composer test                 # Unit + Security
composer test:unit
composer test:security
composer phpstan              # level 8, src/
composer test:type-coverage   # 100% typed parameters/returns
composer rector               # PHP 8.2 dry-run against src/
composer infection            # mutation testing (needs pcov or Xdebug)

SAML fixtures: php tests/fixtures/saml/build.php (requires tests/fixtures/saml/idp.key).

Integration (own test database, never the lab site DB):

bin/install-wp-tests.sh sls_tests root '' 127.0.0.1
export SLS_WP_TESTS_DIR="$PWD/tests/.wordpress/wordpress-tests-lib"
composer test:integration

Plugin Check against the same surface as the distribution zip:

bin/plugin-check.sh

E2E against Keycloak/OpenLDAP is manual (SLS_E2E=1). See tests/E2E/README.md.

Architecture Overview

SAML LDAP SSO uses a small, instance-based service container and four providers. There is no Illuminate/Laravel container and no static service locator.

Key Components

  • Application: Bootstrap class that registers and boots service providers
  • Container: Instance DI; services are bound as factories and resolved by class name
  • SAML: AuthnRequest, ACS, metadata, XML-DSig via Strauss-prefixed xmlseclibs
  • LDAP: Optional fallback bind/search through ClientInterface (native ldap_* only in NativeClient)
  • Users: NameID binding, role mapping, last-admin and protected-login guards
  • Auth: Emergency local password bypass with rate limiting

Core Concepts

Application & Service Providers

The plugin boots from saml-ldap-sso.php:

Application::configure()
    ->withProviders([
        PluginServiceProvider::class,
        SamlServiceProvider::class,
        LdapServiceProvider::class,
        AdminServiceProvider::class,
    ])
    ->boot();

Service providers have two lifecycle methods:

  • register(): Bind services on the container
  • boot(): Hook into WordPress after all providers have registered
Provider Responsibility
PluginServiceProvider Textdomain, upgrade, logger, rate limiter, users, password bypass, WP-CLI
SamlServiceProvider Metadata, ACS/login routes, signature verifier
LdapServiceProvider Connection, lookup, authenticator
AdminServiceProvider Settings page, diagnostics, Site Health, admin assets

Extending the Plugin

These four filters are the public extension API (signatures will not change in patch releases):

add_filter('sls_map_attributes', function (array $mapped, array $rawAttrs): array {
    // Keys: username, email, first, last, groups, nameId
    // nameId is informational; identity stays the signed assertion NameID.
    return $mapped;
}, 10, 2);

add_filter('sls_user_roles', function (array $roles, WP_User $user, array $groups, string $source): array {
    return $roles;
}, 10, 4);

add_filter('sls_allow_login', function ($allow, WP_User $user, array $context) {
    // Return a WP_Error to reject a valid assertion before the auth cookie is set.
    return $allow;
}, 10, 3);

add_filter('sls_authn_request', function (string $xml, array $saml): string {
    return $xml;
}, 10, 2);

sls_trust_proxy_headers remains available for proxy HTTPS detection. Do not rename the sls_ prefix; it is a locked public API.

WP-CLI

Run from the WordPress root with the plugin active:

wp sls sso disable
wp sls sso enable
wp sls rotate-key
wp sls test-ldap
wp sls test-ldap --user=jdoe --password=secret
wp sls test-saml --file=/tmp/response.xml
wp sls doctor

sso disable turns off auto-redirect to the IdP so emergency or local login works without a SAML hop. rotate-key prints the new emergency URL. doctor checks certificate expiry, LDAP configuration, HTTPS/proxy, and the log directory (no live bind).

File Structure

saml-ldap-sso/
├── saml-ldap-sso.php          # Bootstrap, autoload, Application::boot()
├── helpers.php                # config() helper
├── uninstall.php
├── composer.json
├── .distignore                # Files omitted from wp dist-archive
├── src/
│   ├── Application.php
│   ├── Container.php
│   ├── Plugin.php
│   ├── Activator.php
│   ├── Providers/             # Plugin, Saml, Ldap, Admin
│   ├── Saml/                  # Handler, SignatureVerifier, Metadata
│   ├── Ldap/                  # NativeClient, Connection, UserLookup, Authenticator
│   ├── Users/                 # UserManager, RoleMapper
│   ├── Auth/                  # PasswordBypass, AuthFailureReason
│   ├── Admin/                 # Settings, diagnostics, Site Health
│   ├── Settings/              # Options sanitizer
│   ├── Support/               # Logger, Clock, RateLimiter, View
│   ├── Assets/                # ViteManifest, admin enqueue
│   └── Cli/                   # wp sls …
├── resources/
│   ├── assets/                # Vite sources (js, scss)
│   ├── views/admin/           # Settings page markup
│   └── languages/             # POT / sv_SE
├── dist/                      # Built admin assets (checked in)
├── vendor/                    # Composer runtime (gitignored; generated)
├── vendor-prefixed/           # Strauss-prefixed xmlseclibs (gitignored; generated)
├── tests/                     # Pest Unit, Security, Integration, fixtures
├── docs/                      # Instructions, F5, threat model, privacy, roadmap (shipped in the zip)
└── bin/                       # Strauss phar (gitignored), tests, plugin-check

Configuration

Settings live under Settings → SAML LDAP SSO (options-general.php?page=sls-settings).

  • SAML: IdP Entity ID, SSO URL (the plugin sends an HTTP-POST AuthnRequest), primary/secondary X.509, attribute names (uid, mail, givenName, sn, groups by default). Assertions must be signed. Transient NameID is rejected; unspecified/email/persistent are accepted when the value is stable.
  • LDAP (optional): Off by default. When enabled and fully configured, a new WordPress user is created only after a directory lookup. Existing accounts can still sign in via SAML if LDAP is down. Prefer ldaps:// on port 636.
  • Roles: One mapping per line: source|group|role. Role downgrade is on by default. Protected logins and last-admin cannot lose administrator via SSO.
  • Emergency login: ?password=1&sls_key=… under Security. Rotate the key after use. Only local WordPress accounts (not sls_source saml/ldap) may use it.

Further Documentation

File Contents
docs/instructions.md Swedish install, logs, WP-CLI, F5
docs/f5-apm.md F5 APM / eDirectory profile
docs/f5-checklist.md Staging checklist
docs/threat-model.md Threat model
docs/privacy.md Logging and personal data
readme.txt wordpress.org metadata and changelog

Contributing

When contributing, please:

  1. Keep PHP 8.2+, declare(strict_types=1), and PSR-12 in src/
  2. Add or update Pest tests for changes in src/Saml/ or src/Ldap/
  3. Do not lower PHPStan level 8 or type coverage
  4. Run composer test and composer phpstan before merging
  5. Document login-behaviour changes in readme.txt
  6. Add translations for new strings (npm run translate)

License

GPL v2 or later — see LICENSE.

Read the full README on GitHub →