DEV Community

Cover image for Manus OAuth: 5 Common Issues That Will Break Your Integration (And How to Fix Them)
Sinan Akkaya
Sinan Akkaya

Posted on • Originally published at larivid.manus.space

Manus OAuth: 5 Common Issues That Will Break Your Integration (And How to Fix Them)

Manus OAuth: 5 Common Issues That Will Break Your Integration (And How to Fix Them)
TL;DR: Manus OAuth is powerful, but it has sharp edges. After integrating it into LariVid, two SaaS clients, and a marketplace app, I've debugged every possible failure mode. This guide covers the 5 most common issues that will break your integration—and the exact fixes that work in production.

🔥 Why This Matters
Manus OAuth is the authentication backbone for thousands of Manus-powered apps. It handles:

Single Sign-On (SSO) across Manus ecosystem
User identity verification with OpenID
Token refresh for long-lived sessions
Role-based access control (RBAC)
But when it breaks, it breaks silently. Users see "Invalid credentials" or "Session expired"—and you see nothing in your logs.

This post is your debugging playbook.

🕵️ Issue #1: "Invalid Redirect URI" (The Silent Killer)
Symptoms
OAuth flow redirects to Manus login
User logs in successfully
Redirect back to your app fails with "Invalid redirect_uri"
No error in your server logs
Why It Happens
Manus OAuth validates redirect URIs with exact string matching. Even a trailing slash breaks it.

Example:

// ❌ WRONG - Trailing slash
const redirectUri = "https://larivid.com/auth/callback/";

// ✅ CORRECT - No trailing slash
const redirectUri = "https://larivid.com/auth/callback";
But here's the catch: Your browser might add the trailing slash automatically.

The Fix
Step 1: Check your OAuth configuration in Manus Dashboard.

Go to Settings → OAuth → Redirect URIs and verify:

https://larivid.com/auth/callback
https://larivid.com/auth/callback/
Step 2: Normalize URIs in your code.

// server/auth-utils.ts
export function getCallbackUrl(): string {
const baseUrl = process.env.FRONTEND_URL || "http://localhost:3000";
// Remove trailing slash
return ${baseUrl.replace(/\/$/, "")}/auth/callback;
}
Step 3: Test in all environments.

✅ Development: http://localhost:3000/auth/callback
✅ Staging: https://staging.larivid.com/auth/callback
✅ Production: https://larivid.com/auth/callback
Pro Tip: Use environment variables for redirect URIs, not hardcoded strings.

🕵️ Issue #2: Token Refresh Fails After 7 Days
Symptoms
User logs in successfully
App works fine for 7 days
On day 8, user is logged out
Error: "Refresh token expired"
Why It Happens
Manus OAuth refresh tokens have a 7-day lifetime by default. After that, they expire—even if the user is actively using your app.

Most OAuth providers (Google, GitHub) have 90-day refresh tokens. Manus is more aggressive.

The Fix
Option 1: Implement Silent Token Refresh (Recommended)

Refresh tokens before they expire, not after.

// server/auth-middleware.ts
import { refreshManusToken } from './manus-oauth';

export async function validateSession(sessionId: string) {
const session = await db.sessions.findUnique({ where: { id: sessionId } });

if (!session) return null;

// Check if token expires in next 24 hours
const expiresIn = session.expiresAt.getTime() - Date.now();
const oneDayMs = 24 * 60 * 60 * 1000;

if (expiresIn < oneDayMs) {
console.log('[Auth] Token expires soon, refreshing...');

try {
  const newTokens = await refreshManusToken(session.refreshToken);

  await db.sessions.update({
    where: { id: sessionId },
    data: {
      accessToken: newTokens.access_token,
      refreshToken: newTokens.refresh_token,
      expiresAt: new Date(Date.now() + newTokens.expires_in * 1000)
    }
  });

  console.log('[Auth] Token refreshed successfully');
} catch (error) {
  console.error('[Auth] Token refresh failed:', error);
  // Force re-login
  return null;
}
Enter fullscreen mode Exit fullscreen mode

}

return session;
}
Option 2: Extend Refresh Token Lifetime

Contact Manus support to increase refresh token lifetime to 30 or 90 days. This requires approval.

Option 3: Implement "Remember Me" with Long-Lived Sessions

Store a separate long-lived session cookie (30-90 days) and use it to trigger OAuth re-authentication when refresh token expires.

// server/auth-utils.ts
export function createLongLivedSession(userId: string) {
const sessionToken = crypto.randomBytes(32).toString('hex');

await db.longLivedSessions.create({
data: {
token: sessionToken,
userId,
expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 days
}
});

return sessionToken;
}
Pro Tip: Log token refresh events to monitor success rate. If refresh fails >5%, investigate.

🕵️ Issue #3: CORS Errors on Token Exchange
Symptoms
OAuth redirect works
Browser receives authorization code
Token exchange request fails with CORS error
Error: "Access to fetch at 'https://api.manus.im/oauth/token' from origin 'https://larivid.com' has been blocked by CORS policy"
Why It Happens
You're calling the Manus OAuth token endpoint from the browser instead of the server.

OAuth token exchange MUST happen server-side because:

It requires your client_secret (never expose this to the browser)
Manus API doesn't allow CORS requests from arbitrary origins
The Fix
❌ WRONG - Client-Side Token Exchange

// client/src/pages/AuthCallback.tsx
const code = new URLSearchParams(window.location.search).get('code');

const response = await fetch('https://api.manus.im/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
code,
client_id: process.env.VITE_OAUTH_CLIENT_ID,
client_secret: process.env.OAUTH_CLIENT_SECRET, // 🚨 EXPOSED!
redirect_uri: 'https://larivid.com/auth/callback'
})
});
✅ CORRECT - Server-Side Token Exchange

// server/routers/auth.ts
import { router, publicProcedure } from '../trpc';
import { z } from 'zod';
import axios from 'axios';

export const authRouter = router({
exchangeCode: publicProcedure
.input(z.object({ code: z.string() }))
.mutation(async ({ input }) => {
const response = await axios.post('https://api.manus.im/oauth/token', {
grant_type: 'authorization_code',
code: input.code,
client_id: process.env.OAUTH_CLIENT_ID,
client_secret: process.env.OAUTH_CLIENT_SECRET, // ✅ Server-side only
redirect_uri: process.env.FRONTEND_URL + '/auth/callback'
});

  const { access_token, refresh_token, expires_in } = response.data;

  // Create session
  const session = await createSession(access_token, refresh_token, expires_in);

  return { sessionId: session.id };
})
Enter fullscreen mode Exit fullscreen mode

});
Client-Side:

// client/src/pages/AuthCallback.tsx
const code = new URLSearchParams(window.location.search).get('code');

const { sessionId } = await trpc.auth.exchangeCode.mutate({ code });

// Store session ID in cookie
document.cookie = session_id=${sessionId}; path=/; max-age=${90 * 24 * 60 * 60};

// Redirect to dashboard
window.location.href = '/dashboard';
Pro Tip: Never log client_secret or tokens. Use console.log('[Auth] Token exchange successful') instead of console.log(response.data).

🕵️ Issue #4: User Data Not Syncing After Profile Update
Symptoms
User updates their name/email in Manus account
Your app still shows old data
Logout + login doesn't fix it
Why It Happens
You're caching user data in your database and not refreshing it when the OAuth token is refreshed.

Manus OAuth returns user info in the ID token (JWT), but this token is only issued during initial login, not during token refresh.

The Fix
Option 1: Fetch User Info on Every Token Refresh

// server/manus-oauth.ts
export async function refreshManusToken(refreshToken: string) {
const response = await axios.post('https://api.manus.im/oauth/token', {
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: process.env.OAUTH_CLIENT_ID,
client_secret: process.env.OAUTH_CLIENT_SECRET
});

const { access_token, refresh_token: new_refresh_token, expires_in } = response.data;

// Fetch updated user info
const userInfoResponse = await axios.get('https://api.manus.im/oauth/userinfo', {
headers: { Authorization: Bearer ${access_token} }
});

const userInfo = userInfoResponse.data;

// Update user in database
await db.users.update({
where: { openId: userInfo.sub },
data: {
name: userInfo.name,
email: userInfo.email,
updatedAt: new Date()
}
});

return { access_token, refresh_token: new_refresh_token, expires_in };
}
Option 2: Implement Webhook for Profile Updates

Manus supports webhooks for user profile changes. Register a webhook endpoint in Manus Dashboard:

// server/routers/webhooks.ts
import { router, publicProcedure } from '../trpc';
import { z } from 'zod';
import crypto from 'crypto';

export const webhooksRouter = router({
manusUserUpdate: publicProcedure
.input(z.object({
event: z.literal('user.updated'),
data: z.object({
sub: z.string(),
name: z.string(),
email: z.string()
}),
signature: z.string()
}))
.mutation(async ({ input }) => {
// Verify webhook signature
const expectedSignature = crypto
.createHmac('sha256', process.env.MANUS_WEBHOOK_SECRET!)
.update(JSON.stringify(input.data))
.digest('hex');

  if (input.signature !== expectedSignature) {
    throw new Error('Invalid webhook signature');
  }

  // Update user
  await db.users.update({
    where: { openId: input.data.sub },
    data: {
      name: input.data.name,
      email: input.data.email,
      updatedAt: new Date()
    }
  });

  console.log(`[Webhook] User ${input.data.sub} updated`);

  return { success: true };
})
Enter fullscreen mode Exit fullscreen mode

});
Pro Tip: Use Option 2 (webhooks) for real-time updates. Use Option 1 as fallback.

🕵️ Issue #5: "Session Not Found" After Server Restart
Symptoms
User is logged in
Server restarts (deploy, crash, etc.)
User is logged out
Error: "Session not found"
Why It Happens
You're storing sessions in-memory (e.g., in a JavaScript Map or Express session middleware with default settings).

When the server restarts, all in-memory data is lost.

The Fix
Store sessions in a persistent database (PostgreSQL, MySQL, Redis).

❌ WRONG - In-Memory Sessions

// server/sessions.ts
const sessions = new Map();

export function createSession(userId: string, accessToken: string) {
const sessionId = crypto.randomBytes(32).toString('hex');
sessions.set(sessionId, { userId, accessToken, createdAt: new Date() });
return sessionId;
}

export function getSession(sessionId: string) {
return sessions.get(sessionId);
}
✅ CORRECT - Database-Backed Sessions

// server/sessions.ts
import { db } from './db';

export async function createSession(userId: string, accessToken: string, refreshToken: string, expiresIn: number) {
const sessionId = crypto.randomBytes(32).toString('hex');

await db.sessions.create({
data: {
id: sessionId,
userId,
accessToken,
refreshToken,
expiresAt: new Date(Date.now() + expiresIn * 1000),
createdAt: new Date()
}
});

return sessionId;
}

export async function getSession(sessionId: string) {
return await db.sessions.findUnique({ where: { id: sessionId } });
}
Database Schema (Drizzle ORM):

// drizzle/schema.ts
import { pgTable, text, timestamp, integer } from 'drizzle-orm/pg-core';

export const sessions = pgTable('sessions', {
id: text('id').primaryKey(),
userId: integer('user_id').notNull().references(() => users.id),
accessToken: text('access_token').notNull(),
refreshToken: text('refresh_token').notNull(),
expiresAt: timestamp('expires_at').notNull(),
createdAt: timestamp('created_at').notNull().defaultNow()
});
Pro Tip: Use Redis for sessions if you need sub-millisecond latency. Use PostgreSQL if you need ACID guarantees.

📊 Impact: From Broken Auth to 99.9% Uptime
After fixing these 5 issues in LariVid:

Metric Before After
Auth Success Rate 87% 99.2%
Token Refresh Failures 15/day 0.3/day
User Complaints 12/week 0/week
Session Persistence 3 days avg 30 days avg
CORS Errors 50/day 0/day
Key Takeaway: OAuth is not "set it and forget it." You need monitoring, logging, and proactive token refresh.

🛠️ Further Resources
Manus OAuth Documentation
OAuth 2.0 RFC 6749
OpenID Connect Core 1.0
OWASP OAuth Security Cheat Sheet
💬 Have Similar Problems?
If you're building on Manus and hitting OAuth issues, I'm happy to help. Reach out on Twitter @asxim19 or check out LariVid to see OAuth done right.

Tags: #oauth #authentication #manus #web-development #debugging

Published: January 18, 2026 | Author: ASXIM19, Founder & Developer @ LariVid

Share:
Twitter
LinkedIn
Reddit

Top comments (0)