BASE44DEVS

ARTICLE · 10 MIN READ

Base44 Authentication Patterns: Login, MFA, SSO, and Session Management

Authentication on Base44 is mostly handled by the platform's User module, but the defaults are insufficient for production. The July 2025 SSO bypass disclosure proved that. This article walks the auth patterns that work in 2026: email/password with proper reset flows, Google OAuth with domain verification, MFA enforcement at the role level, server-side session validation, and the specific extensions you need on top of the platform's built-in auth to meet OWASP A07.

Last verified
2026-05-01
Published
2026-05-01
Read time
10 min
Words
1,848
  • AUTHENTICATION
  • AUTH
  • SSO
  • MFA
  • OAUTH

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 enforcement, session validation, audit logging. This article covers the patterns that make Base44 auth production-grade.

The July 2025 SSO bypass disclosure is the load-bearing example throughout. The vulnerability worked because a default-permissive trust model allowed signup without verifying the user's email domain. The same class of issue is present elsewhere; the lesson is to verify, never trust.

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.
  • Per-user MFA opt-in.
  • A User entity with id, email, full_name, role, and a few platform fields.
  • Session management via JWT in localStorage.

What it does not give you:

  • Enterprise SSO (Okta, Azure AD, Ping).
  • Role-level MFA enforcement.
  • Server-side session-age validation.
  • Email-domain enforcement for SSO apps.
  • Granular permissions beyond admin/user.
  • Audit logging of auth events.

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:

// backend/functions/secureSignup.ts
export default async function handler(req: Request): Promise<Response> {
  if (req.method !== "POST") return new Response("Method Not Allowed", { status: 405 });

  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 base44.entities.SignupAttempt.list(
    { ip, created_date: { $gte: new Date(Date.now() - 60 * 60 * 1000).toISOString() } }
  );
  if (recent.length > 10) {
    return new Response("Too many attempts", { status: 429 });
  }

  await base44.entities.SignupAttempt.create({ ip, email, created_date: new Date().toISOString() });

  // 4. Delegate to platform signup
  // ... call platform's signup, get the user back ...

  // 5. Audit log
  await base44.entities.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

Default reset endpoints often respond differently for existing vs. nonexistent emails. This is email enumeration. Fix:

// backend/functions/requestPasswordReset.ts
export default async function handler(req: Request): Promise<Response> {
  const { email } = await req.json();

  // 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 base44.entities.User.list({ email }, null, 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 base44.entities.PasswordResetToken.create({
    user_id: users[0].id,
    token_hash: await sha256(resetToken),
    expires_at: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
  });

  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 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

The platform supports per-user MFA but does not enforce it. Build the enforcement:

// backend/functions/enforceAdminMFA.ts (scheduled daily)
export default async function handler(req: Request): Promise<Response> {
  const admins = await base44.entities.User.list({ 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 base44.entities.User.update(admin.id, {
        role: "user",
        notes: `Demoted: 14 days without MFA enrollment`,
      });
      await base44.entities.AuthAuditLog.create({
        event_type: "admin_demoted_no_mfa",
        user_id: admin.id,
        timestamp: new Date().toISOString(),
      });
      continue;
    }

    if (daysWithoutMFA > 7) {
      await base44.entities.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 });
}

Trigger via external scheduler. 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:

// Reusable middleware
async function requireRecentAuth(req: Request, maxAgeMinutes: number) {
  const me = await base44.User.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
export default async function changePassword(req: Request) {
  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

For B2B SaaS that needs to integrate with customers' Okta, Azure AD, Google Workspace, or other IdPs:

  1. Configure Auth0 or Clerk to handle the SAML/OIDC negotiation with the customer's IdP.
  2. The user logs in on auth0.example.com and gets back an Auth0-issued JWT.
  3. Your frontend stores the Auth0 JWT.
  4. Every backend function call includes the Auth0 JWT in the Authorization header.
  5. Backend functions validate the Auth0 JWT signature using Auth0's public keys.
  6. The function maps the Auth0 user to a Base44 User entity (by email or external ID).
// backend/functions/getMyData.ts
import { jwtVerify, createRemoteJWKSet } from "https://esm.sh/jose@5";

const JWKS = createRemoteJWKSet(
  new URL("https://example.auth0.com/.well-known/jwks.json")
);

export default async function handler(req: Request) {
  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;
  const userRecord = (await base44.entities.User.list({ email }, null, 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

Every auth-relevant event into an AuthAuditLog entity:

EventData captured
login_successemail, ip, user_agent, timestamp
login_failureemail, ip, user_agent, timestamp, reason
signupemail, ip, user_agent, timestamp
password_reset_requestedemail, ip, timestamp
password_changeduser_id, ip, timestamp
role_changeduser_id, old_role, new_role, changed_by, timestamp
mfa_enrolled / mfa_disableduser_id, timestamp
admin_demoted_no_mfauser_id, timestamp
suspicious_activityuser_id, reason, timestamp

Ship the log out to an external SIEM (Logflare, Axiom, Datadog) for retention. The platform's logs roll quickly; auth logs need 90+ day retention 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 invalidate the session on the server, not just clear local storage:

// backend/functions/secureLogout.ts
export default async function handler(req: Request) {
  const me = await base44.User.me();
  if (!me) return new Response("OK", { status: 200 });

  // Invalidate the token on the platform
  await base44.auth.signOut();

  // Log the logout
  await base44.entities.AuthAuditLog.create({
    event_type: "logout",
    user_id: me.id,
    timestamp: new Date().toISOString(),
  });

  // Optional: invalidate any refresh tokens you've issued
  // ...

  return new Response("OK", { status: 200 });
}

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.

Per-user MFA opt-in without role-level enforcement. Admins forget to enable it; you have privileged accounts without MFA.

Frontend-only session timeout. Trivially bypassed.

Forgetting to invalidate tokens on logout. Stale tokens stay valid in compromised browsers.

No auth audit log. Post-incident forensics is impossible.

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. Most apps have 3–7 fixable issues. Order an audit or book a free 15-minute call.

QUERIES

Frequently asked questions

Q.01Is Base44's built-in auth secure enough for production?
A.01

For consumer apps with no compliance needs, yes — with the hardening described in this article. For SSO-only enterprise apps, the platform's defaults are not sufficient on their own; you must add domain verification post-signup and audit MFA enrollment. For regulated workloads (HIPAA, PCI Level 1, SOC 2), the platform's auth is not certified to those frameworks and you'll need to layer Auth0 or Clerk on top via backend functions. The right answer depends on your threat model and compliance posture.

Q.02What was the July 2025 SSO bypass and is it still a risk?
A.02

Wiz disclosed that any attacker could register a verified account on private SSO-only Base44 apps using only the publicly-known app_id. The platform patched it within 24 hours. The structural concern remains: the platform's trust model assumes app_id is access control, which it is not. Mitigation: in your signup backend function, re-verify the email's domain against your allowed list before allowing account creation, regardless of what SSO claims about the user. Treat the platform's signup as a starting point you validate.

Q.03How do I require MFA for admin users?
A.03

Base44 supports per-user MFA but doesn't enforce it at the role level. Build a scheduled function that runs daily, lists users with admin role, checks each user's MFA enrollment status (via the User entity), and downgrades any admin without MFA to a 'pending_mfa' role. Email them to enroll. After 7 days, downgrade further. This makes MFA a hard requirement for admin access, even though the platform doesn't enforce it natively.

Q.04How do I prevent email enumeration on the password reset flow?
A.04

Wrap the reset endpoint in a backend function that always returns the same response — '200 OK, if an account exists for this email, a reset link has been sent' — regardless of whether the email matches a real user. Internally, only send the email if the account exists. From the attacker's perspective, both branches look identical. The platform's default reset flow may or may not do this; verify by attempting reset with a fake email and a real email and checking the responses are byte-identical.

Q.05Can I use enterprise SSO providers like Okta or Azure AD with Base44?
A.05

Not natively. Base44's first-class SSO is Google OAuth. For Okta, Azure AD, Ping, and other enterprise IdPs, route auth through Auth0 or Clerk: their service handles the SAML/OIDC negotiation with the customer's IdP, and your Base44 app trusts the auth0/clerk-issued JWT. Implement the trust by validating the JWT in a backend function on every privileged call. This adds operational complexity but is the canonical pattern for B2B SaaS auth on Base44.

Q.06How long should Base44 sessions last?
A.06

Shorter than the platform default. The platform's default JWT TTL has historically been measured in days. For production, configure your auth flow to refresh tokens every 15 minutes, with a sliding session window that requires re-auth after 8 hours of inactivity. Sensitive operations (billing changes, password changes, role changes) should require re-auth within the last 5 minutes. None of this is enforced natively; you build it in backend functions.

NEXT STEP

Need engineers who actually know base44?

Book a free 15-minute call or order a $497 audit.