Base44's built-in auth covers email/password signup, Google OAuth, basic email verification, and JWT sessions in localStorage, with two-factor authentication for Base44's own accounts but none documented for your app's users — so production apps must add five things on top. First, enforce email-domain verification post-signup for SSO apps; the July 2025 Wiz disclosure proved this gap. Second, wrap password reset to return identical responses regardless of whether the email exists, blocking enumeration. Third, add a second factor for admin roles and audit enrollment with a scheduled automation that downgrades non-compliant accounts. Fourth, validate session age server-side in backend functions rather than trusting client timers. Fifth, ship every auth event to an external SIEM with 90-day retention. Use Auth0 or Clerk via backend functions for any enterprise SSO requirement.
Why this matters
Authentication is the front door. Get it right, the rest of the app's security has a chance. Get it wrong, every other control is moot. Base44's built-in auth handles the basics — login, signup, OAuth — but several production-critical concerns are not built in: domain verification, MFA for your app's users, and server-side session validation. Auth events are logged (each app's Logs page records registrations, logins, invites and role changes), but the docs give that log no retention period. This article, written by the lead engineer at Base44Devs, covers the patterns that make Base44 auth production-grade.
The July 2025 SSO bypass disclosure is the load-bearing example throughout. Wiz reported it on 9 July 2025, Wix fixed it within 24 hours, and Wiz verified the fix on 10 July (Wiz disclosure; our SSO bypass audit playbook has the full timeline). The vulnerability worked because undocumented registration and email-verification endpoints accepted sign-ups from anyone who supplied the app's app_id, without checking the user's email domain. The same class of issue is present elsewhere; the lesson is to verify, never trust. For the broader post-acquisition security context, see our timeline article.
What the platform gives you
Out of the box:
- Email/password signup and login.
- Google OAuth.
- Email verification flow (configurable per app).
- Forgot-password flow with reset link.
- Two-factor authentication for Base44 accounts (your builders), which Business or Enterprise workspaces can require for every member.
- A
Userentity withid,email,full_name,role, and a few platform fields. - Session management via JWT in
localStorage. - A Logs page for each app that records registrations, logins, invites and role changes.
What it does not give you:
- SAML-based enterprise SSO, or OIDC SSO on plans that do not include it.
- MFA for your app's own users (none is documented).
- Server-side session-age validation.
- Email-domain enforcement for SSO apps.
- A role design beyond the default Admin and User roles: extra roles are user fields you define and gate with rules — see how to add user roles and permissions in Base44.
- Long-term retention of auth events: the Logs page records them, but the docs give it no retention period.
You build the missing pieces in backend functions.
Pattern 1: signup with domain verification
For B2B apps where users should only be able to register with their corporate email:
// functions/secureSignup.ts
import { createClientFromRequest } from "npm:@base44/sdk";
Deno.serve(async (req) => {
if (req.method !== "POST") return new Response("Method Not Allowed", { status: 405 });
// The caller is not signed in yet, so read and write with the service role.
const db = createClientFromRequest(req).asServiceRole.entities;
const { email, password } = await req.json();
// 1. Format check
if (!isValidEmail(email)) {
return new Response(JSON.stringify({ error: "Invalid email format" }), { status: 400 });
}
// 2. Domain allow-list check
const ALLOWED_DOMAINS = (Deno.env.get("ALLOWED_SIGNUP_DOMAINS") || "")
.split(",")
.map(d => d.trim().toLowerCase());
const domain = email.split("@")[1]?.toLowerCase();
if (!ALLOWED_DOMAINS.includes(domain)) {
return new Response(JSON.stringify({ error: "Email domain not authorized" }), { status: 403 });
}
// 3. Rate limit by IP
const ip = req.headers.get("x-forwarded-for") || "unknown";
const recent = await db.SignupAttempt.filter(
{ ip, created_date: { $gte: new Date(Date.now() - 60 * 60 * 1000).toISOString() } }
);
if (recent.length > 10) {
return new Response("Too many attempts", { status: 429 });
}
// Base44 fills in created_date on every record it creates.
await db.SignupAttempt.create({ ip, email });
// 4. Delegate to platform signup
// ... call platform's signup, get the user back ...
// 5. Audit log
await db.AuthAuditLog.create({
event_type: "signup",
email,
ip,
timestamp: new Date().toISOString(),
});
return new Response(JSON.stringify({ ok: true }), { status: 200 });
});
This is the pattern that would have prevented the July 2025 SSO bypass. The platform's signup checked the app_id; this wrapper additionally checks the email domain, regardless of what the platform infers. Defense in depth.
Pattern 2: password reset that doesn't leak
To reset a Base44 password, use the SDK's documented flow: base44.auth.resetPasswordRequest(email) sends the reset email, and base44.auth.resetPassword(params) sets the new password with the token from that email. The pattern below is for a custom reset step you run on top, and its job is narrower. Default reset endpoints often respond differently for existing vs. nonexistent emails. This is email enumeration. Fix:
// functions/requestPasswordReset.ts
import { createClientFromRequest } from "npm:@base44/sdk";
Deno.serve(async (req) => {
const { email } = await req.json();
// The caller is signed out, so look the account up with the service role.
const db = createClientFromRequest(req).asServiceRole.entities;
// Always return the same response regardless of whether the user exists
const genericResponse = JSON.stringify({
message: "If an account exists for this email, a reset link has been sent.",
});
if (!isValidEmail(email)) {
return new Response(genericResponse, { status: 200 });
}
// Internally, check if the user exists
const users = await db.User.filter({ email }, undefined, 1);
if (users.length === 0) {
// Pretend we sent an email
return new Response(genericResponse, { status: 200 });
}
// Actually send the reset email
const resetToken = generateSecureToken();
await db.PasswordResetToken.create({
user_id: users[0].id,
token_hash: await sha256(resetToken),
expires_at: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
});
// sendEmail() is your own Resend or Postmark wrapper (see the note below).
await sendEmail({
to: email,
subject: "Reset your password",
body: `Click to reset: ${Deno.env.get("APP_URL")}/reset?token=${resetToken}`,
});
return new Response(genericResponse, { status: 200 });
});
Both branches return the same response and the same status. The attacker cannot distinguish them.
Use a real email provider (Resend, Postmark) rather than the platform's SendEmail integration for password reset because deliverability is a security control here — if the email lands in spam, the user is locked out.
Pattern 3: MFA enforcement for admin roles
Base44's docs describe two-factor authentication for Base44 accounts, not for your app's users, so for live-app admins the second factor is yours to add: an identity provider that enforces MFA, or an authenticator-code check in a backend function. Either way, record enrollment in a field you add to the User entity (mfa_enrolled below; Base44 does not provide one) and enforce it on a schedule:
// enforceAdminMFA: a backend function run daily by a scheduled automation.
import { createClientFromRequest } from "npm:@base44/sdk";
Deno.serve(async (req) => {
const base44 = createClientFromRequest(req);
// A scheduled run has no signed-in user, so read and write with service role.
const db = base44.asServiceRole.entities;
const admins = await db.User.filter({ role: "admin" });
for (const admin of admins) {
if (admin.mfa_enrolled === true) continue;
const daysWithoutMFA = daysSince(admin.role_assigned_at);
if (daysWithoutMFA > 14) {
// Hard demote: revoke admin
await db.User.update(admin.id, {
role: "user",
notes: `Demoted: 14 days without MFA enrollment`,
});
await db.AuthAuditLog.create({
event_type: "admin_demoted_no_mfa",
user_id: admin.id,
timestamp: new Date().toISOString(),
});
continue;
}
if (daysWithoutMFA > 7) {
await db.User.update(admin.id, { role: "pending_mfa" });
await sendEmail({
to: admin.email,
subject: "Action required: enable MFA on your admin account",
body: "MFA is required for admin access. Enable within 7 days or your admin role will be revoked.",
});
continue;
}
// Days 1-7: gentle reminder
if (daysWithoutMFA % 2 === 0) {
await sendEmail({
to: admin.email,
subject: "Reminder: enable MFA on your admin account",
body: "MFA is required for admin access...",
});
}
}
return new Response("OK", { status: 200 });
});
Run it daily as a scheduled automation: Base44 automations run backend functions on a cron or simple schedule. The combination of soft warnings, role downgrade, and full revocation makes MFA a hard requirement without surprising anyone.
Pattern 4: server-side session validation
Frontend session timers are advisory. A backend function should validate session age on every privileged call:
import { createClientFromRequest } from "npm:@base44/sdk";
// Reusable middleware
async function requireRecentAuth(req: Request, maxAgeMinutes: number) {
const base44 = createClientFromRequest(req);
const me = await base44.auth.me();
if (!me) throw new AuthError(401, "Not authenticated");
const tokenIssuedAt = getTokenIssuedAt(req);
const ageMinutes = (Date.now() - tokenIssuedAt) / 60_000;
if (ageMinutes > maxAgeMinutes) {
throw new AuthError(401, "Session expired - please re-authenticate");
}
return me;
}
// Usage in a backend function (functions/changePassword.ts)
Deno.serve(async (req) => {
try {
const me = await requireRecentAuth(req, 5); // 5-minute fresh-auth requirement
// ... process password change ...
return new Response("OK", { status: 200 });
} catch (err) {
if (err instanceof AuthError) {
return new Response(err.message, { status: err.status });
}
throw err;
}
});
For sensitive operations (password change, email change, billing change, role change), require auth fresher than the routine session — typically the user must have logged in within the last 5 minutes. For routine operations, an 8-hour sliding window is sufficient.
Pattern 5: enterprise SSO via Auth0 or Clerk
Base44's own app SSO covers OIDC providers such as Okta, Microsoft and Google Workspace on the plans that include it (Business or Enterprise on workspaces created since 1 October 2026, Elite and higher on older workspaces). Use this pattern when the identity provider only speaks SAML, or when your plan does not include app SSO:
- Configure Auth0 or Clerk to handle the SAML/OIDC negotiation with the customer's IdP.
- The user logs in on auth0.example.com and gets back an Auth0-issued JWT.
- Your frontend stores the Auth0 JWT.
- Every backend function call includes the Auth0 JWT in the Authorization header.
- Backend functions validate the Auth0 JWT signature using Auth0's public keys.
- The function maps the Auth0 user to a Base44 User entity (by email or external ID).
// functions/getMyData.ts
import { createClientFromRequest } from "npm:@base44/sdk";
import { jwtVerify, createRemoteJWKSet } from "npm:jose@5";
const JWKS = createRemoteJWKSet(
new URL("https://example.auth0.com/.well-known/jwks.json")
);
Deno.serve(async (req) => {
const auth = req.headers.get("authorization");
if (!auth?.startsWith("Bearer ")) {
return new Response("Unauthorized", { status: 401 });
}
const token = auth.slice(7);
let payload;
try {
const result = await jwtVerify(token, JWKS, {
issuer: "https://example.auth0.com/",
audience: "https://api.example.com",
});
payload = result.payload;
} catch {
return new Response("Invalid token", { status: 401 });
}
const email = payload.email as string;
// The caller signed in with Auth0, not Base44, so look the user up with the service role.
const db = createClientFromRequest(req).asServiceRole.entities;
const userRecord = (await db.User.filter({ email }, undefined, 1))[0];
if (!userRecord) {
// Auto-provision on first login if your policy allows
return new Response("User not provisioned", { status: 403 });
}
// ... return the user's data ...
});
This pattern keeps Base44's User entity as the source of truth for app data while delegating identity to a real auth provider. Base44's native auth becomes secondary; your real users authenticate via the IdP.
Pattern 6: audit logging
Base44's Logs page already records some of these for each app (registrations, logins, invites and role changes), filterable by email. An AuthAuditLog entity gives you a copy you control, with the fields your compliance team asks for. Every auth-relevant event goes into it:
| Event | Data captured |
|---|---|
| login_success | email, ip, user_agent, timestamp |
| login_failure | email, ip, user_agent, timestamp, reason |
| signup | email, ip, user_agent, timestamp |
| password_reset_requested | email, ip, timestamp |
| password_changed | user_id, ip, timestamp |
| role_changed | user_id, old_role, new_role, changed_by, timestamp |
| mfa_enrolled / mfa_disabled | user_id, timestamp |
| admin_demoted_no_mfa | user_id, timestamp |
| suspicious_activity | user_id, reason, timestamp |
Ship the log out to an external SIEM (Logflare, Axiom, Datadog) for retention. Base44's docs give the Logs page no retention period, and auth logs often need 90+ days for compliance.
Set alerts on:
- More than 5 login failures from one IP in a minute.
- Signup rate above the 14-day baseline by 3x.
- Role changes outside of expected admin actions.
- MFA disabled events.
Pattern 7: secure logout
Logout should end the session on the server, not just clear local storage. In the Base44 SDK that is base44.auth.logout(redirectUrl?), which the SDK reference describes as a redirect to server-side logout that "clears HTTP-only cookies and session", then to the URL you pass. Because it navigates away, record the audit event first:
// functions/recordLogout.ts
import { createClientFromRequest } from "npm:@base44/sdk";
Deno.serve(async (req) => {
const base44 = createClientFromRequest(req);
const me = await base44.auth.me();
if (!me) return new Response("OK", { status: 200 });
await base44.entities.AuthAuditLog.create({
event_type: "logout",
user_id: me.id,
timestamp: new Date().toISOString(),
});
// Optional: invalidate any refresh tokens your own services issued.
return new Response("OK", { status: 200 });
});
// Frontend: record the event, then let Base44 end the session.
import { base44 } from "@/api/base44Client";
await base44.functions.invoke("recordLogout", {});
base44.auth.logout("/");
Common authentication mistakes
Trusting the platform's signup without domain verification. The July 2025 SSO bypass exploited exactly this gap.
Differential responses on password reset. Email enumeration vulnerability.
Admin accounts with one factor. Base44's docs describe no MFA for app users, so unless you add one, a stolen admin password is enough. Turn on 2FA for every builder account too.
Frontend-only session timeout. Trivially bypassed.
Forgetting to invalidate tokens on logout. Stale tokens stay valid in compromised browsers. The full failure-mode catalog lives in the Base44 error reference.
Never reading the auth events Base44 already logs. Each app's Logs page records registrations, logins, invites and role changes; filter by email to trace one account. Its docs give no retention period, so copy what forensics needs to storage you control.
Storing passwords in plaintext. The platform doesn't, but if you handle passwords yourself for any reason, never store plaintext.
Long-lived JWTs. Increases blast radius of token theft.
Authentication checklist
- Signup verifies email domain against allow-list for SSO apps.
- Password reset returns identical responses regardless of email existence.
- MFA required for admin role; enforced via scheduled audit.
- Sessions validated server-side on privileged operations.
- Sensitive operations require fresh auth (last 5 minutes).
- Token TTL under 1 hour; rotation on privileged actions.
- Auth audit log persisted to external SIEM with 90+ day retention.
- Alerts configured on auth failure bursts and role changes.
- Logout invalidates session server-side.
- Enterprise SSO via Auth0 or Clerk if needed.
Want us to audit your auth setup?
Our $497 audit reviews your full authentication flow, tests for the SSO-bypass class of issue, verifies MFA enforcement, and checks audit log coverage. Order an audit or book a free 15-minute call.
Related reading
- Base44 Security Hardening Checklist — the broader security drill-down, of which auth is one section.
- OWASP Top 10 in Base44 — the framework that scopes A07 (Identification and Authentication Failures).
- Base44 SDK Reference — the auth module and SDK calls that compose with these patterns.