diff --git a/README.md b/README.md index 3f0fb40..bf35bfe 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ OIDC SSO, TOTP and passkey second-factor authentication for **Bludit 3.22.0**. -Current version: **1.0.1**. +Current version: **1.0.2**. ## Features @@ -10,6 +10,7 @@ Current version: **1.0.1**. - Adds a full-width, slate-grey **Sign in with XYZ** button with a configurable provider name. - Automatically opens the browser passkey prompt after password verification when a passkey is enabled. Cancellation does not trigger a prompt loop; an explicit retry remains available. - Integrates **Security** into the native admin dashboard, including its sidebar, shield icon and Bootstrap controls. Sign out through the existing **Log out** sidebar entry. +- Gives authors, editors and administrators access to their own Security page through desktop and mobile navigation, with an additional shortcut in their own Profile → Security tab. - Shows SSO link status, provider, SSO username and SSO email in the administrator's native **Users** list and the user's **Security** page. - Lets each user register TOTP, multiple named passkeys and an OIDC identity, then choose their own second-factor policy: disabled, TOTP, passkey, or either method. - Optionally requires the local second factor after SSO as well. @@ -55,9 +56,9 @@ For `bludit/docker`, copy the extracted plugin into `/usr/share/nginx/html/bl-pl docker cp bludit:/usr/share/nginx/html/bl-plugins/bludit-secure \ "/tmp/bludit-secure-backup-$(date +%Y%m%d-%H%M%S)" -unzip -o /tmp/bludit-secure-1.0.1.zip -d /tmp/bludit-secure-1.0.1 +unzip -o /tmp/bludit-secure-1.0.2.zip -d /tmp/bludit-secure-1.0.2 -docker cp /tmp/bludit-secure-1.0.1/bludit-secure/. \ +docker cp /tmp/bludit-secure-1.0.2/bludit-secure/. \ bludit:/usr/share/nginx/html/bl-plugins/bludit-secure/ docker restart bludit @@ -91,6 +92,8 @@ Logging out ends the Bludit session only. Provider-wide logout and OIDC back-cha In **Security**: +Every signed-in author, editor and administrator can manage their own methods. Open **Security** in desktop or mobile navigation, or **Profile → Security → Manage authentication methods**. The direct page is `/admin/plugin/pluginbluditsecure`, adjusted for your configured admin path. Global SSO configuration and the Users list remain administrator-only. + - **TOTP:** select **Set up TOTP**, copy the `otpauth://` setup URI into your authenticator app and confirm a code. TOTP uses SHA-1, six digits and a 30-second period. Previously accepted codes cannot be reused. - **Passkeys:** select **Add passkey**, enter a name and choose your password manager or authenticator in the browser. Multiple passkeys are supported, with user verification required. The browser selects the authenticator; the plugin does not force a particular manager. - **Second factor:** choose the required method and whether it also applies after SSO. **TOTP or passkey** requires both methods to be registered. @@ -118,6 +121,12 @@ The plugin protects **web sessions under `/admin`**. It does not change authenti JavaScript URLs include a content hash so updated passkey behavior is not hidden by stale asset caches. +## Changes in 1.0.2 + +- Makes personal Security navigation available to authors, whose native plugin sidebar is hidden by Bludit. +- Adds the personal Security link to mobile navigation for authors and editors. +- Adds an authentication-management shortcut to the user's own Profile → Security tab. + ## Changes in 1.0.1 - Applies native form styling and spacing to the passkey name input. diff --git a/assets/navigation.js b/assets/navigation.js new file mode 100644 index 0000000..6434ddd --- /dev/null +++ b/assets/navigation.js @@ -0,0 +1,43 @@ +'use strict'; +(() => { + const data = document.getElementById('bs-security-navigation'); + if (!data) return; + const config = JSON.parse(data.textContent); + const account = new URL(config.account, document.baseURI).href; + const logout = new URL(config.logout, document.baseURI).href; + const href = link => new URL(link.getAttribute('href'), document.baseURI).href; + const makeLink = () => { + const link = document.createElement('a'); + link.href = account; + link.className = 'nav-link'; + const icon = document.createElement('span'); + icon.className = 'fa fa-shield'; + icon.setAttribute('aria-hidden', 'true'); + link.append(icon, document.createTextNode('Security')); + return link; + }; + // Native themes hide plugin sidebar hooks from authors and mobile editors. + for (const nav of document.querySelectorAll('.sidebar .nav, #navbarNav .navbar-nav')) { + if (Array.from(nav.querySelectorAll('a[href]')).some(link => href(link) === account)) continue; + const item = document.createElement('li'); + item.className = 'nav-item'; + item.appendChild(makeLink()); + const logoutItem = Array.from(nav.children).find(child => Array.from(child.querySelectorAll('a[href]')).some(link => href(link) === logout)); + nav.insertBefore(item, logoutItem || null); + } + if (config.ownProfile) { + const section = document.querySelector('#security.tab-pane'); + if (!section || section.querySelector('[data-bs-security-profile]')) return; + const box = document.createElement('div'); + box.className = 'mb-4'; + box.setAttribute('data-bs-security-profile', 'true'); + const description = document.createElement('p'); + description.textContent = 'Manage TOTP, passkeys, SSO and recovery codes for your account.'; + const link = document.createElement('a'); + link.href = account; + link.className = 'btn btn-outline-primary'; + link.textContent = 'Manage authentication methods'; + box.append(description, link); + section.prepend(box); + } +})(); diff --git a/metadata.json b/metadata.json index c9f5755..6f4bfd5 100644 --- a/metadata.json +++ b/metadata.json @@ -1 +1 @@ -{"author":"Interlope","version":"1.0.1","releaseDate":"2026-11-03","license":"MIT","compatible":"3.22.0","notes":"Per-user OIDC SSO, TOTP, passkeys and recovery codes. Requires PHP 8.2+, HTTPS, sodium, curl, mbstring and openssl."} +{"author":"Interlope","version":"1.0.2","releaseDate":"2026-11-03","license":"MIT","compatible":"3.22.0","notes":"Per-user OIDC SSO, TOTP, passkeys and recovery codes. Requires PHP 8.2+, HTTPS, sodium, curl, mbstring and openssl."} diff --git a/plugin.php b/plugin.php index c19fce0..7e90462 100644 --- a/plugin.php +++ b/plugin.php @@ -77,6 +77,7 @@ class pluginBluditSecure extends Plugin return 'Security'; } public function adminBodyEnd() { - return (new \BluditSecure\App($this))->usersViewEnhancement(); + $app = new \BluditSecure\App($this); + return $app->navigationView().$app->usersViewEnhancement(); } } diff --git a/src/App.php b/src/App.php index 6242001..9326c8a 100644 --- a/src/App.php +++ b/src/App.php @@ -397,6 +397,15 @@ final class App 'emailVerified'=>$oidc->getVerifiedClaims('email_verified') === true ]; } + public function navigationView(): string { + global $layout, $user; + $name = $this->authenticated(); + if ($name === null) { return ''; } + $ownProfile = ($layout['view'] ?? '') === 'edit-user' && isset($user) && is_object($user) && method_exists($user, 'username') && $user->username() === $name; + $data = json_encode(['account'=>$this->accountUrl(), 'logout'=>DOMAIN_ADMIN.'logout', 'ownProfile'=>$ownProfile], JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_THROW_ON_ERROR); + $version = substr(hash_file('sha256', dirname(__DIR__).'/assets/navigation.js'), 0, 16); + return ''; + } public function usersViewEnhancement(): string { global $layout, $users; if (($layout['view'] ?? '') !== 'users') { return ''; }