Release v2.20.4
This commit is contained in:
1 parent
7228406929
commit
e6c48c6340
10 files changed
+95
-7
No files matched your search
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## 2.20.4 - 2026-10-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added optional `AUTH_TRUSTED_PROXIES` configuration for reverse-proxy IP addresses and CIDR ranges so Better Auth can resolve forwarded client IPs for sessions and per-client rate limits.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Restored successful-login audit events for password, two-factor, and passkey authentication, without recording pending two-factor challenges as completed logins.
|
||||||
|
|
||||||
## 2.20.3 - 2026-10-25
|
## 2.20.3 - 2026-10-25
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage-player",
|
"name": "pulse-signage-player",
|
||||||
"version": "2.20.3",
|
"version": "2.20.4",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage player application bundle",
|
"description": "Pulse Signage player application bundle",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage-web",
|
"name": "pulse-signage-web",
|
||||||
"version": "2.20.3",
|
"version": "2.20.4",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage web and bridge application bundle",
|
"description": "Pulse Signage web and bridge application bundle",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ PULSE_SIGNAGE_PLAYER_IMAGE="git.lzstealth.com/lzstealth/pulse-signage-player:lat
|
|||||||
PULSE_SIGNAGE_SHARED_SECRET=""
|
PULSE_SIGNAGE_SHARED_SECRET=""
|
||||||
AUTH_SECRET="REPLACE_WITH_A_RANDOM_SECRET_OF_AT_LEAST_32_CHARACTERS"
|
AUTH_SECRET="REPLACE_WITH_A_RANDOM_SECRET_OF_AT_LEAST_32_CHARACTERS"
|
||||||
AUTH_TRUSTED_ORIGINS="http://localhost:8080,http://127.0.0.1:8080"
|
AUTH_TRUSTED_ORIGINS="http://localhost:8080,http://127.0.0.1:8080"
|
||||||
|
AUTH_TRUSTED_PROXIES=""
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
DB_HOST="mysql"
|
DB_HOST="mysql"
|
||||||
|
|||||||
@@ -62,12 +62,15 @@ Key configuration:
|
|||||||
- `PULSE_SIGNAGE_SHARED_SECRET`
|
- `PULSE_SIGNAGE_SHARED_SECRET`
|
||||||
- `AUTH_SECRET` (generate with `openssl rand -base64 32` and keep stable across restarts)
|
- `AUTH_SECRET` (generate with `openssl rand -base64 32` and keep stable across restarts)
|
||||||
- `AUTH_TRUSTED_ORIGINS` (comma-separated extra browser origins, such as local `localhost` URLs)
|
- `AUTH_TRUSTED_ORIGINS` (comma-separated extra browser origins, such as local `localhost` URLs)
|
||||||
|
- `AUTH_TRUSTED_PROXIES` (optional comma-separated proxy IP addresses or CIDR ranges; leave empty for direct access)
|
||||||
- `WEB_PUBLIC_URL`
|
- `WEB_PUBLIC_URL`
|
||||||
- `BRIDGE_INTERNAL_URL`
|
- `BRIDGE_INTERNAL_URL`
|
||||||
- `DEFAULT_ADMIN_USERNAME`
|
- `DEFAULT_ADMIN_USERNAME`
|
||||||
- `DEFAULT_ADMIN_NAME`
|
- `DEFAULT_ADMIN_NAME`
|
||||||
- `DEFAULT_ADMIN_PASSWORD`
|
- `DEFAULT_ADMIN_PASSWORD`
|
||||||
|
|
||||||
|
For reverse-proxy deployments, set `AUTH_TRUSTED_PROXIES` to the actual proxy addresses or narrowly scoped proxy networks present in the forwarded IP chain. Better Auth uses these to resolve multi-address `X-Forwarded-For` headers for session IPs and per-client rate limits. For example, `AUTH_TRUSTED_PROXIES="192.0.2.10,198.51.100.0/24"` illustrates the format; replace these documentation addresses with your own. Do not use browser origins, trust all networks, or add client networks just to suppress the warning. Ensure the proxy sanitizes forwarded headers and clients cannot bypass it. Recreate the web container after changing the environment. Local development reads this setting from `docker-compose/.local.env`.
|
||||||
|
|
||||||
### `player`
|
### `player`
|
||||||
|
|
||||||
The screen runtime that renders playlists and receives commands.
|
The screen runtime that renders playlists and receives commands.
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ services:
|
|||||||
PULSE_SIGNAGE_SHARED_SECRET: ${PULSE_SIGNAGE_SHARED_SECRET:-}
|
PULSE_SIGNAGE_SHARED_SECRET: ${PULSE_SIGNAGE_SHARED_SECRET:-}
|
||||||
AUTH_SECRET: ${AUTH_SECRET:?Set a random AUTH_SECRET in .env}
|
AUTH_SECRET: ${AUTH_SECRET:?Set a random AUTH_SECRET in .env}
|
||||||
AUTH_TRUSTED_ORIGINS: ${AUTH_TRUSTED_ORIGINS:-http://localhost:8080,http://127.0.0.1:8080}
|
AUTH_TRUSTED_ORIGINS: ${AUTH_TRUSTED_ORIGINS:-http://localhost:8080,http://127.0.0.1:8080}
|
||||||
|
AUTH_TRUSTED_PROXIES: ${AUTH_TRUSTED_PROXIES:-}
|
||||||
WEB_PUBLIC_URL: ${WEB_PUBLIC_URL:-http://localhost:8080}
|
WEB_PUBLIC_URL: ${WEB_PUBLIC_URL:-http://localhost:8080}
|
||||||
BRIDGE_INTERNAL_URL: ${BRIDGE_INTERNAL_URL:-http://player-bridge:8090}
|
BRIDGE_INTERNAL_URL: ${BRIDGE_INTERNAL_URL:-http://player-bridge:8090}
|
||||||
DEFAULT_ADMIN_USERNAME: ${DEFAULT_ADMIN_USERNAME:-admin}
|
DEFAULT_ADMIN_USERNAME: ${DEFAULT_ADMIN_USERNAME:-admin}
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "2.20.3",
|
"version": "2.20.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "2.20.3",
|
"version": "2.20.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@better-auth/passkey": "^1.7.13",
|
"@better-auth/passkey": "^1.7.13",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "2.20.3",
|
"version": "2.20.4",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage application with MySQL and media storage",
|
"description": "Pulse Signage application with MySQL and media storage",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -2,9 +2,34 @@ const crypto = require('crypto');
|
|||||||
const { isIP } = require('node:net');
|
const { isIP } = require('node:net');
|
||||||
const { verifyEncodedPassword } = require('#src/auth');
|
const { verifyEncodedPassword } = require('#src/auth');
|
||||||
const { fetchAppSettings } = require('#src/data/app-settings');
|
const { fetchAppSettings } = require('#src/data/app-settings');
|
||||||
|
const { recordRequestAuditEvent } = require('#src/data/audit-log');
|
||||||
const { formatAccountEmailExpiry, normalizeOtpEmailBody, renderAccountEmailTemplate } = require('#src/data/account-email-templates');
|
const { formatAccountEmailExpiry, normalizeOtpEmailBody, renderAccountEmailTemplate } = require('#src/data/account-email-templates');
|
||||||
|
|
||||||
const LOGIN_RATE_LIMIT_PATHS = ['/sign-in/username', '/sign-in/email', '/two-factor/verify-totp', '/two-factor/verify-backup-code'];
|
const LOGIN_RATE_LIMIT_PATHS = ['/sign-in/username', '/sign-in/email', '/two-factor/verify-totp', '/two-factor/verify-backup-code'];
|
||||||
|
const LOGIN_AUDIT_PATHS = LOGIN_RATE_LIMIT_PATHS.concat('/passkey/verify-authentication');
|
||||||
|
|
||||||
|
function createLoginAuditHook(pool, recordAudit = recordRequestAuditEvent) {
|
||||||
|
return async function (ctx) {
|
||||||
|
const newSession = ctx.context.newSession;
|
||||||
|
if (!LOGIN_AUDIT_PATHS.includes(ctx.path) || !newSession || !newSession.session || !newSession.user) return;
|
||||||
|
const headers = ctx.headers || ctx.request && ctx.request.headers;
|
||||||
|
await recordAudit(pool, {
|
||||||
|
headers: headers ? Object.fromEntries(headers) : {},
|
||||||
|
ip: newSession.session.ipAddress
|
||||||
|
}, {
|
||||||
|
category: 'authentication',
|
||||||
|
eventType: 'login.success',
|
||||||
|
actorUserId: newSession.user.id,
|
||||||
|
targetType: 'user',
|
||||||
|
targetId: newSession.user.id,
|
||||||
|
targetLabel: newSession.user.username || newSession.user.email
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTrustedProxies(value = process.env.AUTH_TRUSTED_PROXIES) {
|
||||||
|
return String(value || '').split(',').map(function (proxy) { return proxy.trim(); }).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
function ensureBetterAuthClientIp(req) {
|
function ensureBetterAuthClientIp(req) {
|
||||||
if (!req.headers['x-forwarded-for'] && req.socket && req.socket.remoteAddress) {
|
if (!req.headers['x-forwarded-for'] && req.socket && req.socket.remoteAddress) {
|
||||||
@@ -272,6 +297,9 @@ async function createBetterAuth(pool, options) {
|
|||||||
after: [{
|
after: [{
|
||||||
matcher: function (ctx) { return LOGIN_RATE_LIMIT_PATHS.includes(ctx.path); },
|
matcher: function (ctx) { return LOGIN_RATE_LIMIT_PATHS.includes(ctx.path); },
|
||||||
handler: apiModule.createAuthMiddleware(createLoginRateLimitResetHook(pool))
|
handler: apiModule.createAuthMiddleware(createLoginRateLimitResetHook(pool))
|
||||||
|
}, {
|
||||||
|
matcher: function (ctx) { return LOGIN_AUDIT_PATHS.includes(ctx.path); },
|
||||||
|
handler: apiModule.createAuthMiddleware(createLoginAuditHook(pool))
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,7 +307,8 @@ async function createBetterAuth(pool, options) {
|
|||||||
disabledPaths: ['/sign-up/email', '/forget-password', '/reset-password', '/change-password', '/send-verification-email', '/verify-email'],
|
disabledPaths: ['/sign-up/email', '/forget-password', '/reset-password', '/change-password', '/send-verification-email', '/verify-email'],
|
||||||
advanced: {
|
advanced: {
|
||||||
ipAddress: {
|
ipAddress: {
|
||||||
ipAddressHeaders: ['x-forwarded-for']
|
ipAddressHeaders: ['x-forwarded-for'],
|
||||||
|
trustedProxies: getTrustedProxies()
|
||||||
},
|
},
|
||||||
useSecureCookies: false,
|
useSecureCookies: false,
|
||||||
defaultCookieAttributes: {
|
defaultCookieAttributes: {
|
||||||
@@ -317,10 +346,12 @@ async function createBetterAuth(pool, options) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createBetterAuth,
|
createBetterAuth,
|
||||||
|
getTrustedProxies,
|
||||||
ensureBetterAuthClientIp,
|
ensureBetterAuthClientIp,
|
||||||
createPasswordCallbacks,
|
createPasswordCallbacks,
|
||||||
createAccountLockSessionHook,
|
createAccountLockSessionHook,
|
||||||
createLoginRateLimitResetHook,
|
createLoginRateLimitResetHook,
|
||||||
|
createLoginAuditHook,
|
||||||
createPasskeyAuthenticationGuard,
|
createPasskeyAuthenticationGuard,
|
||||||
resolvePasskeyRpID,
|
resolvePasskeyRpID,
|
||||||
getRoleTwoFactorAuthError
|
getRoleTwoFactorAuthError
|
||||||
|
|||||||
@@ -1,7 +1,49 @@
|
|||||||
const test = require('node:test');
|
const test = require('node:test');
|
||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
const { hashPassword, encodePasswordRecord, verifyEncodedPassword, hashScryptPassword, verifyCredentialPassword } = require('../src/auth');
|
const { hashPassword, encodePasswordRecord, verifyEncodedPassword, hashScryptPassword, verifyCredentialPassword } = require('../src/auth');
|
||||||
const { createPasswordCallbacks, createAccountLockSessionHook, createLoginRateLimitResetHook, createPasskeyAuthenticationGuard, resolvePasskeyRpID, getRoleTwoFactorAuthError, ensureBetterAuthClientIp } = require('../src/web/lib/auth/better-auth');
|
const { createPasswordCallbacks, createAccountLockSessionHook, createLoginRateLimitResetHook, createLoginAuditHook, createPasskeyAuthenticationGuard, resolvePasskeyRpID, getRoleTwoFactorAuthError, ensureBetterAuthClientIp, getTrustedProxies } = require('../src/web/lib/auth/better-auth');
|
||||||
|
|
||||||
|
test('completed password, two-factor, and passkey logins record an audit event with request metadata', async () => {
|
||||||
|
const events = [];
|
||||||
|
const pool = {};
|
||||||
|
const hook = createLoginAuditHook(pool, async function (actualPool, req, event) {
|
||||||
|
assert.equal(actualPool, pool);
|
||||||
|
events.push({ req, event });
|
||||||
|
});
|
||||||
|
for (const path of ['/sign-in/username', '/sign-in/email', '/two-factor/verify-totp', '/two-factor/verify-backup-code', '/passkey/verify-authentication']) {
|
||||||
|
await hook({
|
||||||
|
path,
|
||||||
|
headers: new Headers({ 'x-forwarded-for': '192.0.2.1', 'user-agent': 'test-browser' }),
|
||||||
|
context: { newSession: { session: { ipAddress: '192.0.2.1' }, user: { id: 7, username: 'alice' } } }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
assert.equal(events.length, 5);
|
||||||
|
for (const { req, event } of events) {
|
||||||
|
assert.deepEqual(event, { category: 'authentication', eventType: 'login.success', actorUserId: 7, targetType: 'user', targetId: 7, targetLabel: 'alice' });
|
||||||
|
assert.equal(req.headers['user-agent'], 'test-browser');
|
||||||
|
assert.equal(req.headers['x-forwarded-for'], '192.0.2.1');
|
||||||
|
assert.equal(req.ip, '192.0.2.1');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('failed login, pending two-factor, and unrelated session creation do not log a successful login', async () => {
|
||||||
|
const hook = createLoginAuditHook({}, async function () { assert.fail('Login must not be audited as successful'); });
|
||||||
|
for (const path of ['/sign-in/username', '/two-factor/verify-totp', '/passkey/verify-authentication']) {
|
||||||
|
await hook({ path, context: { newSession: null } });
|
||||||
|
}
|
||||||
|
await hook({ path: '/two-factor/enable', context: { newSession: { session: {}, user: { id: 7 } } } });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Better Auth trusted proxies accept comma-separated IPs and CIDRs without trusting proxies by default', async () => {
|
||||||
|
const { getIPFromHeader } = await import('@better-auth/core/utils/ip');
|
||||||
|
assert.deepEqual(getTrustedProxies(''), []);
|
||||||
|
assert.deepEqual(getTrustedProxies(' , '), []);
|
||||||
|
const trustedProxies = getTrustedProxies(' 192.0.2.2, 198.51.100.0/24, , ::1 ');
|
||||||
|
assert.deepEqual(trustedProxies, ['192.0.2.2', '198.51.100.0/24', '::1']);
|
||||||
|
const chain = '203.0.113.1, 192.0.2.2, 198.51.100.2';
|
||||||
|
assert.equal(getIPFromHeader(chain, { trustedProxies: getTrustedProxies('') }), null);
|
||||||
|
assert.equal(getIPFromHeader(chain, { trustedProxies }), '203.0.113.1');
|
||||||
|
});
|
||||||
|
|
||||||
test('Better Auth client IP falls back to the socket address for direct requests', () => {
|
test('Better Auth client IP falls back to the socket address for direct requests', () => {
|
||||||
for (const address of ['192.0.2.1', '::1', '::ffff:192.0.2.1']) {
|
for (const address of ['192.0.2.1', '::1', '::ffff:192.0.2.1']) {
|
||||||
|
|||||||
Reference in new issue
Block a user