Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
941312ac35 | ||
|
|
6a90258ddd |
No files matched your search
@@ -2,7 +2,7 @@
|
||||
|
||||
OIDC SSO, TOTP and passkey second-factor authentication for **Bludit 3.22.0**.
|
||||
|
||||
Current version: **1.0.0**.
|
||||
Current version: **1.0.2**.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -10,6 +10,7 @@ Current version: **1.0.0**.
|
||||
- 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.0.zip -d /tmp/bludit-secure-1.0.0
|
||||
unzip -o /tmp/bludit-secure-1.0.2.zip -d /tmp/bludit-secure-1.0.2
|
||||
|
||||
docker cp /tmp/bludit-secure-1.0.0/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,18 @@ 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.
|
||||
- Adds space below passkey registration controls and between saved passkey names and their removal buttons.
|
||||
- Excludes dependency test directories and example entry points from the installable ZIP.
|
||||
|
||||
## Changes in 1.0.0
|
||||
|
||||
- First stable release for Bludit 3.22.0.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
})();
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"author":"Interlope","version":"1.0.0","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."}
|
||||
+2
-1
@@ -77,6 +77,7 @@ class pluginBluditSecure extends Plugin
|
||||
return '<a class="nav-link" href="'.HTML_PATH_ADMIN_ROOT.'plugin/pluginbluditsecure"><span class="fa fa-shield" aria-hidden="true"></span>Security</a>';
|
||||
}
|
||||
public function adminBodyEnd() {
|
||||
return (new \BluditSecure\App($this))->usersViewEnhancement();
|
||||
$app = new \BluditSecure\App($this);
|
||||
return $app->navigationView().$app->usersViewEnhancement();
|
||||
}
|
||||
}
|
||||
+11
-2
@@ -276,7 +276,7 @@ final class App
|
||||
}
|
||||
} else { $body .= $this->form('manage', '<button name="op" value="totp-delete">Remove TOTP</button>'); }
|
||||
$body .= '<h2>Passkeys</h2><p>Register a passkey for this site using your password manager or authenticator.</p>'.$this->keyButton(true);
|
||||
foreach ($a['keys'] as $id=>$k) { $body .= $this->form('manage', '<input type="hidden" name="key" value="'.$this->e($id).'"><span>'.$this->e($k['label']).'</span> <button name="op" value="key-delete">Remove passkey</button>'); }
|
||||
foreach ($a['keys'] as $id=>$k) { $body .= $this->form('manage', '<input type="hidden" name="key" value="'.$this->e($id).'"><span class="mr-3">'.$this->e($k['label']).'</span> <button name="op" value="key-delete">Remove passkey</button>'); }
|
||||
$body .= '<h2>SSO</h2>';
|
||||
if ($a['sso']) {
|
||||
$profile = $a['ssoProfile'] ?? null;
|
||||
@@ -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 '<script type="application/json" id="bs-security-navigation">'.$data.'</script><script src="'.$this->e($this->plugin->htmlPath().'assets/navigation.js?v='.$version).'" defer></script>';
|
||||
}
|
||||
public function usersViewEnhancement(): string {
|
||||
global $layout, $users;
|
||||
if (($layout['view'] ?? '') !== 'users') { return ''; }
|
||||
@@ -422,7 +431,7 @@ final class App
|
||||
return '<script type="application/json" id="bs-sso-users">'.$data.'</script><script src="'.$this->e($this->plugin->htmlPath().'assets/users.js?v='.$version).'" defer></script>';
|
||||
}
|
||||
private function keyButton(bool $create, bool $automatic = false): string {
|
||||
return '<div class="webauthn"'.($automatic ? ' data-auto-passkey="true"' : '').' data-base="'.$this->e($this->base).'" data-csrf="'.$this->e($_SESSION['bs']['csrf']).'">'.($create?'<label>Passkey name<input class="key-label" maxlength="80" value="My passkey"></label>':'').'<button type="button" data-key="'.($create?'create':'get').'">'.($create?'Add passkey':'Try passkey again').'</button></div>';
|
||||
return '<div class="webauthn mb-3"'.($automatic ? ' data-auto-passkey="true"' : '').' data-base="'.$this->e($this->base).'" data-csrf="'.$this->e($_SESSION['bs']['csrf']).'">'.($create?'<label>Passkey name<input class="key-label form-control mt-2" maxlength="80" value="My passkey"></label>':'').'<button type="button" data-key="'.($create?'create':'get').'">'.($create?'Add passkey':'Try passkey again').'</button></div>';
|
||||
}
|
||||
private function oidcConfigStamp(): string {
|
||||
return hash('sha256', json_encode([$this->cfg('issuer'), $this->cfg('clientId'), $this->cfg('clientSecret')], JSON_THROW_ON_ERROR));
|
||||
|
||||
Reference in new issue
Block a user