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

January 18, 2026
15 min read
ASXIM19Founder & Developer @ LariVid
#oauth#authentication#manus#web-development#debugging

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

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:

  1. It requires your client_secret (never expose this to the browser)
  2. 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 };
    })
});

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 };
    })
});

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<string, Session>();

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:

MetricBeforeAfter
Auth Success Rate87%99.2%
Token Refresh Failures15/day0.3/day
User Complaints12/week0/week
Session Persistence3 days avg30 days avg
CORS Errors50/day0/day

Key Takeaway: OAuth is not "set it and forget it." You need monitoring, logging, and proactive token refresh.


🛠️ Further Resources


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

Next Post: "Stripe Webhooks: The 3 Errors That Will Cost You Money" 🔥


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

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

We Use Cookies

We use cookies and similar technologies to enhance your browsing experience, serve personalized content and ads, analyze our website traffic, and understand where our visitors are coming from. You can choose which categories of cookies you allow.