Fix password special characters breaking login via WAF

Base64-encode passwords on the frontend before sending in JSON body,
and decode on the backend before passing to bcrypt. Prevents Nginx WAF
from returning an HTML error page when passwords contain <, >, &, etc.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-03-11 18:40:46 +00:00
parent 0eb2b86bb6
commit e8c708f6eb
2 changed files with 23 additions and 6 deletions

11
auth.js
View file

@ -18,6 +18,11 @@ const loginRequest = { scopes: ['User.Read'] };
const API_BASE = '/loreal-sla-calculator/api';
// Encode password to base64 (unicode-safe) to avoid WAF issues with special chars
function encodePassword(pw) {
return btoa(unescape(encodeURIComponent(pw)));
}
let msalInstance = null;
let currentAccessToken = null; // In-memory only — never persisted
let pendingResetToken = null; // ?reset_token= from URL
@ -149,7 +154,7 @@ async function authEmailLogin(event) {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
body: JSON.stringify({ email, password: encodePassword(password) }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Login failed');
@ -189,7 +194,7 @@ async function authRegister(event) {
const res = await fetch(`${API_BASE}/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
body: JSON.stringify({ email, password: encodePassword(password) }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Registration failed');
@ -273,7 +278,7 @@ async function authResetPassword(event) {
const res = await fetch(`${API_BASE}/auth/reset-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: pendingResetToken, password }),
body: JSON.stringify({ token: pendingResetToken, password: encodePassword(password) }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Reset failed');

View file

@ -32,10 +32,20 @@ function userPublic(user) {
return { id: user.id, email: user.email, displayName: user.display_name, authMethod: user.auth_method };
}
// Decode base64-encoded password from frontend (unicode-safe)
function decodePassword(encoded) {
try {
return Buffer.from(encoded, 'base64').toString('utf-8');
} catch {
return encoded; // fallback: treat as plaintext (backwards compat)
}
}
// ── Register ──────────────────────────────────────────────────────────────────
async function register(req, res) {
const { email, password } = req.body || {};
const { email } = req.body || {};
const password = decodePassword(req.body?.password);
if (!email || !password) return res.status(400).json({ error: 'Email and password are required.' });
if (!isLorealEmail(email)) return res.status(400).json({ error: 'Only @loreal.com email addresses are allowed.' });
@ -74,7 +84,8 @@ async function verifyEmail(req, res) {
// ── Login ─────────────────────────────────────────────────────────────────────
async function login(req, res) {
const { email, password } = req.body || {};
const { email } = req.body || {};
const password = decodePassword(req.body?.password);
if (!email || !password) return res.status(400).json({ error: 'Email and password are required.' });
@ -234,7 +245,8 @@ async function validateResetToken(req, res) {
// ── Reset password ────────────────────────────────────────────────────────────
async function resetPassword(req, res) {
const { token, password } = req.body || {};
const { token } = req.body || {};
const password = decodePassword(req.body?.password);
if (!token || !password) return res.status(400).json({ error: 'Token and password are required.' });
if (!isStrongPassword(password)) return res.status(400).json({ error: 'Password must be at least 8 characters with uppercase, lowercase, and a number.' });