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.
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:
- Development works perfectly—Google login succeeds
- You deploy to production at
https://yourapp.com - Users click "Sign in with Google"
- Google authenticates them successfully
- Google redirects them to
http://localhost:3000/auth/callback - 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
- Open your Supabase project dashboard
- Go to Authentication → URL Configuration
- Update the Site URL to your production domain:
https://yourapp.com - Add your production domain to Redirect URLs:
https://yourapp.com/** - 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.roleto'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
- Always enforce MFA on the backend, not just the UI
- Provide backup codes during MFA setup
- Allow MFA reset via email verification
- 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.