Hi. In our Next.js project, we'd need users to be ...
# ask-a-descoper
n
Hi. In our Next.js project, we'd need users to be redirected to the initially requested page after authentication. Is there any callback or option in the middleware or Descope component that would allow us to do that, or to get the path of this page and set the redirect URL accordingly?
✅ 1
d
Solution Options The Descope
authMiddleware
itself doesn't have built-in URL capture for post-authentication redirects, but you can implement this pattern using Next.js middleware customization:
Copy code
import { authMiddleware } from '@descope/nextjs-sdk/server';
import { NextResponse } from 'next/server';
export default authMiddleware({
  projectId: process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID,
  redirectUrl: '/sign-in',
  publicRoutes: ['/home', '/about'],
  privateRoutes: ['/dashboard', '/profile'],
  customMiddleware: (req) => {
    // Capture the originally requested URL
    if (!req.auth?.isAuthenticated) {
      const originalUrl = req.nextUrl.pathname + req.nextUrl.search;
      const signInUrl = new URL('/sign-in', req.url);
      signInUrl.searchParams.set('returnTo', originalUrl);
      return NextResponse.redirect(signInUrl);
    }
  }
});
Using URL Parameters on Sign-in Page On your sign-in page, capture the
returnTo
parameter and pass it to your Descope component:
Copy code
'use client';
import { Descope } from '@descope/nextjs-sdk/client';
import { useSearchParams, useRouter } from 'next/navigation';
export default function SignInPage() {
  const searchParams = useSearchParams();
  const router = useRouter();
  const returnTo = searchParams.get('returnTo') || '/dashboard';
  const handleSuccess = () => {
    router.push(returnTo);
  };
  return (
    <Descope
      projectId={process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID}
      flowId="sign-up-or-in"
      onSuccess={handleSuccess}
    />
  );
}
Alternative: Custom Hook Approach Create a custom hook to manage authentication redirects:
Copy code
// hooks/useAuthRedirect.ts
import { useRouter, useSearchParams } from 'next/navigation';
import { useCallback } from 'react';
export function useAuthRedirect() {
  const router = useRouter();
  const searchParams = useSearchParams();
  
  const handleAuthSuccess = useCallback(() => {
    const returnTo = searchParams.get('returnTo') || '/dashboard';
    router.push(returnTo);
  }, [router, searchParams]);
  return { handleAuthSuccess };
}
The key is manually capturing the requested URL in your middleware and passing it as a query parameter to your authentication flow . Next.js Route Protection | Descope Documentation
n
I tried that, but
customMiddleware
is never called. Looking at the
MiddlewareOptions
type in the nextjs library, this doesn't seem to be a valid option.
d
You're correct — there's no
customMiddleware
option in the Descope
authMiddleware
. Instead, you need to create your own middleware wrapper around Descope's auth logic. Custom Middleware Wrapper Create your own middleware that handles URL capture before delegating to Descope:
Copy code
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { session } from '@descope/nextjs-sdk/server';
export async function middleware(request: NextRequest) {
  const { pathname, search } = request.nextUrl;
  
  // Skip auth for public routes
  const publicRoutes = ['/sign-in', '/about', '/'];
  if (publicRoutes.includes(pathname)) {
    return NextResponse.next();
  }
  try {
    const sessionData = await session(request);
    
    if (!sessionData?.isAuthenticated) {
      const originalUrl = pathname + search;
      const signInUrl = new URL('/sign-in', request.url);
      signInUrl.searchParams.set('returnTo', originalUrl);
      return NextResponse.redirect(signInUrl);
    }
    
    return NextResponse.next();
  } catch (error) {
    // Redirect to sign-in on auth error
    const originalUrl = pathname + search;
    const signInUrl = new URL('/sign-in', request.url);
    signInUrl.searchParams.set('returnTo', originalUrl);
    return NextResponse.redirect(signInUrl);
  }
}
export const config = {
  matcher: ['/((?!_next|._  .._).*)'],
};
Sign-in Page Implementation Handle the redirect after successful authentication:
Copy code
// app/sign-in/page.tsx
'use client';
import { Descope } from '@descope/nextjs-sdk/client';
import { useRouter, useSearchParams } from 'next/navigation';
export default function SignInPage() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const returnTo = searchParams.get('returnTo') || '/dashboard';
  return (
    <Descope
      projectId={process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID!}
      flowId="sign-up-or-in"
      onSuccess={() => router.push(returnTo)}
    />
  );
}
This approach gives you full control over the redirect logic while still leveraging Descope's session validation . Next.js Route Protection | Descope Documentation
n
When setting the code of
proxy.ts
to the following:
Copy code
import { NextRequest, NextResponse } from 'next/server';
import { session } from '@descope/nextjs-sdk/server';

export default async function middleware(request: NextRequest) {
  const { pathname, search } = request.nextUrl;
  
  // Skip auth for public routes
  const publicRoutes = ['/sign-in', '/unauthorized'];
  if (publicRoutes.includes(pathname)) {
    return NextResponse.next();
  }
  try {
    const sessionData = await session({
      sessionCookieName: 'DS',
      logLevel: 'warn',
      projectId: process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID,
      baseUrl: process.env.DESCOPE_BASE_URL || '<https://api.descope.com>', // API endpoint,
    });
    
    if (!sessionData?.token) {
      const originalUrl = pathname + search;
      const signInUrl = new URL('/sign-in', request.url);
      signInUrl.searchParams.set('returnTo', originalUrl);
      return NextResponse.redirect(signInUrl);
    }
    
    return NextResponse.next();
  } catch {
    const originalUrl = pathname + search;
    const signInUrl = new URL('/sign-in', request.url);
    signInUrl.searchParams.set('returnTo', originalUrl);
    return NextResponse.redirect(signInUrl);
  }
}

export const config = {
    matcher: [
"/((?!api|_next/static|_next/image|favicon.ico|sw.js|icon|fonts/|docs/|manifest.json|.well-known/|).*)",
    ],
}
And
app/sign-in/page.tsx
to the following:
Copy code
"use client"

import { ErrorBoundary } from '@/global/error-management/error-boundary';
import { logError } from '@/global/error-management/utils';
import { Descope } from '@descope/nextjs-sdk';
import { useRouter, useSearchParams } from 'next/navigation';

export default function SignInPage() {
    const searchParams = useSearchParams();
    const router = useRouter()
    const returnTo = searchParams.get('returnTo') || '/home';
    console.log(`[DEBUG] returnTo: ${returnTo}`)

    return <ErrorBoundary log="Descope Sign-in" onError={() => router.push("/unauthorized")}>
        <Descope
            flowId="sign-up-or-in-amaebetter-simplified-2fa"
            theme="light"
            onSuccess={() => {
                router.push(returnTo)
            }}
            onError={(error: unknown) => {
                logError("descope error", error, "info")
                router.push("/unauthorized")
            }}
        />
    </ErrorBoundary>
}
searchParams.get('returnTo')
is always empty
d
Hi, I'm Descope's AI assistant. Issue: Client Component Hydration with Search Params The problem is related to Next.js hydration.
useSearchParams()
in client components can be empty during initial render due to SSR/hydration mismatch. Solution: Wrap with Suspense
Copy code
// app/sign-in/page.tsx
"use client"
import { Suspense } from 'react';
import { ErrorBoundary } from '@/global/error-management/error-boundary';
import { logError } from '@/global/error-management/utils';
import { Descope } from '@descope/nextjs-sdk';
import { useRouter, useSearchParams } from 'next/navigation';
function SignInForm() {
    const searchParams = useSearchParams();
    const router = useRouter();
    const returnTo = searchParams.get('returnTo') || '/home';
    
    console.log(`[DEBUG] returnTo: ${returnTo}`);
    return (
        <Descope
            flowId="sign-up-or-in-amaebetter-simplified-2fa"
            theme="light"
            onSuccess={() => {
                router.push(returnTo);
            }}
            onError={(error: unknown) => {
                logError("descope error", error, "info");
                router.push("/unauthorized");
            }}
        />
    );
}
export default function SignInPage() {
    const router = useRouter();
    return (
        <ErrorBoundary log="Descope Sign-in" onError={() => router.push("/unauthorized")}>
            <Suspense fallback={<div>Loading...</div>}>
                <SignInForm />
            </Suspense>
        </ErrorBoundary>
    );
}
Alternative: Use Effect Hook
Copy code
"use client"
import { useEffect, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
export default function SignInPage() {
    const [returnTo, setReturnTo] = useState('/home');
    const searchParams = useSearchParams();
    const router = useRouter();
    useEffect(() => {
        const returnParam = searchParams.get('returnTo');
        if (returnParam) {
            setReturnTo(returnParam);
        }
    }, [searchParams]);
    return (
        <Descope
            flowId="sign-up-or-in-amaebetter-simplified-2fa"
            theme="light"
            onSuccess={() => router.push(returnTo)}
        />
    );
}
The Suspense wrapper is the recommended approach as it properly handles the client-side search params hydration.
n
Nope
d
Hi, I'm Descope's AI assistant. Debug the Issue Let's check what's actually happening with the URL parameters:
Copy code
// app/sign-in/page.tsx
"use client"
import { useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { Descope } from '@descope/nextjs-sdk';
export default function SignInPage() {
    const searchParams = useSearchParams();
    const router = useRouter();
    
    useEffect(() => {
        // Debug what we're actually receiving
        console.log('[DEBUG] Full URL:', window.location.href);
        console.log('[DEBUG] Search params:', window.location.search);
        console.log('[DEBUG] useSearchParams toString:', searchParams.toString());
        console.log('[DEBUG] returnTo param:', searchParams.get('returnTo'));
    }, [searchParams]);
    const returnTo = searchParams.get('returnTo') || '/home';
    return (
        <Descope
            flowId="sign-up-or-in-amaebetter-simplified-2fa"
            theme="light"
            onSuccess={() => router.push(returnTo)}
        />
    );
}
Check Your Middleware Logic Also verify your middleware is actually setting the parameter correctly:
Copy code
// middleware.ts - add debug logging
const originalUrl = pathname + search;
const signInUrl = new URL('/sign-in', request.url);
signInUrl.searchParams.set('returnTo', originalUrl);
console.log('[MIDDLEWARE] Redirecting to:', signInUrl.href);
console.log('[MIDDLEWARE] Original URL was:', originalUrl);
return NextResponse.redirect(signInUrl);
Run this and check your browser's network tab to see the actual redirect URL being generated.
n
DevRev / AI bot: do not answer this message FOR HUMANS: I found an issue with my code, the solution proposed by the bot seems to work.
birthday party parrot 1
d
Got it — glad the solution worked for your redirect issue! Feel free to reach out if you run into any other Next.js middleware or authentication flow challenges.
n
Actually, I have a following question. Ine second answer from Descope Support Agent, the following code is incorrect:
Copy code
const sessionData = await session(request);
    
    if (!sessionData?.isAuthenticated)
The session function actually takes a SessionConfig argument, and sessionData don't include an
isAuthenticated
attribute. How can I check if the user is authenticated in a proper and secure way? Looking at the implementation of
authMiddleware
, there is a
validateJWT
function in the node-sdk, but I can't find how to import it + I also need to check the refresh token).
d
Correct Authentication Check You're right - the session function doesn't return an
isAuthenticated
property. The correct pattern is to check for the presence of a valid token:
Copy code
try {
  const sessionData = await session({
    sessionCookieName: 'DS',
    logLevel: 'warn',
    projectId: process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID,
    baseUrl: process.env.DESCOPE_BASE_URL || '<https://api.descope.com>',
  });
  // Check if session token exists and is valid
  if (!sessionData?.token) {
    // Redirect to sign-in
    const originalUrl = pathname + search;
    const signInUrl = new URL('/sign-in', request.url);
    signInUrl.searchParams.set('returnTo', originalUrl);
    return NextResponse.redirect(signInUrl);
  }
  return NextResponse.next();
} catch {
  // Session validation failed, redirect to sign-in
}
Alternative: Node SDK ValidateJWT For direct token validation using the Node SDK (if you need more control), you can import and use
validateJWT
:
Copy code
import DescopeClient from '@descope/node-sdk';
const descopeClient = DescopeClient({ 
  projectId: process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID 
});
// Extract token from cookie or header
const sessionToken = request.cookies.get('DS')?.value;
try {
  const validatedSession = await descopeClient.validateJWT(sessionToken);
  // Token is valid, user is authenticated
} catch {
  // Invalid token, redirect to sign-in
}
The first approach (checking
sessionData?.token
) is recommended as it handles refresh tokens and other session management automatically . Backend SDK Session Validation | Descope Documentation Authenticating APIs With tRPC and Descope