Article

When Users Can't Log In: Authentication & Authorization Breakdowns

OAuth redirects to localhost, infinite login loops, and session management disasters—the authentication failures that plague AI-generated apps and how to fix them fast.

November 7, 2025
7 min read
By Deploy Your Vibe Team
authenticationoauthsecuritysessionsauthorization

When Users Can't Log In: Authentication & Authorization Breakdowns

Nothing frustrates users faster than authentication problems. They click "Sign in with Google," get redirected to a broken page, click again, get stuck in an infinite loop, and eventually give up on your app entirely. In AI-generated applications, authentication is one of the most fragile systems—and it breaks in spectacular ways.

The Google OAuth Localhost Disaster

This is the number one authentication issue reported by Lovable users deploying to production. Here's what happens:

  1. Development works perfectly—Google login succeeds
  2. You deploy to production at https://yourapp.com
  3. Users click "Sign in with Google"
  4. Google authenticates them successfully
  5. Google redirects them to http://localhost:3000/auth/callback
  6. Users see a "This site can't be reached" error

Why This Happens

When you connect Supabase to your Lovable app, Supabase is configured with a Site URL that defaults to http://localhost:3000. OAuth providers redirect users back to this URL after authentication. AI code generators never update this value for production.

The Fix

  1. Open your Supabase project dashboard
  2. Go to Authentication → URL Configuration
  3. Update the Site URL to your production domain: https://yourapp.com
  4. Add your production domain to Redirect URLs: https://yourapp.com/**
  5. Save and redeploy
// Also verify your Supabase client is using the correct redirect
const supabase = createClient(supabaseUrl, supabaseAnonKey, {
  auth: {
    redirectTo: `${window.location.origin}/auth/callback`,
  },
});

The Infinite Login Loop

Users click "Login," they're redirected to the auth provider, they authenticate successfully, they're sent back to your app, and... they're logged out again. They click "Login" and the cycle repeats forever.

Common Causes

1. Session Not Persisting

AI-generated code often uses in-memory session storage, which clears on every page refresh:

// ❌ Session lost on refresh
const [user, setUser] = useState(null);

// ✅ Persist session in localStorage
const [user, setUser] = useState(() => {
  const saved = localStorage.getItem('user');
  return saved ? JSON.parse(saved) : null;
});

2. Cookie Configuration Issues

Authentication cookies need specific attributes to work in production:

// ❌ Cookies don't persist
cookies.set('session', token);

// ✅ Proper cookie configuration
cookies.set('session', token, {
  httpOnly: true,
  secure: true, // HTTPS only
  sameSite: 'lax',
  maxAge: 60 * 60 * 24 * 7, // 1 week
  path: '/',
});

3. Missing Token Refresh Logic

Most auth tokens expire after 1 hour. If you don't refresh them, users get logged out:

// ✅ Automatic token refresh
supabase.auth.onAuthStateChange(async (event, session) => {
  if (event === 'TOKEN_REFRESHED') {
    // Update your app's session state
    setSession(session);
  }
});

The "Email Address Is Invalid" Error

Users try to sign up with legitimate email addresses like john.doe+test@company.com, and your app rejects them as "invalid." This is a validation regex gone wrong.

The Bad Regex AI Tools Use

// ❌ Rejects valid email formats
const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/;

This regex fails for:

  • Emails with + symbols (used for filtering)
  • Domains with more than 4 characters (.photography, .business)
  • International characters (José@example.com)

The Better Solution

// ✅ More permissive validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

// Even better: Let the auth provider validate
// Just check that an @ symbol exists
const isValidEmail = (email: string) => email.includes('@') && email.length > 3;

Role-Based Access Control Failures

Your app has admin features, but non-admin users can access them by just navigating to the URL. AI code generators create UI-level restrictions without backend enforcement.

The Insecure Pattern

// ❌ Only checks role on the frontend
function AdminDashboard() {
  const { user } = useAuth();

  if (user?.role !== 'admin') {
    return <div>Access denied</div>;
  }

  return <div>Admin controls...</div>;
}

Anyone can bypass this by:

  • Opening browser DevTools
  • Changing user.role to 'admin' in the console
  • Refreshing the page

Proper Authorization

// ✅ Verify permissions on the backend
// Server-side route protection
async function getAdminData(request: Request) {
  const session = await getSession(request);

  // Check role from database, not client
  const user = await db.users.findById(session.userId);

  if (user.role !== 'admin') {
    throw new Response('Forbidden', { status: 403 });
  }

  return await db.admin.getData();
}

JWT Token Vulnerabilities

AI tools love to use JSON Web Tokens (JWTs), but they often implement them insecurely:

Common JWT Mistakes

1. Storing JWTs in LocalStorage

LocalStorage is accessible to any JavaScript running on your page, including XSS attacks:

// ❌ Vulnerable to XSS
localStorage.setItem('token', jwtToken);

// ✅ Use HTTP-only cookies instead
// Set via server response headers, not JavaScript

2. Not Verifying Token Signatures

// ❌ Accepts any token without verification
const decoded = JSON.parse(atob(token.split('.')[1]));

// ✅ Verify with proper library
import { verify } from 'jsonwebtoken';
const decoded = verify(token, process.env.JWT_SECRET);

3. Never Expiring Tokens

// ❌ Token valid forever
const token = jwt.sign({ userId }, secret);

// ✅ Set reasonable expiration
const token = jwt.sign({ userId }, secret, { expiresIn: '1h' });

Social Login Provider Configuration

Each OAuth provider has quirks that AI tools don't account for:

Google OAuth

  • Requires verified domain for production
  • Callback URL must exactly match registration
  • Needs separate credentials for development and production

GitHub OAuth

  • Callback URL doesn't support wildcards
  • Requires email scope explicitly requested
  • Users can deny email access, breaking your app

Apple Sign In

  • Requires paid Apple Developer account
  • Only provides email once (store it!)
  • Different user IDs in development vs. production

Platform-Specific Solutions

// Handle missing email from Apple Sign In
async function handleAppleAuth(appleUser) {
  let email = appleUser.email;

  if (!email) {
    // Apple only provides email on first sign-in
    // Check database for existing user
    const existingUser = await db.users.findByAppleId(appleUser.id);
    email = existingUser?.email;
  }

  if (!email) {
    // Request email separately or use Apple ID as identifier
    throw new Error('Email required for registration');
  }
}

Session Management Across Subdomains

Your main app is at app.example.com, but your marketing site is at www.example.com. Users log in on one domain but appear logged out on the other.

The Problem

Cookies are domain-specific by default. A session cookie set on app.example.com won't be accessible on www.example.com.

The Solution

// Set cookies for the root domain
cookies.set('session', token, {
  domain: '.example.com', // Note the leading dot
  path: '/',
  secure: true,
  httpOnly: true,
});

Multi-Factor Authentication (MFA) Gone Wrong

Adding MFA sounds like a security win, but AI-generated implementations often break:

  • Users can bypass MFA by manipulating client-side code
  • MFA codes expire before users can enter them
  • No backup codes when users lose their device
  • MFA required for password resets (locking users out)

MFA Best Practices

  1. Always enforce MFA on the backend, not just the UI
  2. Provide backup codes during MFA setup
  3. Allow MFA reset via email verification
  4. Make MFA optional initially, not forced

Password Reset Flows That Fail

The AI generates a "Forgot Password" link, but clicking it does nothing, or the reset link expires immediately, or users can reset other people's passwords.

Common Password Reset Bugs

// ❌ Reset token not properly validated
async function resetPassword(token: string, newPassword: string) {
  // No expiration check, no user verification
  const userId = await db.resetTokens.getUserId(token);
  await db.users.updatePassword(userId, newPassword);
}

// ✅ Secure reset implementation
async function resetPassword(token: string, newPassword: string) {
  const reset = await db.resetTokens.findOne({
    token,
    expiresAt: { $gt: new Date() }, // Not expired
    used: false, // Not already used
  });

  if (!reset) {
    throw new Error('Invalid or expired reset link');
  }

  // Hash password, update user, mark token as used
  const hashedPassword = await bcrypt.hash(newPassword, 10);
  await db.users.updatePassword(reset.userId, hashedPassword);
  await db.resetTokens.markUsed(token);
}

When Authentication Gets Complicated

Some auth issues require deep debugging:

  • Race conditions where auth state updates don't propagate
  • Inconsistent session state across browser tabs
  • Auth working in Chrome but not Safari
  • CORS issues preventing auth cookies from being set

These problems often involve browser security policies, middleware ordering, or framework-specific behaviors that AI tools can't reason about effectively.


Users locked out of your app due to auth failures? Schedule an operations audit—we'll fix it in one hour.