=== Open Access SSO ===
Contributors: idgold
Donate link: https://ko-fi.com/idgold
Tags: sso, saml, oidc, single-sign-on, authentication
Requires at least: 6.0
Tested up to: 7.1
Stable tag: 3.0.0
Requires PHP: 8.1
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Free, privacy-first SAML 2.0 and OpenID Connect single sign-on for WordPress. Role mapping, access control, multi-IdP. No premium tier, no tracking.

== Description ==

**Let your team sign in to WordPress with the company login they already use — one click, no extra password to manage, reset, or chase down.**

Open Access SSO connects your WordPress site to the identity provider your organisation already runs, so people log in through your trusted corporate sign-in instead of juggling yet another WordPress password. It works with any standard SAML 2.0 identity provider — the plugin's mature, fully-featured protocol — and, new in this release, with OpenID Connect providers too. That covers Microsoft Entra ID (Azure AD), Okta, OneLogin, Auth0, Keycloak, ADFS, Shibboleth, and NetIQ Access Manager (now OpenText), and it's completely free and open-source, with no premium tier, no license key, and no upsell.

Everything below is included. Nothing is locked, metered, or "Pro."

= Why site admins choose it =

* **Free, forever.** Every feature ships in the GPLv2-or-later codebase — full role mapping, multi-IdP, access control, and more, at no cost. No paid plan, no per-site license, no feature gates.
* **Privacy-first by design.** No telemetry, no analytics, no "phone home," and no external CDN. Your configuration never leaves your site.
* **Works with the identity provider you already have.** Point-and-click setup for any standards-compliant SAML 2.0 or OpenID Connect provider — no developer required for normal use.
* **Emergency admin access.** A built-in bypass lets an admin get back in even if SSO is misconfigured — through a constant in `wp-config.php` or a pre-set bypass key.
* **Open and original.** A clean-room, from-scratch implementation you can read and audit.

= Connect it to your identity provider =

* **One-click sign-in via SSO** — let users log in through your IdP instead of, or alongside, the WordPress login form, with optional single logout that signs them out of both at once.
* **Multi-IdP support** — connect several identity providers and let users pick with a login button or a simple `?idp=slug` link.
* **Encrypted logins handled out of the box** — including the encryption that modern providers (such as NetIQ Access Manager) turn on by default and that stock PHP can't unwrap on its own, so encrypted sign-in just works.
* **Attribute mapping** — map fields from your IdP (name, email, display name, and custom user data) straight onto WordPress profiles.

= Control who gets in, and what they see =

* **Powerful role mapping** — automatically assign WordPress roles based on a user's groups or attributes, with exact, contains, or regex matching, per-IdP rules, a default-role fallback, and an option to deny anyone who doesn't match. A built-in safeguard means SSO won't hand out admin-level roles unless you explicitly allow it.
* **Account linking** — connect a first SSO login to an existing WordPress account by email (administrators are never auto-linked, for safety).
* **Page and content access control** — restrict pages, posts, and custom post types to chosen roles or to "logged in via SSO" users, with a per-page editor control, the handy `[oasso_restrict]` shortcode, and category/tag-level rules. The same protection also covers the REST API, feeds, and oEmbed, so restricted content doesn't leak through a side door.
* **Protected files and blocks** — keep uploaded files behind a role check, and show or hide individual Gutenberg blocks by login status or role.
* **Email-domain and redirect rules** — limit SSO sign-in to approved email domains and send users to the right place after login, per IdP.
* **WooCommerce integration** (optional) — map attributes to billing and shipping details and auto-link customers by email; loads only when WooCommerce is active.

= Stay in control after go-live =

* **Certificate monitoring** — track your IdP's signing-certificate expiry, get warned before it changes, detect rotation on a daily or weekly check, and optionally pin a certificate for change control, with manual, auto-trust, grace-period, or require-approval handling.
* **Searchable audit log** — a database-backed, filterable record of who signed in and when, with CSV export and a retention period you set.
* **Force-SSO with a safety net** — require SSO across the site while keeping an emergency way back in (a constant in `wp-config.php` or a pre-set bypass key), so a misconfiguration doesn't leave you stranded.
* **Import / export** — back up or move your entire configuration as a single JSON file.

= Easy setup, no coding =

Add your identity provider three ways — upload its metadata XML, paste a metadata URL, or type the details in by hand — then register your site with the IdP using the SP metadata it generates for you. It's all in the WordPress admin. (Developers also get a documented, stable hook API when they want it.)

= Privacy you can verify =

The plugin keeps to itself. The only time it reaches out to the network is when **you** ask it to fetch your IdP's metadata from a URL, plus an optional, off-by-default certificate-rotation check that re-fetches that same address you entered. It never contacts the author or any third party, and every setting stays in your own site's database. The two bundled libraries it relies on (both MIT-licensed) make no network calls at all.

= Security without the homework =

Incoming logins are fully validated before anyone is signed in — the plugin checks the digital signature, the sender, the intended audience, expiry, and replay protection, and accepts only strong, modern cryptography by default. Your SP private keys are encrypted at rest, and the public endpoints are guarded against common abuse. Sensible, secure defaults are on out of the box; the deeper knobs are documented for the rare cases you need them.

= Free and open-source =

Open Access SSO is licensed GPLv2 or later, with the full source available on Codeberg. There is no premium edition and nothing to buy — what you install is the complete plugin. Its only third-party libraries (xmlseclibs and phpseclib, both MIT-licensed) are bundled and make no network calls.

= Requirements =

* WordPress 6.0 or newer
* PHP 8.1 or newer
* A SAML 2.0 identity provider you control or have access to

== Documentation ==

Full guides, setup walkthroughs, and hardening advice live on Codeberg:

* **Full README / technical reference** — https://codeberg.org/idgold/open-access-sso
* **Identity-provider setup guide** (Entra ID, Okta, NetIQ Access Manager, and more) — https://codeberg.org/idgold/open-access-sso/src/branch/main/docs/identity-provider-setup.md
* **Security & hardening guide** — https://codeberg.org/idgold/open-access-sso/src/branch/main/docs/security-and-hardening.md
* **Troubleshooting guide** — https://codeberg.org/idgold/open-access-sso/src/branch/main/docs/troubleshooting.md

== External Services ==

This plugin is a SAML 2.0 Service Provider (SP). It sends no telemetry or analytics and never connects to any service operated by the plugin author. Its only external interactions are with the SAML Identity Provider (IdP) that you, the site administrator, configure — for example Microsoft Entra ID, Okta, OneLogin, Keycloak, ADFS, Shibboleth, or NetIQ Access Manager. There is no built-in or default IdP; the IdP is chosen and operated by you or your organisation.

= Identity Provider metadata fetch =

When an administrator clicks "Fetch IdP Metadata from URL" in the plugin's admin screens, the plugin makes a single server-side HTTP GET request to the metadata URL the administrator entered. No site or user data is sent beyond a standard HTTP request; the response (SAML metadata XML) is parsed and stored in your site's database. This never happens on the front end.

Optionally, you can enable a certificate-rotation check for an IdP (off by default). When enabled, WP-Cron re-fetches that same administrator-entered metadata URL on a schedule (for example daily) so the plugin can warn you before the IdP's signing certificate expires or changes. This is the only automatic outbound request the plugin makes, it is opt-in per IdP, and it contacts only the metadata URL you configured.

= SAML single sign-on flow =

When a visitor signs in through SSO, their browser is redirected to your configured IdP (carrying a standard SAML AuthnRequest). After the visitor authenticates, the IdP returns a signed SAML assertion to your site, which the plugin validates and uses to create or update the corresponding WordPress user. The data exchanged is the SAML authentication request and response — which includes the user identifier and whatever attributes your IdP is configured to release. This exchange happens only when a visitor initiates an SSO login.

Because the IdP is a service you select and operate (or that your organisation operates), its terms of service and privacy policy are defined by that provider. Consult your chosen identity provider's own documentation for those terms (for example, the privacy and terms pages of Microsoft Entra ID, Okta, OneLogin, etc.).

== Installation ==

1. Download the .zip and upload via Plugins → Add New → Upload Plugin, or extract into `/wp-content/plugins/open-access-sso/`.
2. Activate through the Plugins menu.
3. Navigate to **Tools → Open Access SSO** to configure.
4. Add an identity provider: upload the IdP metadata XML, paste a metadata URL, or enter the IdP fields manually.
5. Copy the SP metadata URL shown on the dashboard and register your WordPress site as a Service Provider in your IdP.
6. Test sign-on using the SSO button on the login page.

== Frequently Asked Questions ==

= Is it really free? =

Yes — completely. There is no premium tier, no license key, and nothing to unlock. Every feature you read about ships in the GPLv2-or-later codebase: role mapping, multi-IdP, access control, the audit log, WooCommerce integration, all of it. The full source is on Codeberg, so you can see exactly what you're installing.

= Will it work with my identity provider? =

Almost certainly. Open Access SSO speaks standard SAML 2.0, so it works with any standards-compliant identity provider — Microsoft Entra ID (Azure AD), Okta, OneLogin, Keycloak, ADFS, Shibboleth, and NetIQ Access Manager (now OpenText), among others. There's no built-in or default IdP; you bring your own, and you point the plugin at it. The identity-provider setup guide (see Documentation) walks through the common providers step by step.

= Do I need to be a developer to set it up? =

No. Setup is point-and-click in the WordPress admin. You add your identity provider one of three easy ways — upload its metadata XML file, paste a metadata URL, or type the details in by hand — then copy the SP metadata the plugin generates and register your site with your IdP. No code required for normal use. (If you *are* a developer, there's a documented, stable hook API waiting for you.)

= Is my users' data safe, and does it track anyone? =

Yes, and no tracking whatsoever. The plugin has no telemetry, no analytics, no "phone home," and no external CDN. The only time it ever reaches out to the network is when you ask it to fetch your IdP's metadata from a URL — plus an optional, off-by-default certificate-rotation check that re-fetches that same address you entered. It never contacts the plugin author or any third party, and every setting stays in your own site's database. Sign-ins are validated end to end before anyone is let in, and your SP private keys are encrypted at rest. For the full details, see the security & hardening guide in Documentation.

= What if SSO locks me out? =

You have a built-in emergency way back in. You can either add `define( 'OASSO_BYPASS', true );` to `wp-config.php` to switch off forced SSO entirely, or set a Bypass Secret Key on the dashboard ahead of time and visit `/wp-admin/?oasso_bypass_key=YOUR_KEY` to get back in without touching any files. The key route is rate-limited per IP address to frustrate brute-force guessing. The troubleshooting guide (see Documentation) covers recovery in detail.

= Can I use more than one identity provider? =

Yes. Configure as many IdPs as you need under Tools → Open Access SSO → Identity Providers. Your users pick the right one with a button on the login page or via a simple `?idp=slug` link.

= Can I assign WordPress roles automatically from my IdP? =

Yes — that's one of the headline features. Map WordPress roles from the groups or attributes your IdP sends, using exact, contains, or regex matching, with per-IdP rule sets, a default-role fallback, and an option to deny anyone who doesn't match a rule. For safety, SSO won't grant admin-level roles unless you explicitly turn that on, and existing administrator accounts are never auto-linked to an SSO login.

= Does it support encrypted logins? =

Yes, out of the box — including the modern encryption that some providers (such as NetIQ Access Manager) turn on by default and that stock PHP can't unwrap on its own. The plugin bundles a small MIT-licensed library to handle exactly that, so encrypted sign-in just works where a plain PHP setup would fall short. Encrypted user identifiers are supported too.

= Does it work with NetIQ Access Manager? =

Yes. NetIQ Access Manager (now part of OpenText) is a standard SAML 2.0 identity provider and is fully supported, including its default encrypted assertions. One clarification: NetIQ *Identity Manager* (sometimes called "IDM") is a separate user-provisioning product, not a SAML IdP — it's Access Manager that acts as the identity provider here.

= Can I keep an eye on certificates and sign-in activity? =

Yes. The plugin tracks each IdP's signing-certificate expiry and can warn you before it changes, detecting rotation on a daily or weekly check with your choice of manual, auto-trust, grace-period, or require-approval handling — and you can pin a specific certificate if you want strict change control. A searchable, database-backed audit log records who signed in and when, with CSV export and a retention period you set.

= Where do protected media files go? =

When a page with attached media is restricted, the plugin physically moves those files out of the public uploads folder into a private directory (`uploads/oasso-private/`) with an unguessable name, and serves them through an access-checked gateway URL. Direct links to the old file location stop working for everyone — that is the point. Unprotecting the page (or uninstalling the plugin) moves the files back. Files uploaded before version 2.4.0 are moved only when you click "Relocate existing protected media" under Access Control. After relocating, purge your page cache/CDN. If you deactivate the plugin, relocated files are unreachable (but not exposed) until you reactivate or restore them first.

= Is this related to the old "OpenSSO" product from Sun or Oracle? =

No. Open Access SSO is an independent open-source project. It is not affiliated with, and is not a continuation of, Sun Microsystems' or Oracle's discontinued "OpenSSO" product — the similar name is coincidental.

== Roadmap ==

Open Access SSO is actively developed. Here's what's planned next.

= Under consideration =

* Automated user provisioning and deprovisioning (SCIM)
* Scheduled group-to-role synchronisation
* A branded identity-provider chooser for sites with multiple providers
* Multi-factor authentication step-up hints
* Deeper WooCommerce customer field mapping

Have a feature request? Open an issue on the project repository.

== Screenshots ==

1. Open Access SSO dashboard - SSO status, configured Identity Providers, and module toggles.
2. Identity Provider configuration - SAML endpoints, signing certificate, and attribute mapping.
3. Role mapping rules - assign WordPress roles from SAML attribute values.
4. Per-page access control in the editor - restrict by role or require SSO login.

== Changelog ==

= 3.0.0 =
* New: OpenID Connect (OIDC) Relying Party support. Open Access SSO is no longer SAML-only — configure an OpenID Connect provider (for example Entra ID, Okta, Auth0, or Keycloak) as an Identity Provider alongside your existing SAML providers, using the same role mapping, attribute mapping, and access-control features you already use for SAML. Supply the issuer URL and use "Fetch from Discovery" to populate the provider's endpoints automatically; sign-in uses the authorization-code flow with PKCE.
* New: OIDC claims map onto the same Attribute Mapping and Role Mapping screens SAML attributes already use — a claim name plays the role a SAML attribute name would — and UserInfo is merged in, so a claim a provider only exposes there (commonly `groups`) still reaches role mapping.
* New: the multi-IdP selector (the login chooser and the `?idp=slug` link) treats SAML and OIDC providers alike, so a site can mix both protocols with no special configuration.
* New: RP-initiated logout for OIDC providers that support it, alongside the existing SAML Single Logout.
* New: Test Connection now works against an OIDC provider too, so you can discover the exact claim names your provider sends — some send non-obvious names such as `mail` or `emp_type` — before configuring Attribute Mapping, the same one-click check SAML has always had.
* Security: ID tokens are verified only with asymmetric signature algorithms (RS256/384/512, PS256/384/512, ES256/384/512). There is no HMAC or "none"-algorithm code path to disable — not merely a check that refuses them, but no code path that could reach them.
* Security: if UserInfo is configured for a provider, a fetch that fails during sign-in stops the sign-in rather than continuing with an incomplete set of claims — a timed-out HTTP call can never silently downgrade a user's role.
* Security: the OIDC login transaction is bound to the browser that started it (a single-use, time-limited server-side transaction plus a same-site cookie), on top of PKCE and a nonce.
* Behavior: SAML sign-in is unchanged. The OIDC engine only registers once at least one Identity Provider is configured with protocol OpenID Connect — a SAML-only site runs no new code path.

= 2.7.1 =
* Fix: signing out via SSO is now recorded in the audit log. No sign-out has ever appeared there. WordPress clears the current user before it announces the sign-out, so the audit entry was looking up a user that was already gone; and once Single Logout was switched on, the redirect to the identity provider ended the request before the audit step ran at all.
* New: the debug log now records a completed Single Logout. Only the outgoing request and outright failures were logged before, so a sign-out that worked looked identical to one that never reached the identity provider.

= 2.7.0 =
* Security: an encrypted assertion's wrapped session key is now single-use. A conforming identity provider generates a new session key for every response, so a repeat is either a replayed response or an attempt to use the site as a decryption oracle. Enforced before the private key is used at all.
* Security: new optional setting "Require a Signed Response Before Decrypting" (off by default) — for identity providers that sign the whole SAML response, the assertion is no longer decrypted before it can be verified.
* Security: new optional setting "Minimum Duration of a Failed Login" (0 by default) — makes every failed SSO login take the same minimum time, so the speed of a failure cannot leak information about an encrypted assertion.
* Diagnostics: the debug log now records the encryption algorithms the identity provider actually used, and reports a burst of decryption failures even with debug mode off. Nothing is ever blocked by the burst report.

= 2.6.0 =
* Security (assertion freshness): sign-in assertions are now rejected if they are already stale when they arrive. Previously an assertion was accepted at any age so long as its own expiry had not passed, so an assertion captured in transit and never delivered stayed usable for the identity provider's entire declared window (CWE-294). The new "Maximum assertion age" setting bounds that at 600 seconds by default, with your clock skew tolerance added on top. A conforming identity provider delivers within seconds, so this does not affect normal sign-ins.
* Security (assertion lifetime): assertions declaring an implausibly long validity window are now rejected. The timing checks previously accepted any future expiry, so a misconfigured or hostile identity provider could mint an assertion valid for years and every check would pass it. The new "Maximum assertion lifetime" setting caps this at 86400 seconds (24 hours) by default, measured from the identity provider's IssueInstant rather than from NotBefore, which providers may backdate.
* Both settings accept 0 to disable, and can be overridden per identity provider with the `oasso_assertion_max_age` and `oasso_assertion_max_lifetime` filters, so a multi-IdP site can hold a strict provider to a tighter bound.
* New: the debug log records each assertion's observed age and declared lifetime, so an unfamiliar identity provider's timing can be diagnosed without extra tooling.

= 2.5.0 =
* New: SSO Identity section on the WordPress user profile — link an existing user (including an administrator) to an SSO identity, or unlink them, without recreating the account.
* New: sign-in attempts that match no WordPress account are captured for 7 days so an administrator can pick the identity to link instead of typing an opaque NameID by hand.
* New: linking an identity another user holds moves it in one step; both sides are recorded in the audit log.
* New: `oasso_identity_linked` and `oasso_identity_unlinked` action hooks.
* Improved: failed sign-ins now record why in the debug log — which lookup missed and which creation error followed, naming the conflicting account — instead of only "Failed to create or retrieve user account". Recorded even when debug mode is off.

= 2.4.0 =
* Security (media relocation): protected media files are no longer left in the public uploads directory. The moment a page or post carrying attached media becomes restricted, its files are physically moved into a private, unguessable directory outside the public web path and served only through the access-checked media gateway, closing direct-URL access to a protected file's original public path even if the URL is bookmarked or cached elsewhere (CWE-284). Unprotecting the content, or uninstalling the plugin, moves the files back to their original locations.
* New: files that were already protected before upgrading to 2.4.0 are left in place until you act. A "Relocate existing protected media (N)" button under Access Control migrates them in batches, without interrupting the site.
* New: the media gateway now serves resized image variants (not just the original file), streams video with HTTP Range support for seeking, and honours conditional requests (`If-None-Match` / `If-Modified-Since`) so browsers and caches skip re-downloading unchanged files.
* New: two health checks under Access Control — a relocated file's leftover public copy ("source residue") and orphaned working directories left by an interrupted relocation — each with a one-click cleanup action. Health items are excluded from the pending-migration count and are left untouched when the migration is re-run.
* New: a "Relocate protected media files" setting (Access Control, on by default) turns off automatic relocation for newly-protected content if needed; a server-type readout flags nginx, where `.htaccess`-based protection of the legacy public path does not apply.
* Fail-secure by design: deactivating the plugin makes relocated files unreachable through the gateway, but it never moves them back to a public path or otherwise exposes their new location.
* New `oasso_media_relocated` and `oasso_media_restored` hooks fire when a file moves into or out of the private directory; both are recorded in the audit log.
* Hardened for restrictive managed hosts: on PHP 8 a function listed in the host's `disable_functions` is undefined and calling it is fatal — on one such host this crashed the page save ("There has been a critical error on this website") the moment a restriction change triggered media relocation. All relocation, restore, health-check, and uninstall paths now run inside fail-safe boundaries: a server error can no longer break the page save (or the uninstall), it is recorded in the plugin debug log with the exact cause even when Debug Mode is off, and a persistent, dismissible admin notice names the affected file with retry instructions. The disk-space preflight and the regex rule matchers (role mapping, login redirects) also detect disabled `disk_free_space`/`ini_set` and degrade gracefully instead of crashing the save or the SSO login.
* Editors keep showing protected images: URLs baked into existing content (the classic editor, the block editor, and page builders render these directly) stop resolving once a file moves to the private area. Requests to such a stale URL from anyone who passes the file's access rules are now redirected to the media gateway, so editor previews and old bookmarks keep working for authorized users — while unauthorized visitors still get the same plain "not found" response as before, revealing nothing.

= 2.3.0 =
* Security (certificate pinning): pinning is now anchored to an independent fingerprint recorded when you pin, instead of being recomputed from the stored certificate. Previously the "Auto Trust" and grace-period rotation strategies silently moved the pin together with the certificate, so pinning could not catch an unauthorized certificate swap, for example after a metadata-host takeover (CWE-295). While an IdP is pinned, automatic rotation never replaces its certificate: a new certificate found at the metadata URL is queued for your approval, an email is sent, and a persistent admin notice is shown until you act. Signature verification — for sign-in and logout alike — fails closed if the stored certificate stops matching the pinned fingerprint.
* Only explicit administrator actions move the pin anchor: saving the IdP form, approving a pending certificate, or re-importing metadata. Pin changes and blocked rotations are recorded in the audit log (new `oasso_cert_rotation_blocked` and `oasso_cert_pin_changed` hooks).
* On upgrade, each already-pinned IdP is automatically anchored to the fingerprint of its currently configured certificate, so existing installs keep working with no action needed. Unpinned IdPs rotate exactly as before; nothing changes for them.
* UI: the Identity Provider screen now shows the pinned anchor fingerprint, warns when the stored certificate no longer matches it, and warns when "Auto Trust" or "Grace Period" is selected for a pinned IdP (while pinned, those strategies queue new certificates for approval instead of rotating).
* Security (sign-in redirect): the HTTP Referer fallback used for the post-sign-in return URL is now validated with `wp_validate_redirect()`, so a crafted Referer header can no longer place an external redirect target into the SAML RelayState (CWE-601).

= 2.2.0 =
* Security (Force SSO): when Force SSO is enabled it is now enforced at the authentication layer, not just on the login page. Native username/password sign-in (including over XML-RPC) is rejected, and Application Passwords and XML-RPC are disabled, so a leftover local password, an XML-RPC client, or an Application Password can no longer bypass single sign-on and the central deprovisioning and MFA it provides (CWE-287). The emergency bypass still restores local access (the `OASSO_BYPASS` wp-config constant or the bypass secret key), and sign-in is never enforced while no Identity Provider is enabled, so a misconfiguration cannot lock you out.
* New: two opt-in settings under General Settings, both off by default — "Allow Application Passwords" and "Allow XML-RPC" — re-enable those API surfaces while Force SSO is on, for a site that relies on a trusted integration. Re-enabling XML-RPC also restores its username/password (basic) authentication, the classic brute-force surface, so leave it off unless a legacy integration needs it.
* Hardening: the SSO error message shown on the login screen after a failed sign-in is now carried by a short-lived signed token, so an arbitrary `?oasso_error=` value can no longer suppress the Force-SSO redirect or render chosen text on the login page.
* Note: this does not change a site that does not use Force SSO. Real SSO sign-in, the bypass paths, and existing logged-in sessions are unaffected.

= 2.1.5 =
* Security (access control): restricted media and posts no longer leak through secondary read paths. The REST media endpoint now hides a restricted attachment's metadata and real file URL, including when the restriction is inherited from the attachment's parent post (CWE-639); and restricted posts are excluded from REST search results and the XML sitemap (`wp-sitemap.xml`) for visitors who are not allowed to see them (CWE-200).
* Security (media): new optional "Require SSO for the protected directory" setting under Access Control (off by default). When enabled, files in the protected uploads directory may be downloaded only by users who signed in through SSO — enforced on both the `oasso-protected/` and `oasso-file/` delivery routes (CWE-276).
* Security (SAML replay): the replay-protection record for a consumed assertion now lives at least as long as the assertion's own validity window, closing a small window in which a captured response could otherwise be replayed after the record expired (CWE-294). The record lifetime is capped to a safe maximum.
* Hardening: SSO attribute mapping can no longer write to WordPress privilege or session storage keys (`wp_capabilities`, `wp_user_level`, `session_tokens`, and their multisite variants), even if such a mapping is configured — refused at both the settings save and the login write.
* Fix (multi-IdP): when more than one Identity Provider is configured, an SSO response is now matched to the correct IdP by its Issuer before signature verification, instead of assuming the first enabled IdP. This restores sign-in for a non-first IdP whose response would otherwise be verified against the wrong certificate.
* Change: SSO now auto-creates a WordPress account only when an email address can be resolved from the Identity Provider's attribute mapping. It no longer guesses the email from the NameID or from loose attribute keys, because what a new account's email should be is the administrator's mapping decision. The default SAML email-claim mapping still applies, so conventional IdPs are unaffected; if accounts stop being created after upgrading, map an email attribute to the email field in that IdP's Attribute Mapping.
* Fix (diagnostics): when a user cannot be created because no email address could be resolved from the assertion (for example, the IdP's email attribute is not mapped to a WordPress field), the debug log now records an actionable reason that names the attributes the IdP sent and points to the Attribute Mapping, instead of a generic "could not create user" message.
* The new access-control setting is off by default; the bundle also refreshes the plugin documentation and listing.

= 2.1.4 =
* New: support for IdPs that encrypt the assertion session key with RSA-OAEP using a SHA-256 (or SHA-384/512) digest, via the bundled phpseclib library. PHP's OpenSSL only unwraps RSA-OAEP with SHA-1; the plugin now honours the exact OAEP digest declared in the encrypted assertion, so encrypted-assertion SSO works with NetIQ Access Manager and other IdPs that default to OAEP-SHA-256. AES-CBC/GCM data decryption is unchanged.
* Security (DoS): all SAML XML parsers now reject documents carrying a DOCTYPE / inline DTD, closing an unauthenticated XML internal-entity-expansion denial-of-service reachable at the ACS/SLO endpoints before signature verification (CWE-776). Inbound SAML messages are size-capped before decoding and DEFLATE-compressed redirect-binding messages are inflated with a bounded buffer, closing a decompression-bomb DoS (CWE-409). The cap is filterable via `oasso_max_saml_message_bytes`.
* Security (encryption): encrypted-assertion and EncryptedID decryption now checks the key-transport and data-encryption algorithms against an allowlist before any decryption runs — RSA-OAEP for key transport (RSA-1.5 refused) and AES-CBC/GCM for data (TripleDES refused), mirroring the signature-algorithm allowlist (CWE-780/327). Legacy RSA-1.5 / TripleDES can be re-enabled only via the import-only `allow_legacy_decryption_algorithms` setting (off by default), and any such acceptance is audit-logged.
* Security (assertions): an assertion must now carry an enforceable expiry boundary (a `Conditions` or bearer `SubjectConfirmationData` `NotOnOrAfter`); one with none is rejected (CWE-613). The new "Require assertion expiry" Service Provider setting (on by default) lets you relax this for a non-conforming IdP.
* Security (admin): IdP-supplied SAML attribute names shown in the redirect-rule editor are now HTML-escaped at the rendering sink and hex-encoded in the inline configuration, closing a stored DOM cross-site-scripting issue a malicious or compromised IdP could trigger in an administrator's browser (CWE-79).
* Security (audit export): audit-log CSV export now neutralises spreadsheet formula triggers (a leading `=`, `+`, `-`, `@`, tab or carriage return, plus their full-width Unicode variants) in IdP-controlled fields, preventing CSV / formula injection when an exported log is opened in a spreadsheet (CWE-1236).
* Maintenance: non-Success SAML responses now surface the nested `StatusCode` and `StatusMessage` for clearer diagnostics, and the SP metadata advertises the supported assertion-encryption algorithms (RSA-OAEP key transport, AES-GCM/CBC data) for IdPs that consume them.

= 2.1.2 =
* Security: SSO no longer assigns administrator-level roles (any role carrying capabilities such as `manage_options` or `edit_users`) unless you explicitly enable the new "Allow Administrator-Level Roles via SSO" setting in General Settings (off by default). This prevents a role-mapping rule from silently elevating an auto-provisioned SSO user to a role that can take over the site. Users who would have received such a role get the default role instead. If you deliberately map an IdP identity or group to an administrator-level role, enable this setting; the configuration importer enables it automatically when an imported config already maps to such a role.
* Fix: term (category/tag/custom taxonomy) restriction fields now authorise against each taxonomy's own editing capability via the `edit_term` meta capability, instead of a hardcoded `manage_categories`. Custom taxonomies that use their own capabilities now save restriction settings correctly.

= 2.1.1 =
* Security hardening (SAML signature handling): the IdP's signature algorithm is now read before XML-DSig reference processing and checked against an explicit allowlist of RSA algorithms (RSA-SHA256/384/512; RSA-SHA1 only when the SHA-1 fallback is enabled). This makes the protection against signature-algorithm confusion explicit and robust, and lets IdPs that sign with RSA-SHA384/512 verify (previously only RSA-SHA256 was accepted). The same SHA-1 opt-in now also gates Redirect-binding signatures.
* Security hardening: the assertion-level `Issuer` is now required and must match the configured IdP, and assertions must carry an `AudienceRestriction` naming this Service Provider. A new "Require audience restriction" Service Provider setting (on by default) lets you relax the audience check for an IdP that legitimately omits it.
* Fix: the Service Provider screen showed outdated `?ossa=acs` / `?ossa=slo` URLs; it now shows the correct `?oasso_acs=1` / `?oasso_slo=1` endpoints. Removed a non-functional metadata "Download" button (copy the metadata from the field shown instead).
* Maintenance: uninstall now also removes term-level restriction settings; the optional certificate-rotation cron is documented in the External Services section; assorted internal identifier and PHP-requirement cleanups.

= 2.1.0 =
* WordPress.org review compliance: the plugin's internal prefix was renamed from the 3-character `oas_` / `OAS_` to `oasso_` / `OASSO_` across all options, hooks, transients, cron events, user/post meta, AJAX actions, nonces, asset handles, and custom tables. The PHP namespace (`OpenAccessSSO`) and plugin slug (`open-access-sso`) are unchanged.
* The content-restriction shortcodes are renamed `[oas_restrict]` → `[oasso_restrict]` and `[oas_login_button]` → `[oasso_login_button]`.
* Security: the `[oasso_restrict]` shortcode now passes its returned content through `wp_kses_post()`.
* Security: the admin "Test Connection" link now carries and verifies a nonce.
* **Because the internal prefix changed, this is NOT a drop-in upgrade. Export your configuration first, then reinstall and re-import — see the Upgrade Notice below for the exact steps.**

= 2.0.4 =
* Maintenance release addressing WordPress.org plugin review feedback. All inline scripts and styles are now delivered through the WordPress enqueue APIs (`wp_add_inline_script`, `wp_add_inline_style`, `wp_get_inline_script_tag`) or a linked stylesheet, instead of raw `<script>`/`<style>` tags.
* Removed obsolete pre-PHP-8.0 `libxml_disable_entity_loader()` calls. The plugin requires PHP 8.1+, where libxml ≥ 2.9 already disables external-entity loading by default and `LIBXML_NONET` blocks network access; the calls were dead code and deprecated in PHP 8.0.
* Documented the SAML Identity Provider external-service interaction in a dedicated readme section.
* No functional or behavioural change.

= 2.0.3 =
* Added an optional "support development" link (Ko-fi) in the admin footer of the plugin's own pages, plus a Donate link in the readme. No functional change; the link opens externally and the plugin makes no network calls of its own.
* Compatibility: declared tested up to WordPress 7.0.

= 2.0.2 =
* PCP polish: WordPress Plugin Check now reports zero findings for the distributable zip. Two real sanitiser additions in the admin settings form; the rest are inline `phpcs:ignore` annotations with reason comments at intentional sites (cross-origin POST at the SAML ACS endpoint, PCRE limit hardening before user-supplied regex evaluation, table DROP on uninstall, internal-only DB query composition). No behaviour change.
* Build: `README.md` (GitHub-only readme) is no longer shipped in the distributable zip; `composer.json` is now included so the bundled `vendor/` directory is transparent to plugin reviewers.

= 2.0.1 =
* Fix: Test Connection now populates results and attribute dropdowns reliably regardless of how long the admin has been logged in. The admin's identity is now recorded at initiate-time in a server-side transient keyed by the AuthnRequest ID, and looked up at the ACS callback via the response's InResponseTo. The previous flow depended on the WP auth cookie surviving the cross-origin POST from the IdP, which modern browsers block under SameSite=Lax outside a brief carveout window.

= 2.0.0 =
* Initial release on WordPress.org. (Project formerly known internally as OpenSSO Access; renamed to Open Access SSO ahead of public release.)
* Full SAML 2.0 SP with multi-IdP support.
* Attribute mapping, role mapping, page access control, WooCommerce integration.
* Audit log, force-SSO mode, emergency bypass via `OAS_BYPASS`.

== Upgrade Notice ==

= 3.0.0 =
SAML behaviour is unchanged — this major version reflects the plugin's identity, not a compatibility break. OpenID Connect activates only when you add an OpenID Connect identity provider.

= 2.7.1 =
Diagnostics only, no behaviour change to sign-in. Signing out via SSO is now recorded in the audit log — it never was before — and a completed Single Logout is now recorded in the debug log.

= 2.7.0 =
Behavior change: an encrypted assertion's wrapped session key is now single-use. Conforming identity providers are unaffected. If yours reuses session keys, a developer can opt out via the `oasso_require_single_use_session_key` filter. Both new settings are off by default.

= 2.5.0 =
Administrators can now be linked to an SSO identity from their user profile, without recreating the account. Failed sign-ins record the actual cause in the debug log.

= 2.4.0 =
Behavior change: media on a restricted page now moves out of the public uploads path into a private directory (CWE-284). Pre-existing protected files move only via the new admin button (Access Control). Deactivating the plugin makes relocated files unreachable — restore first if needed.

= 2.3.0 =
Certificate pinning now uses an independent anchor (CWE-295): pinned IdPs never auto-rotate — new certificates queue for your approval with an admin notice, and verification fails closed on a mismatch. Existing pinned IdPs are anchored automatically on upgrade; unpinned IdPs are unaffected.

= 2.2.0 =
Force SSO now blocks native password sign-in and disables XML-RPC + Application Passwords by default (CWE-287), enforced at the auth layer. Recover via OASSO_BYPASS or the bypass key. For a trusted API integration, re-enable Application Passwords or XML-RPC under General Settings.

= 2.1.5 =
Security hardening (REST media/search/sitemap, replay, privilege-meta) plus a multi-IdP sign-in fix. Behavior change: SSO auto-creates accounts only when the email comes from the IdP attribute mapping (no NameID/loose-key guessing); set your IdP email mapping if needed.

= 2.1.4 =
Security hardening (XML-DoS, decryption algorithm allowlist, admin XSS, CSV-export injection, assertion-expiry) plus RSA-OAEP-SHA-256 encrypted-assertion support (e.g. NetIQ) via bundled phpseclib. Drop-in from 2.1.x; default-on tighteners are met by conforming IdPs.

= 2.1.2 =
Security: SSO no longer grants administrator-level roles unless you enable "Allow Administrator-Level Roles via SSO" (General Settings, off by default). If you map an IdP identity or group to an admin role, enable it after upgrading. Re-importing such a config enables it automatically.

= 2.1.1 =
SAML signature, Issuer and Audience validation hardened. If SSO fails after upgrading because your IdP omits the AudienceRestriction, disable "Require audience restriction" in Service Provider settings. Upgrading from 2.0.x still needs the export/reinstall/import steps below.

= 2.1.0 =
Not a drop-in upgrade (internal prefix changed). Export your config (Tools > Import/Export), delete the old version, install 2.1.0, then re-import. Also rename OAS_BYPASS to OASSO_BYPASS in wp-config and point your IdP ACS/SLO URLs at ?oasso_acs / ?oasso_slo. See changelog.

= 2.0.4 =
WordPress.org review compliance and maintenance. No behaviour change; safe drop-in upgrade.

= 2.0.3 =
Adds an optional Ko-fi support link in the admin footer. No behaviour change.

= 2.0.2 =
Plugin Check polish; no behaviour change. Safe drop-in upgrade.

= 2.0.1 =
Fixes Test Connection silently failing for admins whose login is more than ~2 minutes old. Drop-in upgrade; no settings change required.

= 2.0.0 =
First public release.
